Setting up NixOS to use an SSH CA

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!