222 lines
7.9 KiB
Nix
222 lines
7.9 KiB
Nix
{ ... }:
|
||
{
|
||
_class = "clan.service";
|
||
manifest.name = "clan-core/sshd";
|
||
manifest.description = "Enables secure remote access to the machine over SSH with automatic host key management and optional CA-signed host certificates.";
|
||
manifest.categories = [
|
||
"System"
|
||
"Network"
|
||
];
|
||
manifest.readme = builtins.readFile ./README.md;
|
||
|
||
roles.client = {
|
||
description = "Installs the SSH CA public key into known_hosts for the configured domains, so this machine can verify servers’ host certificates without TOFU prompts.";
|
||
interface =
|
||
{ lib, ... }:
|
||
{
|
||
options.certificate = {
|
||
searchDomains = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ ];
|
||
example = [ "mydomain.com" ];
|
||
description = ''
|
||
List of domains to include in the certificate.
|
||
This option will prepend the machine name in front of each domain
|
||
before adding it to the certificate.
|
||
'';
|
||
};
|
||
};
|
||
};
|
||
|
||
perInstance =
|
||
{ settings, roles, ... }:
|
||
{
|
||
nixosModule =
|
||
{
|
||
config,
|
||
lib,
|
||
pkgs,
|
||
...
|
||
}:
|
||
let
|
||
# Collect searchDomains from all servers in this instance
|
||
allServerSearchDomains = lib.flatten (
|
||
lib.mapAttrsToList (_name: machineConfig: machineConfig.settings.certificate.searchDomains or [ ]) (
|
||
roles.server.machines or { }
|
||
)
|
||
);
|
||
# Merge client's searchDomains with all servers' searchDomains
|
||
searchDomains = lib.uniqueStrings (settings.certificate.searchDomains ++ allServerSearchDomains);
|
||
in
|
||
{
|
||
clan.core.vars.generators.openssh-ca = lib.mkIf (searchDomains != [ ]) {
|
||
share = true;
|
||
files.id_ed25519.deploy = false;
|
||
files."id_ed25519.pub" = {
|
||
deploy = false;
|
||
secret = false;
|
||
};
|
||
runtimeInputs = [
|
||
pkgs.openssh
|
||
];
|
||
script = ''
|
||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||
'';
|
||
};
|
||
|
||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (searchDomains != [ ]) {
|
||
certAuthority = true;
|
||
extraHostNames = builtins.map (domain: "*.${domain}") searchDomains;
|
||
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
|
||
roles.server = {
|
||
description = "Runs sshd with persistent host keys and (if certificate.searchDomains is set) a CA‑signed host certificate for <machine>.<domain>, enabling TOFU‑less verification by clients that trust the CA.";
|
||
interface =
|
||
{ lib, ... }:
|
||
{
|
||
options = {
|
||
hostKeys.rsa.enable = lib.mkEnableOption "generating a RSA host key";
|
||
|
||
certificate = {
|
||
searchDomains = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ ];
|
||
example = [ "mydomain.com" ];
|
||
description = ''
|
||
List of domains to include in the certificate. This option will
|
||
prepend the machine name in front of each domain before adding
|
||
it to the certificate.
|
||
'';
|
||
};
|
||
};
|
||
};
|
||
};
|
||
|
||
perInstance =
|
||
{ settings, ... }:
|
||
{
|
||
nixosModule =
|
||
{
|
||
config,
|
||
lib,
|
||
pkgs,
|
||
...
|
||
}:
|
||
{
|
||
clan.core.vars.generators = {
|
||
openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
|
||
share = true;
|
||
files.id_ed25519.deploy = false;
|
||
files."id_ed25519.pub" = {
|
||
deploy = false;
|
||
secret = false;
|
||
};
|
||
runtimeInputs = [
|
||
pkgs.openssh
|
||
];
|
||
script = ''
|
||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
|
||
'';
|
||
};
|
||
|
||
openssh-cert = lib.mkIf (settings.certificate.searchDomains != [ ]) {
|
||
files."ssh.id_ed25519-cert.pub".secret = false;
|
||
dependencies = [
|
||
"openssh"
|
||
"openssh-ca"
|
||
];
|
||
validation = {
|
||
name = config.clan.core.settings.machine.name;
|
||
domains = lib.genAttrs settings.certificate.searchDomains lib.id;
|
||
};
|
||
runtimeInputs = [
|
||
pkgs.openssh
|
||
pkgs.jq
|
||
];
|
||
script =
|
||
let
|
||
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
|
||
domains = stringSet settings.certificate.searchDomains;
|
||
in
|
||
''
|
||
ssh-keygen \
|
||
-s $in/openssh-ca/id_ed25519 \
|
||
-I ${config.clan.core.settings.machine.name} \
|
||
-h \
|
||
-n ${lib.concatMapStringsSep "," (d: "${config.clan.core.settings.machine.name}.${d}") domains} \
|
||
$in/openssh/ssh.id_ed25519.pub
|
||
mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub
|
||
'';
|
||
};
|
||
|
||
openssh-rsa = lib.mkIf settings.hostKeys.rsa.enable {
|
||
files."ssh.id_rsa" = { };
|
||
files."ssh.id_rsa.pub".secret = false;
|
||
runtimeInputs = [
|
||
pkgs.coreutils
|
||
pkgs.openssh
|
||
];
|
||
script = ''
|
||
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
|
||
'';
|
||
};
|
||
|
||
openssh = {
|
||
files."ssh.id_ed25519" = { };
|
||
files."ssh.id_ed25519.pub".secret = false;
|
||
migrateFact = "openssh";
|
||
runtimeInputs = [
|
||
pkgs.coreutils
|
||
pkgs.openssh
|
||
];
|
||
script = ''
|
||
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
|
||
'';
|
||
};
|
||
};
|
||
|
||
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
|
||
certAuthority = true;
|
||
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificate.searchDomains;
|
||
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
|
||
};
|
||
|
||
services.openssh = {
|
||
enable = true;
|
||
settings.PasswordAuthentication = false;
|
||
|
||
settings.HostCertificate = lib.mkIf (
|
||
# this check needs to go first, as otherwise generators.openssh-cert does not exist
|
||
settings.certificate.searchDomains != [ ]
|
||
&& config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".exists
|
||
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
|
||
|
||
hostKeys = [
|
||
{
|
||
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
|
||
type = "ed25519";
|
||
}
|
||
]
|
||
++ lib.optional settings.hostKeys.rsa.enable {
|
||
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
|
||
type = "rsa";
|
||
};
|
||
};
|
||
|
||
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
|
||
hostNames = [
|
||
"localhost"
|
||
config.networking.hostName
|
||
]
|
||
++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
|
||
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
}
|