feat: migrate to agenix for secret management

This commit is contained in:
Jet Pham 2026-03-05 15:10:30 -08:00
parent e7e8d154aa
commit 8e174ba500
No known key found for this signature in database
23 changed files with 234 additions and 120 deletions

1
.envrc
View file

@ -1 +1,2 @@
use flake
export RULES="$PWD/agenix.nix"

View file

@ -16,42 +16,47 @@ services:
## Deployment
This repository uses **untracked secrets**, so you must build the system locally before deploying.
Secrets are managed with [agenix](https://github.com/ryantm/agenix) — encrypted in git, decrypted on the server at runtime.
### 1. Setup Secrets
1. `cp secrets/secrets.nix.example secrets/secrets.nix`
2. Fill in the values (generate random keys, etc).
- `openssl rand -base64 32` is a good way to make a new key
- `tailscaleKey` must be a **Reusable** key from the Tailscale admin console.
### 2. Verify Configuration Locally
Because `secrets/secrets.nix` is untracked by git, standard `nix flake check` will fail.
To build the server configuration locally and ensure there are no syntax or evaluation errors before pushing to the server, run:
Key mapping is in `agenix.nix`. The `agenix` CLI and `RULES` env var are provided by the devShell via direnv.
```bash
nix build path:.#nixosConfigurations.extremist-software.config.system.build.toplevel --impure --dry-run
direnv allow
agenix -e secrets/forgejo-db.age
agenix -e secrets/stalwart-admin.age
agenix -e secrets/searx-env.age # SEARXNG_SECRET=<value>
agenix -e secrets/tailscale-key.age
agenix -e secrets/grafana-secret.age
agenix -e secrets/matrix-macaroon.age # macaroon_secret_key: "<value>"
agenix -e secrets/ntfy-admin-hash.age
agenix -e secrets/mymx-webhook.age
```
To edit an existing secret, run the same command again.
### 2. Verify Configuration
```bash
nix flake check
```
### 3. Initial Install (Wipe & Install)
Run this command to build and deploy. **Warning: Wipes the server disk.**
**Warning: Wipes the server disk.**
```bash
# Replace <TARGET_IP> with your server's IP
nix run github:nix-community/nixos-anywhere -- --store-paths \
$(nix build path:.#nixosConfigurations.extremist-software.config.system.build.diskoScript --impure --print-out-paths --no-link) \
$(nix build path:.#nixosConfigurations.extremist-software.config.system.build.toplevel --impure --print-out-paths --no-link) \
$(nix build path:.#nixosConfigurations.extremist-software.config.system.build.diskoScript --print-out-paths --no-link) \
$(nix build path:.#nixosConfigurations.extremist-software.config.system.build.toplevel --print-out-paths --no-link) \
root@<TARGET_IP> | tee install.log
```
### 4. Update Existing Server (No Wipe)
Once the server is running NixOS, use the `nhs` script to push updates. This repository provides `nhs` and `nh` via `direnv` (loaded from `flake.nix` devShell), so just run `direnv allow` first.
### 4. Update Existing Server
`nhs` and `nh` are provided via direnv.
```bash
# Update via Tailscale (uses nhs convenience script)
nhs
# Or manually via IP
nh os switch --hostname extremist-software --target-host root@<TARGET_IP> --impure path:.
```
repo uses `impure` build to load `secrets/secrets.nix` directly. no encrypted secrets in git.

13
agenix.nix Normal file
View file

@ -0,0 +1,13 @@
let
server = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAING219cDKTDLaZefmqvOHfXvYloA/ErsCGE0pM022vlB";
jet = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu";
in {
"secrets/forgejo-db.age".publicKeys = [ server jet ];
"secrets/stalwart-admin.age".publicKeys = [ server jet ];
"secrets/searx-env.age".publicKeys = [ server jet ];
"secrets/tailscale-key.age".publicKeys = [ server jet ];
"secrets/grafana-secret.age".publicKeys = [ server jet ];
"secrets/matrix-macaroon.age".publicKeys = [ server jet ];
"secrets/ntfy-admin-hash.age".publicKeys = [ server jet ];
"secrets/mymx-webhook.age".publicKeys = [ server jet ];
}

View file

@ -12,13 +12,19 @@
./modules/ntfy.nix
./modules/uptime-kuma.nix
# mymx module is imported via flake input in flake.nix
./secrets/secrets-scheme.nix
# Impure Secrets
./secrets/secrets.nix
];
# ... (rest of imports block replaced by ./secrets/secrets.nix being added to imports)
# Agenix secrets
age.secrets = {
forgejo-db.file = ./secrets/forgejo-db.age;
stalwart-admin = { file = ./secrets/stalwart-admin.age; owner = "stalwart-mail"; };
searx-env.file = ./secrets/searx-env.age;
tailscale-key.file = ./secrets/tailscale-key.age;
grafana-secret = { file = ./secrets/grafana-secret.age; owner = "grafana"; };
matrix-macaroon = { file = ./secrets/matrix-macaroon.age; owner = "matrix-synapse"; };
ntfy-admin-hash.file = ./secrets/ntfy-admin-hash.age;
mymx-webhook = { file = ./secrets/mymx-webhook.age; owner = "mymx"; };
};
# Bootloader
boot.loader.grub.enable = true;
@ -36,7 +42,7 @@
# Users
users.users.root.openssh.authorizedKeys.keys = [
config.mySecrets.sshPublicKey
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"
];
# SSH - Secure it
@ -77,9 +83,17 @@
clean.extraArgs = "--keep 2";
};
# Automatic upgrades
system.autoUpgrade = {
enable = true;
dates = "04:00";
allowReboot = false;
};
# System
system.stateVersion = "24.05";
nix.settings.experimental-features = [ "nix-command" "flakes" ];
services.postgresql.package = pkgs.postgresql_15;
nixpkgs.config.allowUnfree = true; # Allow unfree packages (Minecraft, etc.)
# Time
@ -89,15 +103,12 @@
zramSwap.enable = true;
zramSwap.memoryPercent = 50;
# Secrets handled via ./secrets.nix importing to config.mySecrets
environment.etc."secrets/tailscale-auth".text = config.mySecrets.tailscaleKey;
environment.etc."secrets/mymx-webhook".text = config.mySecrets.mymxWebhookSecret;
services.tailscale.authKeyFile = "/etc/secrets/tailscale-auth";
services.tailscale.authKeyFile = config.age.secrets.tailscale-key.path;
# MyMX
services.mymx = {
enable = true;
webhookSecretFile = "/etc/secrets/mymx-webhook";
webhookSecretFile = config.age.secrets.mymx-webhook.path;
};
# Allow Tailscale traffic

