1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 19:46:05 +01:00

ssh: remove top level options

This commit is contained in:
Aguirre Matteo 2025-08-22 12:37:35 -03:00 committed by Austin Horstman
parent 59aabcd3db
commit 3882f88691
23 changed files with 370 additions and 226 deletions

View file

@ -194,14 +194,14 @@ let
}; };
serverAliveInterval = mkOption { serverAliveInterval = mkOption {
type = types.int; type = types.nullOr types.int;
default = 0; default = null;
description = "Set timeout in seconds after which response will be requested."; description = "Set timeout in seconds after which response will be requested.";
}; };
serverAliveCountMax = mkOption { serverAliveCountMax = mkOption {
type = types.ints.positive; type = types.nullOr types.ints.positive;
default = 3; default = null;
description = '' description = ''
Sets the number of server alive messages which may be sent Sets the number of server alive messages which may be sent
without SSH receiving any messages back from the server. without SSH receiving any messages back from the server.
@ -345,6 +345,65 @@ let
default = { }; default = { };
description = "Extra configuration options for the host."; description = "Extra configuration options for the host.";
}; };
addKeysToAgent = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
When enabled, a private key that is used during authentication will be
added to ssh-agent if it is running (with confirmation enabled if
set to 'confirm'). The argument must be 'no' (the default), 'yes', 'confirm'
(optionally followed by a time interval), 'ask' or a time interval (e.g. '1h').
'';
};
hashKnownHosts = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Indicates that
{manpage}`ssh(1)`
should hash host names and addresses when they are added to
the known hosts file.
'';
};
userKnownHostsFile = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Specifies one or more files to use for the user host key
database, separated by whitespace. The default is
{file}`~/.ssh/known_hosts`.
'';
};
controlMaster = mkOption {
default = null;
type = types.nullOr (
types.enum [
"yes"
"no"
"ask"
"auto"
"autoask"
]
);
description = "Configure sharing of multiple sessions over a single network connection.";
};
controlPath = mkOption {
type = types.nullOr types.str;
default = null;
description = "Specify path to the control socket used for connection sharing.";
};
controlPersist = mkOption {
type = types.nullOr types.str;
default = null;
example = "10m";
description = "Whether control socket should remain open in the background.";
};
}; };
# config.host = mkDefault dagName; # config.host = mkDefault dagName;
@ -368,12 +427,24 @@ let
++ optional (cf.addressFamily != null) " AddressFamily ${cf.addressFamily}" ++ optional (cf.addressFamily != null) " AddressFamily ${cf.addressFamily}"
++ optional (cf.sendEnv != [ ]) " SendEnv ${unwords cf.sendEnv}" ++ optional (cf.sendEnv != [ ]) " SendEnv ${unwords cf.sendEnv}"
++ optional (cf.setEnv != { }) " SetEnv ${mkSetEnvStr cf.setEnv}" ++ optional (cf.setEnv != { }) " SetEnv ${mkSetEnvStr cf.setEnv}"
++ optional (cf.serverAliveInterval != 0) " ServerAliveInterval ${toString cf.serverAliveInterval}" ++ optional (
++ optional (cf.serverAliveCountMax != 3) " ServerAliveCountMax ${toString cf.serverAliveCountMax}" cf.serverAliveInterval != null
) " ServerAliveInterval ${toString cf.serverAliveInterval}"
++ optional (
cf.serverAliveCountMax != null
) " ServerAliveCountMax ${toString cf.serverAliveCountMax}"
++ optional (cf.compression != null) " Compression ${lib.hm.booleans.yesNo cf.compression}" ++ optional (cf.compression != null) " Compression ${lib.hm.booleans.yesNo cf.compression}"
++ optional (!cf.checkHostIP) " CheckHostIP no" ++ optional (!cf.checkHostIP) " CheckHostIP no"
++ optional (cf.proxyCommand != null) " ProxyCommand ${cf.proxyCommand}" ++ optional (cf.proxyCommand != null) " ProxyCommand ${cf.proxyCommand}"
++ optional (cf.proxyJump != null) " ProxyJump ${cf.proxyJump}" ++ optional (cf.proxyJump != null) " ProxyJump ${cf.proxyJump}"
++ optional (cf.addKeysToAgent != null) " AddKeysToAgent ${cf.addKeysToAgent}"
++ optional (
cf.hashKnownHosts != null
) " HashKnownHosts ${lib.hm.booleans.yesNo cf.hashKnownHosts}"
++ optional (cf.userKnownHostsFile != null) " UserKnownHostsFile ${cf.userKnownHostsFile}"
++ optional (cf.controlMaster != null) " ControlMaster ${cf.controlMaster}"
++ optional (cf.controlPath != null) " ControlPath ${cf.controlPath}"
++ optional (cf.controlPersist != null) " ControlPersist ${cf.controlPersist}"
++ map (file: " IdentityFile ${file}") cf.identityFile ++ map (file: " IdentityFile ${file}") cf.identityFile
++ map (file: " IdentityAgent ${file}") cf.identityAgent ++ map (file: " IdentityAgent ${file}") cf.identityAgent
++ map (file: " CertificateFile ${file}") cf.certificateFile ++ map (file: " CertificateFile ${file}") cf.certificateFile
@ -387,6 +458,35 @@ in
{ {
meta.maintainers = [ lib.maintainers.rycee ]; meta.maintainers = [ lib.maintainers.rycee ];
imports =
let
oldPrefix = [
"programs"
"ssh"
];
newPrefix = [
"programs"
"ssh"
"matchBlocks"
"*"
];
renamedOptions = [
"forwardAgent"
"addKeysToAgent"
"compression"
"serverAliveInterval"
"serverAliveCountMax"
"hashKnownHosts"
"userKnownHostsFile"
"controlMaster"
"controlPath"
"controlPersist"
];
in
lib.hm.deprecations.mkSettingsRenamedOptionModules oldPrefix newPrefix {
transform = x: x;
} renamedOptions;
options.programs.ssh = { options.programs.ssh = {
enable = lib.mkEnableOption "SSH client configuration"; enable = lib.mkEnableOption "SSH client configuration";
@ -396,101 +496,6 @@ in
extraDescription = "By default, the client provided by your system is used."; extraDescription = "By default, the client provided by your system is used.";
}; };
forwardAgent = mkOption {
default = false;
type = types.bool;
description = ''
Whether the connection to the authentication agent (if any)
will be forwarded to the remote machine.
'';
};
addKeysToAgent = mkOption {
type = types.str;
default = "no";
description = ''
When enabled, a private key that is used during authentication will be
added to ssh-agent if it is running (with confirmation enabled if
set to 'confirm'). The argument must be 'no' (the default), 'yes', 'confirm'
(optionally followed by a time interval), 'ask' or a time interval (e.g. '1h').
'';
};
compression = mkOption {
default = false;
type = types.bool;
description = "Specifies whether to use compression.";
};
serverAliveInterval = mkOption {
type = types.int;
default = 0;
description = ''
Set default timeout in seconds after which response will be requested.
'';
};
serverAliveCountMax = mkOption {
type = types.ints.positive;
default = 3;
description = ''
Sets the default number of server alive messages which may be
sent without SSH receiving any messages back from the server.
'';
};
hashKnownHosts = mkOption {
default = false;
type = types.bool;
description = ''
Indicates that
{manpage}`ssh(1)`
should hash host names and addresses when they are added to
the known hosts file.
'';
};
userKnownHostsFile = mkOption {
type = types.str;
default = "~/.ssh/known_hosts";
description = ''
Specifies one or more files to use for the user host key
database, separated by whitespace. The default is
{file}`~/.ssh/known_hosts`.
'';
};
controlMaster = mkOption {
default = "no";
type = types.enum [
"yes"
"no"
"ask"
"auto"
"autoask"
];
description = ''
Configure sharing of multiple sessions over a single network connection.
'';
};
controlPath = mkOption {
type = types.str;
default = "~/.ssh/master-%r@%n:%p";
description = ''
Specify path to the control socket used for connection sharing.
'';
};
controlPersist = mkOption {
type = types.str;
default = "no";
example = "10m";
description = ''
Whether control socket should remain open in the background.
'';
};
extraConfig = mkOption { extraConfig = mkOption {
type = types.lines; type = types.lines;
default = ""; default = "";
@ -546,76 +551,108 @@ in
for more information. for more information.
''; '';
}; };
};
config = lib.mkIf cfg.enable { enableDefaultConfig = mkOption {
assertions = [ type = types.bool;
{ default = true;
assertion = example = false;
let description = ''
# `builtins.any`/`lib.lists.any` does not return `true` if there are no elements. Whether to enable or not the old default config values.
any' = pred: items: if items == [ ] then true else lib.any pred items; This option will become deprecated in the future.
# Check that if `entry.address` is defined, and is a path, that `entry.port` has not
# been defined.
noPathWithPort = entry: entry.address != null && isPath entry.address -> entry.port == null;
checkDynamic = block: any' noPathWithPort block.dynamicForwards;
checkBindAndHost = fwd: noPathWithPort fwd.bind && noPathWithPort fwd.host;
checkLocal = block: any' checkBindAndHost block.localForwards;
checkRemote = block: any' checkBindAndHost block.remoteForwards;
checkMatchBlock =
block:
lib.all (fn: fn block) [
checkLocal
checkRemote
checkDynamic
];
in
any' checkMatchBlock (map (block: block.data) (builtins.attrValues cfg.matchBlocks));
message = "Forwarded paths cannot have ports.";
}
];
home.packages = optional (cfg.package != null) cfg.package;
home.file.".ssh/config".text =
let
sortedMatchBlocks = lib.hm.dag.topoSort cfg.matchBlocks;
sortedMatchBlocksStr = builtins.toJSON sortedMatchBlocks;
matchBlocks =
if sortedMatchBlocks ? result then
sortedMatchBlocks.result
else
abort "Dependency cycle in SSH match blocks: ${sortedMatchBlocksStr}";
in
''
${concatStringsSep "\n" (
(mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides)
++ (optional (cfg.includes != [ ]) ''
Include ${concatStringsSep " " cfg.includes}
'')
++ (map (block: matchBlockStr block.name block.data) matchBlocks)
)}
Host *
ForwardAgent ${lib.hm.booleans.yesNo cfg.forwardAgent}
AddKeysToAgent ${cfg.addKeysToAgent}
Compression ${lib.hm.booleans.yesNo cfg.compression}
ServerAliveInterval ${toString cfg.serverAliveInterval}
ServerAliveCountMax ${toString cfg.serverAliveCountMax}
HashKnownHosts ${lib.hm.booleans.yesNo cfg.hashKnownHosts}
UserKnownHostsFile ${cfg.userKnownHostsFile}
ControlMaster ${cfg.controlMaster}
ControlPath ${cfg.controlPath}
ControlPersist ${cfg.controlPersist}
${lib.replaceStrings [ "\n" ] [ "\n " ] cfg.extraConfig}
''; '';
};
warnings =
mapAttrsToList
(n: v: ''
The SSH config match block `programs.ssh.matchBlocks.${n}` sets both of the host and match options.
The match option takes precedence.'')
(lib.filterAttrs (n: v: v.data.host != null && v.data.match != null) cfg.matchBlocks);
}; };
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
assertions = [
{
assertion =
let
# `builtins.any`/`lib.lists.any` does not return `true` if there are no elements.
any' = pred: items: if items == [ ] then true else lib.any pred items;
# Check that if `entry.address` is defined, and is a path, that `entry.port` has not
# been defined.
noPathWithPort = entry: entry.address != null && isPath entry.address -> entry.port == null;
checkDynamic = block: any' noPathWithPort block.dynamicForwards;
checkBindAndHost = fwd: noPathWithPort fwd.bind && noPathWithPort fwd.host;
checkLocal = block: any' checkBindAndHost block.localForwards;
checkRemote = block: any' checkBindAndHost block.remoteForwards;
checkMatchBlock =
block:
lib.all (fn: fn block) [
checkLocal
checkRemote
checkDynamic
];
in
any' checkMatchBlock (map (block: block.data) (builtins.attrValues cfg.matchBlocks));
message = "Forwarded paths cannot have ports.";
}
{
assertion = (cfg.extraConfig != "") -> (cfg.matchBlocks ? "*");
message = ''Cannot set `programs.ssh.extraConfig` if `programs.ssh.matchBlocks."*"` (default host config) is not declared.'';
}
];
home.packages = optional (cfg.package != null) cfg.package;
home.file.".ssh/config".text =
let
sortedMatchBlocks = lib.hm.dag.topoSort (lib.removeAttrs cfg.matchBlocks [ "*" ]);
sortedMatchBlocksStr = builtins.toJSON sortedMatchBlocks;
matchBlocks =
if sortedMatchBlocks ? result then
sortedMatchBlocks.result
else
abort "Dependency cycle in SSH match blocks: ${sortedMatchBlocksStr}";
defaultHostBlock = cfg.matchBlocks."*" or null;
in
''
${concatStringsSep "\n" (
(mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides)
++ (optional (cfg.includes != [ ]) ''
Include ${concatStringsSep " " cfg.includes}
'')
++ (map (block: matchBlockStr block.name block.data) matchBlocks)
)}
${if (defaultHostBlock != null) then (matchBlockStr "*" defaultHostBlock.data) else ""}
${lib.replaceStrings [ "\n" ] [ "\n " ] cfg.extraConfig}
'';
warnings =
mapAttrsToList
(n: v: ''
The SSH config match block `programs.ssh.matchBlocks.${n}` sets both of the host and match options.
The match option takes precedence.'')
(lib.filterAttrs (n: v: v.data.host != null && v.data.match != null) cfg.matchBlocks);
}
(lib.mkIf cfg.enableDefaultConfig {
warnings = [
''
`programs.ssh` default values will be removed in the future.
Consider setting `programs.ssh.enableDefaultConfig` to false,
and manually set the default values you want to keep at
`programs.ssh.matchBlocks."*"`.
''
];
programs.ssh.matchBlocks."*" = {
forwardAgent = lib.mkDefault false;
addKeysToAgent = lib.mkDefault "no";
compression = lib.mkDefault false;
serverAliveInterval = lib.mkDefault 0;
serverAliveCountMax = lib.mkDefault 3;
hashKnownHosts = lib.mkDefault false;
userKnownHostsFile = lib.mkDefault "~/.ssh/known_hosts";
controlMaster = lib.mkDefault "no";
controlPath = lib.mkDefault "~/.ssh/master-%r@%n:%p";
controlPersist = lib.mkDefault "no";
};
})
]
);
} }

