diff --git a/Makefile b/Makefile index f927f5f..e852e32 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,43 @@ +FLAKE ?= . +AGENIX ?= nix run ${FLAKE}\#agenix -- + +SECRETS := forgejo/nsc-token \ + forgejo/nsc-dispatcher-config \ + forgejo/nsc-autoscaler-config + tun := $(shell ifconfig -l | sed 's/ /\n/g' | grep utun | tail -n 1) cargo_console := env RUST_BACKTRACE=1 RUST_LOG=debug RUSTFLAGS='--cfg tokio_unstable' cargo run --all-features -- cargo_norm := env RUST_BACKTRACE=1 RUST_LOG=debug cargo run -- sudo_cargo_console := sudo -E env RUST_BACKTRACE=1 RUST_LOG=debug RUSTFLAGS='--cfg tokio_unstable' cargo run --all-features -- sudo_cargo_norm := sudo -E env RUST_BACKTRACE=1 RUST_LOG=debug cargo run -- +.PHONY: secret secret-file secrets-list + +secret: + @if [ -z "${name}" ]; then \ + printf 'Usage: make secret name=\nAvailable secrets:\n %s\n' "${SECRETS}"; \ + exit 1; \ + fi + ${AGENIX} -e secrets/${name}.age + +secret-file: + @if [ -z "${name}" ]; then \ + printf 'Usage: make secret-file name= file=\nAvailable secrets:\n %s\n' "${SECRETS}"; \ + exit 1; \ + fi + @if [ -z "${file}" ]; then \ + printf 'Usage: make secret-file name= file=\n'; \ + exit 1; \ + fi + @if [ ! -f "${file}" ]; then \ + printf 'Source file "%s" not found.\n' "${file}"; \ + exit 1; \ + fi + SECRET_SOURCE_FILE="${file}" EDITOR="${PWD}/Scripts/agenix-load-file.sh" ${AGENIX} -e secrets/${name}.age " >&2 + exit 1 +fi + +dest="${!#}" +source_path="${SECRET_SOURCE_FILE:-}" + +if [[ -z "$source_path" ]]; then + echo "SECRET_SOURCE_FILE is not set; point it at the source file to encrypt." >&2 + exit 1 +fi + +if [[ ! -f "$source_path" ]]; then + echo "Source file '$source_path' does not exist." >&2 + exit 1 +fi + +cp "$source_path" "$dest" diff --git a/Scripts/provision-forgejo-nsc.sh b/Scripts/provision-forgejo-nsc.sh index f6ab4d9..9e6e4b5 100755 --- a/Scripts/provision-forgejo-nsc.sh +++ b/Scripts/provision-forgejo-nsc.sh @@ -272,4 +272,5 @@ PY chmod 600 "${dispatcher_out}" "${autoscaler_out}" echo "Rendered intake/forgejo_nsc_token.txt, intake/forgejo_nsc_dispatcher.yaml, and intake/forgejo_nsc_autoscaler.yaml." +echo "Re-encrypt them into secrets/forgejo/{nsc-token,nsc-dispatcher-config,nsc-autoscaler-config}.age before deploying the forge host." echo "Minted Forgejo PAT ${token_name} for ${CONTACT_USER} on ${HOST}." diff --git a/flake.lock b/flake.lock index 677bd0d..6f7f20c 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,47 @@ { "nodes": { + "agenix": { + "inputs": { + "darwin": "darwin", + "home-manager": "home-manager", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1770165109, + "narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=", + "type": "tarball", + "url": "https://codeload.github.com/ryantm/agenix/tar.gz/main" + }, + "original": { + "type": "tarball", + "url": "https://codeload.github.com/ryantm/agenix/tar.gz/main" + } + }, + "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 +61,7 @@ }, "flake-utils": { "inputs": { - "systems": "systems" + "systems": "systems_2" }, "locked": { "lastModified": 1731533236, @@ -45,6 +87,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 +122,7 @@ }, "root": { "inputs": { + "agenix": "agenix", "disko": "disko", "flake-utils": "flake-utils", "hcloud-upload-image-src": "hcloud-upload-image-src", @@ -79,6 +143,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 b49b330..51e4bc9 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 = "tarball+https://codeload.github.com/ryantm/agenix/tar.gz/main"; + 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" @@ -29,6 +33,7 @@ inherit system; }; lib = pkgs.lib; + agenixPkg = agenix.packages.${system}.agenix; commonPackages = with pkgs; [ cargo rustc @@ -141,6 +146,7 @@ packages = commonPackages ++ [ + agenixPkg hcloudUploadImagePkg forgejoNscDispatcher forgejoNscAutoscaler @@ -152,6 +158,7 @@ packages = commonPackages ++ [ + agenixPkg hcloudUploadImagePkg ] ++ lib.optionals (nscPkg != null) [ nscPkg ]; @@ -161,6 +168,7 @@ packages = { + agenix = agenixPkg; hcloud-upload-image = hcloudUploadImagePkg; forgejo-nsc-dispatcher = forgejoNscDispatcher; forgejo-nsc-autoscaler = forgejoNscAutoscaler; @@ -176,8 +184,10 @@ system = "x86_64-linux"; specialArgs = { inherit self; + agenixPackage = agenix.packages.x86_64-linux.agenix; }; modules = [ + agenix.nixosModules.default disko.nixosModules.disko ./nixos/hosts/burrow-forge/default.nix ]; diff --git a/nixos/README.md b/nixos/README.md index a682db0..f37637c 100644 --- a/nixos/README.md +++ b/nixos/README.md @@ -19,8 +19,8 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B - `../Scripts/check-forge-host.sh`: verify Forgejo, Caddy, the local runner, and optional NSC services after boot - `../Scripts/cloudflare-upsert-a-record.sh`: upsert DNS-only Cloudflare `A` records for Burrow host cutovers - `../Scripts/forge-deploy.sh`: remote `nixos-rebuild` entrypoint for the forge host -- `../Scripts/provision-forgejo-nsc.sh`: render Burrow Namespace dispatcher/autoscaler runtime inputs and ensure the default Forgejo scope exists -- `../Scripts/sync-forgejo-nsc-config.sh`: copy intake-backed dispatcher/autoscaler inputs to the host +- `../Scripts/provision-forgejo-nsc.sh`: render Burrow Namespace dispatcher/autoscaler bootstrap inputs and ensure the default Forgejo scope exists +- `../secrets/forgejo/*.age`: authoritative encrypted Namespace token + dispatcher/autoscaler configs for the forge host ## Intended Flow @@ -29,7 +29,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B 3. Run `Scripts/bootstrap-forge-intake.sh` to place the Forgejo bootstrap password file and automation SSH key under `/var/lib/burrow/intake/`. 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/`. +6. Run `Scripts/provision-forgejo-nsc.sh` locally, re-encrypt the resulting NSC token + configs into `secrets/forgejo/*.age`, then deploy with `Scripts/forge-deploy.sh` so agenix updates the live forgejo-nsc runtime paths. 7. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME. 8. Use `Scripts/forge-deploy.sh --allow-dirty` for subsequent remote `nixos-rebuild` runs from the live workspace. 9. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`. diff --git a/nixos/hosts/burrow-forge/default.nix b/nixos/hosts/burrow-forge/default.nix index d600539..7dc828d 100644 --- a/nixos/hosts/burrow-forge/default.nix +++ b/nixos/hosts/burrow-forge/default.nix @@ -1,4 +1,4 @@ -{ self, ... }: +{ config, self, ... }: { imports = [ @@ -32,15 +32,36 @@ sshPrivateKeyFile = "/var/lib/burrow/intake/agent_at_burrow_net_ed25519"; }; + age.secrets.forgejoNscToken = { + file = ../../../secrets/forgejo/nsc-token.age; + mode = "0400"; + owner = "forgejo-nsc"; + group = "forgejo-nsc"; + }; + + age.secrets.forgejoNscDispatcherConfig = { + file = ../../../secrets/forgejo/nsc-dispatcher-config.age; + mode = "0400"; + owner = "forgejo-nsc"; + group = "forgejo-nsc"; + }; + + age.secrets.forgejoNscAutoscalerConfig = { + file = ../../../secrets/forgejo/nsc-autoscaler-config.age; + mode = "0400"; + owner = "forgejo-nsc"; + group = "forgejo-nsc"; + }; + services.burrow.forgejoNsc = { enable = true; - nscTokenFile = "/var/lib/burrow/intake/forgejo_nsc_token.txt"; + nscTokenFile = config.age.secrets.forgejoNscToken.path; dispatcher = { - configFile = "/var/lib/burrow/intake/forgejo_nsc_dispatcher.yaml"; + configFile = config.age.secrets.forgejoNscDispatcherConfig.path; }; autoscaler = { enable = true; - configFile = "/var/lib/burrow/intake/forgejo_nsc_autoscaler.yaml"; + configFile = config.age.secrets.forgejoNscAutoscalerConfig.path; }; }; } diff --git a/secrets.nix b/secrets.nix new file mode 100644 index 0000000..1e49f5d --- /dev/null +++ b/secrets.nix @@ -0,0 +1 @@ +import ./secrets/secrets.nix diff --git a/secrets/README.md b/secrets/README.md new file mode 100644 index 0000000..2132079 --- /dev/null +++ b/secrets/README.md @@ -0,0 +1,17 @@ +# Secrets + +Burrow secrets live in `secrets/.age` and are managed with `agenix`. + +For the Forgejo Namespace Cloud runtime: + +- `secrets/forgejo/nsc-token.age` +- `secrets/forgejo/nsc-dispatcher-config.age` +- `secrets/forgejo/nsc-autoscaler-config.age` + +Use: + +- `make secret name=forgejo/nsc-token` +- `make secret-file name=forgejo/nsc-token file=/path/to/source` + +The forge host decrypts these files at activation time and feeds the resulting +paths into `services.burrow.forgejoNsc`. diff --git a/secrets/forgejo/nsc-autoscaler-config.age b/secrets/forgejo/nsc-autoscaler-config.age new file mode 100644 index 0000000..243394a Binary files /dev/null and b/secrets/forgejo/nsc-autoscaler-config.age differ diff --git a/secrets/forgejo/nsc-dispatcher-config.age b/secrets/forgejo/nsc-dispatcher-config.age new file mode 100644 index 0000000..a314430 Binary files /dev/null and b/secrets/forgejo/nsc-dispatcher-config.age differ diff --git a/secrets/forgejo/nsc-token.age b/secrets/forgejo/nsc-token.age new file mode 100644 index 0000000..7a515c1 Binary files /dev/null and b/secrets/forgejo/nsc-token.age differ diff --git a/secrets/secrets.nix b/secrets/secrets.nix new file mode 100644 index 0000000..1cacc6a --- /dev/null +++ b/secrets/secrets.nix @@ -0,0 +1,12 @@ +{ }: +let + contact = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO42guJ5QvNMw3k6YKWlQnjcTsc+X4XI9F2GBtl8aHOa"; + agent = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEN0+tRJy7Y2DW0uGYHb86N2t02WyU5lDNX6FaxBF/G8 agent@burrow.net"; + forge = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAlkGo4lwpwIIZ0J01KjTuJuf/U/wGgy4/aKwPIUzutL root@burrow-forge"; + + forgeAutomation = [ contact agent forge ]; +in { + "secrets/forgejo/nsc-token.age".publicKeys = forgeAutomation; + "secrets/forgejo/nsc-dispatcher-config.age".publicKeys = forgeAutomation; + "secrets/forgejo/nsc-autoscaler-config.age".publicKeys = forgeAutomation; +} diff --git a/services/forgejo-nsc/README.md b/services/forgejo-nsc/README.md index dbd7e78..6f55717 100644 --- a/services/forgejo-nsc/README.md +++ b/services/forgejo-nsc/README.md @@ -152,19 +152,21 @@ instances: ``` For Burrow, use `Scripts/provision-forgejo-nsc.sh` to mint the Forgejo PAT, -generate a Namespace token from the logged-in namespace account, and render the -dispatcher/autoscaler configs into `intake/forgejo_nsc_{dispatcher,autoscaler}.yaml` -plus `intake/forgejo_nsc_token.txt`. The token file is emitted as JSON with a +generate a Namespace token from the logged-in namespace account, and render +bootstrap artifacts into `intake/forgejo_nsc_{dispatcher,autoscaler}.yaml` plus +`intake/forgejo_nsc_token.txt`. The token file is emitted as JSON with a `bearer_token` field so both the Compute API path and the `nsc` CLI fallback can consume the same secret material. -For ongoing operations, use `Scripts/sync-forgejo-nsc-config.sh`: +Long-lived runtime state is now sourced from age-encrypted files: -- `Scripts/sync-forgejo-nsc-config.sh` copies the intake-backed configs and - Namespace token onto `/var/lib/burrow/intake/` on the forge host, reapplies - file ownership for `forgejo-nsc`, and restarts the dispatcher/autoscaler. -- `Scripts/sync-forgejo-nsc-config.sh --rotate-pat` additionally mints a new - Forgejo PAT on the Burrow forge host and refreshes the local intake files. +- `secrets/forgejo/nsc-token.age` +- `secrets/forgejo/nsc-dispatcher-config.age` +- `secrets/forgejo/nsc-autoscaler-config.age` + +After refreshing the intake files, re-encrypt them into `secrets/forgejo/*.age` +and deploy the forge host so `config.age.secrets.*` updates the live paths for +`services.burrow.forgejoNsc`. Run it next to the dispatcher: