diff --git a/Scripts/check-forge-host.sh b/Scripts/check-forge-host.sh index 90a6dcf..f4d646d 100755 --- a/Scripts/check-forge-host.sh +++ b/Scripts/check-forge-host.sh @@ -157,6 +157,13 @@ done echo "== intake ==" ls -l /var/lib/burrow/intake || true +if [[ "${EXPECT_TAILNET}" == "1" ]]; then + echo "== agenix ==" + ls -l /run/agenix || true + test -s /run/agenix/burrowAuthentikEnv + test -s /run/agenix/burrowHeadscaleOidcClientSecret +fi + if command -v curl >/dev/null 2>&1; then echo "== http-local ==" curl -fsS -o /dev/null -w 'forgejo_login %{http_code}\n' http://127.0.0.1:3000/user/login diff --git a/flake.lock b/flake.lock index 677bd0d..599e193 100644 --- a/flake.lock +++ b/flake.lock @@ -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": [ @@ -19,7 +64,7 @@ }, "flake-utils": { "inputs": { - "systems": "systems" + "systems": "systems_2" }, "locked": { "lastModified": 1731533236, @@ -45,6 +90,27 @@ "url": "https://codeload.github.com/apricote/hcloud-upload-image/tar.gz/v1.3.0" } }, + "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" + } + }, "nixpkgs": { "locked": { "lastModified": 1773389992, @@ -59,6 +125,7 @@ }, "root": { "inputs": { + "agenix": "agenix", "disko": "disko", "flake-utils": "flake-utils", "hcloud-upload-image-src": "hcloud-upload-image-src", @@ -79,6 +146,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "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", diff --git a/flake.nix b/flake.nix index 38e38b6..5814c19 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,10 @@ inputs = { nixpkgs.url = "tarball+https://codeload.github.com/NixOS/nixpkgs/tar.gz/nixos-unstable"; flake-utils.url = "tarball+https://codeload.github.com/numtide/flake-utils/tar.gz/main"; + agenix = { + url = "github:ryantm/agenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; disko = { url = "tarball+https://codeload.github.com/nix-community/disko/tar.gz/master"; inputs.nixpkgs.follows = "nixpkgs"; @@ -14,7 +18,7 @@ }; }; - outputs = { self, nixpkgs, flake-utils, disko, hcloud-upload-image-src }: + outputs = { self, nixpkgs, flake-utils, agenix, disko, hcloud-upload-image-src }: let supportedSystems = [ "x86_64-linux" @@ -161,6 +165,7 @@ packages = { + agenix = agenix.packages.${system}.agenix; hcloud-upload-image = hcloudUploadImagePkg; forgejo-nsc-dispatcher = forgejoNscDispatcher; forgejo-nsc-autoscaler = forgejoNscAutoscaler; @@ -180,6 +185,7 @@ inherit self; }; modules = [ + agenix.nixosModules.default disko.nixosModules.disko ./nixos/hosts/burrow-forge/default.nix ]; diff --git a/nixos/README.md b/nixos/README.md index b546f1a..7924944 100644 --- a/nixos/README.md +++ b/nixos/README.md @@ -12,6 +12,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B - `modules/burrow-forgejo-nsc.nix`: Namespace-backed ephemeral Forgejo runner services - `modules/burrow-authentik.nix`: minimal Authentik IdP for Burrow control planes - `modules/burrow-headscale.nix`: Headscale control plane rooted in Authentik OIDC +- `../secrets.nix`: agenix recipient map for tracked Burrow forge secrets - `hetzner-cloud-config.yaml`: desired Hetzner host shape - `keys/contact_at_burrow_net.pub`: initial operator SSH public key - `keys/agent_at_burrow_net.pub`: automation SSH public key @@ -32,7 +33,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B 4. Let `burrow-forgejo-bootstrap.service` create or rotate the initial Forgejo admin account. 5. Let `burrow-forgejo-runner-bootstrap.service` register the self-hosted Forgejo runner and seed Git identity as `agent `. 6. Run `Scripts/provision-forgejo-nsc.sh` locally, then `Scripts/sync-forgejo-nsc-config.sh` to place the Namespace dispatcher/autoscaler runtime inputs under `/var/lib/burrow/intake/`. -7. Ensure `/var/lib/burrow/intake/authentik.env` exists on the host, and let `services.burrow.headscale` generate `/var/lib/burrow/intake/authentik_headscale_client_secret.txt` on first boot if it is absent. +7. Ensure `/var/lib/agenix/agenix.key` exists on the host, encrypt `secrets/infra/authentik.env.age` and `secrets/infra/headscale-oidc-client-secret.age`, and let agenix materialize them under `/run/agenix/`. 8. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, `auth.burrow.net`, `ts.burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME. 9. Use `Scripts/forge-deploy.sh --allow-dirty` for subsequent remote `nixos-rebuild` runs from the live workspace. 10. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`. @@ -40,6 +41,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B ## Current Constraints - `burrow-forge` is live on NixOS in `hel1` at `89.167.47.21`, and `Scripts/check-forge-host.sh --expect-nsc` passes locally against that host. +- Authentik and Headscale secrets now live in tracked agenix blobs under `secrets/infra/` and decrypt to `/run/agenix/` on the forge host. - Public Burrow forge cutover completed on March 15, 2026: - `burrow.net`, `git.burrow.net`, and `nsc-autoscaler.burrow.net` now publish public `A` records to `89.167.47.21` - HTTP redirects to HTTPS on all three names diff --git a/nixos/hosts/burrow-forge/default.nix b/nixos/hosts/burrow-forge/default.nix index 344fe96..43f65a3 100644 --- a/nixos/hosts/burrow-forge/default.nix +++ b/nixos/hosts/burrow-forge/default.nix @@ -1,4 +1,4 @@ -{ self, ... }: +{ config, self, ... }: { imports = [ @@ -20,6 +20,20 @@ "flakes" ]; + age.identityPaths = [ "/var/lib/agenix/agenix.key" ]; + age.secrets.burrowAuthentikEnv = { + file = ../../../secrets/infra/authentik.env.age; + owner = "root"; + group = "root"; + mode = "0400"; + }; + age.secrets.burrowHeadscaleOidcClientSecret = { + file = ../../../secrets/infra/headscale-oidc-client-secret.age; + owner = "root"; + group = "root"; + mode = "0400"; + }; + networking.extraHosts = '' 127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net ::1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net @@ -53,11 +67,12 @@ services.burrow.authentik = { enable = true; - envFile = "/var/lib/burrow/intake/authentik.env"; - headscaleClientSecretFile = "/var/lib/burrow/intake/authentik_headscale_client_secret.txt"; + envFile = config.age.secrets.burrowAuthentikEnv.path; + headscaleClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path; }; services.burrow.headscale = { enable = true; + oidcClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path; }; } diff --git a/nixos/modules/burrow-headscale.nix b/nixos/modules/burrow-headscale.nix index 120468b..ad5ec68 100644 --- a/nixos/modules/burrow-headscale.nix +++ b/nixos/modules/burrow-headscale.nix @@ -191,7 +191,9 @@ in set -euo pipefail list_users() { - ${pkgs.headscale}/bin/headscale users list -o json + local users_json + users_json="$(${pkgs.headscale}/bin/headscale users list -o json)" + printf '%s\n' "$users_json" | ${pkgs.jq}/bin/jq -c 'if type == "array" then . else [] end' } ensure_user() { diff --git a/secrets.nix b/secrets.nix new file mode 100644 index 0000000..4382fd6 --- /dev/null +++ b/secrets.nix @@ -0,0 +1,14 @@ +let + contact = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO42guJ5QvNMw3k6YKWlQnjcTsc+X4XI9F2GBtl8aHOa"; + agent = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEN0+tRJy7Y2DW0uGYHb86N2t02WyU5lDNX6FaxBF/G8 agent@burrow.net"; + burrowForgeHost = "age1quxf27gnun0xghlnxf3jrmqr3h3a3fzd8qxpallsaztd2u74pdfq9e7w9l"; + burrowForgeRecipients = [ + contact + agent + burrowForgeHost + ]; +in +{ + "secrets/infra/authentik.env.age".publicKeys = burrowForgeRecipients; + "secrets/infra/headscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients; +} diff --git a/secrets/infra/authentik.env.age b/secrets/infra/authentik.env.age new file mode 100644 index 0000000..f9f6136 Binary files /dev/null and b/secrets/infra/authentik.env.age differ diff --git a/secrets/infra/headscale-oidc-client-secret.age b/secrets/infra/headscale-oidc-client-secret.age new file mode 100644 index 0000000..925512c Binary files /dev/null and b/secrets/infra/headscale-oidc-client-secret.age differ