diff --git a/doc/manpage-urls.json b/doc/manpage-urls.json index 63f877dcb6604..8b3b58c151259 100644 --- a/doc/manpage-urls.json +++ b/doc/manpage-urls.json @@ -228,6 +228,8 @@ "systemd-socket-activate(1)": "https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html", "systemd-socket-proxyd(8)": "https://www.freedesktop.org/software/systemd/man/systemd-socket-proxyd.html", "systemd-soft-reboot.service(8)": "https://www.freedesktop.org/software/systemd/man/systemd-soft-reboot.service.html", + "systemd-ssh-generator(8)": "https://www.freedesktop.org/software/systemd/man/systemd-ssh-generator.html", + "systemd-ssh-proxy(1)": "https://www.freedesktop.org/software/systemd/man/systemd-ssh-proxy.html", "systemd-stdio-bridge(1)": "https://www.freedesktop.org/software/systemd/man/systemd-stdio-bridge.html", "systemd-stub(7)": "https://www.freedesktop.org/software/systemd/man/systemd-stub.html", "systemd-suspend-then-hibernate.service(8)": "https://www.freedesktop.org/software/systemd/man/systemd-suspend-then-hibernate.service.html", diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 37dab65d2ebab..401dcff540bdb 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -457,6 +457,19 @@ - GOverlay has been updated to 1.2, please check the [upstream changelog](https://github.com/benjamimgois/goverlay/releases) for more details. +- systemd's {manpage}`systemd-ssh-generator(8)` now works out of the box on NixOS. + - You can ssh into VMs without any networking configuration if your hypervisor configures the vm to support AF_VSOCK. + It still requires the usual ssh authentication methods. + - An SSH key for the root user can be provisioned using the `ssh.authorized_keys.root` systemd credential. + This can be useful for booting an installation image and providing the SSH key with an smbios string. + - SSH can be used for suid-less privilege escalation on the local system without having to rely on networking: + ```shell + ssh root@.host + ``` + - systemd's {manpage}`systemd-ssh-proxy(1)` is enabled by default. It can be disabled using [`programs.ssh.systemd-ssh-proxy.enable`](#opt-programs.ssh.systemd-ssh-proxy.enable). + +- SSH host key generation has been separated into the dedicated systemd service sshd-keygen.service. + - [`services.mongodb`](#opt-services.mongodb.enable) is now compatible with the `mongodb-ce` binary package. To make use of it, set [`services.mongodb.package`](#opt-services.mongodb.package) to `pkgs.mongodb-ce`. - [`services.jupyter`](#opt-services.jupyter.enable) is now compatible with `Jupyter Notebook 7`. See [the migration guide](https://jupyter-notebook.readthedocs.io/en/latest/migrate_to_notebook7.html) for details. diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix index fbc59c09a68f6..0faf14fd4c29f 100644 --- a/nixos/modules/programs/ssh.nix +++ b/nixos/modules/programs/ssh.nix @@ -49,6 +49,15 @@ in description = "Whether to configure SSH_ASKPASS in the environment."; }; + systemd-ssh-proxy.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to enable systemd's ssh proxy plugin. + See {manpage}`systemd-ssh-proxy(1)`. + ''; + }; + askPassword = lib.mkOption { type = lib.types.str; default = "${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass"; @@ -332,6 +341,11 @@ in # Custom options from `extraConfig`, to override generated options ${cfg.extraConfig} + ${lib.optionalString cfg.systemd-ssh-proxy.enable '' + # See systemd-ssh-proxy(1) + Include ${config.systemd.package}/lib/systemd/ssh_config.d/20-systemd-ssh-proxy.conf + ''} + # Generated options from other settings Host * GlobalKnownHostsFile ${builtins.concatStringsSep " " knownHostsFiles} diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix index ddab9f24b0b82..42014f52aab84 100644 --- a/nixos/modules/services/networking/ssh/sshd.nix +++ b/nixos/modules/services/networking/ssh/sshd.nix @@ -560,67 +560,16 @@ in "ssh/sshd_config".source = sshconf; }; - systemd = - let - service = - { description = "SSH Daemon"; - wantedBy = lib.optional (!cfg.startWhenNeeded) "multi-user.target"; - after = [ "network.target" ]; - stopIfChanged = false; - path = [ cfg.package pkgs.gawk ]; - environment.LD_LIBRARY_PATH = nssModulesPath; - - restartTriggers = lib.optionals (!cfg.startWhenNeeded) [ - config.environment.etc."ssh/sshd_config".source - ]; - - preStart = - '' - # Make sure we don't write to stdout, since in case of - # socket activation, it goes to the remote side (#19589). - exec >&2 - - ${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 ? rounds) "-a ${toString k.rounds}"} \ - ${lib.optionalString (k ? comment) "-C '${k.comment}'"} \ - ${lib.optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \ - -f "${k.path}" \ - -N "" - fi - '')} - ''; - - serviceConfig = - { ExecStart = - (lib.optionalString cfg.startWhenNeeded "-") + - "${cfg.package}/bin/sshd " + (lib.optionalString cfg.startWhenNeeded "-i ") + - "-D " + # don't detach into a daemon process - "-f /etc/ssh/sshd_config"; - KillMode = "process"; - } // (if cfg.startWhenNeeded then { - StandardInput = "socket"; - StandardError = "journal"; - } else { - Restart = "always"; - Type = "simple"; - }); - - }; - in - - if cfg.startWhenNeeded then { + systemd.tmpfiles.settings."ssh-root-provision" = { + "/root"."d-" = { user = "root"; group = ":root"; mode = ":700"; }; + "/root/.ssh"."d-" = { user = "root"; group = ":root"; mode = ":700"; }; + "/root/.ssh/authorized_keys"."f^" = { user = "root"; group = ":root"; mode = ":600"; argument = "ssh.authorized_keys.root"; }; + }; - sockets.sshd = - { description = "SSH Socket"; + systemd = + { + sockets.sshd = lib.mkIf cfg.startWhenNeeded { + description = "SSH Socket"; wantedBy = [ "sockets.target" ]; socketConfig.ListenStream = if cfg.listenAddresses != [] then lib.concatMap @@ -633,14 +582,81 @@ in socketConfig.Accept = true; # Prevent brute-force attacks from shutting down socket socketConfig.TriggerLimitIntervalSec = 0; + }; + + services."sshd@" = { + description = "SSH per-connection Daemon"; + after = [ "network.target" "sshd-keygen.service" ]; + wants = [ "sshd-keygen.service" ]; + stopIfChanged = false; + path = [ cfg.package ]; + environment.LD_LIBRARY_PATH = nssModulesPath; + + serviceConfig = { + Type = "notify"; + ExecStart = lib.concatStringsSep " " [ + "-${lib.getExe' cfg.package "sshd"}" + "-i" + "-D" + "-f /etc/ssh/sshd_config" + ]; + KillMode = "process"; + StandardInput = "socket"; + StandardError = "journal"; }; + }; + + services.sshd = lib.mkIf (! cfg.startWhenNeeded) { + description = "SSH Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "sshd-keygen.service" ]; + wants = [ "sshd-keygen.service" ]; + stopIfChanged = false; + path = [ cfg.package ]; + environment.LD_LIBRARY_PATH = nssModulesPath; + + restartTriggers = [ config.environment.etc."ssh/sshd_config".source ]; + + serviceConfig = { + Type = "notify"; + Restart = "always"; + ExecStart = lib.concatStringsSep " " [ + (lib.getExe' cfg.package "sshd") + "-D" + "-f" "/etc/ssh/sshd_config" + ]; + KillMode = "process"; + }; + }; - services."sshd@" = service; - - } else { - - services.sshd = service; - + 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 ? rounds) "-a ${toString k.rounds}"} \ + ${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; diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 7ae06b509a509..e80164da2b11a 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -628,7 +628,12 @@ in systemd.managerEnvironment = { # Doesn't contain systemd itself - everything works so it seems to use the compiled-in value for its tools # util-linux is needed for the main fsck utility wrapping the fs-specific ones - PATH = lib.makeBinPath (config.system.fsPackages ++ [cfg.package.util-linux]); + PATH = lib.makeBinPath ( + config.system.fsPackages + ++ [cfg.package.util-linux] + # systemd-ssh-generator needs sshd in PATH + ++ lib.optional config.services.openssh.enable config.services.openssh.package + ); LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive"; TZDIR = "/etc/zoneinfo"; # If SYSTEMD_UNIT_PATH ends with an empty component (":"), the usual unit load path will be appended to the contents of the variable diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 24d6e0725f2a5..9193156cfd531 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1067,6 +1067,7 @@ in { systemd-portabled = handleTest ./systemd-portabled.nix {}; systemd-repart = handleTest ./systemd-repart.nix {}; systemd-resolved = handleTest ./systemd-resolved.nix {}; + systemd-ssh-proxy = runTest ./systemd-ssh-proxy.nix; systemd-shutdown = handleTest ./systemd-shutdown.nix {}; systemd-sysupdate = runTest ./systemd-sysupdate.nix; systemd-sysusers-mutable = runTest ./systemd-sysusers-mutable.nix; diff --git a/nixos/tests/openssh.nix b/nixos/tests/openssh.nix index d420c482ca7f2..92829001f2dde 100644 --- a/nixos/tests/openssh.nix +++ b/nixos/tests/openssh.nix @@ -174,7 +174,6 @@ in { server_lazy_socket.wait_for_unit("sshd.socket", timeout=30) with subtest("manual-authkey"): - client.succeed("mkdir -m 700 /root/.ssh") client.succeed( '${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""' ) @@ -184,9 +183,7 @@ in { public_key = public_key.strip() client.succeed("chmod 600 /root/.ssh/id_ed25519") - server.succeed("mkdir -m 700 /root/.ssh") server.succeed("echo '{}' > /root/.ssh/authorized_keys".format(public_key)) - server_lazy.succeed("mkdir -m 700 /root/.ssh") server_lazy.succeed("echo '{}' > /root/.ssh/authorized_keys".format(public_key)) client.wait_for_unit("network.target") diff --git a/nixos/tests/systemd-ssh-proxy.nix b/nixos/tests/systemd-ssh-proxy.nix new file mode 100644 index 0000000000000..7f19cfcce09dd --- /dev/null +++ b/nixos/tests/systemd-ssh-proxy.nix @@ -0,0 +1,76 @@ +{ + pkgs, + lib, + config, + ... +}: +# This tests that systemd-ssh-proxy and systemd-ssh-generator work correctly with: +# - a local unix socket on the same system +# - a vsock socket inside a vm +let + inherit (import ./ssh-keys.nix pkgs) + snakeOilEd25519PrivateKey + snakeOilEd25519PublicKey + ; + qemu = config.nodes.virthost.virtualisation.qemu.package; + iso = + (import ../lib/eval-config.nix { + inherit (pkgs.stdenv.hostPlatform) system; + modules = [ + ../modules/installer/cd-dvd/iso-image.nix + { + services.openssh = { + enable = true; + settings.PermitRootLogin = "prohibit-password"; + }; + isoImage.isoBaseName = lib.mkForce "nixos"; + isoImage.makeBiosBootable = true; + system.stateVersion = lib.trivial.release; + } + ]; + }).config.system.build.isoImage; +in +{ + name = "systemd-ssh-proxy"; + meta.maintainers = with pkgs.lib.maintainers; [ marie ]; + + nodes = { + virthost = { + services.openssh = { + enable = true; + settings.PermitRootLogin = "prohibit-password"; + }; + users.users = { + root.openssh.authorizedKeys.keys = [ snakeOilEd25519PublicKey ]; + nixos = { + isNormalUser = true; + }; + }; + systemd.services.test-vm = { + script = "${lib.getExe qemu} --nographic -smp 1 -m 512 -cdrom ${iso}/iso/nixos.iso -device vhost-vsock-pci,guest-cid=3 -smbios type=11,value=\"io.systemd.credential:ssh.authorized_keys.root=${snakeOilEd25519PublicKey}\""; + }; + }; + }; + + testScript = '' + virthost.systemctl("start test-vm.service") + + virthost.succeed("mkdir -p ~/.ssh") + virthost.succeed("cp '${snakeOilEd25519PrivateKey}' ~/.ssh/id_ed25519") + virthost.succeed("chmod 600 ~/.ssh/id_ed25519") + + with subtest("ssh into a vm with vsock"): + virthost.wait_until_succeeds("systemctl is-active test-vm.service") + virthost.wait_until_succeeds("ssh -i ~/.ssh/id_ed25519 vsock/3 echo meow | grep meow") + virthost.wait_until_succeeds("ssh -i ~/.ssh/id_ed25519 vsock/3 shutdown now") + virthost.wait_until_succeeds("! systemctl is-active test-vm.service") + + with subtest("elevate permissions using local ssh socket"): + virthost.wait_for_unit("sshd-unix-local.socket") + virthost.succeed("sudo --user=nixos mkdir -p /home/nixos/.ssh") + virthost.succeed("cp ~/.ssh/id_ed25519 /home/nixos/.ssh/id_ed25519") + virthost.succeed("chmod 600 /home/nixos/.ssh/id_ed25519") + virthost.succeed("chown nixos /home/nixos/.ssh/id_ed25519") + virthost.succeed("sudo --user=nixos ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i /home/nixos/.ssh/id_ed25519 root@.host whoami | grep root") + ''; +} diff --git a/pkgs/os-specific/linux/systemd/0019-meson-Don-t-link-ssh-dropins.patch b/pkgs/os-specific/linux/systemd/0019-meson-Don-t-link-ssh-dropins.patch new file mode 100644 index 0000000000000..a5b7c168ee943 --- /dev/null +++ b/pkgs/os-specific/linux/systemd/0019-meson-Don-t-link-ssh-dropins.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Marie Ramlow +Date: Sun, 24 Nov 2024 20:04:35 +0100 +Subject: [PATCH] meson: Don't link ssh dropins + +--- + meson.build | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/meson.build b/meson.build +index d392610625..c17d0a1feb 100644 +--- a/meson.build ++++ b/meson.build +@@ -211,13 +211,13 @@ sshconfdir = get_option('sshconfdir') + if sshconfdir == '' + sshconfdir = sysconfdir / 'ssh/ssh_config.d' + endif +-conf.set10('LINK_SSH_PROXY_DROPIN', sshconfdir != 'no' and not sshconfdir.startswith('/usr/')) ++conf.set10('LINK_SSH_PROXY_DROPIN', 0) + + sshdconfdir = get_option('sshdconfdir') + if sshdconfdir == '' + sshdconfdir = sysconfdir / 'ssh/sshd_config.d' + endif +-conf.set10('LINK_SSHD_USERDB_DROPIN', sshdconfdir != 'no' and not sshdconfdir.startswith('/usr/')) ++conf.set10('LINK_SSHD_USERDB_DROPIN', 0) + + sshdprivsepdir = get_option('sshdprivsepdir') + conf.set10('CREATE_SSHDPRIVSEPDIR', sshdprivsepdir != 'no' and not sshdprivsepdir.startswith('/usr/')) +-- +2.47.0 + diff --git a/pkgs/os-specific/linux/systemd/0020-install-unit_file_exists_full-follow-symlinks.patch b/pkgs/os-specific/linux/systemd/0020-install-unit_file_exists_full-follow-symlinks.patch new file mode 100644 index 0000000000000..e138aca05ac20 --- /dev/null +++ b/pkgs/os-specific/linux/systemd/0020-install-unit_file_exists_full-follow-symlinks.patch @@ -0,0 +1,25 @@ +From 7be486fb25dc4ea212cb17f6a3f4a434a557b0d9 Mon Sep 17 00:00:00 2001 +From: Marie Ramlow +Date: Fri, 10 Jan 2025 15:51:33 +0100 +Subject: [PATCH] install: unit_file_exists_full: follow symlinks + +--- + src/shared/install.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/shared/install.c b/src/shared/install.c +index 53566b7eef..0975cd47c7 100644 +--- a/src/shared/install.c ++++ b/src/shared/install.c +@@ -3217,7 +3217,7 @@ int unit_file_exists_full(RuntimeScope scope, const LookupPaths *lp, const char + &c, + lp, + name, +- /* flags= */ 0, ++ /* flags= */ SEARCH_FOLLOW_CONFIG_SYMLINKS, + ret_path ? &info : NULL, + /* changes= */ NULL, + /* n_changes= */ NULL); +-- +2.47.0 + diff --git a/pkgs/os-specific/linux/systemd/0019-timesyncd-disable-NSCD-when-DNSSEC-validation-is-dis.patch b/pkgs/os-specific/linux/systemd/0021-timesyncd-disable-NSCD-when-DNSSEC-validation-is-dis.patch similarity index 100% rename from pkgs/os-specific/linux/systemd/0019-timesyncd-disable-NSCD-when-DNSSEC-validation-is-dis.patch rename to pkgs/os-specific/linux/systemd/0021-timesyncd-disable-NSCD-when-DNSSEC-validation-is-dis.patch diff --git a/pkgs/os-specific/linux/systemd/default.nix b/pkgs/os-specific/linux/systemd/default.nix index 11492bd163e34..f9177561b1c75 100644 --- a/pkgs/os-specific/linux/systemd/default.nix +++ b/pkgs/os-specific/linux/systemd/default.nix @@ -242,9 +242,14 @@ stdenv.mkDerivation (finalAttrs: { ./0016-systemctl-edit-suggest-systemdctl-edit-runtime-on-sy.patch ./0017-meson.build-do-not-create-systemdstatedir.patch ./0018-Revert-bootctl-update-list-remove-all-instances-of-s.patch # https://github.com/systemd/systemd/issues/33392 + # systemd tries to link the systemd-ssh-proxy ssh config snippet with tmpfiles + # if the install prefix is not /usr, but that does not work for us + # because we include the config snippet manually + ./0019-meson-Don-t-link-ssh-dropins.patch + ./0020-install-unit_file_exists_full-follow-symlinks.patch ] ++ lib.optionals (stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isGnu) [ - ./0019-timesyncd-disable-NSCD-when-DNSSEC-validation-is-dis.patch + ./0021-timesyncd-disable-NSCD-when-DNSSEC-validation-is-dis.patch ] ++ lib.optionals stdenv.hostPlatform.isMusl ( let @@ -492,8 +497,7 @@ stdenv.mkDerivation (finalAttrs: { (lib.mesonOption "umount-path" "${lib.getOutput "mount" util-linux}/bin/umount") # SSH - # Disabled for now until someone makes this work. - (lib.mesonOption "sshconfdir" "no") + (lib.mesonOption "sshconfdir" "") (lib.mesonOption "sshdconfdir" "no") # Features