{ inputs, ... }: { flake.modules.nixos.ssh-certs = { config, pkgs, lib, ... }: let cfg = config.ssh-certs; provisionerPasswordPath = config.sops.secrets."janus/admin_jwk".path; sshKeyPath = "/etc/ssh/ssh_host_ed25519_key"; sshCertPath = "${sshKeyPath}-cert.pub"; mkPrincipalArgs = principals: lib.concatMapStringsSep " " (principal: ''--principal "${principal}"'') principals; principalArgs = mkPrincipalArgs ([ cfg.hostname "${cfg.hostname}.john-stream.com" ] ++ cfg.extraPrincipals); sshHostCertRenew = pkgs.writeShellScriptBin "ssh-host-cert-renew" '' set -euo pipefail if [ ! -s "${sshKeyPath}.pub" ]; then ${lib.getExe' pkgs.openssh "ssh-keygen"} -y -f "${sshKeyPath}" > "${sshKeyPath}.pub" chmod 0644 "${sshKeyPath}.pub" fi ${lib.getExe pkgs.step-cli} ssh certificate \ --host --sign \ --provisioner "${cfg.provisioner}" \ --provisioner-password-file "${provisionerPasswordPath}" \ ${principalArgs} \ "${cfg.hostname}" "${sshKeyPath}.pub" ''; sshHostCertCheck = pkgs.writeShellScriptBin "ssh-host-cert-check" '' ${lib.getExe' pkgs.openssh "ssh-keygen"} -Lf ${sshCertPath} ''; in { # NixOS Options options.ssh-certs = { hostname = lib.mkOption { description = "Networking host name to register with the CA"; type = lib.types.str; }; provisioner = lib.mkOption { description = "Provisioner inside Step CA to use for the SSH certificates"; type = lib.types.str; default = "admin"; }; extraPrincipals = lib.mkOption { description = "Additional SSH host certificate principals to include per host"; type = with lib.types; listOf str; default = [ ]; }; }; imports = with inputs.self.modules.nixos; [ ssh ]; # NixOS Config config = { ssh.certificates.enable = true; sops.secrets."janus/admin_jwk" = { # Shared provisioner credential is intentionally centralized. sopsFile = ../../../keys/secrets.yaml; owner = "root"; group = "root"; mode = "0400"; }; networking.nameservers = [ "192.168.1.150" ]; networking.dhcpcd.extraConfig = "nohook resolv.conf"; environment.systemPackages = [ sshHostCertRenew sshHostCertCheck ]; systemd.services.ssh-certs-renew = { description = "Renew Step SSH host certificate if needed"; wantedBy = [ "multi-user.target" ]; before = [ "sshd.service" ]; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; path = with pkgs; [ coreutils systemd step-cli openssh ]; serviceConfig = { Type = "oneshot"; User = "root"; Group = "root"; }; script = '' set -euo pipefail renew=0 if [ ! -s "${sshCertPath}" ]; then echo "SSH host cert missing: ${sshCertPath}" renew=1 elif ${lib.getExe pkgs.step-cli} ssh needs-renewal "${sshCertPath}" --expires-in "4h"; then echo "SSH host cert needs renewal" renew=1 else rc=$? if [ "$rc" -eq 1 ]; then echo "SSH host cert does not need renewal" exit 0 fi if [ "$rc" -eq 2 ]; then echo "SSH host cert missing or unreadable: ${sshCertPath}" renew=1 else echo "step ssh needs-renewal failed with rc=$rc" >&2 exit "$rc" fi fi if [ "$renew" -eq 1 ]; then ${lib.getExe sshHostCertRenew} ${lib.getExe sshHostCertCheck} fi ''; }; systemd.timers.ssh-certs-renew = { description = "Periodic Step SSH host certificate renewal"; wantedBy = [ "timers.target" ]; timerConfig = { OnBootSec = "5m"; OnUnitActiveSec = "4h"; RandomizedDelaySec = "15m"; Persistent = true; Unit = "ssh-certs-renew.service"; }; }; # Ensure sshd waits for a cert reconciliation attempt at boot. systemd.services.sshd.wants = [ "ssh-certs-renew.service" ]; systemd.services.sshd.after = [ "ssh-certs-renew.service" ]; }; }; }