Commit 375fc85a authored by Adam Dinwoodie's avatar Adam Dinwoodie
Browse files

nixos/sshd: add generateHostKeys setting

If a user doesn't want to enable the SSH daemon, but does want to have
SSH host keys configured for some other reason (e.g. they're used for
host identification in some other way), provide a `generateHostKeys`
setting that will generate the keys without otherwise setting up sshd.
parent ece6e266
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -22,4 +22,4 @@

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

- Create the first release note entry in this section!
- `services.openssh` now supports generating host SSH keys by setting `services.openssh.generateHostKeys = true` while leaving `services.openssh.enable` disabled.  This is particularly useful for systems that have no need of an SSH daemon but want SSH host keys for other purposes such as using agenix or sops-nix.
+225 −207
Original line number Diff line number Diff line
@@ -381,6 +381,19 @@ in
        '';
      };

      generateHostKeys = lib.mkOption {
        type = lib.types.bool;
        default = config.services.openssh.enable;
        defaultText = lib.literalExpression "services.openssh.enable";
        description = ''
          Whether to generate SSH host keys.

          This can be enabled explicitly if you want to generate host keys but
          don't want to enable the SSH daemon.
        '';
        example = true;
      };

      banner = lib.mkOption {
        type = lib.types.nullOr lib.types.lines;
        default = null;
@@ -669,7 +682,8 @@ in

  ###### implementation

  config = lib.mkIf cfg.enable {
  config = lib.mkMerge [
    (lib.mkIf cfg.enable {

      users.users.sshd = {
        isSystemUser = true;
@@ -731,7 +745,7 @@ in
            "network.target"
            "sshd-keygen.service"
          ];
        wants = [ "sshd-keygen.service" ];
          wants = lib.mkIf cfg.generateHostKeys [ "sshd-keygen.service" ];
          stopIfChanged = false;
          path = [ cfg.package ];
          environment.LD_LIBRARY_PATH = nssModulesPath;
@@ -756,7 +770,7 @@ in
            "network.target"
            "sshd-keygen.service"
          ];
        wants = [ "sshd-keygen.service" ];
          wants = lib.mkIf cfg.generateHostKeys [ "sshd-keygen.service" ];
          stopIfChanged = false;
          path = [ cfg.package ];
          environment.LD_LIBRARY_PATH = nssModulesPath;
@@ -775,33 +789,6 @@ in
            KillMode = "process";
          };
        };

      services.sshd-keygen = {
        description = "SSH Host Keys Generation";
        unitConfig = {
          ConditionFileNotEmpty = map (k: "|!${k.path}") cfg.hostKeys;
        };
        serviceConfig = {
          Type = "oneshot";
        };
        path = [ cfg.package ];
        script = lib.flip lib.concatMapStrings cfg.hostKeys (k: ''
          if ! [ -s "${k.path}" ]; then
              if ! [ -h "${k.path}" ]; then
                  rm -f "${k.path}"
              fi
              mkdir -p "$(dirname '${k.path}')"
              chmod 0755 "$(dirname '${k.path}')"
              ssh-keygen \
                -t "${k.type}" \
                ${lib.optionalString (k ? bits) "-b ${toString k.bits}"} \
                ${lib.optionalString (k ? comment) "-C '${k.comment}'"} \
                ${lib.optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \
                -f "${k.path}" \
                -N ""
          fi
        '');
      };
      };

      networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall cfg.ports;
@@ -915,6 +902,37 @@ in
          message = "addr must be specified in each listenAddresses entry";
        }
      );
    })

    (lib.mkIf cfg.generateHostKeys {
      systemd.services.sshd-keygen = {
        description = "SSH Host Keys Generation";
        wantedBy = [ "multi-user.target" ];
        unitConfig = {
          ConditionFileNotEmpty = map (k: "|!${k.path}") cfg.hostKeys;
        };
        serviceConfig = {
          Type = "oneshot";
        };
        path = [ cfg.package ];
        script = lib.flip lib.concatMapStrings cfg.hostKeys (k: ''
          if ! [ -s "${k.path}" ]; then
              if ! [ -h "${k.path}" ]; then
                  rm -f "${k.path}"
              fi
              mkdir -p "$(dirname '${k.path}')"
              chmod 0755 "$(dirname '${k.path}')"
              ssh-keygen \
                -t "${k.type}" \
                ${lib.optionalString (k ? bits) "-b ${toString k.bits}"} \
                ${lib.optionalString (k ? comment) "-C '${k.comment}'"} \
                ${lib.optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \
                -f "${k.path}" \
                -N ""
          fi
        '');
      };
    })
  ];

}
+26 −0
Original line number Diff line number Diff line
@@ -250,6 +250,15 @@ in
        };
      };

    server-no-sshd-with-key =
      { pkgs, ... }:
      {
        services.openssh.generateHostKeys = true;
        users.users.root.openssh.authorizedKeys.keys = [
          snakeOilPublicKey
        ];
      };

    client =
      { ... }:
      {
@@ -276,6 +285,10 @@ in
    server_localhost_only_lazy.wait_for_unit("sshd.socket", timeout=30)
    server_lazy_socket.wait_for_unit("sshd.socket", timeout=30)

    # sshd-keygen is a oneshot unit, so just wait for multi-user.target, which
    # pulls it in.
    server_no_sshd_with_key.wait_for_unit("multi-user.target", timeout=30)

    with subtest("manual-authkey"):
        client.succeed(
            '${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""'
@@ -408,6 +421,19 @@ in

        server_sftp.wait_for_file("/srv/sftp/uploads/test-file")

    with subtest("keygen without sshd"):
        client.fail(
            "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@server-no-sshd-with-key true",
            timeout=30
        )
        server_no_sshd_with_key.succeed("test -e /etc/ssh/ssh_host_ed25519_key")
        server_no_sshd_with_key.succeed("test -e /etc/ssh/ssh_host_ed25519_key.pub")
        server_no_sshd_with_key.fail("pgrep sshd")

        # Validate the above check for sshd using pgrep does pass on a server
        # that should have sshd running, just to prove it's a useful test.
        server.succeed("pgrep sshd")

    # None of the per-connection units should have failed.
    server_lazy.fail("systemctl is-failed 'sshd@*.service'")
  '';