Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/manpage-urls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a proper issue about this yet, but systemd itself would also support passing credentials via kernel params which could be very useful for VMs. But that doesn't work with our current systemd-in-stage-1 implementation. @ElvishJerricco wrote a reproducer for that in https://gist.github.com/ElvishJerricco/dca95eb4ea9fc410bd525c3b15b68fdd

- 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.
Expand Down
14 changes: 14 additions & 0 deletions nixos/modules/programs/ssh.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

''}

# Generated options from other settings
Host *
GlobalKnownHostsFile ${builtins.concatStringsSep " " knownHostsFiles}
Expand Down
148 changes: 82 additions & 66 deletions nixos/modules/services/networking/ssh/sshd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://linux.die.net/man/8/sshd:

-i Specifies that sshd is being run from inetd(8). sshd is normally not run from inetd because it needs to generate the server key before it can respond to the client, and this may take tens of seconds. Clients would have to wait too long if the key was regenerated every time. However, with small key sizes (e.g. 512) using sshd from inetd may be feasible.

This documentation makes me confused. I'm assuming socket activation is inetd-like, and this is the flag that's different. However the rest (about waiting too long, generating keys) is odd.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also confused by this, not sure if host keys are meant?
Since those are generated a single time

"-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;
Expand Down
7 changes: 6 additions & 1 deletion nixos/modules/system/boot/systemd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemd-ssh-generator only runs the thing it does when it can find sshd in PATH, I should probably add a comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, what would be the alternative for systemd-ssh-generator to detect if it should enable the thing(tm)?

);
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
Expand Down
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 0 additions & 3 deletions nixos/tests/openssh.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""'
)
Expand All @@ -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")
Expand Down
76 changes: 76 additions & 0 deletions nixos/tests/systemd-ssh-proxy.nix
Original file line number Diff line number Diff line change
@@ -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
Comment on lines 12 to 13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐍🪔

Wonderful.

;
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}\"";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this worked example of how to do this. One of the huge benefits of tests is just showing how to do things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have been nice to avoid using an ISO for this. That's a very large build to be pushing to the cache that no one will ever use (yes, Hydra pushes these intermediate builds).

Also, this is technically nested virtualisation (double nested if the builder itself is a VM), and I'm not sure if we can take that as a given on the hardware we use for Hydra builders?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we can test this functionality with nixos-containers instead. That would avoid both problems. I'll give it a look.

};
};
};

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")
'';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Marie Ramlow <me@nycode.dev>
Date: Sun, 24 Nov 2024 20:04:35 +0100
Subject: [PATCH] meson: Don't link ssh dropins

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit introducing this patch says:

systemd tries to link the ssh config dropins by default with tmpfiles to /usr, that is not possible, so we include the snippet manually.

---
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

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
From 7be486fb25dc4ea212cb17f6a3f4a434a557b0d9 Mon Sep 17 00:00:00 2001
From: Marie Ramlow <me@nycode.dev>
Date: Fri, 10 Jan 2025 15:51:33 +0100
Subject: [PATCH] install: unit_file_exists_full: follow symlinks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit introducing this patch says:

systemd doesn't follow symlinks when checking for a packaged sshd@.service unit

---
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

Loading