82
flake.lock generated
View file

@ -1,5 +1,50 @@
{
"nodes": {
"agenix": {
"inputs": {
"darwin": "darwin",
"home-manager": "home-manager",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1770165109,
"narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=",
"owner": "ryantm",
"repo": "agenix",
"rev": "b027ee29d959fda4b60b57566d64c98a202e0feb",
"type": "github"
},
"original": {
"owner": "ryantm",
"repo": "agenix",
"type": "github"
}
},
"darwin": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744478979,
"narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "43975d782b418ebf4969e9ccba82466728c2851b",
"type": "github"
},
"original": {
"owner": "lnl7",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
@ -20,6 +65,27 @@
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1745494811,
"narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"mymx": {
"inputs": {
"nixpkgs": [
@ -59,6 +125,7 @@
},
"root": {
"inputs": {
"agenix": "agenix",
"disko": "disko",
"mymx": "mymx",
"nixpkgs": "nixpkgs"
@ -84,6 +151,21 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -10,7 +10,8 @@
mymx.url = "git+https://git.extremist.software/jet/mymx";
mymx.inputs.nixpkgs.follows = "nixpkgs";
agenix.url = "github:ryantm/agenix";
agenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, disko, ... }@inputs: {
@ -20,6 +21,7 @@
modules = [
disko.nixosModules.disko
inputs.mymx.nixosModules.default
inputs.agenix.nixosModules.default
./disk-config.nix
./configuration.nix
@ -29,11 +31,12 @@
devShells.x86_64-linux.default = let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
deploy = pkgs.writeShellScriptBin "nhs" ''
nh os switch --hostname extremist-software --target-host root@extremist-software --impure path:. "$@"
nh os switch --hostname extremist-software --target-host root@extremist-software path:. "$@"
'';
in pkgs.mkShell {
packages = [
pkgs.nh
inputs.agenix.packages.x86_64-linux.default
deploy
];
};

View file

@ -170,5 +170,4 @@
# Ensure Caddy can read the certs too now that they are in the acme group
users.users.caddy.extraGroups = [ "acme" ];
networking.firewall.allowedTCPPorts = [ 80 443 ];
}

View file

@ -14,12 +14,8 @@
};
# You can configure SMTP here using secrets if needed
};
# Secret for DB password
settings.database.PASSWORD = config.mySecrets.forgejoDb;
database.passwordFile = config.age.secrets.forgejo-db.path;
};
services.postgresql = {
enable = true;
package = pkgs.postgresql_15;
};
services.postgresql.enable = true;
}

View file

@ -43,7 +43,7 @@
authentication.fallback-admin = {
user = "admin";
secret = config.mySecrets.stalwartAdmin;
secret = "%{file:/run/agenix/stalwart-admin}%";
};
};
};
@ -51,10 +51,4 @@
# Allow Stalwart to read the ACME certificate procured for Caddy
systemd.services.stalwart.serviceConfig.SupplementaryGroups = [ "acme" ];
# Open Firewalls for Mail
networking.firewall.allowedTCPPorts = [
993 # IMAP (Secure)
4190 # Sieve
8080 # Admin UI (Reverse proxied, but good to double check loopback)
];
}

