212 lines
5.8 KiB
Nix
212 lines
5.8 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
registry,
|
|
...
|
|
}:
|
|
# Configures inbound and outbound SSH access.
|
|
let
|
|
cfg = config.modules.network.ssh;
|
|
user = config.user.name; # NOLINT(let_order)
|
|
|
|
# NOLINT(let_order)
|
|
workstationNames = [
|
|
"neogaia"
|
|
"zeus"
|
|
];
|
|
serverNames = [ "pikachu" ];
|
|
|
|
isWorkstation = lib.elem config.networking.hostName workstationNames; # NOLINT(let_order)
|
|
isServer = lib.elem config.networking.hostName serverNames; # NOLINT(let_order)
|
|
|
|
# NOLINT(let_order)
|
|
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
|
hostKeysFor =
|
|
host:
|
|
map builtins.readFile [
|
|
(../../hosts + "/${host}/ssh_host_ed25519_key.pub")
|
|
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
|
];
|
|
|
|
# NOLINT(let_order)
|
|
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
|
hostName = registry.address.${name}.self;
|
|
clientKey = clientKeyFor name;
|
|
hostKeys = hostKeysFor name;
|
|
});
|
|
|
|
extraTargets = {
|
|
# TODO: Pin Gitea's host key after migrating it from Proxmox.
|
|
gitea = {
|
|
hostName = "git.alexion.dev";
|
|
port = 2022;
|
|
user = "gitea";
|
|
aliases = [
|
|
"gitea"
|
|
"git.alexion.dev"
|
|
];
|
|
};
|
|
};
|
|
|
|
# NOLINT(let_order)
|
|
mkTarget = name: target: {
|
|
hostName = target.hostName or name;
|
|
user = target.user or user;
|
|
port = target.port or 22;
|
|
aliases = target.aliases or [ name ];
|
|
clientKey = target.clientKey or null;
|
|
hostKeys = target.hostKeys or [ ];
|
|
};
|
|
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets); # NOLINT(let_order)
|
|
|
|
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names); # NOLINT(let_order)
|
|
# NOLINT(let_order)
|
|
defaultAuthorizedKeys = lib.flatten (
|
|
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
|
);
|
|
|
|
outboundTargetNames = # NOLINT(let_order)
|
|
let
|
|
groupTargets =
|
|
if isServer then
|
|
[ "gitea" ] ++ serverNames
|
|
else if isWorkstation then
|
|
[ "gitea" ] ++ serverNames ++ workstationNames
|
|
else
|
|
[ "gitea" ];
|
|
in
|
|
lib.filter (name: name != config.networking.hostName) groupTargets;
|
|
|
|
knownHosts = lib.listToAttrs (
|
|
lib.concatLists (
|
|
lib.mapAttrsToList (
|
|
targetName: target:
|
|
lib.imap0 (
|
|
i: key:
|
|
lib.nameValuePair "${targetName}-${toString i}" {
|
|
hostNames = knownHostNamesFor target;
|
|
publicKey = keyWithoutComment key;
|
|
}
|
|
) target.hostKeys
|
|
) targets
|
|
)
|
|
);
|
|
knownHostNamesFor =
|
|
target:
|
|
let
|
|
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
|
names = lib.unique (target.aliases ++ [ target.hostName ]);
|
|
in
|
|
map withPort names;
|
|
|
|
keyWithoutComment = key: lib.concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
|
|
|
sshSettingsFor =
|
|
name:
|
|
let
|
|
target = targets.${name};
|
|
in
|
|
{
|
|
header = "Host ${lib.concatStringsSep " " target.aliases}";
|
|
HostName = target.hostName;
|
|
User = target.user;
|
|
}
|
|
// lib.optionalAttrs (target.port != 22) { Port = target.port; };
|
|
|
|
hostKeySecret = type: "ssh-host-${type}-key";
|
|
hostKeyTypes = [
|
|
"ed25519"
|
|
"rsa"
|
|
];
|
|
userKeySecret = "ssh-user-ed25519-key";
|
|
|
|
in
|
|
{
|
|
options.modules.network.ssh = {
|
|
enable = lib.mkEnableOption "the OpenSSH daemon";
|
|
|
|
hostKeys.restore = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Restores host keys from secrets.
|
|
Preserves the fingerprint across reimages.
|
|
Guests disable this option and generate their own host keys.
|
|
'';
|
|
};
|
|
|
|
hostKeys.sopsFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
Sets encrypted file that contains host private keys.
|
|
Names each secret `ssh-host-<type>-key`.
|
|
Requires a value when `restore` is enabled.
|
|
'';
|
|
};
|
|
|
|
userKey.sopsFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
Sets encrypted file that contains the SSH client private key.
|
|
Names the secret `ssh-user-ed25519-key`.
|
|
Guests leave this unset because they do not authenticate to remote servers.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable (
|
|
lib.mkMerge [
|
|
{
|
|
services.openssh.enable = true;
|
|
|
|
users.users.${user}.openssh.authorizedKeys.keys = defaultAuthorizedKeys;
|
|
|
|
programs.ssh.knownHosts = knownHosts;
|
|
|
|
home-manager.users.${user}.programs.ssh = {
|
|
enable = true;
|
|
enableDefaultConfig = false;
|
|
settings = lib.mapAttrs (name: _: sshSettingsFor name) (
|
|
lib.genAttrs outboundTargetNames (name: name)
|
|
);
|
|
};
|
|
}
|
|
|
|
(lib.mkIf cfg.hostKeys.restore {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.hostKeys.sopsFile != null;
|
|
message = "Set `modules.network.ssh.hostKeys.sopsFile` when host-key restoration is enabled.";
|
|
}
|
|
];
|
|
|
|
sops.secrets = lib.genAttrs (map hostKeySecret hostKeyTypes) (_: {
|
|
inherit (cfg.hostKeys) sopsFile;
|
|
mode = "0400";
|
|
restartUnits = [ "sshd.service" ];
|
|
});
|
|
|
|
services.openssh.hostKeys = [ ];
|
|
services.openssh.extraConfig = lib.concatStringsSep "" (
|
|
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") hostKeyTypes
|
|
);
|
|
})
|
|
|
|
(lib.mkIf (cfg.userKey.sopsFile != null) {
|
|
sops.secrets.${userKeySecret} = {
|
|
inherit (cfg.userKey) sopsFile;
|
|
mode = "0400";
|
|
owner = user;
|
|
};
|
|
|
|
home-manager.users.${user}.programs.ssh.settings."*" = {
|
|
IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
|
IdentitiesOnly = true;
|
|
};
|
|
})
|
|
]
|
|
);
|
|
}
|