All checks were successful
CI / Format Check (push) Successful in 2s
CI / Flake Check (push) Successful in 1m42s
CI / Evaluate Key Configurations (nix-builder) (push) Successful in 14s
CI / Evaluate Key Configurations (nix-desktop1) (push) Successful in 8s
CI / Evaluate Key Configurations (nix-laptop1) (push) Successful in 9s
CI / Evaluate Artifacts (installer-iso-nix-laptop1) (push) Successful in 20s
CI / Evaluate Artifacts (lxc-nix-builder) (push) Successful in 13s
CI / Build and Publish Documentation (push) Successful in 10s
188 lines
6.1 KiB
Nix
188 lines
6.1 KiB
Nix
# ============================================================================
|
|
# Agenix Secret Recipients Configuration (Auto-Generated)
|
|
# ============================================================================
|
|
# This file automatically discovers hosts and their public keys from the
|
|
# secrets/ directory structure and generates recipient configurations.
|
|
#
|
|
# Directory structure:
|
|
# secrets/{hostname}/*.pub -> SSH/age public keys for that host
|
|
# secrets/global/*.pub -> Keys accessible to all hosts
|
|
#
|
|
# Usage:
|
|
# ragenix -e secrets/global/example.age # Edit/create secret
|
|
# ragenix -r # Re-key all secrets
|
|
#
|
|
# To add admin keys for editing secrets, create secrets/admins/*.pub files
|
|
# with your personal age public keys (generated with: age-keygen)
|
|
|
|
let
|
|
lib = builtins;
|
|
|
|
# Helper functions not in builtins
|
|
filterAttrs =
|
|
pred: set:
|
|
lib.listToAttrs (
|
|
lib.filter (item: pred item.name item.value) (
|
|
lib.map (name: {
|
|
inherit name;
|
|
value = set.${name};
|
|
}) (lib.attrNames set)
|
|
)
|
|
);
|
|
|
|
concatLists = lists: lib.foldl' (acc: list: acc ++ list) [ ] lists;
|
|
|
|
unique =
|
|
list:
|
|
let
|
|
go =
|
|
acc: remaining:
|
|
if remaining == [ ] then
|
|
acc
|
|
else if lib.elem (lib.head remaining) acc then
|
|
go acc (lib.tail remaining)
|
|
else
|
|
go (acc ++ [ (lib.head remaining) ]) (lib.tail remaining);
|
|
in
|
|
go [ ] list;
|
|
|
|
hasSuffix =
|
|
suffix: str:
|
|
let
|
|
lenStr = lib.stringLength str;
|
|
lenSuffix = lib.stringLength suffix;
|
|
in
|
|
lenStr >= lenSuffix && lib.substring (lenStr - lenSuffix) lenSuffix str == suffix;
|
|
|
|
nameValuePair = name: value: { inherit name value; };
|
|
|
|
secretsPath = ./secrets;
|
|
|
|
# Read all directories in secrets/
|
|
secretDirs = if lib.pathExists secretsPath then lib.readDir secretsPath else { };
|
|
|
|
# Filter to only directories (excludes files)
|
|
isDirectory = name: type: type == "directory";
|
|
directories = lib.filter (name: isDirectory name secretDirs.${name}) (lib.attrNames secretDirs);
|
|
|
|
# Read public keys from a directory and convert to age format
|
|
readHostKeys =
|
|
dirName:
|
|
let
|
|
dirPath = secretsPath + "/${dirName}";
|
|
files = if lib.pathExists dirPath then lib.readDir dirPath else { };
|
|
|
|
# Prefer .age.pub files (pre-converted), fall back to .pub files
|
|
agePubFiles = filterAttrs (name: type: type == "regular" && hasSuffix ".age.pub" name) files;
|
|
|
|
sshPubFiles = filterAttrs (
|
|
name: type: type == "regular" && hasSuffix ".pub" name && !(hasSuffix ".age.pub" name)
|
|
) files;
|
|
|
|
# Read age public keys (already in correct format)
|
|
ageKeys = lib.map (
|
|
name:
|
|
let
|
|
content = lib.readFile (dirPath + "/${name}");
|
|
# Trim whitespace/newlines
|
|
trimmed = lib.replaceStrings [ "\n" " " "\r" "\t" ] [ "" "" "" "" ] content;
|
|
in
|
|
trimmed
|
|
) (lib.attrNames agePubFiles);
|
|
|
|
# For SSH keys, just include them as-is (user needs to convert with ssh-to-age)
|
|
# Or they can run the update-age-keys.sh script
|
|
sshKeys =
|
|
if (lib.length (lib.attrNames sshPubFiles)) > 0 then
|
|
lib.trace "Warning: ${dirName} has unconverted SSH keys. Run secrets/update-age-keys.sh" [ ]
|
|
else
|
|
[ ];
|
|
in
|
|
lib.filter (k: k != null && k != "") (ageKeys ++ sshKeys);
|
|
|
|
# Build host key mappings: { hostname = [ "age1..." "age2..." ]; }
|
|
hostKeys = lib.listToAttrs (
|
|
lib.map (dir: nameValuePair dir (readHostKeys dir)) (
|
|
lib.filter (d: d != "global" && d != "admins") directories
|
|
)
|
|
);
|
|
|
|
# Global keys that all hosts can use
|
|
globalKeys = if lib.elem "global" directories then readHostKeys "global" else [ ];
|
|
|
|
# Admin keys for editing secrets
|
|
adminKeys = if lib.elem "admins" directories then readHostKeys "admins" else [ ];
|
|
|
|
# All host keys combined
|
|
allHostKeys = concatLists (lib.attrValues hostKeys);
|
|
|
|
# Find all .age files in the secrets directory
|
|
findSecrets =
|
|
dir:
|
|
let
|
|
dirPath = secretsPath + "/${dir}";
|
|
files = if lib.pathExists dirPath then lib.readDir dirPath else { };
|
|
ageFiles = filterAttrs (name: type: type == "regular" && hasSuffix ".age" name) files;
|
|
in
|
|
lib.map (name: "secrets/${dir}/${name}") (lib.attrNames ageFiles);
|
|
|
|
# Generate recipient list for a secret based on its location
|
|
getRecipients =
|
|
secretPath:
|
|
let
|
|
# Extract directory name from path: "secrets/nix-builder/foo.age" -> "nix-builder"
|
|
pathParts = lib.split "/" secretPath;
|
|
dirName = lib.elemAt pathParts 2;
|
|
in
|
|
if dirName == "global" then
|
|
# Global secrets: all hosts + admins
|
|
allHostKeys ++ globalKeys ++ adminKeys
|
|
else if hostKeys ? ${dirName} then
|
|
# Host-specific secrets: that host + global keys + admins
|
|
hostKeys.${dirName} ++ globalKeys ++ adminKeys
|
|
else
|
|
# Fallback: just admins
|
|
adminKeys;
|
|
|
|
# Find all secrets across all directories
|
|
allSecrets = concatLists (lib.map findSecrets directories);
|
|
|
|
# Generate the configuration
|
|
secretsConfig = lib.listToAttrs (
|
|
lib.map (
|
|
secretPath:
|
|
let
|
|
recipients = getRecipients secretPath;
|
|
# Remove duplicates and empty keys
|
|
uniqueRecipients = unique (lib.filter (k: k != null && k != "") recipients);
|
|
in
|
|
nameValuePair secretPath {
|
|
publicKeys = uniqueRecipients;
|
|
}
|
|
) allSecrets
|
|
);
|
|
|
|
# Generate wildcard rules for each directory to allow creating new secrets
|
|
wildcardRules = lib.listToAttrs (
|
|
lib.concatMap (dir: [
|
|
# Match with and without .age extension for ragenix compatibility
|
|
(nameValuePair "secrets/${dir}/*" {
|
|
publicKeys =
|
|
let
|
|
recipients = getRecipients "secrets/${dir}/dummy.age";
|
|
in
|
|
unique (lib.filter (k: k != null && k != "") recipients);
|
|
})
|
|
(nameValuePair "secrets/${dir}/*.age" {
|
|
publicKeys =
|
|
let
|
|
recipients = getRecipients "secrets/${dir}/dummy.age";
|
|
in
|
|
unique (lib.filter (k: k != null && k != "") recipients);
|
|
})
|
|
]) (lib.filter (d: d != "admins") directories)
|
|
);
|
|
|
|
in
|
|
secretsConfig // wildcardRules
|