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": {
|
"inputs": {
|
||||||
"systems": "systems_4"
|
"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": {
|
"locked": {
|
||||||
"lastModified": 1681202837,
|
"lastModified": 1681202837,
|
||||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||||
@@ -636,6 +654,7 @@
|
|||||||
"nixos-wsl": "nixos-wsl",
|
"nixos-wsl": "nixos-wsl",
|
||||||
"nixpkgs": "nixpkgs_2",
|
"nixpkgs": "nixpkgs_2",
|
||||||
"nixpkgs-old-kernel": "nixpkgs-old-kernel",
|
"nixpkgs-old-kernel": "nixpkgs-old-kernel",
|
||||||
|
"usda-vision": "usda-vision",
|
||||||
"vscode-server": "vscode-server"
|
"vscode-server": "vscode-server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -720,6 +739,21 @@
|
|||||||
"type": "github"
|
"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": {
|
"treefmt-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -742,13 +776,34 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vscode-server": {
|
"usda-vision": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils_3",
|
"flake-utils": "flake-utils_3",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"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": {
|
"locked": {
|
||||||
"lastModified": 1753541826,
|
"lastModified": 1753541826,
|
||||||
"narHash": "sha256-foGgZu8+bCNIGeuDqQ84jNbmKZpd+JvnrL2WlyU4tuU=",
|
"narHash": "sha256-foGgZu8+bCNIGeuDqQ84jNbmKZpd+JvnrL2WlyU4tuU=",
|
||||||
|
|||||||
@@ -59,6 +59,12 @@
|
|||||||
url = "github:nix-community/NixOS-WSL/main";
|
url = "github:nix-community/NixOS-WSL/main";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
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 =
|
outputs =
|
||||||
|
|||||||
@@ -129,8 +129,8 @@
|
|||||||
};
|
};
|
||||||
"usda-dash".external = {
|
"usda-dash".external = {
|
||||||
url = "https://git.factory.uga.edu/MODEL/usda-dash-config.git";
|
url = "https://git.factory.uga.edu/MODEL/usda-dash-config.git";
|
||||||
rev = "dab32f5884895cead0fae28cb7d88d17951d0c12";
|
rev = "9d2783981de95bdaaf46a8f0743245566b028d64";
|
||||||
submodules = true;
|
submodules = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
overrides = {
|
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
|
./gc.nix
|
||||||
./updater.nix
|
./updater.nix
|
||||||
./update-ref.nix
|
./update-ref.nix
|
||||||
|
./secrets.nix
|
||||||
./desktop
|
./desktop
|
||||||
./headless
|
./headless
|
||||||
./builders
|
./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