refactor(ssh): centralize fleet access
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
config,
|
|
||||||
inputs,
|
inputs,
|
||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
@@ -39,22 +38,8 @@
|
|||||||
modules.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
modules.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
||||||
modules.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
modules.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
||||||
|
|
||||||
# A machine the operator works from, so it admits the workstation keys alone.
|
|
||||||
modules.ssh.authorizedKeys = config.modules.ssh.workstationKeys;
|
|
||||||
|
|
||||||
modules.toolkit.enable = true;
|
modules.toolkit.enable = true;
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.programs.ssh.settings = {
|
|
||||||
neogaia = {
|
|
||||||
HostName = "10.23.50.146";
|
|
||||||
User = config.user.name;
|
|
||||||
};
|
|
||||||
pikachu = {
|
|
||||||
HostName = "10.23.10.102";
|
|
||||||
User = config.user.name;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# The walking-skeleton guest, enabled like any module: proves the guest path
|
# The walking-skeleton guest, enabled like any module: proves the guest path
|
||||||
# end to end through this host's `nix flake check`.
|
# end to end through this host's `nix flake check`.
|
||||||
# Modest caps keep the skeleton guest from starving the laptop.
|
# Modest caps keep the skeleton guest from starving the laptop.
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{ pkgs, ... }:
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# pikachu — AZW ME Pro server.
|
# pikachu — AZW ME Pro server.
|
||||||
# Disk layout is in ./disk.nix.
|
# Disk layout is in ./disk.nix.
|
||||||
# `fileSystems` for the root disk are derived from it.
|
# `fileSystems` for the root disk are derived from it.
|
||||||
@@ -43,20 +39,10 @@
|
|||||||
modules.ssh.enable = true;
|
modules.ssh.enable = true;
|
||||||
modules.ssh.hostKeys.sopsFile = ../../secrets/pikachu.yaml;
|
modules.ssh.hostKeys.sopsFile = ../../secrets/pikachu.yaml;
|
||||||
modules.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml;
|
modules.ssh.userKey.sopsFile = ../../secrets/pikachu.yaml;
|
||||||
modules.ssh.authorizedKeys =
|
|
||||||
config.modules.ssh.workstationKeys
|
|
||||||
++ config.modules.ssh.serverKeys;
|
|
||||||
|
|
||||||
modules.git.enable = true;
|
modules.git.enable = true;
|
||||||
modules.toolkit.enable = true;
|
modules.toolkit.enable = true;
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.programs.ssh.settings.git-alexion-dev = {
|
|
||||||
header = "Host git.alexion.dev";
|
|
||||||
HostName = "git.alexion.dev";
|
|
||||||
Port = 2022;
|
|
||||||
User = "gitea";
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
pciutils
|
pciutils
|
||||||
smartmontools
|
smartmontools
|
||||||
|
|||||||
348
modules/ssh.nix
348
modules/ssh.nix
@@ -8,121 +8,298 @@ let
|
|||||||
cfg = config.modules.ssh;
|
cfg = config.modules.ssh;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
|
inherit (lib)
|
||||||
|
concatLists
|
||||||
|
concatStringsSep
|
||||||
|
elem
|
||||||
|
filter
|
||||||
|
genAttrs
|
||||||
|
hasAttr
|
||||||
|
imap0
|
||||||
|
listToAttrs
|
||||||
|
mapAttrs
|
||||||
|
mapAttrsToList
|
||||||
|
mkIf
|
||||||
|
mkMerge
|
||||||
|
mkOption
|
||||||
|
nameValuePair
|
||||||
|
optional
|
||||||
|
optionalAttrs
|
||||||
|
types
|
||||||
|
unique
|
||||||
|
;
|
||||||
|
|
||||||
hostKeySecret = type: "ssh-host-${type}-key";
|
hostKeySecret = type: "ssh-host-${type}-key";
|
||||||
userKeySecret = "ssh-user-ed25519-key";
|
userKeySecret = "ssh-user-ed25519-key";
|
||||||
|
|
||||||
|
targetType = types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
hostName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
description = ''
|
||||||
|
The network address OpenSSH connects to for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = config.user.name;
|
||||||
|
description = ''
|
||||||
|
The remote login name OpenSSH uses for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 22;
|
||||||
|
description = ''
|
||||||
|
The TCP port OpenSSH uses for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
aliases = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ name ];
|
||||||
|
description = ''
|
||||||
|
Host patterns written into the generated OpenSSH client block.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
clientKey = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The public key this target offers when it connects outward.
|
||||||
|
Other machines admit this key according to the host groups below.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
The public keys this target presents when it accepts inbound SSH.
|
||||||
|
These keys generate system-wide known-host entries.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
||||||
|
|
||||||
|
targetNamesIn = names: filter (name: hasAttr name cfg.targets) names;
|
||||||
|
|
||||||
|
workstationNames = targetNamesIn cfg.hosts.workstations;
|
||||||
|
serverNames = targetNamesIn cfg.hosts.servers;
|
||||||
|
|
||||||
|
clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names);
|
||||||
|
|
||||||
|
currentHost = config.networking.hostName;
|
||||||
|
isServer = elem currentHost cfg.hosts.servers;
|
||||||
|
isWorkstation = elem currentHost cfg.hosts.workstations;
|
||||||
|
|
||||||
|
defaultAuthorizedKeys = lib.flatten (
|
||||||
|
clientKeysFor workstationNames
|
||||||
|
++ optional isServer (clientKeysFor serverNames)
|
||||||
|
);
|
||||||
|
|
||||||
|
outboundTargetNames =
|
||||||
|
let
|
||||||
|
groupTargets =
|
||||||
|
if isServer then
|
||||||
|
[ "gitea" ] ++ cfg.hosts.servers
|
||||||
|
else if isWorkstation then
|
||||||
|
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
||||||
|
else
|
||||||
|
[ "gitea" ];
|
||||||
|
in
|
||||||
|
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
||||||
|
|
||||||
|
sshSettingsFor = name:
|
||||||
|
let
|
||||||
|
target = cfg.targets.${name};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
header = "Host ${concatStringsSep " " target.aliases}";
|
||||||
|
HostName = target.hostName;
|
||||||
|
User = target.user;
|
||||||
|
}
|
||||||
|
// optionalAttrs (target.port != 22) { Port = target.port; };
|
||||||
|
|
||||||
|
knownHostNamesFor = target:
|
||||||
|
let
|
||||||
|
names = unique (target.aliases ++ [ target.hostName ]);
|
||||||
|
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
||||||
|
in
|
||||||
|
map withPort names;
|
||||||
|
|
||||||
|
knownHosts = listToAttrs (
|
||||||
|
concatLists (
|
||||||
|
mapAttrsToList (
|
||||||
|
targetName: target:
|
||||||
|
imap0 (i: key:
|
||||||
|
nameValuePair "${targetName}-${toString i}" {
|
||||||
|
hostNames = knownHostNamesFor target;
|
||||||
|
publicKey = keyWithoutComment key;
|
||||||
|
}
|
||||||
|
) target.hostKeys
|
||||||
|
) cfg.targets
|
||||||
|
)
|
||||||
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.ssh = {
|
options.modules.ssh = {
|
||||||
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
||||||
|
|
||||||
workstationKeys = lib.mkOption {
|
hosts = {
|
||||||
type = lib.types.listOf lib.types.str;
|
servers = mkOption {
|
||||||
default = [
|
type = types.listOf types.str;
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia"
|
default = [ "pikachu" ];
|
||||||
|
description = ''
|
||||||
|
Hosts that serve durable services.
|
||||||
|
They admit workstation keys and server keys, and they receive aliases for other servers and the forge.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
workstations = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ "neogaia" ];
|
||||||
|
description = ''
|
||||||
|
Hosts the operator works from.
|
||||||
|
Their keys are admitted by every host, and they receive aliases for the whole fleet and the forge.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
targets = mkOption {
|
||||||
|
type = types.attrsOf targetType;
|
||||||
|
default = {
|
||||||
|
gitea = {
|
||||||
|
hostName = "git.alexion.dev";
|
||||||
|
port = 2022;
|
||||||
|
user = "gitea";
|
||||||
|
aliases = [
|
||||||
|
"gitea"
|
||||||
|
"git.alexion.dev"
|
||||||
];
|
];
|
||||||
description = ''
|
|
||||||
Client public keys of the machines the operator works from.
|
|
||||||
|
|
||||||
Every machine admits these, so any of them reaches the whole fleet.
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
serverKeys = lib.mkOption {
|
neogaia = {
|
||||||
type = lib.types.listOf lib.types.str;
|
hostName = "10.23.50.146";
|
||||||
default = [
|
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia";
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu"
|
hostKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJS+wp7K123+4BT6G4f954R6WyrbWveY7VlpoBUf6I5p neogaia"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCo2wWUKxyAS4J5TqbWf8glDhJvS5XmdRqFhMeJwG3pOB+4AccZ1T8LU7ZN+RjtRi3j2qXBJvIHuzhtQNtmT59TxocvfobYiqOgJpvVO5K6yD8ZoUJs6ziDkIduI9w9mdRIESoi+dBbVu8n24r61cKDVh+jWX+yjzkOcWcOzqDyQhhkjqblZ1WMAdujEMuEPvif1i2LCxStUaZqRGcx09m/ME2fYcaJrpuxxxvX2+CPJNicoo6Rx9i7ZjAoNuvH+jui4KT62DzlQtQtCl2CFUOM0gCPSa+MbNQ9elfHPvGzEcwOIMo2cuy9KURUkQu+sAgaG8S1PEniDDTecskHtuRdmPZawnQGpIhzo919Q6wUgjT8scK4mmSXRWmGmkMt0GNA2tfj5tDks6r5Q8XsYqtWs4rsOEvfmxVSdM771w+fqDBAil99Jsh0ksPK9+Bwgg8cMDzLLFDn8JA5y2G1HocMMom+u5DYKwPXEKnCILkasB8y24+O3PhSu1EuWw277w6EUEXvU03rCf0Ak/ULjxp9a00EGlloEwSmFI7Aub9XHDr87IdbGInEn+PMqyBYADiN+3h6nE2JO+nMa6i/CHdebmT+T7YJvuTKHD9sjFmQsYaghlq03DZrhHcm4hgUvE1dqGojHrhk/WgA3EWTWtK/+BP0Vy2jXaaz+qAx+EGnhQ== neogaia"
|
||||||
];
|
];
|
||||||
description = ''
|
};
|
||||||
Client public keys of the machines that serve.
|
|
||||||
|
|
||||||
Only other servers admit these, so one that is compromised reaches no
|
pikachu = {
|
||||||
machine the operator works from.
|
hostName = "10.23.10.102";
|
||||||
|
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu";
|
||||||
|
hostKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKljRf4pJO+pqEqjpPz08gOYq3g1PpxvE66xVw7uMEnA root@pikachu"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy/riwm7dflA3mT+3a0/2CIoS2LbAsK/vn35kOoNeuzn0yhiF+imexP6tkB3S2t+H5ybRzkbbuNZcynFfeCqthFc8kvbdCnt8Diqoeg96fZ6ecvh5QE5yH9op8534EySetZ/exakFLnF+6EiWMuWUW3DFwsc2kcgDJObqSE8gTx/d7JK953MiTFmSJBFyg1RtQ3ZnMT+iCrvY2dyCLQai7VeF8koVKF2c0leAq2Hc75rb/L9md8MoJa64iPiz7hwTCin3xoFyaY/5hNVvyqFd5PivgR69gLdJkuVsUYO2mJzhur8cYmJD+pGjJ0U45hyE9TMrCFjeJHHuvSt3+2kph62wv95jLNk0WmMlwgyunISxENCSVVtNYdBMXhUh8VhEAW17QpVUg9EnPvxOdTKEjrvfOZYASWUa51JKbgBgexVgFbxdjDZR88DZa31AVBts/cx/59gXTUahFXMYLdZgssx+5uibZQWnvCyfUV9WLbfmK1lgL6hzReg1VkQ87iGr6skjtQYemJxRaFNA1+Q5f3kmG3KncuK/594a3qXYP4gC6A2blf8om1YZ4aXXh6f+GFKLjoEw1vvM2rJ+rjzfymwDX+pxVQ9L13OEtVZc9Ez76pOkbm1hqdbL0gY45+0cpxodhWV0wMQJBDXL1MHP8qcs+/vw0GxVK5l1SnWBGlw== root@pikachu"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
SSH targets known to the fleet.
|
||||||
|
The inventory holds connection details plus public keys used for authorization and host verification.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
authorizedKeys = lib.mkOption {
|
extraAuthorizedKeys = mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = types.listOf types.str;
|
||||||
default = cfg.workstationKeys;
|
default = [ ];
|
||||||
defaultText = lib.literalExpression "config.modules.ssh.workstationKeys";
|
|
||||||
description = ''
|
description = ''
|
||||||
Client public keys this machine admits for the primary user, drawn from
|
Additional client public keys admitted by this host.
|
||||||
the lists above.
|
|
||||||
|
|
||||||
A machine the operator works from takes the workstation keys. One that
|
|
||||||
serves takes both, so servers reach each other. The default admits the
|
|
||||||
workstation keys, since a machine admitting none is unreachable.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.restore = lib.mkOption {
|
authorizedKeys = mkOption {
|
||||||
type = lib.types.bool;
|
type = types.nullOr (types.listOf types.str);
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Restore the host keys from secrets rather than letting the daemon
|
|
||||||
generate its own.
|
|
||||||
|
|
||||||
A machine with its own identity keeps its fingerprint across a reimage
|
|
||||||
by restoring committed keys. A guest carries no host identity, so it
|
|
||||||
turns this off and presents a self-generated key instead.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
hostKeys.sopsFile = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.path;
|
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Encrypted file holding this host's SSH host private keys, one entry per
|
Complete override for client public keys admitted by this host.
|
||||||
key type, named `ssh-host-<type>-key`. Required when `restore` is on.
|
Leave null to derive access from `modules.ssh.hosts` and `modules.ssh.targets`.
|
||||||
|
|
||||||
These are the keys the daemon presents to identify itself to connecting
|
|
||||||
clients, not keys used to authenticate anyone to a remote server.
|
|
||||||
Restoring them from secrets rather than generating them keeps the host's
|
|
||||||
fingerprint across a reimage, so every client's `known_hosts` entry
|
|
||||||
stays valid.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.types = lib.mkOption {
|
extraSettings = mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = types.attrsOf types.anything;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Additional OpenSSH client settings merged into the generated Home Manager configuration.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys.restore = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Restore the host keys from secrets rather than letting the daemon generate its own.
|
||||||
|
A machine with its own identity keeps its fingerprint across a reimage by restoring committed keys.
|
||||||
|
A guest carries no host identity, so it turns this off and presents a self-generated key instead.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys.sopsFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Encrypted file holding this host's SSH host private keys, one entry per key type, named `ssh-host-<type>-key`.
|
||||||
|
Required when `restore` is on.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys.types = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
default = [
|
default = [
|
||||||
"ed25519"
|
"ed25519"
|
||||||
"rsa"
|
"rsa"
|
||||||
];
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Key types to restore, naming both the entries read from the encrypted
|
Key types to restore, naming both the entries read from the encrypted file and the algorithms the daemon offers.
|
||||||
file and the algorithms the daemon offers. Dropping a type a client has
|
|
||||||
already pinned makes the host unrecognisable to it.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
userKey.sopsFile = lib.mkOption {
|
userKey.sopsFile = mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Encrypted file holding this machine's SSH client private key, under the
|
Encrypted file holding this machine's SSH client private key, under the entry `ssh-user-ed25519-key`.
|
||||||
entry `ssh-user-ed25519-key`. Left unset on a machine that authenticates
|
Left unset on a machine that authenticates to no remote server, such as a guest.
|
||||||
to no remote server, such as a guest.
|
|
||||||
|
|
||||||
This is the key the primary user offers to authenticate to a remote
|
|
||||||
server, not a key the daemon presents to identify this machine.
|
|
||||||
It belongs to this machine alone, so withdrawing its access does not
|
|
||||||
re-key any other.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
config = mkIf cfg.enable (
|
||||||
lib.mkMerge [
|
mkMerge [
|
||||||
{
|
{
|
||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
|
|
||||||
# The primary user is the only account reachable over SSH.
|
users.users.${user}.openssh.authorizedKeys.keys =
|
||||||
users.users.${user}.openssh.authorizedKeys.keys = cfg.authorizedKeys;
|
if cfg.authorizedKeys != null then
|
||||||
|
cfg.authorizedKeys
|
||||||
|
else
|
||||||
|
defaultAuthorizedKeys ++ cfg.extraAuthorizedKeys;
|
||||||
|
|
||||||
|
programs.ssh.knownHosts = knownHosts;
|
||||||
|
|
||||||
|
home-manager.users.${user}.programs.ssh = {
|
||||||
|
enable = true;
|
||||||
|
enableDefaultConfig = false;
|
||||||
|
settings =
|
||||||
|
mapAttrs (name: _: sshSettingsFor name) (genAttrs outboundTargetNames (name: name))
|
||||||
|
// cfg.extraSettings;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
# A machine with its own identity restores its host keys from secrets.
|
(mkIf cfg.hostKeys.restore {
|
||||||
(lib.mkIf cfg.hostKeys.restore {
|
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.hostKeys.sopsFile != null;
|
assertion = cfg.hostKeys.sopsFile != null;
|
||||||
@@ -130,42 +307,27 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
# The daemon reads its host keys once at startup, so a re-key has to
|
sops.secrets = genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
||||||
# restart it to take effect.
|
|
||||||
sops.secrets = lib.genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
|
||||||
inherit (cfg.hostKeys) sopsFile;
|
inherit (cfg.hostKeys) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
restartUnits = [ "sshd.service" ];
|
restartUnits = [ "sshd.service" ];
|
||||||
});
|
});
|
||||||
|
|
||||||
# An empty list is what stops the daemon generating keys of its own.
|
|
||||||
services.openssh.hostKeys = [ ];
|
services.openssh.hostKeys = [ ];
|
||||||
services.openssh.extraConfig = lib.concatMapStrings (
|
services.openssh.extraConfig = concatStringsSep "" (
|
||||||
type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n"
|
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") cfg.hostKeys.types
|
||||||
) cfg.hostKeys.types;
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
# The client key the primary user offers to remote servers, present only on
|
(mkIf (cfg.userKey.sopsFile != null) {
|
||||||
# a machine that has one.
|
|
||||||
(lib.mkIf (cfg.userKey.sopsFile != null) {
|
|
||||||
# The primary user is the only account that authenticates with this key,
|
|
||||||
# and the mode admits no other.
|
|
||||||
# The client rereads it per connection, so no unit restarts on a re-key.
|
|
||||||
sops.secrets.${userKeySecret} = {
|
sops.secrets.${userKeySecret} = {
|
||||||
inherit (cfg.userKey) sopsFile;
|
inherit (cfg.userKey) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = user;
|
owner = user;
|
||||||
};
|
};
|
||||||
|
|
||||||
# The client reads the decrypted key where it is written, so no copy of it
|
home-manager.users.${user}.programs.ssh.settings."*".IdentityFile =
|
||||||
# lives in the user's home to drift from the secret.
|
config.sops.secrets.${userKeySecret}.path;
|
||||||
# Declaring no defaults of home-manager's own leaves every other directive
|
|
||||||
# at the one OpenSSH itself ships.
|
|
||||||
home-manager.users.${user}.programs.ssh = {
|
|
||||||
enable = true;
|
|
||||||
enableDefaultConfig = false;
|
|
||||||
settings."*".IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user