View file

@ -3,6 +3,7 @@
{
services.matrix-synapse = {
enable = true;
extraConfigFiles = [ config.age.secrets.matrix-macaroon.path ];
settings = {
server_name = "extremist.software";
public_baseurl = "https://matrix.extremist.software";
@ -24,8 +25,6 @@
];
enable_registration = false;
registration_shared_secret = "extremist_software_admin_creation";
macaroon_secret_key = config.mySecrets.matrixMacaroon;
database = {
name = "psycopg2";
allow_unsafe_locale = true;

View file

@ -31,7 +31,7 @@
domain = "status.extremist.software";
};
security = {
secret_key = config.mySecrets.grafanaSecret;
secret_key = "$__file{/run/agenix/grafana-secret}";
};
};
provision = {

View file

@ -1,4 +1,4 @@
{ config, pkgs, ... }:
{ config, pkgs, lib, ... }:
{
services.ntfy-sh = {
@ -10,12 +10,22 @@
auth-file = "/var/lib/ntfy-sh/user.db";
auth-default-access = "deny-all";
enable-login = true;
auth-users = [
"jet:${config.mySecrets.ntfyAdminHash}:admin"
];
auth-access = [
"*:up*:write-only"
];
};
};
# Patch the generated config at runtime to inject the admin bcrypt hash
systemd.services.ntfy-sh = {
serviceConfig.RuntimeDirectory = "ntfy-sh";
serviceConfig.ExecStartPre = let
script = pkgs.writeShellScript "ntfy-patch-config" ''
cp /etc/ntfy/server.yml /run/ntfy-sh/server.yml
HASH=$(cat ${config.age.secrets.ntfy-admin-hash.path})
printf '\nauth-users:\n - "jet:%s:admin"\n' "$HASH" >> /run/ntfy-sh/server.yml
'';
in [ "+${script}" ];
serviceConfig.ExecStart = lib.mkForce "${pkgs.ntfy-sh}/bin/ntfy serve --config /run/ntfy-sh/server.yml";
};
}

View file

@ -5,11 +5,12 @@
enable = true;
package = pkgs.searxng;
redisCreateLocally = true;
environmentFile = config.age.secrets.searx-env.path;
settings = {
server = {
port = 8082;
bind_address = "127.0.0.1";
secret_key = config.mySecrets.searxKey;
secret_key = "@SEARXNG_SECRET@";
};
search = {
request_timeout = 1.5;

7
secrets/forgejo-db.age Normal file
View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg mtSxHYyX33fx/dUTpNGgu4ah3X/I6zTB0amu7Ji+iWU
6EXDWMEoDuDZ36rYqUR52IQFASZb5s0bm3KRyAKIXUg
-> ssh-ed25519 Ziw7aw zqjgjZGh9C3H/gpuLx+dUC9EngSoHB/feiyCgqss+F4
MyCY88yFfDSqAr0PbYSg/FbHo+B6rxXBPkVxczgW93E
--- qGC9Dxmqtgm92IqNd3azWYEtkMEwwWRNsuXow6oZjlE
ìX)1±s™tr(fæÕPµ,Û78Öƒ™ ŠVøÍÖ”·1õ1&%ŒÃ(¶Fë-úˆD"(7ów=äéþîßxmÙžãväS

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg 6TMM/HxgvFAlsOOJuEhoKfnN5CcjEvck9BKUXTNQsjk
Y0G/GK6+t5jFK+cPqovD/oxs1ZLRAprstr27pZ6mb0c
-> ssh-ed25519 Ziw7aw TQWn+XR8FHTv2+ol4id6hcL3C+Jk92jsB2hHFacoD3o
fr+xO4DvOHLSPn05u6JZi++wBABw0z9WqghdwJ62pz0
--- PS3uOR8IZPAUoS8XA5WsBcCsLEfTxwS+vW6eHdZy3Fo
£È¯Ê”¼1È/Ûœ<C39B>%®öÆr¹Ë)+í°Ãý0ÚWg¯?hÌJÍYãåÄœ¢Û®öçiÝŒ%[ê=æyÔd·à˜w§€¦õ,xS

View file

@ -0,0 +1,8 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg gbXdVVx0trOnWR5v3V4jjfP58B5jXWKwoi8Da2JKx1Y
s8rPw207y5TzjlLPXm+gG+eQqBqh6geeFvnn4iH3s84
-> ssh-ed25519 Ziw7aw 99vuNfyVaByhU5bwxJTuoxeYoQWryP36ddAd/fZOhBY
hdtoLgoFVslZpm9luo3Edns4hYMQESIReI7laFDjeOQ
--- Zgwav28km0/q1wX2FZDT5xpVQurkcjqu0lmOWr8ZH38
K)-¢áÊy•˜.uƒì€è³%Tµo(ñ:^ßE°p"ëé9>ºj´#ÔF­ë•ž8Wž-ñ S1jIò§4·Án
2

7
secrets/mymx-webhook.age Normal file
View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg EC9vi+nqoSqUHET3/4fWoiuW9vTZo5XOB1dc+Fe36U0
FYKWAiLaAbotst3AuOulpgqAg+JHUqD3uWWLk7hxrH8
-> ssh-ed25519 Ziw7aw naV+WKfldJhOnIzz13Q9zKSK+z+oRhiVfeEYuG+dtS0
/GLmF3ws0aUsSVTAv9zzzD+8Cp/IkMlHWFzv1CbgSiM
--- PdqmGwHvR/R0tqf46e1ZJl/QIzB1qadFtNyONpoQl30
wnÿ4Âò@ŠýÐÞD~Ír³×Ë*þj·®!„-*½ûv})0<30>±ú´÷ÞÏÉFÛ7)®}rá/>Häè/3åS$ }ÅÙµìû«@¯Ð

