From 2544666cc9311e12e396f0893d1b4c8f0366a37a Mon Sep 17 00:00:00 2001 From: Philip Peterson Date: Sun, 24 May 2026 23:31:05 -0800 Subject: [PATCH] fixes --- nixos/CLAUDE.md | 101 +++++++++++++++++++++++++ nixos/linux.nix | 3 +- nixos/secrets/default.nix | 2 +- nixos/secrets/forgejo-runner-token.age | 9 ++- 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 nixos/CLAUDE.md diff --git a/nixos/CLAUDE.md b/nixos/CLAUDE.md new file mode 100644 index 0000000..5bbdc16 --- /dev/null +++ b/nixos/CLAUDE.md @@ -0,0 +1,101 @@ +# petersweb-infra/nixos — CLAUDE.md + +## What this repo is + +NixOS configuration for a single Hetzner server ("mainframe") running Philip Peterson's personal/Quine Foundation infrastructure. One machine, one flake configuration: `nixosConfigurations.mainframe`. + +## Applying changes + +```bash +./apply.sh # git pull + nixos-rebuild switch --flake .#mainframe +# or manually: +nixos-rebuild switch --flake /root/petersweb-infra/nixos#mainframe +``` + +## File layout + +| Path | Purpose | +|---|---| +| `flake.nix` | Single flake, defines `nixosConfigurations.mainframe` | +| `hetzner.nix` | Hardware config: GRUB on `/dev/sda`, static networking, openssh | +| `linux.nix` | Main system config: services, secrets, docker containers, ACME certs | +| `nginx.nix` | Nginx virtual hosts and reverse proxies | +| `firewall.nix` | Open TCP ports | +| `disk-config.nix` | disko disk layout | +| `cloned_repos/` | `pullomatic` configs for auto-pulling git repos to `/etc/pullomatic/` | +| `arion/` | Arion (docker-compose-like) for Forgejo | +| `arion-riverside/` | Arion for the Riverside service | +| `pullomatic/` | Rust tool that watches git remotes and pulls on a schedule | +| `invoke-ddns/` | Python DDNS updater for NearlyFreeSpeech DNS | +| `secrets/` | agenix-encrypted secrets | +| `keys/` | SSH public keys used as age recipients | +| `system/` | User definitions and home-manager config | +| `pdxdestiny/` | Static site files for pdxdestiny.com | + +## Secrets (agenix) + +Secrets live in `secrets/*.age`. They are encrypted with the key in `keys/mainframe.pub` (which is identical to `/root/.ssh/id_rsa_nix.pub` on the server). + +**Important:** Agenix uses three identity paths for decryption (see activation script): +1. `/etc/ssh/ssh_host_rsa_key` +2. `/etc/ssh/ssh_host_ed25519_key` +3. `/root/.ssh/id_rsa_nix` ← **this is the actual working key** + +The decrypted secrets land at `/run/agenix/` at boot. + +### Secret format matters + +The NixOS `gitea-actions-runner` module reads the token via `EnvironmentFile=`, so the secret file must be in `KEY=VALUE` format: + +- `forgejo-runner-token.age` → must contain `TOKEN=` (not just the raw token) +- `nearlyfreespeech.age` → contains `NEARLYFREESPEECH_API_KEY=...` and `NEARLYFREESPEECH_LOGIN=...` +- `webdav.age` → contains `WEBDAV_PASSWORD=...` +- `anthropic-api-key.age` → contains `ANTHROPIC_API_KEY=...` +- `postmark.age` → contains `POSTMARK_SERVER_TOKEN=...` + +### Re-encrypting a secret + +```bash +# Encrypt new content for the mainframe key +printf "TOKEN=newvalue\n" | nix run nixpkgs#age -- \ + -r "$(cat /root/petersweb-infra/nixos/keys/mainframe.pub)" \ + -o /root/petersweb-infra/nixos/secrets/forgejo-runner-token.age + +# Verify it decrypts correctly +nix run nixpkgs#age -- -d -i /root/.ssh/id_rsa_nix \ + /root/petersweb-infra/nixos/secrets/forgejo-runner-token.age +``` + +Note: `secrets/default.nix` is the agenix recipients file. Agenix looks for `secrets.nix` by default — to use the CLI with this repo's `default.nix`, you'd need a symlink or pass the path manually. Use `age` directly instead (as above). + +## Key services + +| Service | Description | +|---|---| +| `gitea-runner-ubuntu.service` | Forgejo (Gitea) Actions CI runner, uses docker images | +| `forgejo-arion.service` | Forgejo itself, run via Arion/Podman | +| `riverside-arion.service` | Riverside app, run via Arion/Docker | +| `docker-navidrome.service` | Navidrome music server on port 4533 | +| `docker-nextcloud.service` | Nextcloud/SSH container on port 8087 | +| `docker-sync.io.service` | sync.io app on port 9090 | +| `docker-blog-quine.service` | Blog on port 3010 | +| `docker-coldairnetworks.service` | Cold Air Networks site on port 3012 | +| nginx | Reverse proxy + ACME certs for multiple domains | + +## Virtualisation + +- **Podman** is used for Forgejo (via Arion). `DOCKER_HOST` for the gitea-runner is set to `unix:///run/podman/podman.sock`. +- **Docker** is used for the OCI containers (navidrome, nextcloud, etc.) and the riverside Arion stack. +- The gitea-runner runs docker images for CI jobs, so the `gitea-runner` user is in the `docker` and `podman` supplementary groups. + +## Networking / DNS + +- Dynamic DNS via `invoke-ddns` (NearlyFreeSpeech provider). +- ACME certs issued via DNS challenge for `philippeterson.com` and `webdav.philippeterson.com`. +- Forgejo accessible on ports 3000 (HTTP) and 2200 (SSH). + +## Known gotchas + +- `gitea-runner` is a `DynamicUser` in the systemd service, so it has no persistent uid. Setting `age.secrets.forgejo-runner-token.owner = "gitea-runner"` causes a chown error at activation; use `owner = "root"` instead (the service reads it via `EnvironmentFile` which runs as root before privilege drop). +- `secrets/default.nix` must have the public key from `keys/mainframe.pub` as the recipient — if the host SSH keys change, you must also update `mainframe.pub` and re-key all secrets. +- `pullomatic` uses `/root/.ssh/id_rsa.pem` (a PEM-format SSH key) to pull private git repos. diff --git a/nixos/linux.nix b/nixos/linux.nix index efd3fbb..4501488 100644 --- a/nixos/linux.nix +++ b/nixos/linux.nix @@ -81,7 +81,7 @@ in { forgejo-runner-token = { file = ./secrets/forgejo-runner-token.age; - owner = "gitea-runner"; + owner = "root"; }; }; @@ -137,6 +137,7 @@ in { services.gitea-actions-runner.instances."ubuntu" = { enable = true; + name = "ubuntu"; url = "http://localhost:3000"; tokenFile = config.age.secrets.forgejo-runner-token.path; labels = [ diff --git a/nixos/secrets/default.nix b/nixos/secrets/default.nix index c892907..974a585 100644 --- a/nixos/secrets/default.nix +++ b/nixos/secrets/default.nix @@ -15,6 +15,6 @@ in { # POSTMARK_SERVER_TOKEN "./postmark.age".publicKeys = [mainframePublicKey]; - # Forgejo runner registration token (plain text token from Forgejo admin) + # TOKEN= "./forgejo-runner-token.age".publicKeys = [mainframePublicKey]; } diff --git a/nixos/secrets/forgejo-runner-token.age b/nixos/secrets/forgejo-runner-token.age index 6b9ef91..0584c16 100644 --- a/nixos/secrets/forgejo-runner-token.age +++ b/nixos/secrets/forgejo-runner-token.age @@ -1,5 +1,6 @@ age-encryption.org/v1 --> ssh-ed25519 NFD/vg clf6vMNmGue91peB+0Gn38wS4bNOIT8O2lbngF6sJCI -Zy2TQnIQ68z5WaMzukLTYrWD8bYk3nf0y6JmlhqtZA4 ---- UWiqR61EQhaDVOOdi8Zo7J4b9bDwOIyKDTlpisgTGr8 -XE`qrRjnй^möȌO*9|5hyj1)yxu/3zL@ם XDB7ah \ No newline at end of file +-> ssh-ed25519 NFD/vg FbWFwP9GHWMSJ4MKdwRV7PW8AT2tmSeNYdGKKmYzEjs +3/YoeBz2TrdCoT0p7j3Lg1olOh7DM8J1jye9QFFMlK0 +--- 9KlLY+1IjO4e9Q3ofPqgArUoxkomNwh41TWZkiRq48g +"W#4~A=QX^eӨzzi-92`Z%/S8 +cºG&ל*~,|= \ No newline at end of file