feat: Ragenix secret management per host
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:
57
flake.lock
generated
57
flake.lock
generated
@@ -239,6 +239,24 @@
|
||||
"inputs": {
|
||||
"systems": "systems_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_4": {
|
||||
"inputs": {
|
||||
"systems": "systems_5"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
@@ -636,6 +654,7 @@
|
||||
"nixos-wsl": "nixos-wsl",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-old-kernel": "nixpkgs-old-kernel",
|
||||
"usda-vision": "usda-vision",
|
||||
"vscode-server": "vscode-server"
|
||||
}
|
||||
},
|
||||
@@ -720,6 +739,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_5": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -742,13 +776,34 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"vscode-server": {
|
||||
"usda-vision": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_3",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769795328,
|
||||
"narHash": "sha256-zrmwORj8qk2ONDYFs0ZpzdVLxnolMebNrDIfV8KoNao=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "20a01c89afd8e54fba2f345fb9d72eec1cbafda4",
|
||||
"revCount": 118,
|
||||
"type": "git",
|
||||
"url": "https://git.factory.uga.edu/MODEL/usda-vision.git"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.factory.uga.edu/MODEL/usda-vision.git"
|
||||
}
|
||||
},
|
||||
"vscode-server": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_4",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753541826,
|
||||
"narHash": "sha256-foGgZu8+bCNIGeuDqQ84jNbmKZpd+JvnrL2WlyU4tuU=",
|
||||
|
||||
@@ -59,6 +59,12 @@
|
||||
url = "github:nix-community/NixOS-WSL/main";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# USDA Vision Dashboard application
|
||||
usda-vision = {
|
||||
url = "git+https://git.factory.uga.edu/MODEL/usda-vision.git";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
||||
@@ -129,8 +129,8 @@
|
||||
};
|
||||
"usda-dash".external = {
|
||||
url = "https://git.factory.uga.edu/MODEL/usda-dash-config.git";
|
||||
rev = "dab32f5884895cead0fae28cb7d88d17951d0c12";
|
||||
submodules = true;
|
||||
rev = "9d2783981de95bdaaf46a8f0743245566b028d64";
|
||||
submodules = false;
|
||||
};
|
||||
};
|
||||
overrides = {
|
||||
|
||||
225
secrets/README.md
Normal file
225
secrets/README.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Secrets Management with Agenix
|
||||
|
||||
This directory contains age-encrypted secrets for Athenix hosts. Secrets are automatically loaded based on directory structure.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
secrets/
|
||||
├── global/ # Secrets installed on ALL systems
|
||||
│ ├── default.nix # Optional: Custom config for global secrets
|
||||
│ └── example.age # Decrypted to /run/agenix/example on all hosts
|
||||
├── nix-builder/ # Secrets only for nix-builder host
|
||||
│ ├── default.nix # Optional: Custom config for nix-builder secrets
|
||||
│ └── ssh_host_ed25519_key.age
|
||||
└── usda-dash/ # Secrets only for usda-dash host
|
||||
└── ssh_host_ed25519_key.age
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Global secrets** (`./secrets/global/*.age`) are installed on every system
|
||||
2. **Host-specific secrets** (`./secrets/{hostname}/*.age`) are only installed on matching hosts
|
||||
3. Only `.age` encrypted files are loaded; `.pub` public keys are ignored
|
||||
4. Secrets are decrypted at boot to `/run/agenix/{secret-name}` with mode `0400` and owner `root:root`
|
||||
5. **Custom configurations** can be defined in `default.nix` files within each directory
|
||||
|
||||
## Creating Secrets
|
||||
|
||||
### 1. Generate Age Keys
|
||||
|
||||
For a new host, generate an age identity:
|
||||
```bash
|
||||
# On the target system
|
||||
mkdir -p /etc/age
|
||||
age-keygen -o /etc/age/identity.key
|
||||
chmod 600 /etc/age/identity.key
|
||||
```
|
||||
|
||||
Or use SSH host keys (automatically done by Athenix):
|
||||
```bash
|
||||
# Get the age public key from SSH host key
|
||||
nix shell nixpkgs#ssh-to-age -c sh -c 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
|
||||
```
|
||||
|
||||
### 2. Store Public Keys
|
||||
|
||||
Save the public key to `secrets/{hostname}/` for reference:
|
||||
```bash
|
||||
# Example for nix-builder
|
||||
echo "age1..." > secrets/nix-builder/identity.pub
|
||||
```
|
||||
|
||||
Or from SSH host key:
|
||||
```bash
|
||||
cat /etc/ssh/ssh_host_ed25519_key.pub > secrets/nix-builder/ssh_host_ed25519_key.pub
|
||||
```
|
||||
|
||||
### 3. Encrypt Secrets
|
||||
|
||||
Encrypt a secret for specific hosts:
|
||||
```bash
|
||||
# For a single host
|
||||
age -r age1publickey... -o secrets/nix-builder/my-secret.age <<< "secret value"
|
||||
|
||||
# For multiple hosts (recipient list)
|
||||
age -R recipients.txt -o secrets/global/shared-secret.age < plaintext-file
|
||||
|
||||
# Using SSH public keys
|
||||
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)
|
||||
|
||||
The `ragenix` CLI tool simplifies secret management:
|
||||
|
||||
```bash
|
||||
# Install ragenix
|
||||
nix shell github:yaxitech/ragenix
|
||||
|
||||
# Edit a secret (creates if doesn't exist)
|
||||
ragenix -e secrets/global/example.age
|
||||
|
||||
# Re-key secrets after adding/removing hosts
|
||||
ragenix -r
|
||||
```
|
||||
|
||||
Create a `secrets.nix` file in the repository root to define recipients:
|
||||
```nix
|
||||
# secrets.nix
|
||||
let
|
||||
# System public keys (age format)
|
||||
nix-builder = "age1...";
|
||||
usda-dash = "age1...";
|
||||
|
||||
# User keys for editing secrets
|
||||
admin = "age1...";
|
||||
|
||||
allHosts = [ nix-builder usda-dash ];
|
||||
in
|
||||
{
|
||||
"secrets/global/example.age".publicKeys = allHosts ++ [ admin ];
|
||||
"secrets/nix-builder/ssh_host_key.age".publicKeys = [ nix-builder admin ];
|
||||
"secrets/usda-dash/ssh_host_key.age".publicKeys = [ usda-dash admin ];
|
||||
}
|
||||
```
|
||||
|
||||
## Using Secrets in Configuration
|
||||
|
||||
Secrets are automatically loaded. Reference them in your NixOS configuration:
|
||||
|
||||
```nix
|
||||
# Example: Using a secret for a service
|
||||
services.myservice = {
|
||||
enable = true;
|
||||
passwordFile = config.age.secrets.my-password.path; # /run/agenix/my-password
|
||||
};
|
||||
|
||||
# Example: Setting up SSH host key from secret
|
||||
services.openssh = {
|
||||
hostKeys = [{
|
||||
path = config.age.secrets.ssh_host_ed25519_key.path;
|
||||
type = "ed25519";
|
||||
}];
|
||||
};
|
||||
```
|
||||
|
||||
## Custom Secret Configuration
|
||||
|
||||
For secrets needing custom permissions, use `athenix.sw.secrets.extraSecrets`:
|
||||
|
||||
```nix
|
||||
# In inventory.nix or host config
|
||||
athenix.sw.secrets.extraSecrets = {
|
||||
"nginx-cert" = {
|
||||
file = ./secrets/custom/cert.age;
|
||||
mode = "0440";
|
||||
owner = "nginx";
|
||||
group = "nginx";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Using default.nix in Secret Directories
|
||||
|
||||
Alternatively, create a `default.nix` file in the secret directory to configure all secrets in that directory:
|
||||
|
||||
```nix
|
||||
# secrets/global/default.nix
|
||||
{
|
||||
"example" = {
|
||||
mode = "0440"; # Custom file mode (default: "0400")
|
||||
owner = "nginx"; # Custom owner (default: "root")
|
||||
group = "nginx"; # Custom group (default: "root")
|
||||
path = "/run/secrets/example"; # Custom path (default: /run/agenix/{name})
|
||||
};
|
||||
|
||||
"api-key" = {
|
||||
mode = "0400";
|
||||
owner = "myservice";
|
||||
group = "myservice";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The `default.nix` file should return an attribute set where:
|
||||
- **Keys** are secret names (without the `.age` extension)
|
||||
- **Values** are configuration objects with optional fields:
|
||||
- `mode` - File permissions (string, e.g., `"0440"`)
|
||||
- `owner` - File owner (string, e.g., `"nginx"`)
|
||||
- `group` - File group (string, e.g., `"nginx"`)
|
||||
- `path` - Custom installation path (string, e.g., `"/custom/path"`)
|
||||
|
||||
Secrets not listed in `default.nix` will use default settings.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit unencrypted secrets** - Only `.age` and `.pub` files belong in this directory
|
||||
2. **Use host-specific secrets** when possible - Limit exposure by using hostname directories
|
||||
3. **Rotate secrets regularly** - Re-encrypt with new keys periodically
|
||||
4. **Backup age identity keys** - Store `/etc/age/identity.key` securely offline
|
||||
5. **Use SSH keys** - Leverage existing SSH host keys for age encryption when possible
|
||||
6. **Pin to commits** - When using external secrets modules, always use `rev = "commit-hash"`
|
||||
|
||||
## Converting SSH Keys to Age Format
|
||||
|
||||
```bash
|
||||
# Convert SSH public key to age public key
|
||||
nix shell nixpkgs#ssh-to-age -c ssh-to-age < secrets/nix-builder/ssh_host_ed25519_key.pub
|
||||
|
||||
# Convert SSH private key to age identity (for editing secrets)
|
||||
nix shell nixpkgs#ssh-to-age -c ssh-to-age -private-key -i ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
## Disabling Automatic Secrets
|
||||
|
||||
To disable automatic secret loading:
|
||||
|
||||
```nix
|
||||
# In inventory.nix or host config
|
||||
athenix.sw.secrets.enable = false;
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Secret not found
|
||||
- Ensure the `.age` file exists in `secrets/global/` or `secrets/{hostname}/`
|
||||
- Check `hostname` matches directory name: `echo $HOSTNAME` on the target system
|
||||
- Run `nix flake check` to verify secrets are discovered
|
||||
|
||||
### Permission denied
|
||||
- Verify secret permissions in `/run/agenix/`
|
||||
- Check if custom permissions are needed (use `extraSecrets`)
|
||||
- Ensure the service user/group has access to the secret file
|
||||
|
||||
### Age decrypt failed
|
||||
- Verify the host's age identity exists: `ls -l /etc/age/identity.key`
|
||||
- Check that the secret was encrypted with the host's public key
|
||||
- Confirm SSH host key hasn't changed (would change derived age key)
|
||||
|
||||
## References
|
||||
|
||||
- [ragenix GitHub](https://github.com/yaxitech/ragenix)
|
||||
- [agenix upstream](https://github.com/ryantm/agenix)
|
||||
- [age encryption tool](https://age-encryption.org/)
|
||||
10
secrets/global/default.nix
Normal file
10
secrets/global/default.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
# Custom configuration for global secrets
|
||||
# This file defines additional options for secrets beyond the auto-discovered .age files
|
||||
{
|
||||
# Example: Custom permissions for the example secret
|
||||
example = {
|
||||
mode = "0440";
|
||||
owner = "root";
|
||||
group = "root";
|
||||
};
|
||||
}
|
||||
6
secrets/global/example.age
Normal file
6
secrets/global/example.age
Normal file
@@ -0,0 +1,6 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 2lgS/8zamn6WD6eKM+Mcms+TY/PlzLr43WoWk6EZ7BU
|
||||
/H6DRcTul3ZtlfyYRFS6v+YZIYYi/sNdkaqZYdKbBIE
|
||||
--- NSMzaAQArlhVdSuRICYD6lAiq/nOd2c5n/wcye4FXao
|
||||
D]t=<3D>m<EFBFBD>
|
||||
<EFBFBD><EFBFBD><EFBFBD>@<40><1A>
|
||||
1
secrets/global/hunter_halloran_key.pub
Normal file
1
secrets/global/hunter_halloran_key.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBC7xzHxY2BfFUybMvG4wHSF9oEAGzRiLTFEndLvWV/X hdh20267@engr733847d.engr.uga.edu
|
||||
10
secrets/nix-builder/default.nix
Normal file
10
secrets/nix-builder/default.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
# Host-specific secret configuration for nix-builder
|
||||
{
|
||||
# SSH host key should be readable by sshd
|
||||
ssh_host_ed25519_key = {
|
||||
mode = "0600";
|
||||
owner = "root";
|
||||
group = "root";
|
||||
path = "/etc/ssh/ssh_host_ed25519_key";
|
||||
};
|
||||
}
|
||||
1
secrets/nix-builder/ssh_host_ed25519_key.pub
Normal file
1
secrets/nix-builder/ssh_host_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHW0Hxldo3EkniotitzJ2XiZbIq9Rfo27yI1+sBrgG39 root@nix-builder
|
||||
52
secrets/secrets.nix
Normal file
52
secrets/secrets.nix
Normal file
@@ -0,0 +1,52 @@
|
||||
# ============================================================================
|
||||
# Agenix Secret Recipients Configuration
|
||||
# ============================================================================
|
||||
# This file defines which age public keys can decrypt which secrets.
|
||||
# Used by the ragenix CLI tool for encrypting/editing secrets.
|
||||
#
|
||||
# Usage:
|
||||
# ragenix -e secrets/global/example.age # Edit/create secret
|
||||
# ragenix -r # Re-key all secrets
|
||||
|
||||
let
|
||||
# ========== System Public Keys (Age Format) ==========
|
||||
# Convert SSH host keys to age format:
|
||||
# ssh-to-age < secrets/{hostname}/ssh_host_ed25519_key.pub
|
||||
|
||||
# Example (replace with actual age keys):
|
||||
# nix-builder = "age1...";
|
||||
# usda-dash = "age1...";
|
||||
|
||||
# ========== User Public Keys (for editing secrets) ==========
|
||||
# These are personal age keys for administrators who need to edit secrets
|
||||
# Generate with: age-keygen
|
||||
|
||||
# Example:
|
||||
# admin1 = "age1...";
|
||||
# admin2 = "age1...";
|
||||
|
||||
# ========== Host Groups ==========
|
||||
allHosts = [
|
||||
# Add all system keys here
|
||||
# nix-builder
|
||||
# usda-dash
|
||||
];
|
||||
|
||||
admins = [
|
||||
# Add all admin user keys here
|
||||
# admin1
|
||||
# admin2
|
||||
];
|
||||
|
||||
in
|
||||
{
|
||||
# Global secrets (encrypted for all hosts + admins)
|
||||
# "secrets/global/example.age".publicKeys = allHosts ++ admins;
|
||||
|
||||
# Host-specific secrets (encrypted for specific host + admins)
|
||||
# "secrets/nix-builder/ssh_host_ed25519_key.age".publicKeys = [ nix-builder ] ++ admins;
|
||||
# "secrets/usda-dash/ssh_host_ed25519_key.age".publicKeys = [ usda-dash ] ++ admins;
|
||||
|
||||
# NOTE: Until you populate the keys above, you can create secrets with:
|
||||
# age -r <public-key> -o secrets/path/to/secret.age <<< "secret content"
|
||||
}
|
||||
1
secrets/usda-dash/ssh_host_ed25519_key.pub
Normal file
1
secrets/usda-dash/ssh_host_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHI73LOEK2RgfjhZWpryntlLbx0LouHrhQ6v0vZu4Etr root@usda-dash
|
||||
@@ -26,6 +26,7 @@ in
|
||||
./gc.nix
|
||||
./updater.nix
|
||||
./update-ref.nix
|
||||
./secrets.nix
|
||||
./desktop
|
||||
./headless
|
||||
./builders
|
||||
|
||||
210
sw/secrets.nix
Normal file
210
sw/secrets.nix
Normal file
@@ -0,0 +1,210 @@
|
||||
# ============================================================================
|
||||
# Automatic Secret Management with Agenix
|
||||
# ============================================================================
|
||||
# This module automatically loads age-encrypted secrets from ./secrets based on
|
||||
# the hostname. Secrets are organized by directory:
|
||||
# - ./secrets/global/ -> Installed on ALL systems
|
||||
# - ./secrets/{hostname}/ -> Installed only on matching host
|
||||
#
|
||||
# Secret files should be .age encrypted files. Public keys (.pub) are ignored.
|
||||
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.athenix.sw;
|
||||
secretsPath = ../secrets;
|
||||
|
||||
# Get the current hostname
|
||||
hostname = config.networking.hostName;
|
||||
|
||||
# Read all directories in ./secrets
|
||||
secretDirs =
|
||||
if builtins.pathExists secretsPath then
|
||||
builtins.readDir secretsPath
|
||||
else
|
||||
{ };
|
||||
|
||||
# Filter to only directories (excludes files)
|
||||
isDirectory = name: type: type == "directory";
|
||||
directories = lib.filterAttrs isDirectory secretDirs;
|
||||
|
||||
# Read secrets from a specific directory
|
||||
readSecretsFromDir =
|
||||
dirName:
|
||||
let
|
||||
dirPath = secretsPath + "/${dirName}";
|
||||
files = builtins.readDir dirPath;
|
||||
|
||||
# Check if there's a default.nix with custom secret configurations
|
||||
hasDefaultNix = files ? "default.nix";
|
||||
customConfigs = if hasDefaultNix then import (dirPath + "/default.nix") else { };
|
||||
|
||||
# Only include .age files (exclude .pub public keys and other files)
|
||||
secretFiles = lib.filterAttrs (
|
||||
name: type: type == "regular" && lib.hasSuffix ".age" name
|
||||
) files;
|
||||
in
|
||||
lib.mapAttrs' (
|
||||
name: _:
|
||||
let
|
||||
# Remove .age extension for the secret name
|
||||
secretName = lib.removeSuffix ".age" name;
|
||||
|
||||
# Get custom config for this secret if defined
|
||||
customConfig = customConfigs.${secretName} or { };
|
||||
|
||||
# Base configuration with file path
|
||||
baseConfig = {
|
||||
file = dirPath + "/${name}";
|
||||
};
|
||||
in
|
||||
lib.nameValuePair secretName (baseConfig // customConfig)
|
||||
) secretFiles;
|
||||
|
||||
# Read public keys from a specific directory and map to private key paths
|
||||
readIdentityPathsFromDir =
|
||||
dirName:
|
||||
let
|
||||
dirPath = secretsPath + "/${dirName}";
|
||||
files = if builtins.pathExists dirPath then builtins.readDir dirPath else { };
|
||||
# Only include .pub public key files
|
||||
pubKeyFiles = lib.filterAttrs (
|
||||
name: type: type == "regular" && lib.hasSuffix ".pub" name
|
||||
) files;
|
||||
in
|
||||
lib.mapAttrsToList (
|
||||
name: _:
|
||||
let
|
||||
# Map public key filename to expected private key location
|
||||
baseName = lib.removeSuffix ".pub" name;
|
||||
filePath = dirPath + "/${name}";
|
||||
fileContent = builtins.readFile filePath;
|
||||
# Check if it's an SSH key by looking at the content
|
||||
isSSHKey = lib.hasPrefix "ssh-" fileContent || lib.hasPrefix "ecdsa-" fileContent;
|
||||
in
|
||||
if lib.hasPrefix "ssh_host_" name then
|
||||
# SSH host keys: ssh_host_ed25519_key.pub -> /etc/ssh/ssh_host_ed25519_key
|
||||
"/etc/ssh/${baseName}"
|
||||
else if name == "identity.pub" then
|
||||
# Standard age identity: identity.pub -> /etc/age/identity.key
|
||||
"/etc/age/identity.key"
|
||||
else if isSSHKey then
|
||||
# Other SSH keys (user keys, etc.): hunter_halloran_key.pub -> /etc/ssh/hunter_halloran_key
|
||||
"/etc/ssh/${baseName}"
|
||||
else
|
||||
# Generic age keys: key.pub -> /etc/age/key
|
||||
"/etc/age/${baseName}"
|
||||
) pubKeyFiles;
|
||||
|
||||
# Determine which secrets apply to this host
|
||||
applicableSecrets =
|
||||
let
|
||||
# Global secrets apply to all hosts
|
||||
globalSecrets = if directories ? "global" then readSecretsFromDir "global" else { };
|
||||
|
||||
# Host-specific secrets
|
||||
hostSecrets = if directories ? ${hostname} then readSecretsFromDir hostname else { };
|
||||
in
|
||||
globalSecrets // hostSecrets; # Host-specific secrets override global if same name
|
||||
|
||||
# Determine which identity paths (private keys) to use for decryption
|
||||
identityPaths =
|
||||
let
|
||||
# Global identity paths (keys in global/ that all hosts can use)
|
||||
globalPaths = if directories ? "global" then readIdentityPathsFromDir "global" else [ ];
|
||||
|
||||
# Host-specific identity paths
|
||||
hostPaths = if directories ? ${hostname} then readIdentityPathsFromDir hostname else [ ];
|
||||
|
||||
# Default paths that NixOS/agenix use
|
||||
defaultPaths = [
|
||||
"/etc/ssh/ssh_host_rsa_key"
|
||||
"/etc/ssh/ssh_host_ed25519_key"
|
||||
"/etc/age/identity.key"
|
||||
];
|
||||
|
||||
# Combine all paths and remove duplicates
|
||||
allPaths = lib.unique (defaultPaths ++ globalPaths ++ hostPaths);
|
||||
in
|
||||
allPaths;
|
||||
in
|
||||
{
|
||||
options.athenix.sw.secrets = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Enable automatic secret management using agenix.
|
||||
|
||||
Secrets are loaded from ./secrets based on directory structure:
|
||||
- ./secrets/global/ -> All systems
|
||||
- ./secrets/{hostname}/ -> Specific host only
|
||||
|
||||
Only .age encrypted files are loaded; .pub files are ignored.
|
||||
'';
|
||||
};
|
||||
|
||||
extraSecrets = mkOption {
|
||||
type = types.attrsOf (types.submodule {
|
||||
options = {
|
||||
file = mkOption {
|
||||
type = types.path;
|
||||
description = "Path to the encrypted secret file";
|
||||
};
|
||||
mode = mkOption {
|
||||
type = types.str;
|
||||
default = "0400";
|
||||
description = "Permissions mode for the decrypted secret";
|
||||
};
|
||||
owner = mkOption {
|
||||
type = types.str;
|
||||
default = "root";
|
||||
description = "Owner of the decrypted secret file";
|
||||
};
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "root";
|
||||
description = "Group of the decrypted secret file";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = { };
|
||||
description = ''
|
||||
Additional secrets to define manually, beyond the auto-discovered ones.
|
||||
Use this for secrets that need custom permissions or are stored elsewhere.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"my-secret" = {
|
||||
file = ./secrets/custom/secret.age;
|
||||
mode = "0440";
|
||||
owner = "nginx";
|
||||
group = "nginx";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (cfg.enable && cfg.secrets.enable) {
|
||||
# Auto-discovered secrets with default permissions
|
||||
age.secrets = applicableSecrets // cfg.secrets.extraSecrets;
|
||||
|
||||
# Configure identity paths for decryption based on discovered public keys
|
||||
# These are added in addition to agenix's defaults
|
||||
age.identityPaths = identityPaths;
|
||||
|
||||
# Optional: Add assertion to warn if no secrets found
|
||||
warnings =
|
||||
let
|
||||
hasSecrets = (builtins.length (builtins.attrNames applicableSecrets)) > 0;
|
||||
in
|
||||
lib.optional (!hasSecrets) "No age-encrypted secrets found in ./secrets/global/ or ./secrets/${hostname}/";
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user