From 435007655593296921ce6e9e1113e642826643ab Mon Sep 17 00:00:00 2001 From: Andrii Kohut Date: Tue, 18 Aug 2026 14:48:54 +0200 Subject: [PATCH] docs: the production stack has now actually been run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deploy/compose shipped verified only by `docker compose config`. The README said artifact upload was unproven, which was honest and is no longer true. Brought the whole stack up behind Caddy on local hostnames and checked what it claims: only 80 and 443 published, Postgres and MinIO reachable only inside the network, both apps answering /health on their own hostnames with different bodies — which is the concrete reason they are separate hostnames rather than paths — the object store refusing an unsigned GET, and an artifact surviving a presign, a PUT and a presigned GET with the same bytes. One property nobody had written down turned up on the way. A presigned upload is bound to the size it was issued for: a body larger than the declared sizeBytes is refused with 403. That is a grant for a screenshot being unable to carry a gigabyte, and it is worth reading as the limit working rather than as a misconfiguration, which is what a bare 403 usually means to someone deploying. Certificate issuance is the one thing local testing cannot cover — Caddy uses an internal certificate for a .localhost name — so the DNS-first instruction stays, and now says why. --- deploy/RUNBOOK.md | 7 +++++++ deploy/compose/README.md | 34 ++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/deploy/RUNBOOK.md b/deploy/RUNBOOK.md index 0d77cfd..e9ccb40 100644 --- a/deploy/RUNBOOK.md +++ b/deploy/RUNBOOK.md @@ -173,6 +173,13 @@ recovered. Without that step a drill where the volume quietly survived would pas would one where the restore did nothing — the seeded database looks much like the backed-up one, which is exactly how this check was found to be necessary. +### Artifact grants are bounded + +A presigned upload is issued for a specific key, content type and **size**. A body that does +not match the declared `sizeBytes` is refused with `403`, so a grant meant for a screenshot +cannot be spent on a gigabyte — read a `403` there as the limit working, not as a +misconfiguration. + ## Platform observability Flakemetry is OpenTelemetry-native and should dogfood its own telemetry: api and worker diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 4ce76e5..434be4e 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -144,13 +144,27 @@ the whole path is drilled in CI on every change: it destroys the database volume the instance back from a dump. See the [runbook](../RUNBOOK.md#backups--disaster-recovery) for RPO/RTO and the reasoning. -## One thing to verify yourself - -Artifact upload end to end — reporter → presigned PUT → browser download — is **not covered by -CI**, so treat it as unproven until you have watched a screenshot appear in the dashboard. - -The failure mode to expect is a `403` on the presigned URL. SigV4 signs the `Host` header, so -a URL signed for `minio:9000` is rejected when a browser presents `artifacts.example.com`. -That is why every service here signs against the public artifact host even for traffic that -never leaves the machine. If you move to real S3 or R2, set `FLAKEMETRY_S3_ENDPOINT` to the -same hostname your CI runners and browsers will actually connect to. +## Verified, and what that covers + +This stack has been run end to end — not only `docker compose config`: + +| | | +| --- | --- | +| Only Caddy publishes | 80 and 443; Postgres and MinIO are reachable only inside the network | +| Both apps answer `/health` | `{"service":"api"}` and `{"service":"web"}` on their own hostnames, which is why they are separate hostnames | +| Artifacts round-trip | presign → `PUT` → presigned `GET` returns the same bytes | +| The object store refuses unsigned requests | `403 AccessDenied` on a plain `GET` | + +**A presigned upload is bound to the size it was issued for.** A body larger than the +declared `sizeBytes` is refused with `403`, so a grant for a screenshot cannot be spent on a +gigabyte. Worth knowing before you read a `403` as a misconfiguration. + +The one thing local testing cannot cover is certificate issuance: Caddy uses an internal +certificate for a `.localhost` name and only talks to Let's Encrypt for a real domain. Point +the DNS records at the host before the first start, or the initial order fails. + +If you move to real S3 or R2, set `FLAKEMETRY_S3_ENDPOINT` to the hostname your CI runners +and browsers actually connect to. SigV4 signs the `Host` header, so a URL signed for +`minio:9000` is rejected the moment a browser presents the public name — which is why every +service here signs against the public artifact host even for traffic that never leaves the +machine.