treewide: implement auto importing for modules
Reduce maintenance burden and increase efficiency by automatically importing modules following a specific convention. Co-authored-by: awwpotato <awwpotato@voidq.com> Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
parent
fefeb0e928
commit
4fca600cb1
461 changed files with 72 additions and 474 deletions
621
modules/programs/ssh/default.nix
Normal file
621
modules/programs/ssh/default.nix
Normal file
|
|
@ -0,0 +1,621 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
concatStringsSep
|
||||
literalExpression
|
||||
mapAttrsToList
|
||||
mkOption
|
||||
optional
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.programs.ssh;
|
||||
|
||||
isPath = x: builtins.substring 0 1 (toString x) == "/";
|
||||
|
||||
addressPort =
|
||||
entry:
|
||||
if isPath entry.address then " ${entry.address}" else " [${entry.address}]:${toString entry.port}";
|
||||
|
||||
unwords = builtins.concatStringsSep " ";
|
||||
|
||||
mkSetEnvStr =
|
||||
envStr:
|
||||
unwords (
|
||||
mapAttrsToList (name: value: ''${name}="${lib.escape [ ''"'' "\\" ] (toString value)}"'') envStr
|
||||
);
|
||||
|
||||
bindOptions = {
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
example = "example.org";
|
||||
description = "The address where to bind the port.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
example = 8080;
|
||||
description = "Specifies port number to bind on bind address.";
|
||||
};
|
||||
};
|
||||
|
||||
dynamicForwardModule = types.submodule { options = bindOptions; };
|
||||
|
||||
forwardModule = types.submodule {
|
||||
options = {
|
||||
bind = bindOptions;
|
||||
|
||||
host = {
|
||||
address = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "example.org";
|
||||
description = "The address where to forward the traffic to.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
example = 80;
|
||||
description = "Specifies port number to forward the traffic to.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
matchBlockModule = types.submodule {
|
||||
options = {
|
||||
host = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "*.example.org";
|
||||
description = ''
|
||||
`Host` pattern used by this conditional block.
|
||||
See
|
||||
{manpage}`ssh_config(5)`
|
||||
for `Host` block details.
|
||||
This option is ignored if
|
||||
{option}`ssh.matchBlocks.*.match`
|
||||
if defined.
|
||||
'';
|
||||
};
|
||||
|
||||
match = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = ''
|
||||
host <hostname> canonical
|
||||
host <hostname> exec "ping -c1 -q 192.168.17.1"'';
|
||||
description = ''
|
||||
`Match` block conditions used by this block. See
|
||||
{manpage}`ssh_config(5)`
|
||||
for `Match` block details.
|
||||
This option takes precedence over
|
||||
{option}`ssh.matchBlocks.*.host`
|
||||
if defined.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = "Specifies port number to connect on remote host.";
|
||||
};
|
||||
|
||||
forwardAgent = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr types.bool;
|
||||
description = ''
|
||||
Whether the connection to the authentication agent (if any)
|
||||
will be forwarded to the remote machine.
|
||||
'';
|
||||
};
|
||||
|
||||
forwardX11 = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Specifies whether X11 connections will be automatically redirected
|
||||
over the secure channel and {env}`DISPLAY` set.
|
||||
'';
|
||||
};
|
||||
|
||||
forwardX11Trusted = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Specifies whether remote X11 clients will have full access to the
|
||||
original X11 display.
|
||||
'';
|
||||
};
|
||||
|
||||
identitiesOnly = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Specifies that ssh should only use the authentication
|
||||
identity explicitly configured in the
|
||||
{file}`~/.ssh/config` files or passed on the
|
||||
ssh command-line, even if {command}`ssh-agent`
|
||||
offers more identities.
|
||||
'';
|
||||
};
|
||||
|
||||
identityFile = mkOption {
|
||||
type = with types; either (listOf str) (nullOr str);
|
||||
default = [ ];
|
||||
apply =
|
||||
p:
|
||||
if p == null then
|
||||
[ ]
|
||||
else if lib.isString p then
|
||||
[ p ]
|
||||
else
|
||||
p;
|
||||
description = ''
|
||||
Specifies files from which the user identity is read.
|
||||
Identities will be tried in the given order.
|
||||
'';
|
||||
};
|
||||
|
||||
identityAgent = mkOption {
|
||||
type = with types; either (listOf str) (nullOr str);
|
||||
default = [ ];
|
||||
apply =
|
||||
p:
|
||||
if p == null then
|
||||
[ ]
|
||||
else if lib.isString p then
|
||||
[ p ]
|
||||
else
|
||||
p;
|
||||
description = ''
|
||||
Specifies the location of the ssh identity agent.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Specifies the user to log in as.";
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Specifies the real host name to log into.";
|
||||
};
|
||||
|
||||
serverAliveInterval = mkOption {
|
||||
type = types.int;
|
||||
default = 0;
|
||||
description = "Set timeout in seconds after which response will be requested.";
|
||||
};
|
||||
|
||||
serverAliveCountMax = mkOption {
|
||||
type = types.ints.positive;
|
||||
default = 3;
|
||||
description = ''
|
||||
Sets the number of server alive messages which may be sent
|
||||
without SSH receiving any messages back from the server.
|
||||
'';
|
||||
};
|
||||
|
||||
sendEnv = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Environment variables to send from the local host to the
|
||||
server.
|
||||
'';
|
||||
};
|
||||
|
||||
setEnv = mkOption {
|
||||
type =
|
||||
with types;
|
||||
attrsOf (oneOf [
|
||||
str
|
||||
path
|
||||
int
|
||||
float
|
||||
]);
|
||||
default = { };
|
||||
description = ''
|
||||
Environment variables and their value to send to the server.
|
||||
'';
|
||||
};
|
||||
|
||||
compression = mkOption {
|
||||
type = types.nullOr types.bool;
|
||||
default = null;
|
||||
description = ''
|
||||
Specifies whether to use compression. Omitted from the host
|
||||
block when `null`.
|
||||
'';
|
||||
};
|
||||
|
||||
checkHostIP = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Check the host IP address in the
|
||||
{file}`known_hosts` file.
|
||||
'';
|
||||
};
|
||||
|
||||
proxyCommand = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "The command to use to connect to the server.";
|
||||
};
|
||||
|
||||
proxyJump = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "The proxy host to use to connect to the server.";
|
||||
};
|
||||
|
||||
certificateFile = mkOption {
|
||||
type = with types; either (listOf str) (nullOr str);
|
||||
default = [ ];
|
||||
apply =
|
||||
p:
|
||||
if p == null then
|
||||
[ ]
|
||||
else if lib.isString p then
|
||||
[ p ]
|
||||
else
|
||||
p;
|
||||
description = ''
|
||||
Specifies files from which the user certificate is read.
|
||||
'';
|
||||
};
|
||||
|
||||
addressFamily = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr (
|
||||
types.enum [
|
||||
"any"
|
||||
"inet"
|
||||
"inet6"
|
||||
]
|
||||
);
|
||||
description = ''
|
||||
Specifies which address family to use when connecting.
|
||||
'';
|
||||
};
|
||||
|
||||
localForwards = mkOption {
|
||||
type = types.listOf forwardModule;
|
||||
default = [ ];
|
||||
example = literalExpression ''
|
||||
[
|
||||
{
|
||||
bind.port = 8080;
|
||||
host.address = "10.0.0.13";
|
||||
host.port = 80;
|
||||
}
|
||||
];
|
||||
'';
|
||||
description = ''
|
||||
Specify local port forwardings. See
|
||||
{manpage}`ssh_config(5)` for `LocalForward`.
|
||||
'';
|
||||
};
|
||||
|
||||
remoteForwards = mkOption {
|
||||
type = types.listOf forwardModule;
|
||||
default = [ ];
|
||||
example = literalExpression ''
|
||||
[
|
||||
{
|
||||
bind.port = 8080;
|
||||
host.address = "10.0.0.13";
|
||||
host.port = 80;
|
||||
}
|
||||
];
|
||||
'';
|
||||
description = ''
|
||||
Specify remote port forwardings. See
|
||||
{manpage}`ssh_config(5)` for `RemoteForward`.
|
||||
'';
|
||||
};
|
||||
|
||||
dynamicForwards = mkOption {
|
||||
type = types.listOf dynamicForwardModule;
|
||||
default = [ ];
|
||||
example = literalExpression ''
|
||||
[ { port = 8080; } ];
|
||||
'';
|
||||
description = ''
|
||||
Specify dynamic port forwardings. See
|
||||
{manpage}`ssh_config(5)` for `DynamicForward`.
|
||||
'';
|
||||
};
|
||||
|
||||
extraOptions = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
description = "Extra configuration options for the host.";
|
||||
};
|
||||
};
|
||||
|
||||
# config.host = mkDefault dagName;
|
||||
};
|
||||
|
||||
matchBlockStr =
|
||||
key: cf:
|
||||
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 " IdentitiesOnly yes"
|
||||
++ 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 != 0) " ServerAliveInterval ${toString cf.serverAliveInterval}"
|
||||
++ optional (cf.serverAliveCountMax != 3) " 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}"
|
||||
++ 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
|
||||
++ mapAttrsToList (n: v: " ${n} ${v}") cf.extraOptions
|
||||
);
|
||||
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.rycee ];
|
||||
|
||||
options.programs.ssh = {
|
||||
enable = lib.mkEnableOption "SSH client configuration";
|
||||
|
||||
package = lib.mkPackageOption pkgs "openssh" {
|
||||
nullable = true;
|
||||
default = null;
|
||||
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 {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Extra configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
extraOptionOverrides = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
description = ''
|
||||
Extra SSH configuration options that take precedence over any
|
||||
host specific configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
includes = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
File globs of ssh config files that should be included via the
|
||||
`Include` directive.
|
||||
|
||||
See
|
||||
{manpage}`ssh_config(5)`
|
||||
for more information.
|
||||
'';
|
||||
};
|
||||
|
||||
matchBlocks = mkOption {
|
||||
type = lib.hm.types.dagOf matchBlockModule;
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
"john.example.com" = {
|
||||
hostname = "example.com";
|
||||
user = "john";
|
||||
};
|
||||
foo = lib.hm.dag.entryBefore ["john.example.com"] {
|
||||
hostname = "example.com";
|
||||
identityFile = "/home/john/.ssh/foo_rsa";
|
||||
};
|
||||
};
|
||||
'';
|
||||
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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
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.";
|
||||
}
|
||||
];
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue