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
226 lines
6.7 KiB
Markdown
226 lines
6.7 KiB
Markdown
# 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/)
|