diff --git a/infra/ACCESS.md b/infra/ACCESS.md index 4ffec1e..2116eeb 100644 --- a/infra/ACCESS.md +++ b/infra/ACCESS.md @@ -26,7 +26,7 @@ rather than which environment happens to live there: | Host | Public IP | Mesh IP | Role | |------|-----------|---------|------| -| **vps1** | `75.119.132.91` | `10.10.0.2` | "Operational programs": Forgejo (git + CI + registry, `git.thermograph.org`, also served at `dev.jinemi.com` — migration in progress, `.org` still canonical), Grafana/Loki/Alloy (`dashboard.thermograph.org`), the `emigriffith.dev` portfolio, and the **dev** environment (mesh-only, `10.10.0.2:8137`, no public DNS/Caddy/TLS) | +| **vps1** | `75.119.132.91` | `10.10.0.2` | "Operational programs": Forgejo (git + CI + registry; `dev.jinemi.com` canonical, `git.thermograph.org` still served and still used by the registry/mesh pins/runners), Grafana/Loki/Alloy (`dashboard.thermograph.org`), the `emigriffith.dev` portfolio, and the **dev** environment (mesh-only, `10.10.0.2:8137`, no public DNS/Caddy/TLS) | | **vps2** | `169.58.46.181` | `10.10.0.1` | "The deployed environment": **prod** and **beta**, as two separate Docker Swarm stacks, plus Centralis, Postfix, the backups, and the one shared TimescaleDB instance both stacks use | | **desktop** | — | `10.10.0.3` | AI-model hosting (voice-to-text, an upcoming-feature LLM) plus flex Swarm-worker capacity. Hosts **no** Thermograph environment — `make dev-up` still works there, but only as a laptop-local rehearsal | diff --git a/infra/CLAUDE.md b/infra/CLAUDE.md index f8dc94b..c739633 100644 --- a/infra/CLAUDE.md +++ b/infra/CLAUDE.md @@ -12,9 +12,10 @@ Two VPS boxes plus the operator's desktop, on one WireGuard mesh, named by ROLE rather than by environment: - **vps1** — `75.119.132.91`, mesh `10.10.0.2`. "Operational programs": Forgejo - (git + CI + registry, `git.thermograph.org`, also served at `dev.jinemi.com` - — one Caddy site block, the `.org` name still canonical; migration in - progress, see `deploy/forgejo/README.md`), Grafana/Loki/Alloy + (git + CI + registry; `dev.jinemi.com` is canonical — Forgejo's `ROOT_URL` — + and `git.thermograph.org` is still served off the same Caddy site block and + still load-bearing for the registry, mesh pins and runner URLs, so don't + retire it; see `deploy/forgejo/README.md`), Grafana/Loki/Alloy (`dashboard.thermograph.org`), the `emigriffith.dev` portfolio site, and the **dev** environment (`/opt/thermograph-dev`, tracks `dev`), with its own Postgres container. Dev is mesh-only here — published on `10.10.0.2:8137`, diff --git a/infra/deploy/Caddyfile.vps1 b/infra/deploy/Caddyfile.vps1 index 9c81727..8532948 100644 --- a/infra/deploy/Caddyfile.vps1 +++ b/infra/deploy/Caddyfile.vps1 @@ -2,10 +2,11 @@ # # vps1 serves: # emigriffith.dev -> static portfolio site (from disk) -# git.thermograph.org -> Forgejo (see deploy/forgejo/caddy-git.conf) -# dev.jinemi.com -> Forgejo, same site block — migration target for -# git.thermograph.org, both served, the .org one -# still canonical (that file's header explains) +# dev.jinemi.com -> Forgejo, CANONICAL (Forgejo's ROOT_URL) +# git.thermograph.org -> Forgejo, same site block — still served and +# still needed: the registry host in image names, +# the mesh /etc/hosts pins and the runners' URLs +# all use it (see deploy/forgejo/caddy-git.conf) # dashboard.thermograph.org -> Grafana (see observability/caddy-grafana.conf) # # and hosts the **dev** environment at dev.thermograph.org (below). diff --git a/infra/deploy/forgejo/README.md b/infra/deploy/forgejo/README.md index 7c2a5ed..38b047b 100644 --- a/infra/deploy/forgejo/README.md +++ b/infra/deploy/forgejo/README.md @@ -85,50 +85,55 @@ pattern as its other site blocks, same automatic-HTTPS. ## Migrating the domain (git.thermograph.org -> dev.jinemi.com) -In progress. **Both hostnames are served**; `git.thermograph.org` is still the -canonical one. +**Web UI and OAuth: done.** `dev.jinemi.com` is Forgejo's `ROOT_URL` +(`FORGEJO_DOMAIN` in `docker-stack.yml`), so it is the name Forgejo generates in +clone URLs, the Google OAuth callback, webhook payload URLs and mail links. + +**Registry: deliberately not done.** Renaming the registry host is a separate +migration from renaming the web UI, and the two need not happen together. `caddy-git.conf` lists both names on a *single* site block. That is deliberate, not cosmetic: the `/v2/*` mesh-only matcher is per-block, so a second block for -`dev.jinemi.com` would re-expose the registry API publicly and undo hazard #15. +either name would re-expose the registry API publicly and undo hazard #15. Whatever else changes, keep the two names in one block. -Serving a second hostname is the cheap half. Forgejo has exactly one -`ROOT_URL`, and it builds every absolute URL from it, so on the non-canonical -host today: +**`git.thermograph.org` must stay served.** It is not a courtesy redirect for +old bookmarks — CI resolves it: -- clone URLs on repo pages read `git.thermograph.org`, -- the OAuth callback and post-login redirect land on `git.thermograph.org` - (login is Google-SSO-only — see `docker-stack.yml`), -- webhook payload URLs and notification mail links use `git.thermograph.org`. +- images are named by registry host, and the `git.thermograph.org/` prefix is + baked into the runner labels (`register-lan-runner.sh`, + `runner-vps2/README.md`), the CI-runner image (`ci-runner/Dockerfile`), + `REGISTRY_HOST` in `infra/.env.example`, and every already-pushed tag; +- every mesh client in "Registry access from mesh clients" below pins + `git.thermograph.org` to `10.10.0.2` in `/etc/hosts`, which is what makes + Caddy's `/v2/*` matcher see a mesh source IP instead of returning 403; +- registered runners hold the instance URL they registered with + (`https://git.thermograph.org`); they keep working while that name resolves. -Browsing, git-over-HTTP and the API all work on `dev.jinemi.com` regardless. +### Before changing ROOT_URL again -Flipping canonical (`FORGEJO_DOMAIN=dev.jinemi.com`, then `docker stack deploy`) -needs these **first**, or it breaks login and CI rather than just renaming -things: +Login is Google-SSO-only, so the Google OAuth client must already carry a +matching redirect URI, or nobody can reach the UI — including to undo the +change. Both of these are registered on client +`337247886376-4360kepi20f24pue62ptg0lnqhcht46l.apps.googleusercontent.com`: -1. **Google OAuth** — add `https://dev.jinemi.com/user/oauth2//callback` - as an authorized redirect URI in the Google Cloud console *before* the flip. - Not in this repo; nothing here can verify it. Miss it and no one can log in, - including to undo the flip. -2. **The registry host in image names** — images are named by registry host, and - the `git.thermograph.org/` prefix is baked into the runner labels - (`register-lan-runner.sh`, `runner-vps2/README.md`), the CI-runner image - (`ci-runner/Dockerfile`), `REGISTRY_HOST` in `infra/.env.example`, and every - already-pushed tag. Renaming the registry host is a separate migration from - renaming the web UI; the two need not happen together, and doing the web UI - alone is much the smaller change. -3. **Mesh `/etc/hosts` pins** — every node in "Registry access from mesh - clients" below pins `git.thermograph.org` to `10.10.0.2`. A node pulling - under a new hostname needs its own pin, or Caddy's `/v2/*` matcher sees a - public source IP and returns 403. -4. **Runner instance URL** — registered runners hold the instance URL they were - registered with. Existing runners keep working against the old name as long - as it resolves; a *re*-registration must use whichever name is canonical then. + https://dev.jinemi.com/user/oauth2/google/callback + https://git.thermograph.org/user/oauth2/google/callback -Until all four are handled, leaving `ROOT_URL` on `git.thermograph.org` is the -correct state, not an unfinished one. +Verify rather than assume. Google answers this with no credentials: a +registered URI serves the sign-in page, an unregistered one returns +`redirect_uri_mismatch`. Substitute the host you intend to make canonical: + +```sh +CID=337247886376-4360kepi20f24pue62ptg0lnqhcht46l.apps.googleusercontent.com +curl -sL "https://accounts.google.com/o/oauth2/v2/auth?client_id=$CID&redirect_uri=https%3A%2F%2F%2Fuser%2Foauth2%2Fgoogle%2Fcallback&response_type=code&scope=openid+email+profile&state=probe" \ + | grep -q redirect_uri_mismatch && echo REJECTED || echo ACCEPTED +``` + +The stack has **no auto-deploy**: a `FORGEJO_DOMAIN` change here only takes +effect when someone re-runs `docker stack deploy` by hand on the manager (vps2) +— see "Deploy / update" above. Until then the running service keeps whatever +`ROOT_URL` it was last deployed with, regardless of what this file says. ## Registry access from mesh clients diff --git a/infra/deploy/forgejo/caddy-git.conf b/infra/deploy/forgejo/caddy-git.conf index fdaefee..1acf0cd 100644 --- a/infra/deploy/forgejo/caddy-git.conf +++ b/infra/deploy/forgejo/caddy-git.conf @@ -14,15 +14,16 @@ # both. Splitting dev.jinemi.com into its own block serves the same Forgejo # with the registry API open to the public internet. # -# dev.jinemi.com is the migration target for git.thermograph.org; both are -# served during the transition. Each gets its own Let's Encrypt cert -# automatically (HTTP-01) — both A records already point at vps1. +# dev.jinemi.com is CANONICAL — it is Forgejo's ROOT_URL (docker-stack.yml, +# FORGEJO_DOMAIN), so it is the name Forgejo puts in clone URLs, the OAuth +# callback, webhook payload URLs and mail links. # -# git.thermograph.org stays CANONICAL until Forgejo's ROOT_URL is flipped -# (docker-stack.yml, FORGEJO_DOMAIN). Until then dev.jinemi.com serves every -# page, but Forgejo builds absolute URLs from ROOT_URL, so clone URLs, the -# OAuth callback and post-login redirects still name git.thermograph.org. -# Flipping it is NOT just this file — see README.md, "Migrating the domain". +# git.thermograph.org is kept as a served alias, NOT as dead weight: the +# registry host baked into image names, the mesh /etc/hosts pins and the +# runners' registered instance URL all still say git.thermograph.org. Removing +# this name from the block breaks CI, not just old bookmarks. Both names get +# their own Let's Encrypt cert automatically (HTTP-01); both A records point +# at vps1. See README.md, "Migrating the domain". # # Registry exposure (hazard #15, see docker-stack.yml's header comment): # /v2/* is the built-in OCI registry API. It's blocked from anywhere except diff --git a/infra/deploy/forgejo/docker-stack.yml b/infra/deploy/forgejo/docker-stack.yml index 7d1603f..77e5466 100644 --- a/infra/deploy/forgejo/docker-stack.yml +++ b/infra/deploy/forgejo/docker-stack.yml @@ -80,8 +80,25 @@ services: FORGEJO__database__HOST: db:5432 FORGEJO__database__NAME: forgejo FORGEJO__database__USER: forgejo - FORGEJO__server__DOMAIN: "${FORGEJO_DOMAIN:-git.thermograph.org}" - FORGEJO__server__ROOT_URL: "https://${FORGEJO_DOMAIN:-git.thermograph.org}/" + # Canonical domain. Forgejo has exactly ONE ROOT_URL and derives every + # absolute URL from it — clone URLs, the Google OAuth callback, webhook + # payload URLs, mail links. git.thermograph.org is still served (Caddy + # answers for both names off one site block, deploy/forgejo/caddy-git.conf) + # and still works for browsing, git-over-HTTP and the API; it is simply no + # longer the name Forgejo generates. + # + # Changing this breaks login unless the matching redirect URI exists on + # the Google OAuth client FIRST — login is SSO-only, so a mismatch locks + # everyone out of the UI, including out of the ability to undo it. Both + # https://dev.jinemi.com/user/oauth2/google/callback and the + # git.thermograph.org one are registered; verify with a probe before + # changing this again (deploy/forgejo/README.md, "Migrating the domain"). + # + # NOT changed by this: the registry host baked into image names, the mesh + # /etc/hosts pins, and the runners' registered instance URL all still say + # git.thermograph.org. That is a separate migration. + FORGEJO__server__DOMAIN: "${FORGEJO_DOMAIN:-dev.jinemi.com}" + FORGEJO__server__ROOT_URL: "https://${FORGEJO_DOMAIN:-dev.jinemi.com}/" FORGEJO__server__SSH_PORT: "2222" # Without this, Forgejo starts no SSH server at all despite SSH_PORT # being set — git@ clones get "connection refused", not a slow failure.