View file

@ -0,0 +1,8 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg Cccnzwl3XTJOW5+IuxDAsiI0L8Fy8JhJnpdERg9qgXU
vgvQdUbmwRna+gLjGsmsheGGeG2KIxsWoDw4XAVSjEA
-> ssh-ed25519 Ziw7aw vMnvy4HgMvhwALtUI14DmX6LbQiLXROINbJPlVfoW0g
FGxDYfiejy2a5W9eZKww1YgQ3mQFTj/mORwBwTsEW80
--- lThDR400zmmiBqnNmi2QKp2l3z3wCZ0jAxqIROLWn74
?¿3JÀ4zýrˆK
œk×Ï[ˆ€Àå“5D/Ò×DTX+lü<1F>©frjг„± ½v©Î€›ñ³,ø…¤•#ê“z*}*Q°Ç¤Jèœ´åÆ¡ŽX1<JÑ¿n<>

9
secrets/searx-env.age Normal file
View file

@ -0,0 +1,9 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg s5orwA5GrqKWguh/hIhdJGyUP+Vx7iGqoQKuEO48DiY
K+CrOTAFATdTsax+GwQBjJkni4IYDnfPdsVop8eMkKs
-> ssh-ed25519 Ziw7aw 27Zr3vWFaQNfeTxJmNajNkigC5RUcwgz6Qs7183fUTM
Bmj69hGO8tIZUJG5tiXqZHy+Ft6T5J2iJAYIxyYxZj8
--- rC5PWCFkjuuPrSWRImrY7IzODjxevS30MFSXdV5qpG4
#N¥<4E>R!F”²3{
Š1bF?ùœÇ¯ßg!o}$…iR½ƒ5øÞ×
¶ûçjÈõýMÿgÕqµÝÈS,­_r:ªqf