View file

@ -1,18 +0,0 @@
{ config, lib, ... }:
{
config = {
programs.ssh = {
enable = true;
};
home.file.assertions.text = builtins.toJSON (
map (a: a.message) (lib.filter (a: !a.assertion) config.assertions)
);
nmt.script = ''
assertFileExists home-files/.ssh/config
assertFileContent home-files/.ssh/config ${./default-config-expected.conf}
assertFileContent home-files/assertions ${./no-assertions.json}
'';
};
}

View file

@ -1,9 +1,11 @@
{ {
ssh-defaults = ./default-config.nix; ssh-old-defaults = ./old-defaults.nix;
ssh-old-defaults-extra-config = ./old-defaults-extra-config.nix;
ssh-extra-config-no-default-host = ./extra-config-no-default-host.nix;
ssh-renamed-options = ./renamed-options.nix;
ssh-includes = ./includes.nix; ssh-includes = ./includes.nix;
ssh-match-blocks = ./match-blocks-attrs.nix; ssh-match-blocks = ./match-blocks-attrs.nix;
ssh-match-blocks-match-and-hosts = ./match-blocks-match-and-hosts.nix; ssh-match-blocks-match-and-hosts = ./match-blocks-match-and-hosts.nix;
ssh-forwards-dynamic-valid-bind-no-asserts = ./forwards-dynamic-valid-bind-no-asserts.nix; ssh-forwards-dynamic-valid-bind-no-asserts = ./forwards-dynamic-valid-bind-no-asserts.nix;
ssh-forwards-dynamic-bind-path-with-port-asserts = ./forwards-dynamic-bind-path-with-port-asserts.nix; ssh-forwards-dynamic-bind-path-with-port-asserts = ./forwards-dynamic-bind-path-with-port-asserts.nix;
ssh-forwards-local-bind-path-with-port-asserts = ./forwards-local-bind-path-with-port-asserts.nix; ssh-forwards-local-bind-path-with-port-asserts = ./forwards-local-bind-path-with-port-asserts.nix;

View file

@ -0,0 +1,16 @@
Host *
ForwardAgent no
ServerAliveInterval 0
ServerAliveCountMax 3
Compression no
AddKeysToAgent no
HashKnownHosts no
UserKnownHostsFile ~/.ssh/known_hosts
ControlMaster no
ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist no
MyExtraOption no
AnotherOption 3

View file

@ -0,0 +1,14 @@
{
programs.ssh = {
enable = true;
enableDefaultConfig = false;
extraConfig = ''
MyExtraOption no
AnotherOption 3
'';
};
test.asserts.assertions.expected = [
''Cannot set `programs.ssh.extraConfig` if `programs.ssh.matchBlocks."*"` (default host config) is not declared.''
];
}

View file

@ -2,6 +2,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
dynamicBindPathWithPort = { dynamicBindPathWithPort = {
dynamicForwards = [ dynamicForwards = [

View file

@ -3,16 +3,5 @@ Host dynamicBindAddressWithPort
Host dynamicBindPathNoPort Host dynamicBindPathNoPort
DynamicForward /run/user/1000/gnupg/S.gpg-agent.extra DynamicForward /run/user/1000/gnupg/S.gpg-agent.extra
Host *
ForwardAgent no
AddKeysToAgent no
Compression no
ServerAliveInterval 0
ServerAliveCountMax 3
HashKnownHosts no
UserKnownHostsFile ~/.ssh/known_hosts
ControlMaster no
ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist no

View file

@ -3,6 +3,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
dynamicBindPathNoPort = { dynamicBindPathNoPort = {
dynamicForwards = [ dynamicForwards = [

View file

@ -2,6 +2,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
localBindPathWithPort = { localBindPathWithPort = {
localForwards = [ localForwards = [

View file

@ -2,6 +2,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
localHostPathWithPort = { localHostPathWithPort = {
localForwards = [ localForwards = [

View file

@ -2,6 +2,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
remoteBindPathWithPort = { remoteBindPathWithPort = {
remoteForwards = [ remoteForwards = [

View file

@ -2,6 +2,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
remoteHostPathWithPort = { remoteHostPathWithPort = {
remoteForwards = [ remoteForwards = [

View file

@ -7,6 +7,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
includes = [ includes = [
"config.d/*" "config.d/*"
"other/dir" "other/dir"

View file

@ -16,16 +16,5 @@ Host xyz
Host ordered Host ordered
Port 1 Port 1
Host *
ForwardAgent no
AddKeysToAgent no
Compression no
ServerAliveInterval 0
ServerAliveCountMax 3
HashKnownHosts no
UserKnownHostsFile ~/.ssh/known_hosts
ControlMaster no
ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist no

View file

@ -3,6 +3,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
abc = { abc = {
identityFile = null; identityFile = null;

View file

@ -5,16 +5,5 @@ Host abc
Match host xyz canonical Match host xyz canonical
Port 2223 Port 2223
Host *
ForwardAgent no
AddKeysToAgent no
Compression no
ServerAliveInterval 0
ServerAliveCountMax 3
HashKnownHosts no
UserKnownHostsFile ~/.ssh/known_hosts
ControlMaster no
ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist no

View file

@ -3,6 +3,7 @@
config = { config = {
programs.ssh = { programs.ssh = {
enable = true; enable = true;
enableDefaultConfig = false;
matchBlocks = { matchBlocks = {
abc = { abc = {
port = 2222; port = 2222;

View file

@ -2,14 +2,13 @@
Host * Host *
ForwardAgent no ForwardAgent no
AddKeysToAgent no
Compression no
ServerAliveInterval 0 ServerAliveInterval 0
ServerAliveCountMax 3 ServerAliveCountMax 3
Compression no
AddKeysToAgent no
HashKnownHosts no HashKnownHosts no
UserKnownHostsFile ~/.ssh/known_hosts UserKnownHostsFile ~/.ssh/known_hosts
ControlMaster no ControlMaster no
ControlPath ~/.ssh/master-%r@%n:%p ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist no ControlPersist no

View file

@ -0,0 +1,16 @@
Host *
ForwardAgent no
ServerAliveInterval 0
ServerAliveCountMax 3
Compression no
AddKeysToAgent no
HashKnownHosts no
UserKnownHostsFile ~/.ssh/known_hosts
ControlMaster no
ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist no
MyExtraOption no
AnotherOption 3

View file

@ -0,0 +1,24 @@
{
programs.ssh = {
enable = true;
extraConfig = ''
MyExtraOption no
AnotherOption 3
'';
};
test.asserts.warnings.expected = [
''
`programs.ssh` default values will be removed in the future.
Consider setting `programs.ssh.enableDefaultConfig` to false,
and manually set the default values you want to keep at
`programs.ssh.matchBlocks."*"`.
''
];
nmt.script = ''
assertFileExists home-files/.ssh/config
assertFileContent home-files/.ssh/config \
${./old-defaults-extra-config-expected.conf}
'';
}

View file

@ -0,0 +1,18 @@
{
programs.ssh.enable = true;
test.asserts.warnings.expected = [
''
`programs.ssh` default values will be removed in the future.
Consider setting `programs.ssh.enableDefaultConfig` to false,
and manually set the default values you want to keep at
`programs.ssh.matchBlocks."*"`.
''
];
nmt.script = ''
assertFileExists home-files/.ssh/config
assertFileContent home-files/.ssh/config \
${./old-defaults-expected.conf}
'';
}

View file

@ -0,0 +1,14 @@
Host *
ForwardAgent yes
ServerAliveInterval 1
ServerAliveCountMax 2
Compression yes
AddKeysToAgent yes
HashKnownHosts yes
UserKnownHostsFile ~/.ssh/my_known_hosts
ControlMaster yes
ControlPath ~/.ssh/myfile-%r@%n:%p
ControlPersist 10m

View file

@ -0,0 +1,46 @@
{ lib, options, ... }:
{
programs.ssh = {
enable = true;
enableDefaultConfig = false;
forwardAgent = true;
addKeysToAgent = "yes";
compression = true;
serverAliveInterval = 1;
serverAliveCountMax = 2;
hashKnownHosts = true;
userKnownHostsFile = "~/.ssh/my_known_hosts";
controlMaster = "yes";
controlPath = "~/.ssh/myfile-%r@%n:%p";
controlPersist = "10m";
};
test.asserts.warnings.expected =
let
renamedOptions = [
"controlPersist"
"controlPath"
"controlMaster"
"userKnownHostsFile"
"hashKnownHosts"
"serverAliveCountMax"
"serverAliveInterval"
"compression"
"addKeysToAgent"
"forwardAgent"
];
in
map (
o:
"The option `programs.ssh.${o}' defined in ${
lib.showFiles options.programs.ssh.${o}.files
} has been renamed to `programs.ssh.matchBlocks.*.${o}'."
) renamedOptions;
nmt.script = ''
assertFileExists home-files/.ssh/config
assertFileContent home-files/.ssh/config \
${./renamed-options-expected.conf}
'';
}