Repository navigation
fix(deploy): containment stopgap for api.nesy-prover.dev #461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9a7a0bd
b3af878
cdca71c
55877bd
7186ae9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,88 @@ Let's Encrypt), orchestrated with podman systemd quadlets. | |
| If abuse appears, add a bearer-token matcher in the Caddyfile for | ||
| POST routes. A server-side per-request prover timeout is tracked as a | ||
| follow-up issue in the main repo. | ||
| *Superseded 2026-11-05*: see <<containment-stopgap>>. | ||
|
|
||
| [[containment-stopgap]] | ||
| == Containment stopgap (owner ruling 2026-11-05) | ||
|
|
||
| Until bearer authentication lands on every execution route, this host | ||
| serves read-only routes only, and it no longer rolls out new images by | ||
| itself. | ||
|
|
||
| * The `Caddyfile` forwards only `GET`/`HEAD` on `/api/health` and | ||
| `/api/provers`. Every other request is answered `403` by Caddy and | ||
| never reaches the container. That includes `/api/prove`, | ||
| `/api/verify*`, `/api/session/*`, `/api/agent/*` and `/api/search`. | ||
| * `echidna.container` drops `AutoUpdate=registry` and sets `Pull=never`. | ||
| The unit runs the image already on the host, so a newly published | ||
| `:latest` is not deployed. This *freezes* the running image. It does | ||
| not verify it. | ||
|
|
||
| Apply on the host as the `deploy` user. | ||
|
|
||
| *1. Find out which install this host has.* These steps are for a host | ||
| installed with the quadlets in §2 (units `echidna.service` and | ||
| `caddy.service`). A host installed from the appendix runs | ||
| `container-echidna` and `container-caddy` instead. For that kind of host, | ||
| use step 4 instead of step 3. | ||
|
|
||
| [source,console] | ||
| ---- | ||
| systemctl --user cat echidna.service caddy.service >/dev/null && echo quadlet | ||
| systemctl --user cat container-echidna.service container-caddy.service >/dev/null && echo appendix | ||
| ---- | ||
|
|
||
| *2. Record the image the container is running.* Record the running | ||
| container's image ID, not the tag's digest. If an auto-update or a manual | ||
| pull has moved the local `:latest` tag since the container started, a | ||
| restart with `Pull=never` would start the image the tag now points to. | ||
|
|
||
| [source,console] | ||
| ---- | ||
| systemctl --user disable --now podman-auto-update.timer | ||
| RUNNING=$(podman container inspect echidna --format '{{.Image}}') || exit 1 | ||
| TAGGED=$(podman image inspect --format '{{.Id}}' ghcr.io/hyperpolymath/echidna:latest) || exit 1 | ||
| echo "running=$RUNNING tagged=$TAGGED" | ||
| # If they differ, point :latest back at the running image before restarting: | ||
| [ "$RUNNING" = "$TAGGED" ] || podman tag "$RUNNING" ghcr.io/hyperpolymath/echidna:latest || exit 1 | ||
| ---- | ||
|
|
||
| *3. Quadlet host.* Copy the updated `Caddyfile` and `echidna.container` | ||
| into place, then: | ||
|
|
||
| [source,console] | ||
| ---- | ||
| systemctl --user daemon-reload | ||
| systemctl --user restart echidna caddy | ||
| ---- | ||
|
|
||
| *4. Appendix host.* Those containers carry no auto-update label, and | ||
| `podman generate systemd --new` units do not pull an image that is | ||
| already present. So only the proxy changes. Copy the updated | ||
| `Caddyfile` to `/opt/echidna/Caddyfile`, then: | ||
|
|
||
| [source,console] | ||
| ---- | ||
| systemctl --user restart container-caddy | ||
| ---- | ||
|
|
||
| *5. Verify, on either kind of host.* The container must still run the | ||
| image recorded in step 2. | ||
|
|
||
| [source,console] | ||
| ---- | ||
| [ "$(podman container inspect echidna --format '{{.Image}}')" = "$RUNNING" ] && echo same-image | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Persist the recorded image ID.
|
||
| curl -s -o /dev/null -w '%{http_code}\n' https://api.nesy-prover.dev/api/health # expect 200 | ||
| curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.nesy-prover.dev/api/verify # expect 403 | ||
| ---- | ||
|
|
||
| Stop and reassess if step 1 does not print exactly one of `quadlet` or `appendix`, or if | ||
| step 5 does not print `same-image`, `200` and `403`. | ||
|
|
||
| The stopgap ends in the change that enables authentication. That change | ||
| re-pins `Image=` to the signed image by digest and removes the `@public` | ||
| matcher. Do not re-enable `podman-auto-update.timer` before then. | ||
|
|
||
| == 0. Preflight (read-only, do first) | ||
|
|
||
|
|
@@ -195,6 +277,13 @@ podman ps && curl -s https://api.nesy-prover.dev/api/health | |
|
|
||
| == 5. Updates and rollback | ||
|
|
||
| NOTE: Unattended rollout is *disabled* while the | ||
| <<containment-stopgap>> is in force. The commands below apply only to a | ||
| tag-based `Image=` with `AutoUpdate=registry`, because `podman | ||
| auto-update` acts only on containers that carry that label. A | ||
| digest-pinned image, which is what the change ending the stopgap | ||
| introduces, changes only through a PR that bumps the digest. | ||
|
|
||
| `ghcr-publish.yml` pushes a new `:latest` on every GitHub release. | ||
| Enable unattended rollout: | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,9 +84,13 @@ through Caddy | |
| a *1 MB request body cap* | ||
| * the container is capped at `+--memory=1500m --pids-limit=512+`, with a | ||
| tmpfs `+/tmp+` and `+NoNewPrivileges+` | ||
| * `+AutoUpdate=registry+` plus `+HealthOnFailure=kill+` means a new | ||
| release rolls out unattended and an unhealthy container *fails the | ||
| unit*, which is what makes rollback actually fire | ||
| * `+HealthOnFailure=kill+` means an unhealthy container *fails the | ||
| unit*, so systemd restarts it | ||
| * *Containment stopgap (2026-11-05):* Caddy forwards only `GET`/`HEAD` | ||
| on `+/api/health+` and `+/api/provers+`, and refuses every execution | ||
| route with `403`. `+AutoUpdate=registry+` is off, and `+Pull=never+` | ||
| freezes the running image until authentication lands. See | ||
|
Comment on lines
+91
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The hosting document now says automatic updates are disabled, but its declared companion Useful? React with 👍 / 👎. |
||
| link:../deploy/hetzner/README.adoc#containment-stopgap[the runbook]. | ||
|
|
||
| ==== Why the caging matters | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the daily timer has already fired when this step runs,
disable --nowstops the specified timer but does not stop the separately activatedpodman-auto-update.service; thesystemctldocumentation defines--nowas stopping the units being disabled, while the Podman documentation confirms that the timer activates a service which pulls images and restarts units. That service can therefore retag or restartechidnaafterRUNNINGis captured—or after the final check—defeating the freeze during this race; explicitly stop and wait forpodman-auto-update.servicebefore inspecting the image.Useful? React with 👍 / 👎.