mirror of
https://github.com/nix-community/home-manager.git
synced 2026-06-05 21:02:51 +00:00
ssh: add RFC 42 settings option
Add programs.ssh.settings as a freeform DAG for OpenSSH client configuration blocks. Render Host and Match blocks directly from the new settings option while preserving ordering support. Render known ssh_config comma-list directives from Nix lists as single comma-separated lines and known space-list directives as single whitespace-separated lines. This keeps directives like KexAlgorithms, Ciphers, MACs, HostKeyAlgorithms, ProxyJump, SendEnv, GlobalKnownHostsFile, and PermitRemoteOpen from being emitted as duplicate directives where OpenSSH may only use the first value. Migrate legacy matchBlocks into settings, keep root SSH option redirects pointed at the new option names, and hide the deprecated matchBlocks option from generated docs. Update SSH tests, docs references, and news coverage for the new option.
This commit is contained in:
@@ -4,7 +4,7 @@ Overall the basic option types are the same in Home Manager as NixOS. A
|
||||
few Home Manager options, however, make use of custom types that are
|
||||
worth describing in more detail. These are the option types `dagOf` and
|
||||
`gvariant` that are used, for example, by
|
||||
[programs.ssh.matchBlocks](#opt-programs.ssh.matchBlocks) and [dconf.settings](#opt-dconf.settings).
|
||||
[programs.ssh.settings](#opt-programs.ssh.settings) and [dconf.settings](#opt-dconf.settings).
|
||||
|
||||
[]{#sec-option-types-dag}`hm.types.dagOf`
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ This release has the following notable changes:
|
||||
no longer packages compton, and instead packages the (mostly)
|
||||
compatible fork called picom.
|
||||
|
||||
- The list form of the [programs.ssh.matchBlocks](#opt-programs.ssh.matchBlocks) option has
|
||||
- The list form of the `programs.ssh.matchBlocks` option has
|
||||
been deprecated and configurations requiring match blocks in a
|
||||
defined order should switch to using DAG entries instead. For
|
||||
example, a configuration
|
||||
|
||||
10
modules/misc/news/2026/05/2026-05-15_18-12-18.nix
Normal file
10
modules/misc/news/2026/05/2026-05-15_18-12-18.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-05-15T18:12:18+00:00";
|
||||
condition = config.programs.ssh.enable;
|
||||
message = ''
|
||||
`programs.ssh` now supports RFC 42-style configuration through
|
||||
`programs.ssh.settings`. The existing `programs.ssh.matchBlocks`
|
||||
option is deprecated and automatically migrated to the new option.
|
||||
'';
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
options,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
@@ -11,6 +12,7 @@ let
|
||||
mapAttrsToList
|
||||
mkOption
|
||||
optional
|
||||
optionalString
|
||||
types
|
||||
;
|
||||
|
||||
@@ -20,13 +22,29 @@ let
|
||||
|
||||
addressPort =
|
||||
entry:
|
||||
if isPath entry.address then " ${entry.address}" else " [${entry.address}]:${toString entry.port}";
|
||||
|
||||
unwords = builtins.concatStringsSep " ";
|
||||
let
|
||||
address = entry.address or "localhost";
|
||||
port = entry.port or null;
|
||||
in
|
||||
(lib.findFirst (candidate: candidate.when)
|
||||
{
|
||||
out = " [${address}]:${toString port}";
|
||||
}
|
||||
[
|
||||
{
|
||||
when = address == null;
|
||||
out = " ${toString port}";
|
||||
}
|
||||
{
|
||||
when = address != null && isPath address;
|
||||
out = " ${address}";
|
||||
}
|
||||
]
|
||||
).out;
|
||||
|
||||
mkSetEnvStr =
|
||||
envStr:
|
||||
unwords (
|
||||
builtins.concatStringsSep " " (
|
||||
mapAttrsToList (name: value: ''${name}="${lib.escape [ ''"'' "\\" ] (toString value)}"'') envStr
|
||||
);
|
||||
|
||||
@@ -320,7 +338,11 @@ let
|
||||
extraOptions = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
description = "Extra configuration options for the host.";
|
||||
visible = false;
|
||||
description = ''
|
||||
Deprecated extra configuration options for this host. Use
|
||||
{option}`programs.ssh.settings` instead.
|
||||
'';
|
||||
};
|
||||
|
||||
addKeysToAgent = mkOption {
|
||||
@@ -398,62 +420,148 @@ let
|
||||
# config.host = mkDefault dagName;
|
||||
};
|
||||
|
||||
renderValue = value: if lib.isBool value then lib.hm.booleans.yesNo value else toString value;
|
||||
|
||||
renderValues = sep: values: concatStringsSep sep (map renderValue (lib.toList values));
|
||||
|
||||
renderForward =
|
||||
f: if lib.isAttrs f then addressPort f.bind + addressPort f.host else " ${renderValue f}";
|
||||
|
||||
renderDynamicForward = f: if lib.isAttrs f then addressPort f else " ${renderValue f}";
|
||||
|
||||
renderDuplicateDirective =
|
||||
indent: name: renderItem: values:
|
||||
concatStringsSep "\n" (map (value: "${indent}${name}${renderItem value}") (lib.toList values));
|
||||
|
||||
# Per ssh_config(5), the first obtained value for most parameters wins.
|
||||
# These directives take comma/space-separated lists, so rendering Nix lists as
|
||||
# duplicate directives can silently ignore values after the first line.
|
||||
# Reference: https://man.openbsd.org/ssh_config
|
||||
commaListDirectives = [
|
||||
"CASignatureAlgorithms"
|
||||
"Ciphers"
|
||||
"HostbasedAcceptedAlgorithms"
|
||||
"HostbasedKeyTypes"
|
||||
"HostKeyAlgorithms"
|
||||
"IgnoreUnknown"
|
||||
"KbdInteractiveDevices"
|
||||
"KexAlgorithms"
|
||||
"MACs"
|
||||
"PreferredAuthentications"
|
||||
"ProxyJump"
|
||||
"PubkeyAcceptedAlgorithms"
|
||||
"PubkeyAcceptedKeyTypes"
|
||||
];
|
||||
|
||||
spaceListDirectives = [
|
||||
"CanonicalDomains"
|
||||
"CanonicalizePermittedCNAMEs"
|
||||
"ChannelTimeout"
|
||||
"GlobalKnownHostsFile"
|
||||
"PermitRemoteOpen"
|
||||
"SendEnv"
|
||||
"UserKnownHostsFile"
|
||||
];
|
||||
|
||||
directiveRenderers =
|
||||
indent:
|
||||
lib.genAttrs commaListDirectives (name: value: "${indent}${name} ${renderValues "," value}")
|
||||
// lib.genAttrs spaceListDirectives (name: value: "${indent}${name} ${renderValues " " value}")
|
||||
// {
|
||||
SetEnv = value: "${indent}SetEnv ${mkSetEnvStr value}";
|
||||
LocalForward = renderDuplicateDirective indent "LocalForward" renderForward;
|
||||
RemoteForward = renderDuplicateDirective indent "RemoteForward" renderForward;
|
||||
DynamicForward = renderDuplicateDirective indent "DynamicForward" renderDynamicForward;
|
||||
};
|
||||
|
||||
sshDirectiveStrWithIndent =
|
||||
indent: name: value:
|
||||
let
|
||||
renderDirective =
|
||||
(directiveRenderers indent).${name}
|
||||
or (values: renderDuplicateDirective indent name (v: " ${renderValue v}") values);
|
||||
in
|
||||
optionalString (value != null && value != [ ] && value != { }) (renderDirective value);
|
||||
|
||||
sshDirectiveStr = sshDirectiveStrWithIndent " ";
|
||||
|
||||
blockHeader =
|
||||
name:
|
||||
let
|
||||
isLiteralHeader = lib.any (prefix: lib.hasPrefix prefix name) [
|
||||
"Host "
|
||||
"Match "
|
||||
];
|
||||
in
|
||||
optionalString (!isLiteralHeader) "Host " + name;
|
||||
|
||||
matchBlockStr =
|
||||
key: cf:
|
||||
let
|
||||
header = cf.__hmSshBlockHeader or (blockHeader key);
|
||||
extraOptions = cf.__hmSshBlockExtraOptions or { };
|
||||
settings = lib.removeAttrs cf [
|
||||
"__hmSshBlockHeader"
|
||||
"__hmSshBlockExtraOptions"
|
||||
"extraOptions"
|
||||
];
|
||||
orderedNames =
|
||||
# IgnoreUnknown only applies to unknown options that appear after it.
|
||||
optional (builtins.hasAttr "IgnoreUnknown" settings) "IgnoreUnknown"
|
||||
++ builtins.filter (name: name != "IgnoreUnknown") (lib.attrNames settings);
|
||||
in
|
||||
concatStringsSep "\n" (
|
||||
let
|
||||
hostOrDagName = if cf.host != null then cf.host else key;
|
||||
matchHead = if cf.match != null then "Match ${cf.match}" else "Host ${hostOrDagName}";
|
||||
in
|
||||
[ "${matchHead}" ]
|
||||
++ optional (cf.port != null) " Port ${toString cf.port}"
|
||||
++ optional (cf.forwardAgent != null) " ForwardAgent ${lib.hm.booleans.yesNo cf.forwardAgent}"
|
||||
++ optional cf.forwardX11 " ForwardX11 yes"
|
||||
++ optional cf.forwardX11Trusted " ForwardX11Trusted yes"
|
||||
++ optional (
|
||||
cf.identitiesOnly != null
|
||||
) " IdentitiesOnly ${lib.hm.booleans.yesNo cf.identitiesOnly}"
|
||||
++ optional (cf.user != null) " User ${cf.user}"
|
||||
++ optional (cf.hostname != null) " HostName ${cf.hostname}"
|
||||
++ optional (cf.addressFamily != null) " AddressFamily ${cf.addressFamily}"
|
||||
++ optional (cf.sendEnv != [ ]) " SendEnv ${unwords cf.sendEnv}"
|
||||
++ optional (cf.setEnv != { }) " SetEnv ${mkSetEnvStr cf.setEnv}"
|
||||
++ optional (
|
||||
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.checkHostIP) " CheckHostIP no"
|
||||
++ optional (cf.proxyCommand != null) " ProxyCommand ${cf.proxyCommand}"
|
||||
++ 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: " IdentityAgent ${file}") cf.identityAgent
|
||||
++ map (file: " CertificateFile ${file}") cf.certificateFile
|
||||
++ map (f: " LocalForward" + addressPort f.bind + addressPort f.host) cf.localForwards
|
||||
++ map (f: " RemoteForward" + addressPort f.bind + addressPort f.host) cf.remoteForwards
|
||||
++ map (f: " DynamicForward" + addressPort f) cf.dynamicForwards
|
||||
++ optional (
|
||||
cf.kexAlgorithms != null
|
||||
) " KexAlgorithms ${builtins.concatStringsSep "," cf.kexAlgorithms}"
|
||||
++ [
|
||||
(lib.generators.toKeyValue {
|
||||
[ header ]
|
||||
++ lib.filter (line: line != "") (map (name: sshDirectiveStr name settings.${name}) orderedNames)
|
||||
++ optional (extraOptions != { }) (
|
||||
lib.generators.toKeyValue {
|
||||
mkKeyValue = lib.generators.mkKeyValueDefault { } " ";
|
||||
listsAsDuplicateKeys = true;
|
||||
indent = " ";
|
||||
} cf.extraOptions)
|
||||
]
|
||||
} extraOptions
|
||||
)
|
||||
);
|
||||
|
||||
legacyBlockSettings =
|
||||
cf:
|
||||
let
|
||||
headers =
|
||||
optional (cf.match != null) "Match ${cf.match}" ++ optional (cf.host != null) "Host ${cf.host}";
|
||||
in
|
||||
lib.filterAttrs (_: v: v != null && v != [ ] && v != { }) {
|
||||
__hmSshBlockHeader = lib.head (headers ++ [ null ]);
|
||||
Port = cf.port;
|
||||
ForwardAgent = cf.forwardAgent;
|
||||
ForwardX11 = if cf.forwardX11 then true else null;
|
||||
ForwardX11Trusted = if cf.forwardX11Trusted then true else null;
|
||||
IdentitiesOnly = cf.identitiesOnly;
|
||||
IdentityFile = cf.identityFile;
|
||||
IdentityAgent = cf.identityAgent;
|
||||
User = cf.user;
|
||||
HostName = cf.hostname;
|
||||
ServerAliveInterval = cf.serverAliveInterval;
|
||||
ServerAliveCountMax = cf.serverAliveCountMax;
|
||||
SendEnv = cf.sendEnv;
|
||||
SetEnv = cf.setEnv;
|
||||
Compression = cf.compression;
|
||||
CheckHostIP = if cf.checkHostIP then null else false;
|
||||
ProxyCommand = cf.proxyCommand;
|
||||
ProxyJump = cf.proxyJump;
|
||||
CertificateFile = cf.certificateFile;
|
||||
AddressFamily = cf.addressFamily;
|
||||
LocalForward = cf.localForwards;
|
||||
RemoteForward = cf.remoteForwards;
|
||||
DynamicForward = cf.dynamicForwards;
|
||||
AddKeysToAgent = cf.addKeysToAgent;
|
||||
HashKnownHosts = cf.hashKnownHosts;
|
||||
UserKnownHostsFile = cf.userKnownHostsFile;
|
||||
ControlMaster = cf.controlMaster;
|
||||
ControlPath = cf.controlPath;
|
||||
ControlPersist = cf.controlPersist;
|
||||
KexAlgorithms = cf.kexAlgorithms;
|
||||
__hmSshBlockExtraOptions = cf.extraOptions;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.rycee ];
|
||||
@@ -467,7 +575,7 @@ in
|
||||
newPrefix = [
|
||||
"programs"
|
||||
"ssh"
|
||||
"matchBlocks"
|
||||
"settings"
|
||||
"*"
|
||||
];
|
||||
renamedOptions = [
|
||||
@@ -482,9 +590,21 @@ in
|
||||
"controlPath"
|
||||
"controlPersist"
|
||||
];
|
||||
oldOptionNameToSetting = {
|
||||
forwardAgent = "ForwardAgent";
|
||||
addKeysToAgent = "AddKeysToAgent";
|
||||
compression = "Compression";
|
||||
serverAliveInterval = "ServerAliveInterval";
|
||||
serverAliveCountMax = "ServerAliveCountMax";
|
||||
hashKnownHosts = "HashKnownHosts";
|
||||
userKnownHostsFile = "UserKnownHostsFile";
|
||||
controlMaster = "ControlMaster";
|
||||
controlPath = "ControlPath";
|
||||
controlPersist = "ControlPersist";
|
||||
};
|
||||
in
|
||||
lib.hm.deprecations.mkSettingsRenamedOptionModules oldPrefix newPrefix {
|
||||
transform = x: x;
|
||||
transform = x: oldOptionNameToSetting.${x};
|
||||
} renamedOptions;
|
||||
|
||||
options.programs.ssh = {
|
||||
@@ -505,7 +625,7 @@ in
|
||||
};
|
||||
|
||||
extraOptionOverrides = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
type = types.attrsOf types.anything;
|
||||
default = { };
|
||||
description = ''
|
||||
Extra SSH configuration options that take precedence over any
|
||||
@@ -526,9 +646,64 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = lib.hm.types.dagOf (
|
||||
types.submodule {
|
||||
freeformType = types.attrsOf types.anything;
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
"github.com" = {
|
||||
HostName = "github.com";
|
||||
User = "git";
|
||||
IdentityFile = "~/.ssh/github";
|
||||
};
|
||||
|
||||
"Host *.example.org" = lib.hm.dag.entryBefore [ "github.com" ] {
|
||||
IdentityFile = "~/.ssh/example";
|
||||
LocalForward = [
|
||||
{
|
||||
bind.port = 8080;
|
||||
host.address = "10.0.0.13";
|
||||
host.port = 80;
|
||||
}
|
||||
"9000 10.0.0.2:90"
|
||||
];
|
||||
DynamicForward = "127.0.0.1:1080";
|
||||
};
|
||||
|
||||
"Match host *.corp exec \"test -f ~/.corp\"" = {
|
||||
ProxyJump = "bastion";
|
||||
RemoteForward = {
|
||||
bind.port = 8081;
|
||||
host.address = "10.0.0.14";
|
||||
host.port = 80;
|
||||
};
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
OpenSSH client configuration blocks written to
|
||||
{file}`~/.ssh/config`.
|
||||
|
||||
Attribute names are interpreted as `Host` patterns unless they
|
||||
start with `Host ` or `Match `, in which case they are written
|
||||
literally as block headers. If the order of rules matter then
|
||||
use the DAG functions to express the dependencies as shown in
|
||||
the example.
|
||||
|
||||
See
|
||||
{manpage}`ssh_config(5)`
|
||||
for more information.
|
||||
'';
|
||||
};
|
||||
|
||||
matchBlocks = mkOption {
|
||||
type = lib.hm.types.dagOf matchBlockModule;
|
||||
default = { };
|
||||
visible = false;
|
||||
example = literalExpression ''
|
||||
{
|
||||
"john.example.com" = {
|
||||
@@ -542,13 +717,7 @@ in
|
||||
};
|
||||
'';
|
||||
description = ''
|
||||
Specify per-host settings. Note, if the order of rules matter
|
||||
then use the DAG functions to express the dependencies as
|
||||
shown in the example.
|
||||
|
||||
See
|
||||
{manpage}`ssh_config(5)`
|
||||
for more information.
|
||||
Deprecated alias for {option}`programs.ssh.settings`.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -562,17 +731,17 @@ in
|
||||
For an equivalent, copy and paste the following
|
||||
code snippet in your config:
|
||||
|
||||
programs.ssh.matchBlocks."*" = {
|
||||
forwardAgent = false;
|
||||
addKeysToAgent = "no";
|
||||
compression = false;
|
||||
serverAliveInterval = 0;
|
||||
serverAliveCountMax = 3;
|
||||
hashKnownHosts = false;
|
||||
userKnownHostsFile = "~/.ssh/known_hosts";
|
||||
controlMaster = "no";
|
||||
controlPath = "~/.ssh/master-%r@%n:%p";
|
||||
controlPersist = "no";
|
||||
programs.ssh.settings."*" = {
|
||||
ForwardAgent = false;
|
||||
AddKeysToAgent = "no";
|
||||
Compression = false;
|
||||
ServerAliveInterval = 0;
|
||||
ServerAliveCountMax = 3;
|
||||
HashKnownHosts = false;
|
||||
UserKnownHostsFile = "~/.ssh/known_hosts";
|
||||
ControlMaster = "no";
|
||||
ControlPath = "~/.ssh/master-%r@%n:%p";
|
||||
ControlPersist = "no";
|
||||
};
|
||||
'';
|
||||
};
|
||||
@@ -581,19 +750,32 @@ in
|
||||
config = lib.mkIf cfg.enable (
|
||||
lib.mkMerge [
|
||||
{
|
||||
programs.ssh.settings = lib.mapAttrs (
|
||||
_name: entry:
|
||||
entry
|
||||
// {
|
||||
data = legacyBlockSettings entry.data;
|
||||
}
|
||||
) cfg.matchBlocks;
|
||||
|
||||
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;
|
||||
noPathWithPort =
|
||||
entry:
|
||||
let
|
||||
address = entry.address or null;
|
||||
port = entry.port or null;
|
||||
in
|
||||
address != null && isPath address -> port == null;
|
||||
checkForwardValues = pred: values: lib.all pred (lib.filter lib.isAttrs (lib.toList values));
|
||||
checkDynamic = block: checkForwardValues noPathWithPort (block.DynamicForward or [ ]);
|
||||
checkBindAndHost = fwd: noPathWithPort fwd.bind && noPathWithPort fwd.host;
|
||||
checkLocal = block: any' checkBindAndHost block.localForwards;
|
||||
checkRemote = block: any' checkBindAndHost block.remoteForwards;
|
||||
checkLocal = block: checkForwardValues checkBindAndHost (block.LocalForward or [ ]);
|
||||
checkRemote = block: checkForwardValues checkBindAndHost (block.RemoteForward or [ ]);
|
||||
checkMatchBlock =
|
||||
block:
|
||||
lib.all (fn: fn block) [
|
||||
@@ -602,12 +784,20 @@ in
|
||||
checkDynamic
|
||||
];
|
||||
in
|
||||
any' checkMatchBlock (map (block: block.data) (builtins.attrValues cfg.matchBlocks));
|
||||
lib.all checkMatchBlock (map (block: block.data) (builtins.attrValues cfg.settings));
|
||||
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.'';
|
||||
assertion = (cfg.extraConfig != "") -> (cfg.settings ? "*");
|
||||
message = ''Cannot set `programs.ssh.extraConfig` if `programs.ssh.settings."*"` (default host config) is not declared.'';
|
||||
}
|
||||
{
|
||||
assertion = lib.all (
|
||||
block: !(builtins.hasAttr "extraOptions" block.data) || block.data.extraOptions == { }
|
||||
) (builtins.attrValues cfg.settings);
|
||||
message = ''
|
||||
`programs.ssh.settings.*.extraOptions` defined in ${lib.showFiles options.programs.ssh.settings.files} is not supported. Move these OpenSSH options directly into `programs.ssh.settings.*` using upstream directive names.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
@@ -615,32 +805,39 @@ in
|
||||
|
||||
home.file.".ssh/config".text =
|
||||
let
|
||||
sortedMatchBlocks = lib.hm.dag.topoSort (lib.removeAttrs cfg.matchBlocks [ "*" ]);
|
||||
sortedMatchBlocks = lib.hm.dag.topoSort (lib.removeAttrs cfg.settings [ "*" ]);
|
||||
sortedMatchBlocksStr = builtins.toJSON sortedMatchBlocks;
|
||||
matchBlocks =
|
||||
sortedMatchBlocks.result or (abort "Dependency cycle in SSH match blocks: ${sortedMatchBlocksStr}");
|
||||
|
||||
defaultHostBlock = cfg.matchBlocks."*" or null;
|
||||
defaultHostBlock = cfg.settings."*" or null;
|
||||
globalConfig =
|
||||
(mapAttrsToList (sshDirectiveStrWithIndent "") cfg.extraOptionOverrides)
|
||||
++ optional (cfg.includes != [ ]) "Include ${concatStringsSep " " cfg.includes}";
|
||||
blockConfig =
|
||||
(map (block: matchBlockStr block.name block.data) matchBlocks)
|
||||
++ optional (defaultHostBlock != null) (matchBlockStr "*" defaultHostBlock.data);
|
||||
extraConfig = optional (cfg.extraConfig != "") (
|
||||
" " + lib.replaceStrings [ "\n" ] [ "\n " ] (lib.removeSuffix "\n" cfg.extraConfig)
|
||||
);
|
||||
sections =
|
||||
optional (globalConfig != [ ]) (concatStringsSep "\n" globalConfig) ++ blockConfig ++ extraConfig;
|
||||
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}
|
||||
'';
|
||||
optionalString (sections != [ ]) (concatStringsSep "\n\n" sections + "\n");
|
||||
|
||||
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);
|
||||
optional (cfg.matchBlocks != { }) ''
|
||||
`programs.ssh.matchBlocks` defined in ${lib.showFiles options.programs.ssh.matchBlocks.files} is deprecated. Use `programs.ssh.settings`.
|
||||
''
|
||||
++
|
||||
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)
|
||||
++ mapAttrsToList (n: _v: ''
|
||||
`programs.ssh.matchBlocks.${n}.extraOptions` defined in ${lib.showFiles options.programs.ssh.matchBlocks.files} is deprecated. Move these OpenSSH options to `programs.ssh.settings.${n}` using upstream directive names.
|
||||
'') (lib.filterAttrs (_n: v: v.data.extraOptions != { }) cfg.matchBlocks);
|
||||
}
|
||||
(lib.mkIf cfg.enableDefaultConfig {
|
||||
warnings = [
|
||||
@@ -648,21 +845,21 @@ in
|
||||
`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.settings."*"`.
|
||||
''
|
||||
];
|
||||
|
||||
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";
|
||||
programs.ssh.settings."*" = {
|
||||
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";
|
||||
};
|
||||
})
|
||||
]
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
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-extra-option-overrides = ./extra-option-overrides.nix;
|
||||
ssh-renamed-options = ./renamed-options.nix;
|
||||
ssh-includes = ./includes.nix;
|
||||
ssh-settings = ./settings.nix;
|
||||
ssh-settings-extra-options-assertion = ./settings-extra-options-assertion.nix;
|
||||
ssh-settings-raw-forwards = ./settings-raw-forwards.nix;
|
||||
ssh-match-blocks = ./match-blocks-attrs.nix;
|
||||
ssh-match-blocks-extra-options-duplicates = ./match-blocks-extra-options-duplicates.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-bind-path-with-port-asserts = ./forwards-dynamic-bind-path-with-port-asserts.nix;
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
''Cannot set `programs.ssh.extraConfig` if `programs.ssh.matchBlocks."*"` (default host config) is not declared.''
|
||||
''Cannot set `programs.ssh.extraConfig` if `programs.ssh.settings."*"` (default host config) is not declared.''
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
ForwardAgent no
|
||||
GlobalKnownHostsFile /etc/ssh/ssh_known_hosts ~/.ssh/global_known_hosts
|
||||
|
||||
Host space-list
|
||||
CanonicalDomains example.org corp.example
|
||||
ChannelTimeout session=5m direct-tcpip=30s
|
||||
GlobalKnownHostsFile ~/.ssh/known_hosts ~/.ssh/known_hosts2
|
||||
PermitRemoteOpen localhost:8080 example.org:443
|
||||
UserKnownHostsFile ~/.ssh/user_known_hosts ~/.ssh/user_known_hosts2
|
||||
42
tests/modules/programs/ssh/extra-option-overrides.nix
Normal file
42
tests/modules/programs/ssh/extra-option-overrides.nix
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
extraOptionOverrides = {
|
||||
ForwardAgent = false;
|
||||
GlobalKnownHostsFile = [
|
||||
"/etc/ssh/ssh_known_hosts"
|
||||
"~/.ssh/global_known_hosts"
|
||||
];
|
||||
};
|
||||
settings.space-list = {
|
||||
CanonicalDomains = [
|
||||
"example.org"
|
||||
"corp.example"
|
||||
];
|
||||
ChannelTimeout = [
|
||||
"session=5m"
|
||||
"direct-tcpip=30s"
|
||||
];
|
||||
GlobalKnownHostsFile = [
|
||||
"~/.ssh/known_hosts"
|
||||
"~/.ssh/known_hosts2"
|
||||
];
|
||||
UserKnownHostsFile = [
|
||||
"~/.ssh/user_known_hosts"
|
||||
"~/.ssh/user_known_hosts2"
|
||||
];
|
||||
PermitRemoteOpen = [
|
||||
"localhost:8080"
|
||||
"example.org:443"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.ssh/config
|
||||
assertFileContent \
|
||||
home-files/.ssh/config \
|
||||
${./extra-option-overrides-expected.conf}
|
||||
'';
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
matchBlocks = {
|
||||
settings = {
|
||||
dynamicBindPathWithPort = {
|
||||
dynamicForwards = [
|
||||
DynamicForward = [
|
||||
{
|
||||
# Error:
|
||||
address = "/run/user/1000/gnupg/S.gpg-agent.extra";
|
||||
|
||||
@@ -3,7 +3,3 @@ Host dynamicBindAddressWithPort
|
||||
|
||||
Host dynamicBindPathNoPort
|
||||
DynamicForward /run/user/1000/gnupg/S.gpg-agent.extra
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
matchBlocks = {
|
||||
settings = {
|
||||
dynamicBindPathNoPort = {
|
||||
dynamicForwards = [
|
||||
DynamicForward = [
|
||||
{
|
||||
# OK:
|
||||
address = "/run/user/1000/gnupg/S.gpg-agent.extra";
|
||||
@@ -15,7 +15,7 @@
|
||||
};
|
||||
|
||||
dynamicBindAddressWithPort = {
|
||||
dynamicForwards = [
|
||||
DynamicForward = [
|
||||
{
|
||||
# OK:
|
||||
address = "127.0.0.1";
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
matchBlocks = {
|
||||
settings = {
|
||||
localBindPathWithPort = {
|
||||
localForwards = [
|
||||
LocalForward = [
|
||||
{
|
||||
# OK:
|
||||
host.address = "127.0.0.1";
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
matchBlocks = {
|
||||
settings = {
|
||||
localHostPathWithPort = {
|
||||
localForwards = [
|
||||
LocalForward = [
|
||||
{
|
||||
# OK:
|
||||
bind.address = "127.0.0.1";
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
matchBlocks = {
|
||||
settings = {
|
||||
remoteBindPathWithPort = {
|
||||
remoteForwards = [
|
||||
RemoteForward = [
|
||||
{
|
||||
# OK:
|
||||
host.address = "127.0.0.1";
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
matchBlocks = {
|
||||
settings = {
|
||||
remoteHostPathWithPort = {
|
||||
remoteForwards = [
|
||||
RemoteForward = [
|
||||
{
|
||||
# OK:
|
||||
bind.address = "127.0.0.1";
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
Host * !github.com
|
||||
Port 516
|
||||
IdentityFile file1
|
||||
IdentityFile file2
|
||||
Port 516
|
||||
|
||||
Host abc
|
||||
ProxyJump jump-host
|
||||
|
||||
Host xyz
|
||||
SetEnv BAR="_bar_ 42" FOO="foo12"
|
||||
ServerAliveInterval 60
|
||||
ServerAliveCountMax 10
|
||||
DynamicForward [localhost]:2839
|
||||
IdentityFile file
|
||||
KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256
|
||||
LocalForward [localhost]:8080 [10.0.0.1]:80
|
||||
RemoteForward [localhost]:8081 [10.0.0.2]:80
|
||||
RemoteForward /run/user/1000/gnupg/S.gpg-agent.extra /run/user/1000/gnupg/S.gpg-agent
|
||||
DynamicForward [localhost]:2839
|
||||
KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,mlkem768x25519-sha256
|
||||
ServerAliveCountMax 10
|
||||
ServerAliveInterval 60
|
||||
SetEnv BAR="_bar_ 42" BAZ="with \" some \\ very \\\" fun \\\\ escapes" FOO="foo12"
|
||||
|
||||
Host ordered
|
||||
Port 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
{
|
||||
config = {
|
||||
programs.ssh = {
|
||||
@@ -43,6 +48,7 @@
|
||||
setEnv = {
|
||||
FOO = "foo12";
|
||||
BAR = "_bar_ 42";
|
||||
BAZ = ''with " some \ very \" fun \\ escapes'';
|
||||
};
|
||||
};
|
||||
|
||||
@@ -60,6 +66,12 @@
|
||||
map (a: a.message) (lib.filter (a: !a.assertion) config.assertions)
|
||||
);
|
||||
|
||||
test.asserts.warnings.expected = [
|
||||
''
|
||||
`programs.ssh.matchBlocks` defined in ${lib.showFiles options.programs.ssh.matchBlocks.files} is deprecated. Use `programs.ssh.settings`.
|
||||
''
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.ssh/config
|
||||
assertFileContent \
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Host legacy
|
||||
HostName example.org
|
||||
User typed-user
|
||||
ForwardAgent yes
|
||||
HostName extra.example.org
|
||||
User extra-user
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
{
|
||||
config = {
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
matchBlocks.legacy = {
|
||||
user = "typed-user";
|
||||
hostname = "example.org";
|
||||
extraOptions = {
|
||||
ForwardAgent = "yes";
|
||||
HostName = "extra.example.org";
|
||||
User = "extra-user";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
home.file.assertions.text = builtins.toJSON (
|
||||
map (a: a.message) (lib.filter (a: !a.assertion) config.assertions)
|
||||
);
|
||||
|
||||
test.asserts.warnings.expected = [
|
||||
''
|
||||
`programs.ssh.matchBlocks` defined in ${lib.showFiles options.programs.ssh.matchBlocks.files} is deprecated. Use `programs.ssh.settings`.
|
||||
''
|
||||
''
|
||||
`programs.ssh.matchBlocks.legacy.extraOptions` defined in ${lib.showFiles options.programs.ssh.matchBlocks.files} is deprecated. Move these OpenSSH options to `programs.ssh.settings.legacy` using upstream directive names.
|
||||
''
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.ssh/config
|
||||
assertFileContent \
|
||||
home-files/.ssh/config \
|
||||
${./match-blocks-extra-options-duplicates-expected.conf}
|
||||
assertFileContent home-files/assertions ${./no-assertions.json}
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,3 @@ Host abc
|
||||
|
||||
Match host xyz canonical
|
||||
Port 2223
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
{
|
||||
config = {
|
||||
programs.ssh = {
|
||||
@@ -24,6 +29,12 @@
|
||||
map (a: a.message) (lib.filter (a: !a.assertion) config.assertions)
|
||||
);
|
||||
|
||||
test.asserts.warnings.expected = [
|
||||
''
|
||||
`programs.ssh.matchBlocks` defined in ${lib.showFiles options.programs.ssh.matchBlocks.files} is deprecated. Use `programs.ssh.settings`.
|
||||
''
|
||||
];
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.ssh/config
|
||||
assertFileContent \
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
|
||||
|
||||
Host *
|
||||
ForwardAgent no
|
||||
ServerAliveInterval 0
|
||||
ServerAliveCountMax 3
|
||||
Compression no
|
||||
AddKeysToAgent no
|
||||
HashKnownHosts no
|
||||
UserKnownHostsFile ~/.ssh/known_hosts
|
||||
Compression no
|
||||
ControlMaster no
|
||||
ControlPath ~/.ssh/master-%r@%n:%p
|
||||
ControlPersist no
|
||||
|
||||
|
||||
ForwardAgent no
|
||||
HashKnownHosts no
|
||||
ServerAliveCountMax 3
|
||||
ServerAliveInterval 0
|
||||
UserKnownHostsFile ~/.ssh/known_hosts
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
|
||||
|
||||
Host *
|
||||
ForwardAgent no
|
||||
ServerAliveInterval 0
|
||||
ServerAliveCountMax 3
|
||||
Compression no
|
||||
AddKeysToAgent no
|
||||
HashKnownHosts no
|
||||
UserKnownHostsFile ~/.ssh/known_hosts
|
||||
Compression no
|
||||
ControlMaster no
|
||||
ControlPath ~/.ssh/master-%r@%n:%p
|
||||
ControlPersist no
|
||||
ForwardAgent no
|
||||
HashKnownHosts no
|
||||
ServerAliveCountMax 3
|
||||
ServerAliveInterval 0
|
||||
UserKnownHostsFile ~/.ssh/known_hosts
|
||||
|
||||
MyExtraOption no
|
||||
AnotherOption 3
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
`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.settings."*"`.
|
||||
''
|
||||
];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
`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.settings."*"`.
|
||||
''
|
||||
];
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
|
||||
|
||||
Host *
|
||||
ForwardAgent yes
|
||||
ServerAliveInterval 1
|
||||
ServerAliveCountMax 2
|
||||
Compression yes
|
||||
AddKeysToAgent yes
|
||||
HashKnownHosts yes
|
||||
UserKnownHostsFile ~/.ssh/my_known_hosts
|
||||
Compression yes
|
||||
ControlMaster yes
|
||||
ControlPath ~/.ssh/myfile-%r@%n:%p
|
||||
ControlPersist 10m
|
||||
|
||||
|
||||
ForwardAgent yes
|
||||
HashKnownHosts yes
|
||||
ServerAliveCountMax 2
|
||||
ServerAliveInterval 1
|
||||
UserKnownHostsFile ~/.ssh/my_known_hosts
|
||||
|
||||
@@ -18,7 +18,19 @@
|
||||
|
||||
test.asserts.warnings.expected =
|
||||
let
|
||||
renamedOptions = [
|
||||
renamedOptions = {
|
||||
forwardAgent = "ForwardAgent";
|
||||
addKeysToAgent = "AddKeysToAgent";
|
||||
compression = "Compression";
|
||||
serverAliveInterval = "ServerAliveInterval";
|
||||
serverAliveCountMax = "ServerAliveCountMax";
|
||||
hashKnownHosts = "HashKnownHosts";
|
||||
userKnownHostsFile = "UserKnownHostsFile";
|
||||
controlMaster = "ControlMaster";
|
||||
controlPath = "ControlPath";
|
||||
controlPersist = "ControlPersist";
|
||||
};
|
||||
renamedOptionOrder = [
|
||||
"controlPersist"
|
||||
"controlPath"
|
||||
"controlMaster"
|
||||
@@ -32,11 +44,14 @@
|
||||
];
|
||||
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;
|
||||
old:
|
||||
let
|
||||
new = renamedOptions.${old};
|
||||
in
|
||||
"The option `programs.ssh.${old}' defined in ${
|
||||
lib.showFiles options.programs.ssh.${old}.files
|
||||
} has been renamed to `programs.ssh.settings.*.${new}'."
|
||||
) renamedOptionOrder;
|
||||
|
||||
nmt.script = ''
|
||||
assertFileExists home-files/.ssh/config
|
||||
|
||||
31
tests/modules/programs/ssh/settings-expected.conf
Normal file
31
tests/modules/programs/ssh/settings-expected.conf
Normal file
@@ -0,0 +1,31 @@
|
||||
Host *.corp !bastion.corp
|
||||
ProxyJump bastion-a.corp,bastion-b.corp
|
||||
SendEnv LANG LC_*
|
||||
User corp
|
||||
|
||||
Host github
|
||||
CertificateFile ~/.ssh/github-cert.pub
|
||||
CertificateFile ~/.ssh/github-alt-cert.pub
|
||||
HostName github.com
|
||||
IdentitiesOnly yes
|
||||
IdentityAgent ~/.ssh/agent
|
||||
IdentityAgent SSH_AUTH_SOCK
|
||||
IdentityFile ~/.ssh/github
|
||||
User git
|
||||
|
||||
Match host *.corp exec "test -f ~/.corp"
|
||||
IgnoreUnknown PubkeyAcceptedAlgorithms,PubkeyAcceptedKeyTypes
|
||||
CASignatureAlgorithms ssh-ed25519,rsa-sha2-512
|
||||
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
|
||||
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512
|
||||
HostbasedAcceptedAlgorithms ssh-ed25519,rsa-sha2-512
|
||||
HostbasedKeyTypes ssh-ed25519,rsa-sha2-512
|
||||
KbdInteractiveDevices bsdauth,pam
|
||||
KexAlgorithms sntrup761x25519-sha512,mlkem768x25519-sha256
|
||||
LocalForward [localhost]:8080 [10.0.0.1]:80
|
||||
LocalForward 9000 10.0.0.2:90
|
||||
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com
|
||||
PreferredAuthentications publickey,keyboard-interactive
|
||||
PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512
|
||||
PubkeyAcceptedKeyTypes ssh-ed25519,rsa-sha2-512
|
||||
SetEnv BAR="bar baz" BAZ="with \" some \\ very \\\" fun \\\\ escapes" FOO="foo"
|
||||
@@ -0,0 +1,17 @@
|
||||
{ lib, options, ... }:
|
||||
{
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
settings.legacy.extraOptions = {
|
||||
AddKeysToAgent = "yes";
|
||||
HostName = "example.org";
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
''
|
||||
`programs.ssh.settings.*.extraOptions` defined in ${lib.showFiles options.programs.ssh.settings.files} is not supported. Move these OpenSSH options directly into `programs.ssh.settings.*` using upstream directive names.
|
||||
''
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
Host raw
|
||||
DynamicForward 127.0.0.1:1080
|
||||
LocalForward 9000 10.0.0.2:90
|
||||
RemoteForward 9001 10.0.0.3:91
|
||||
26
tests/modules/programs/ssh/settings-raw-forwards.nix
Normal file
26
tests/modules/programs/ssh/settings-raw-forwards.nix
Normal file
@@ -0,0 +1,26 @@
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
config = {
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
settings.raw = {
|
||||
LocalForward = "9000 10.0.0.2:90";
|
||||
RemoteForward = [ "9001 10.0.0.3:91" ];
|
||||
DynamicForward = "127.0.0.1:1080";
|
||||
};
|
||||
};
|
||||
|
||||
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 \
|
||||
${./settings-raw-forwards-expected.conf}
|
||||
assertFileContent home-files/assertions ${./no-assertions.json}
|
||||
'';
|
||||
};
|
||||
}
|
||||
113
tests/modules/programs/ssh/settings.nix
Normal file
113
tests/modules/programs/ssh/settings.nix
Normal file
@@ -0,0 +1,113 @@
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
config = {
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
settings = {
|
||||
github = {
|
||||
HostName = "github.com";
|
||||
User = "git";
|
||||
IdentityFile = "~/.ssh/github";
|
||||
IdentityAgent = [
|
||||
"~/.ssh/agent"
|
||||
"SSH_AUTH_SOCK"
|
||||
];
|
||||
CertificateFile = [
|
||||
"~/.ssh/github-cert.pub"
|
||||
"~/.ssh/github-alt-cert.pub"
|
||||
];
|
||||
IdentitiesOnly = true;
|
||||
};
|
||||
|
||||
"Host *.corp !bastion.corp" = lib.hm.dag.entryBefore [ "github" ] {
|
||||
User = "corp";
|
||||
ProxyJump = [
|
||||
"bastion-a.corp"
|
||||
"bastion-b.corp"
|
||||
];
|
||||
SendEnv = [
|
||||
"LANG"
|
||||
"LC_*"
|
||||
];
|
||||
};
|
||||
|
||||
"Match host *.corp exec \"test -f ~/.corp\"" = lib.hm.dag.entryAfter [ "github" ] {
|
||||
SetEnv = {
|
||||
FOO = "foo";
|
||||
BAR = "bar baz";
|
||||
BAZ = ''with " some \ very \" fun \\ escapes'';
|
||||
};
|
||||
IgnoreUnknown = [
|
||||
"PubkeyAcceptedAlgorithms"
|
||||
"PubkeyAcceptedKeyTypes"
|
||||
];
|
||||
Ciphers = [
|
||||
"chacha20-poly1305@openssh.com"
|
||||
"aes256-gcm@openssh.com"
|
||||
];
|
||||
CASignatureAlgorithms = [
|
||||
"ssh-ed25519"
|
||||
"rsa-sha2-512"
|
||||
];
|
||||
HostbasedAcceptedAlgorithms = [
|
||||
"ssh-ed25519"
|
||||
"rsa-sha2-512"
|
||||
];
|
||||
HostbasedKeyTypes = [
|
||||
"ssh-ed25519"
|
||||
"rsa-sha2-512"
|
||||
];
|
||||
HostKeyAlgorithms = [
|
||||
"ssh-ed25519"
|
||||
"rsa-sha2-512"
|
||||
];
|
||||
KbdInteractiveDevices = [
|
||||
"bsdauth"
|
||||
"pam"
|
||||
];
|
||||
KexAlgorithms = [
|
||||
"sntrup761x25519-sha512"
|
||||
"mlkem768x25519-sha256"
|
||||
];
|
||||
MACs = [
|
||||
"hmac-sha2-256-etm@openssh.com"
|
||||
"hmac-sha2-512-etm@openssh.com"
|
||||
];
|
||||
PreferredAuthentications = [
|
||||
"publickey"
|
||||
"keyboard-interactive"
|
||||
];
|
||||
PubkeyAcceptedAlgorithms = [
|
||||
"ssh-ed25519"
|
||||
"rsa-sha2-512"
|
||||
];
|
||||
PubkeyAcceptedKeyTypes = [
|
||||
"ssh-ed25519"
|
||||
"rsa-sha2-512"
|
||||
];
|
||||
LocalForward = [
|
||||
{
|
||||
bind.port = 8080;
|
||||
host.address = "10.0.0.1";
|
||||
host.port = 80;
|
||||
}
|
||||
"9000 10.0.0.2:90"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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 \
|
||||
${./settings-expected.conf}
|
||||
assertFileContent home-files/assertions ${./no-assertions.json}
|
||||
'';
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user