View file

@ -1,45 +0,0 @@
{ lib, ... }:
with lib;
{
options.mySecrets = {
forgejoDb = mkOption {
type = types.str;
description = "Forgejo Database Password";
};
stalwartAdmin = mkOption {
type = types.str;
description = "Stalwart Mail Admin Password";
};
searxKey = mkOption {
type = types.str;
description = "Searx Secret Key";
};
tailscaleKey = mkOption {
type = types.str;
description = "Tailscale Auth Key";
};
sshPublicKey = mkOption {
type = types.str;
description = "SSH Public Key for Root User";
};
grafanaSecret = mkOption {
type = types.str;
description = "Grafana Secret Key for security";
};
matrixMacaroon = mkOption {
type = types.str;
description = "Macaroon Secret Key for Matrix Synapse";
};
ntfyAdminHash = mkOption {
type = types.str;
description = "Bcrypt hash for ntfy admin user";
};
mymxWebhookSecret = mkOption {
type = types.str;
description = "MyMX Webhook Secret for signature verification";
};
};
}

View file

@ -1,17 +0,0 @@
{ pkgs, config, lib, ... }:
{
# Copy this file to secrets.nix and fill in real values
mySecrets = {
forgejoDb = "changeme_forgejo_db";
stalwartAdmin = "changeme_stalwart_admin";
searxKey = "changeme_searx_secret";
minecraftRcon = "changeme_rcon";
tailscaleKey = "tskey-auth-PLACEHOLDER";
sshPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...";
grafanaSecret = "changeme_grafana_secret";
matrixMacaroon = "changeme_matrix_macaroon_secret_key";
ntfyAdminHash = "changeme_bcrypt_hash_from_ntfy_user_hash";
mymxWebhookSecret = "changeme_mymx_webhook_secret";
};
}

View file

@ -0,0 +1,9 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg E7BMWjT2cbnomhydZCaRs5EMKoDGyU9O+NAvKHjflzs
8yl7y2iXNrBuCyT05sOatAHiJhizUSFgFJt0NlMZ9pY
-> ssh-ed25519 Ziw7aw PTAzjpRIfFk86q3docaVsh4CbXjDiCNJR2Of8YAYSBQ
5WLY3czA6TKBJyTMwGVxSR7kuIVxBDMaKZ41VYgGhN8
--- DHfY8BOaO+vb2MYxX/3XbgAIlwilFEPLRGUlZGJh1g0
<04>{<7B>-L^ツ粮8ホハ;ネヌモヌ碓
・7Лy囹aユ]€<EFBE9A>,<2C> jEス\<5C><>\セ
:"yX<゙ 7Xネオ綏ユ浜M

View file

@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 uKftJg c78IHZJHcr9y//w/tqXHsuwqPjclpCPeGUzCQ1Huwkw
h/3PruYSzkFbrGPPLrYpqoo+btj2NAHS0BlJk//U8x0
-> ssh-ed25519 Ziw7aw O/aFm27iQeYXA04hqRNGcoUy0JmAAKDLsK1Bp/p/miY
EBqXc31Ymh3YgjagBvICwQvX6KKwkkMF3Tv7XqsAvPs
--- sIkeKQZHLKTLXEVZdwmP/FpjbUWyyIZYx2/nKswFWoQ
ö6¥àô§™v<>†Iú.`<60>\cZÒÕB³á;»‰x«mHR©€3ÕoÁ§· Ó£T«qÇeÐldÇ"'«ý£\I]T2ÑKõl ùú¾¯§¨â~ÜÂO±B0Æ