GPG & SSH key pairs management

Hello,

I recently started exploring NixOS and whilst writing my home-manager git configuration started wondering how one manages their GPG and SSH key pairs on NixOS to for example sign a commit message and connect to GitHub via SSH.

Of course, I can’t have my private keys in plaintext inside my NixOS configuration.

Are you supposed to manage these key pairs manually as you would on any other system, or is there some way to accomplish this in a declarative way that I am missing?

I noticed some configurations use sops-nix to manage some of their passwords and key files, but couldn’t find an example config where someone uses sops-nix to manage their git related keys.

Thanks

1 Like

I think you could use the sops-nix home-manager integration and something like:

sops = {
  secrets.gpg-key = {
    mode = "0400";
    path = "${config.home.homeDirectory}/.gnupg/private-keys-v1.d/<id>.key";
  };
};

Not sure if gpg would pick it up like that though. Might need to then add an activationScript that rescans that directory or somesuch.

I don’t think you need to copy around the public key, but sharing that is trivial because it doesn’t need to be protected by definition.

Something equivalent would work for SSH, but probably easier because you can configure the path to secrets in ~/.ssh/config (just manage that file with home-manager and set the paths to ${config.sops.secrets.ssh-key.path}) and I am fairly sure SSH doesn’t keep a database or anything.

As a side note - if you already have a gpg key you can also use it for ssh, which can be helpful.

That’s a fairly reasonable approach as well; ultimately all of your secrets will need to be protected by some kind of “master” secret anyway, and your gpg/ssh key is a pretty good candidate for that. At least one key will always need to be managed non-declaratively.

If you care about not exposing these secrets, I’d at least recommend not adding the secrets.yaml to the repository and sharing it independently, as described in the sops-nix docs.

Personally, I use a yubikey to handle all of these secrets, which is both more secure and easier to maintain; sharing your secrets between computers doesn’t get easier than wearing them around your neck. It comes with an additional cost, of course, but they’re well worth it in my opinion.

If you want a low-cost alternative to a yubikey, a LUKS-encrypted USB is also an option, just set your GPGHOME and stuff to the path you dedicate to mounting your USB stick. It’s significantly less convenient and not nearly as safe as a yubikey, but it probably beats cloud storage.

2 Likes

A YubiKey is probably the best long term solution, and I thought about getting one for quite a while already.

My only concern with a YubiKey is it getting stolen and my private keys being used because as far as I am aware they don’t require a password/pin to be used, I might be wrong though?

1 Like

They require a pin to be used, and it’s supposedly impossible to extract the keys even destructively (though that last bit should be taken with a grain of salt).

You can set a limit to the number of times the pin can be entered wrongly, after which the yubikey is locked, which makes it so you can use a more convenient, short pin, especially since its entropy isn’t used for encryption.

It’s kind of their whole point that even when stolen they aren’t an issue, and unlike a LUKS-based USB stick when connected to a computer using the secrets always requires entering a PIN so there’s no window where an application can read use (sorry, misphrased that, the operations happen on the device, so the keys are of course never readable at all) your secrets without confirmation.

The main risk is losing your yubikey and having to set up a new one (and expire the keys on the old one just in case), so you really need to keep a backup of your keys somewhere safe. And ideally have multiple yubikeys for convenience if that happens.

1 Like

Good to know, I will probably just get 2 YubiKeys, one as a backup and also keep a backup of my keys on an encrypted USB drive just in case.

2 Likes

Sorry for necrobumping this, but…

@suspect @TLATER Did you actually put these ideas in practice?
After a broken nvme replacement I wanted to get around to put all my remaining configs in HM/sops, but I’m now stuck at the point where I can’t just put the gpg’s keys content in a decrypted yaml, because it’s binary data.

One way would be a two-stage process, where the gpg keys are written after base64 encoding the contents, and then placed in .gnupg by a custom systemd service that translates from the mounted sops secrets to the key files. I’ve done something like this before for my .netrc, but it feels rather hacky…

Any better ideas?

EDIT: I saw that sops(nix) also handles binary, at the inconvenience of needing one file per binary key. Still cumbersome I guess but workable. I’ll just make a seed script that generates the necessary sops references.

EDIT2:

I made a little script that automates the migration for having the gnupg rpivate keys in sops for in the flake (<flakeroot>/mkPrivGpg.sh):

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail
set +x

THIS=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
NIXOUT=$THIS/home/${USER}/sops-gpg.nix
GPGSECDIR="$THIS/home/${USER}/_secrets/gpg-private"
GPKDIR="private-keys-v1.d"
GPGDIR=~/.gnupg
KBX="pubring.kbx"

enc_privkeys() {
	(
		cd $GPGDIR || return 1
		for k in "${GPKDIR}"/*.key $KBX; do
			# if the key is a link, we assume it's already a sops one
			if [ -L "$k" ]; then
				printf 'Ignoring symlink %s (assumed already in sops)\n' "$k" >&2
			else
				tgt="${GPGSECDIR}/$(basename "${k}").enc"
				#shellcheck disable=SC2015
				cp -f "$k" "${tgt}" &&
					sops --config "${THIS}/.sops.yaml" --encrypt --in-place "${tgt}" ||
					rm -f "${tgt}"
			fi
		done
	)
}

get_secrets_sops() {
	(
		cd "$GPGSECDIR" || return 1
		for k in *.key.enc; do
			kf="$(basename "$k" .enc)"
			#shellcheck disable=SC2016
			printf '\n      "%s" = 
                { 
                  format = "binary";
                  sopsFile = ./_secrets/gpg-private/%s;
                  mode = "0400";
                  path = "${config.home.homeDirectory}/.gnupg/private-keys-v1.d/%s";
                };' \
				"$kf" \
				"$k" \
				"$kf"
		done

		for k in "${KBX}"*; do
			kf="$(basename "$k" .enc)"
			#shellcheck disable=SC2016
			printf '\n      "%s" = 
                { 
                  format = "binary";
                  sopsFile = ./_secrets/gpg-private/%s;
                  mode = "0444";
                  path = "${config.home.homeDirectory}/.gnupg/%s";
                };' \
				"$kf" \
				"$k" \
				"$kf"
		done
	)
}

printf 'Writing sops secrets to %s\n' "${GPGSECDIR}/"
enc_privkeys

printf 'Writing sops gpg keys HM config to %s\n' "$NIXOUT"
cat <<EOF | tee "$NIXOUT"
{ config, ...}: {
    sops.secrets = {
        $(get_secrets_sops)
    };
}
EOF

2 Likes