feat: Use age for env secret managment
Some checks failed
CI / Flake Check (push) Has been cancelled
CI / Evaluate Key Configurations (nix-builder) (push) Has been cancelled
CI / Evaluate Key Configurations (nix-desktop1) (push) Has been cancelled
CI / Evaluate Key Configurations (nix-laptop1) (push) Has been cancelled
CI / Evaluate Artifacts (installer-iso-nix-laptop1) (push) Has been cancelled
CI / Evaluate Artifacts (lxc-nix-builder) (push) Has been cancelled
CI / Build and Publish Documentation (push) Has been cancelled
CI / Format Check (push) Has been cancelled
Some checks failed
CI / Flake Check (push) Has been cancelled
CI / Evaluate Key Configurations (nix-builder) (push) Has been cancelled
CI / Evaluate Key Configurations (nix-desktop1) (push) Has been cancelled
CI / Evaluate Key Configurations (nix-laptop1) (push) Has been cancelled
CI / Evaluate Artifacts (installer-iso-nix-laptop1) (push) Has been cancelled
CI / Evaluate Artifacts (lxc-nix-builder) (push) Has been cancelled
CI / Build and Publish Documentation (push) Has been cancelled
CI / Format Check (push) Has been cancelled
This commit is contained in:
@@ -78,29 +78,48 @@ age -R secrets/nix-builder/ssh_host_ed25519_key.pub \
|
||||
-o secrets/nix-builder/ssh_host_key.age < /etc/ssh/ssh_host_ed25519_key
|
||||
```
|
||||
|
||||
### 4. Using ragenix CLI (Recommended)
|
||||
### 4. Creating and Editing Secrets
|
||||
|
||||
The `ragenix` CLI tool simplifies secret management. The `secrets/secrets.nix` file **automatically discovers** hosts and their keys from the directory structure:
|
||||
**For new secrets**, use the helper script (automatically determines recipients):
|
||||
|
||||
```bash
|
||||
cd secrets/
|
||||
|
||||
# Create a host-specific secret
|
||||
./create-secret.sh usda-dash/database-url.age <<< "postgresql://..."
|
||||
|
||||
# Create a global secret
|
||||
echo "shared-api-key" | ./create-secret.sh global/api-key.age
|
||||
|
||||
# From a file
|
||||
./create-secret.sh nix-builder/ssh-key.age < ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
The script automatically includes the correct recipients:
|
||||
- **Host-specific**: that host's keys + global keys + admin keys
|
||||
- **Global**: all host keys + admin keys
|
||||
|
||||
**To edit existing secrets**, use `ragenix`:
|
||||
|
||||
```bash
|
||||
# Install ragenix
|
||||
nix shell github:yaxitech/ragenix
|
||||
|
||||
# Edit a secret (creates if doesn't exist)
|
||||
# Recipients are automatically determined based on the path:
|
||||
# - secrets/global/*.age -> encrypted for ALL hosts + admins
|
||||
# - secrets/{hostname}/*.age -> encrypted for that host + global keys + admins
|
||||
ragenix -e secrets/global/example.age
|
||||
# Edit an existing secret (you must have a decryption key)
|
||||
ragenix -e secrets/global/existing-secret.age
|
||||
|
||||
# Re-key all secrets after adding/removing hosts
|
||||
# Re-key all secrets after adding new hosts
|
||||
ragenix -r
|
||||
```
|
||||
|
||||
The `secrets.nix` file automatically:
|
||||
- **Discovers hosts** from directory names in `secrets/`
|
||||
- **Reads age public keys** from `.age.pub` files in each directory
|
||||
- **Generates recipient lists** based on secret location (global vs host-specific)
|
||||
- **Includes admin keys** from `secrets/admins/*.age.pub` for editing
|
||||
**Why create with `age` first?** Ragenix requires the `.age` file to exist before editing. The `secrets/secrets.nix` configuration auto-discovers recipients from the directory structure, but ragenix doesn't support wildcard patterns for creating new files.
|
||||
|
||||
**Recipient management** is automatic:
|
||||
- **Global secrets** (`secrets/global/*.age`): encrypted for ALL hosts + admins
|
||||
- **Host secrets** (`secrets/{hostname}/*.age`): encrypted for that host + global keys + admins
|
||||
- **Admin keys** from `secrets/admins/*.age.pub` allow editing from your workstation
|
||||
|
||||
After creating new .age files with `age`, use `ragenix -r` to re-key all secrets with the updated recipient configuration.
|
||||
|
||||
To add admin keys for editing secrets:
|
||||
```bash
|
||||
|
||||
1
secrets/admins/temp-admin.age.pub
Normal file
1
secrets/admins/temp-admin.age.pub
Normal file
@@ -0,0 +1 @@
|
||||
age14emzyraytqzmre58c452t07rtcj87cwqwmd9z3gj7upugtxk8s3sda5tju
|
||||
BIN
secrets/core
Normal file
BIN
secrets/core
Normal file
Binary file not shown.
121
secrets/create-secret.sh
Executable file
121
secrets/create-secret.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Create a new age-encrypted secret with auto-determined recipients
|
||||
# Usage: ./create-secret.sh <path> [content]
|
||||
# path: relative to secrets/ (e.g., "usda-dash/my-secret.age" or "global/shared.age")
|
||||
# content: stdin if not provided
|
||||
|
||||
SECRETS_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <path> [content]" >&2
|
||||
echo "Examples:" >&2
|
||||
echo " $0 usda-dash/database-url.age <<< 'postgresql://...'" >&2
|
||||
echo " $0 global/api-key.age < secret-file.txt" >&2
|
||||
echo " echo 'secret' | $0 nix-builder/token.age" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SECRET_PATH="$1"
|
||||
shift
|
||||
|
||||
# Extract directory from path (e.g., "usda-dash/file.age" -> "usda-dash")
|
||||
SECRET_DIR="$(dirname "$SECRET_PATH")"
|
||||
SECRET_FILE="$(basename "$SECRET_PATH")"
|
||||
|
||||
# Ensure .age extension
|
||||
if [[ ! "$SECRET_FILE" =~ \.age$ ]]; then
|
||||
echo "Error: Secret file must have .age extension" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_FILE="$SECRETS_DIR/$SECRET_PATH"
|
||||
|
||||
# Ensure target directory exists
|
||||
mkdir -p "$(dirname "$TARGET_FILE")"
|
||||
|
||||
# Collect recipient keys
|
||||
RECIPIENTS=()
|
||||
|
||||
if [ "$SECRET_DIR" = "global" ]; then
|
||||
echo "Creating global secret (encrypted for all hosts + admins)..." >&2
|
||||
|
||||
# Add all host keys
|
||||
for host_dir in "$SECRETS_DIR"/*/; do
|
||||
host_name="$(basename "$host_dir")"
|
||||
# Skip non-host directories
|
||||
if [ "$host_name" = "admins" ] || [ "$host_name" = "global" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Add all .age.pub files from this host
|
||||
while IFS= read -r -d '' key_file; do
|
||||
RECIPIENTS+=("$key_file")
|
||||
done < <(find "$host_dir" -maxdepth 1 -name "*.age.pub" -print0)
|
||||
done
|
||||
|
||||
# Add global keys
|
||||
while IFS= read -r -d '' key_file; do
|
||||
RECIPIENTS+=("$key_file")
|
||||
done < <(find "$SECRETS_DIR/global" -maxdepth 1 -name "*.age.pub" -print0 2>/dev/null || true)
|
||||
|
||||
else
|
||||
echo "Creating host-specific secret for $SECRET_DIR..." >&2
|
||||
|
||||
# Check if host directory exists
|
||||
if [ ! -d "$SECRETS_DIR/$SECRET_DIR" ]; then
|
||||
echo "Error: Host directory $SECRET_DIR does not exist" >&2
|
||||
echo "Create it first: mkdir -p secrets/$SECRET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add this host's keys
|
||||
while IFS= read -r -d '' key_file; do
|
||||
RECIPIENTS+=("$key_file")
|
||||
done < <(find "$SECRETS_DIR/$SECRET_DIR" -maxdepth 1 -name "*.age.pub" -print0)
|
||||
|
||||
# Add global keys (so global hosts can also decrypt)
|
||||
while IFS= read -r -d '' key_file; do
|
||||
RECIPIENTS+=("$key_file")
|
||||
done < <(find "$SECRETS_DIR/global" -maxdepth 1 -name "*.age.pub" -print0 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# Add admin keys (for editing from workstations)
|
||||
if [ -d "$SECRETS_DIR/admins" ]; then
|
||||
while IFS= read -r -d '' key_file; do
|
||||
RECIPIENTS+=("$key_file")
|
||||
done < <(find "$SECRETS_DIR/admins" -maxdepth 1 -name "*.age.pub" -print0 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# Check if we have any recipients
|
||||
if [ ${#RECIPIENTS[@]} -eq 0 ]; then
|
||||
echo "Error: No recipient keys found!" >&2
|
||||
echo "Run ./update-age-keys.sh first to generate .age.pub files" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found ${#RECIPIENTS[@]} recipient key(s):" >&2
|
||||
for key in "${RECIPIENTS[@]}"; do
|
||||
echo " - $(basename "$key")" >&2
|
||||
done
|
||||
|
||||
# Create recipient list file (temporary)
|
||||
RECIPIENT_LIST=$(mktemp)
|
||||
trap "rm -f $RECIPIENT_LIST" EXIT
|
||||
|
||||
for key in "${RECIPIENTS[@]}"; do
|
||||
cat "$key" >> "$RECIPIENT_LIST"
|
||||
done
|
||||
|
||||
# Encrypt the secret
|
||||
if [ $# -gt 0 ]; then
|
||||
# Content provided as argument
|
||||
echo "$@" | age -R "$RECIPIENT_LIST" -o "$TARGET_FILE"
|
||||
else
|
||||
# Content from stdin
|
||||
age -R "$RECIPIENT_LIST" -o "$TARGET_FILE"
|
||||
fi
|
||||
|
||||
echo "✓ Created $TARGET_FILE" >&2
|
||||
echo " Edit with: ragenix -e secrets/$SECRET_PATH" >&2
|
||||
@@ -63,7 +63,7 @@ let
|
||||
|
||||
nameValuePair = name: value: { inherit name value; };
|
||||
|
||||
secretsPath = ./.;
|
||||
secretsPath = ./secrets;
|
||||
|
||||
# Helper to convert SSH public key content to age public key
|
||||
sshToAge =
|
||||
|
||||
8
secrets/usda-dash/default.nix
Normal file
8
secrets/usda-dash/default.nix
Normal file
@@ -0,0 +1,8 @@
|
||||
# Host-specific secret configuration for usda-dash
|
||||
{
|
||||
usda-vision-azure-env = {
|
||||
mode = "0600";
|
||||
owner = "root";
|
||||
group = "root";
|
||||
};
|
||||
}
|
||||
BIN
secrets/usda-dash/usda-vision-env.age
Normal file
BIN
secrets/usda-dash/usda-vision-env.age
Normal file
Binary file not shown.
Reference in New Issue
Block a user