nixos/sshd: add enableRecommendedAlgorithms option

Prior to this commit, NixOS enabled a set of curated algorithms.

This commit allows users to opt-out of this curation, and instead use
the upstream algorithms. This also allows users to set
Ciphers/KexAlgorithms/Macs themselves without lib.mkForce (and thus
wield NixOS modules to build the list).

Tests have been added to ensure test this new option works.
This commit is contained in:
Tom Fitzhenry
2026-01-07 20:50:50 +11:00
parent 23d7be2248
commit 41c383338e
3 changed files with 134 additions and 38 deletions

View File

@@ -342,6 +342,8 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.
- `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.
- `services.openssh.enableRecommendedAlgorithms` has been added to allow users to opt out of NixOS's curated set of recommended algorithms. This set to true by default, and thus is not a breaking change. Users may want to set this to false if they prefer upstream's default algorithms. See <https://github.com/NixOS/nixpkgs/pull/471330>.
- IPVLAN interfaces can now be configured through the `networking.ipvlans` option in the networking module.
- `services.caddy` now supports setting `httpPort` and `httpsPort` and opening them in the firewall via `openFirewall`.

View File

@@ -412,6 +412,16 @@ in
'';
};
enableRecommendedAlgorithms = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Use algorithms curated and recommended by NixOS.
Set to false to use upstream's default algorithms.
'';
};
authorizedKeysFiles = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
@@ -571,37 +581,64 @@ in
};
KexAlgorithms = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = [
"mlkem768x25519-sha256"
"sntrup761x25519-sha512"
"sntrup761x25519-sha512@openssh.com"
"curve25519-sha256"
"curve25519-sha256@libssh.org"
"diffie-hellman-group-exchange-sha256"
];
default =
if config.services.openssh.enableRecommendedAlgorithms then
[
"mlkem768x25519-sha256"
"sntrup761x25519-sha512"
"sntrup761x25519-sha512@openssh.com"
"curve25519-sha256"
"curve25519-sha256@libssh.org"
"diffie-hellman-group-exchange-sha256"
]
else
null;
defaultText = ''
if config.services.openssh.enableRecommendedAlgorithms then
[
"mlkem768x25519-sha256"
"sntrup761x25519-sha512"
"sntrup761x25519-sha512@openssh.com"
"curve25519-sha256"
"curve25519-sha256@libssh.org"
"diffie-hellman-group-exchange-sha256"
]
else
null;
'';
description = ''
Allowed key exchange algorithms
Uses the lower bound recommended in both
<https://stribika.github.io/2015/01/04/secure-secure-shell.html>
and
<https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
Defaults to a curated set of algorithms.
Set enableRecommendedAlgorithms to false to use upstream's defaults.
'';
};
Macs = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = [
"hmac-sha2-512-etm@openssh.com"
"hmac-sha2-256-etm@openssh.com"
"umac-128-etm@openssh.com"
];
default =
if config.services.openssh.enableRecommendedAlgorithms then
[
"hmac-sha2-512-etm@openssh.com"
"hmac-sha2-256-etm@openssh.com"
"umac-128-etm@openssh.com"
]
else
null;
defaultText = ''
if config.services.openssh.enableRecommendedAlgorithms then
[
"hmac-sha2-512-etm@openssh.com"
"hmac-sha2-256-etm@openssh.com"
"umac-128-etm@openssh.com"
]
else
null;
'';
description = ''
Allowed MACs
Defaults to recommended settings from both
<https://stribika.github.io/2015/01/04/secure-secure-shell.html>
and
<https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
Defaults to a curated set of algorithms.
Set enableRecommendedAlgorithms to false to use upstream's defaults.
'';
};
StrictModes = lib.mkOption {
@@ -613,21 +650,36 @@ in
};
Ciphers = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = [
"chacha20-poly1305@openssh.com"
"aes256-gcm@openssh.com"
"aes128-gcm@openssh.com"
"aes256-ctr"
"aes192-ctr"
"aes128-ctr"
];
default =
if config.services.openssh.enableRecommendedAlgorithms then
[
"chacha20-poly1305@openssh.com"
"aes256-gcm@openssh.com"
"aes128-gcm@openssh.com"
"aes256-ctr"
"aes192-ctr"
"aes128-ctr"
]
else
null;
defaultText = ''
if config.services.openssh.enableRecommendedAlgorithms then
[
"chacha20-poly1305@openssh.com"
"aes256-gcm@openssh.com"
"aes128-gcm@openssh.com"
"aes256-ctr"
"aes192-ctr"
"aes128-ctr"
]
else
null;
'';
description = ''
Allowed ciphers
Defaults to recommended settings from both
<https://stribika.github.io/2015/01/04/secure-secure-shell.html>
and
<https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
Defaults to a curated set of algorithms.
Set enableRecommendedAlgorithms to false to use upstream's defaults.
'';
};
AllowUsers = lib.mkOption {

View File

@@ -193,12 +193,32 @@ in
path = "/etc/ssh/ssh_host_ed25519_key";
}
];
# The NixOS-curated algorithms require OpenSSL, and so since this test is against an OpenSSH-without-OpenSSL, we have to use the default algorithms, which adapt to not having OpenSSL.
enableRecommendedAlgorithms = false;
};
users.users.root.openssh.authorizedKeys.keys = [
snakeOilEd25519PublicKey
];
};
server-default-algorithms =
{ ... }:
{
services.openssh = {
enable = true;
enableRecommendedAlgorithms = false;
};
users.users.root.openssh.authorizedKeys.keys = [
snakeOilEd25519PublicKey
];
};
server-null-algorithms =
{ ... }:
{
services.openssh = {
enable = true;
settings = {
# Since this test is against an OpenSSH-without-OpenSSL,
# we have to override NixOS's defaults ciphers (which require OpenSSL)
# and instead set these to null, which will mean OpenSSH uses its defaults.
# Expectedly, OpenSSH's defaults don't require OpenSSL when it's compiled
# without OpenSSL.
Ciphers = null;
KexAlgorithms = null;
Macs = null;
@@ -294,10 +314,12 @@ in
server.wait_for_unit("sshd", timeout=60)
server_allowed_users.wait_for_unit("sshd", timeout=60)
server_default_algorithms.wait_for_unit("sshd", timeout=60)
server_localhost_only.wait_for_unit("sshd", timeout=60)
server_match_rule.wait_for_unit("sshd", timeout=60)
server_no_openssl.wait_for_unit("sshd", timeout=60)
server_no_pam.wait_for_unit("sshd", timeout=60)
server_null_algorithms.wait_for_unit("sshd", timeout=60)
server_null_pam.wait_for_unit("sshd", timeout=60)
server_null_pam.fail("journalctl -u sshd.service | grep 'Unsupported option UsePAM'")
server_sftp.wait_for_unit("sshd", timeout=60)
@@ -402,6 +424,26 @@ in
timeout=30
)
with subtest("null-algorithms"):
client.succeed(
"cat ${snakeOilEd25519PrivateKey} > privkey.snakeoil"
)
client.succeed("chmod 600 privkey.snakeoil")
client.succeed(
"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server-null-algorithms true",
timeout=30
)
with subtest("no-openssl"):
client.succeed(
"cat ${snakeOilEd25519PrivateKey} > privkey.snakeoil"
)
client.succeed("chmod 600 privkey.snakeoil")
client.succeed(
"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server-default-algorithms true",
timeout=30
)
with subtest("no-pam"):
client.succeed(
"cat ${snakeOilPrivateKey} > privkey.snakeoil"