In my previous post I described how to set up an SSH certificate authority using a YubiKey.

I’ve been experimenting with NixOS again recently — having had several failed attempts before due to the utterly impenetrable documentation — but I’m curious enough to spend a bit more effort on it.

Even though I’m only running it in test VMs right now I still want to get a good feel for running it on a server, so I want to be able to log in using a signed certificate.

Initially I did it the “easy” way by adding a cert-authority key for my user in configuration.nix:

{ config, pkgs, ... }:

{
  users.users.jamesog = {
    # Other user options omitted
    openssh.authorizedKeys.keys = [
      ''cert-authority,principals="jamesog" ssh-rsa ...''
    ];
  };
}

But ideally I wanted to set it up system-wide, which requires putting the CA public key in a file and adding TrustedUserCAKeys to sshd_config.

I hoped NixOS would have a knob for this already, but searching the options for services.openssh revealed nothing for that, but it does have services.openssh.extraConfig to put any arbitrary configuration. That’ll do, I suppose.

I wasn’t sure how to get the CA key into a file at first. Some digging made me come across pkgs.writeText for building derivations. Even though it’s in pkgs, which I wasn’t sure about, I figured that it’s already passed to configuration.nix so maybe it’ll work. I tried this:

{ config, pkgs, ... }:

let
  trustedCAUserKeys = pkgs.writeText "/etc/ssh/ca.pub"
    ''
    ssh-rsa ...
    '';
in
{
  services.openssh.extraConfig =
    ''
    TrustedUserCAKeys ${trustedCAUserKeys}
    '';
}

But this was very unhappy. It turns out — and this should have been obvious from how it’s used in environment.packages — that this pkgs is just a list, so it doesn’t have writeText.

After some more digging I came across environment.etc whose description says:

Set of files that have to be linked in /etc.

Well, that’s exactly what I want in this case. I don’t know what I’d do if I wanted a file outside of /etc though. And it’s kind of weird it’s called environment. Let’s give this a go then.

{ config, pkgs, ... }:

{
  environment.etc = {
    "ssh/ca.pub".text = ''
      ssh-rsa ...
    '';
  };

  services.openssh.extraConfig =
    ''
    TrustedUserCAKeys ${environment.etc."ssh/ca.pub"}
    '';
}

This didn’t quite work due to the variable expansion I tried to use in extraConfig. I was hoping environment.etc would somewhere inject the full path to the file it created like writeText does. But then I realised I don’t need to: I’m literally telling environment.etc that I want a file in /etc and the path. I can just hard-code it:

{ config, pkgs, ... }:

{
  environment.etc = {
    "ssh/ca.pub".text = ''
      ssh-rsa ...
    '';
  };

  services.openssh.extraConfig =
    ''
    TrustedUserCAKeys /etc/ssh/ca.pub
    '';

  # Note that nixos-rebuild will complain if no users have a password nor authorizedKeys set, so add a backup key
  users.users.jamesog = {
    # Other user options omitted
    openssh.authorizedKeys.keys = [
      "ecdsa-sha2-nistp256 ..."
    ];
  };
}

Note the last block: nixos-rebuild will refuse to build if no users have a password or SSH keys defined as it doesn’t want to leave you with a system you can’t log in to. Fair enough. There’s probably some option I could tweak to bypass that, but it seemed just as safe to set at least one normal SSH public key on my user as a backup in case I nerf the sshd config.

Now it works. Nice!