Common SSH configuration

{ config, lib, ... }:
let
  cfg = config.mjm.ssh;
in
{
  options.mjm.ssh = {
    <<options>>
  };

  config = lib.mkIf cfg.enable {
    <<config>>
  };
}

This module has common SSH configuration for both NixOS and nix-darwin hosts.

It is extended with platform-specific configuration:

=> nixos/ssh

Options

enable = lib.mkEnableOption "SSH configuration" // {
  default = true;
};

The SSH module has an enable option, but it is generally useful and mostly harmless where it's not needed, it is enabled by default.

hostCA = lib.mkOption {
  type = lib.types.path;
  default = builtins.fetchurl {
    url = "https://vault.midna.dev/v1/ssh-host-signer/public_key";
    sha256 = "1zy0wvd26iaypwf7zpvdxfhmabdg191q4986aw993j23q87z3g59";
  };
  readOnly = true;
};

The hostCA option isn't meant to be set: it's just exposing a value that this module and possibly others can use. That value is the SSH certificate authority for my server hosts. This authority is fetched from Vault. By configuring this certificate authority on my SSH clients, I can securely log in to my machines without maintaining a known_hosts file and relying on TOFU.

trustedKeys = lib.mkOption {
  type = lib.types.path;
  default = builtins.fetchurl {
    url = "https://vault.midna.dev/v1/ssh-client-signer/public_key";
    sha256 = "12kcpl2mnfds458fv0c0jb0lz122q9jd7vqcfc7cw27giqis2dgl";
  };
  readOnly = true;
};

The trustedKeys option is similar to the hostCA option, but in the other direction. My clients will get Vault to sign a public key to produce an SSH certificate, and then use that certificate to login. The trustedKeys value can be configured on the server to make it accept those logins, which avoids needing to maintain an authorized_keys file on the servers.

This option is not read directly in this module, but rather is exposed for other SSH-related modules to use.

Config

programs.ssh.extraConfig = ''
  CanonicalizeHostname yes
  CanonicalDomains home.mattmoriarity.com

  Host aion
    Hostname 5.78.46.61

  Host niobe
    Hostname 152.53.116.186
'';

This configuration just makes it easier to log in to my machines by only needing to pass their short hostname to the ssh command. For most machines, that's accomplished by the canonicalization options: all of my homelab servers are .home.mattmoriarity.com. The VPSes I run just get explicit rules to match the short hostname and have it connect by IP address instead.

programs.ssh.knownHosts = {
  <<known-hosts>>
};

I have a little bit of common known hosts configuration on all machines.

"*.home.mattmoriarity.com" = {
  publicKeyFile = "${cfg.hostCA}";
  certAuthority = true;
};

Most hosts are trusted via an SSH certificate authority. The hosts will sign their host key with Vault and configure the SSH server to present that certificate. Clients don't need a list of individual public keys for hosts. This also means that on machines that don't persist their root filesystem, I don't need to worry about persisting SSH host keys, since they can be regenerated every boot without invalidating trust.

aion = {
  hostNames = [
    "aion"
    "5.78.46.61"
  ];
  publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDUWju/ZTNyivso/yzx6RFE/9D50qTiWVXDvITrkyEVh";
};
niobe = {
  hostNames = [
    "niobe"
    "152.53.116.186"
  ];
  publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILDn8zp1d/s5OoGgAqtH14KETvBEMH9pDEf6c2yjcdYs";
};

The VPSes don't have direct access to Vault, so they are trusted the old-fashioned way.

Proxy Information
Original URL
gemini://midna.dev/homelab/modules/common/ssh/
Status Code
Success (20)
Meta
text/gemini;lang=en-US
Capsule Response Time
24.392646 milliseconds
Gemini-to-HTML Time
0.364736 milliseconds

This content has been proxied by September (UNKNO).