Feature: Implement sops-nix:

- .sops.yaml: Rotate keys, narrow path_regex to secrets.
  - sops.nix: Init module, init `sops-nix` script.
  - configuration.nix: Add sops-nix to desktop configuration.
This commit is contained in:
Joey Hafner 2025-01-30 14:55:56 -08:00
parent 09c2066504
commit efa8265c3b
Signed by: Jafner
GPG Key ID: 6D9A24EF2F389E55
3 changed files with 125 additions and 18 deletions

View File

@ -1,19 +1,9 @@
keys:
- &joey_desktop_jafner_net age1v5wy7epv5mm8ddf3cfv8m0e9w4s693dw7djpuytz9td8ycha5f0sv2se9n
- &joey_age age1zswcq6t5wl8spr3g2wpxhxukjklngcav0vw8py0jnfkqd2jm2ypq53ga00
creation_rules:
- path_regex: .*
# The below key_group and shamir_threshold configuration
# allows any single author key to decrypt secrets,
# but hosts and deploy runners *must* use both keys
# to decrypt. This prevents a compromised host or
# runner from leaking all secrets.
shamir_threshold: 2
- path_regex: ^.*(\.(secrets|token|passwd)|secrets.env|config.boot)
key_groups:
- age: # Author keys; to be held by contributors to the repo
- 'age1zswcq6t5wl8spr3g2wpxhxukjklngcav0vw8py0jnfkqd2jm2ypq53ga00' # joey@dungeon-master
- age: # Author keys (again); hacky way to give author keys a weight of 2 shares
- 'age1zswcq6t5wl8spr3g2wpxhxukjklngcav0vw8py0jnfkqd2jm2ypq53ga00' # joey@dungeon-master
- age: # Deploy keys; to be held by the deploy environment (e.g. Gitea Actions)
- 'age193t908fjxl8ekl77p5xqnpj4xmw3y0khvyzlrw22hdzjduk6l53q05spq3' # deploy@gitea.jafner.tools
- age: # Host key; to be held by hosts to which Stacks should be deployed
- 'age13prhyye2jy3ysa6ltnjgkrqtxrxgs0035d86jyn4ltgk3wxtqgrqgav855' # fighter
- 'age1n20krynrj75jqfy2muvhrygvzd4ee8ngamljqavsrk033zwx0ses2tdtfe' # druid
- 'age1m0jpnk4t7hph5tdva3y9ap7scl8vfly9ufazr0h3cuwpcytlsulqjrt58y' # wizard
- age:
- *joey_desktop_jafner_net
- *joey_age

112
dotfiles/modules/sops.nix Normal file
View File

@ -0,0 +1,112 @@
{ sys, pkgs, inputs, flake, ... }: {
imports = [ inputs.sops-nix.nixosModules.sops ];
sops = {
age.sshKeyPaths = [ "${sys.ssh.path}/${sys.ssh.privateKey}" ];
#age.keyFile = "/home/${sys.username}/.config/sops/age/keys.txt"; # This file is expected to be provided from outside the nix-store
age.generateKey = false;
};
home-manager.users.${sys.username}.home.packages = with pkgs; [
sops
age
ssh-to-age
( writeShellApplication {
name = "sops-nix";
runtimeInputs = [ git ssh-to-age openssh yq ];
text = ''
#! bash
# shellcheck disable=SC2002
# shellcheck disable=SC2016
REPO_ROOT="/home/${sys.username}/${flake.repoPath}"
# shellcheck disable=SC2034
SOPS_AGE_KEY_FILE="/home/${sys.username}/.config/sops/age/keys.txt"
listSecrets () {
# shellcheck disable=SC2002
SOPS_REGEX="$(cat "$REPO_ROOT/.sops.yaml" | yq -y '.creation_rules.[].path_regex' | head -n1)"
find "$REPO_ROOT" -regextype posix-extended -regex "$SOPS_REGEX"
}
getPubkey () {
ADDRESS="$1"
AGE_PUBKEY="$(ssh-keyscan -t ed25519 "$ADDRESS" | ssh-to-age)"
echo "$AGE_PUBKEY"
}
addPubkey () { # Note: Does not edit any files. Prints .sops.yaml with the given key added.
AGE_PUBKEY="$1"
# shellcheck disable=SC2002,SC2016
cat "$REPO_ROOT"/.sops.yaml |\
yq -y --arg key "$AGE_PUBKEY" '.keys += [$key]' |\
yq -y --arg key "$AGE_PUBKEY" '.creation_rules.[].key_groups.[].age += [$key]'
}
updateKey () { # Note: Interactive
sops updatekeys --input-type json "$1"
}
encryptFile () { # Note: All encrypted files are in json format
FILE="$1"
FILE_EXT="''$''\{FILE##*.}"
case "$FILE_EXT" in
"env") FILE_TYPE=dotenv ;;
"json") FILE_TYPE=json ;;
"yaml|yml") FILE_TYPE=yaml ;;
"ini") FILE_TYPE=ini ;;
*) FILE_TYPE=binary ;;
esac
sops --encrypt --config "$REPO_ROOT/.sops.yaml" --input-type "$FILE_TYPE" --output-type json "$FILE"
}
decryptFile () { # Note: All encrypted files are in json format.
# File extension is used as the hint for decrypted file type.
FILE="$1"
FILE_EXT="''$''\{FILE##*.}"
case "$FILE_EXT" in
"env") FILE_TYPE=dotenv ;;
"json") FILE_TYPE=json ;;
"yaml|yml") FILE_TYPE=yaml ;;
"ini") FILE_TYPE=ini ;;
*) FILE_TYPE=binary ;;
esac
sops --decrypt --config "$REPO_ROOT/.sops.yaml" --input-type json --output-type "$FILE_TYPE" "$FILE"
}
isJson () {
FILE="$1"
jq -e . >/dev/null 2>&1 <<<"$(cat "$FILE")" && return 0 || return 1;
}
isEncrypted () {
FILE="$1"
FILE_EXT="''$''\{FILE##*.}"
case "$FILE_EXT" in
"env") FILE_TYPE=dotenv ;;
"json") FILE_TYPE=json ;;
"yaml|yml") FILE_TYPE=yaml ;;
"ini") FILE_TYPE=ini ;;
*) FILE_TYPE=binary ;;
esac
if isJson "$FILE" && [[ "$(sops filestatus "$FILE")" == '{"encrypted":true}' ]]; then echo True; else echo False; fi
}
listSecretStatus () {
for file in $(listSecrets); do
FILE="$(realpath -s --relative-to="$REPO_ROOT" "$file")"
echo -n "$FILE: " |\
xargs echo -n
if sops --decrypt --input-type json "$file" >/dev/null 2>&1; then
echo Decryptable
else
echo "Not decryptable"
fi
done
}
"$@"
'';
} )
];
}

View File

@ -1,12 +1,17 @@
{ sys, pkgs, inputs, ... }: {
{ sys, pkgs, inputs, flake, ... }: {
imports = [
./hardware.nix
./services.nix
./desktop-environment.nix
./terminal-environment.nix
./theme.nix
../../modules/sops.nix
];
environment.sessionVariables = {
"FLAKE_DIR" = "/home/${sys.username}/${flake.repoPath}/${flake.path}/";
};
home-manager.backupFileExtension = "bk";
home-manager.users."${sys.username}" = {
nixGL = {