From 913ae0f052c99d62f48d0014026f7e096c5fb7b9 Mon Sep 17 00:00:00 2001 From: Devinder Singh Date: Thu, 29 Oct 2026 17:11:55 +0100 Subject: [PATCH] docs: close Phase 1 (review points from #17 and #18, live test results) --- docs/CONTEXT.md | 25 +++++++++++-------- .../decisions/0004-public-container-images.md | 10 ++++---- infra/server/README.md | 17 ++++++++++--- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 36322c4..4be344b 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -127,13 +127,18 @@ encrypting the signing-secret column (we protect the backups instead), OpenAPI t 443 over IPv4, and nothing over IPv6. ufw on the server allows 22, 80, 443 and the k3s networks 10.42.0.0/16 and 10.43.0.0/16. ufw can't filter ports that Kubernetes publishes itself (NodePorts, such as Traefik's 30795 and 32338), so only one.com's firewall blocks - those. It must never get an "All" rule again (it had one until 29 Oct). Check from outside: - `nc -z -G 3 81.88.23.71 30795` must fail. + those. It must never get an "All" rule again (it had one until 29 Oct). Check from outside, + both ways: `443` must answer AND `30795` must not. A check that only expects a failure can't + tell a blocked port from a broken command. macOS: `nc -z -G 3 81.88.23.71 `; + Linux (netcat-openbsd has no `-G`): `nc -z -w 3 81.88.23.71 `. - **kubectl:** only on the server (`ssh relay`, then `kubectl …`). Port 6443 stays closed. - **Deploy path:** GitHub Actions (environment `production`, main only) logs in as the Linux user `deploy`, whose key can only start `/usr/local/bin/relay-deploy`. The script deploys a commit only if it is on main, using the Kubernetes account `deployer` (namespace `relay` only, no Secrets). Which folder is applied how: [`k8s/README.md`](../k8s/README.md). +- **Two replicas, one node:** the API runs 2 pods behind Traefik. That gives zero-downtime + rollouts and protection against one crashing process. It is **not** high availability: if + the VPS goes down, everything is down. - **Domain:** `myrelay.tech`, DNS at get.tech. A records for `api`, `cdn` (the Relay dashboard) and `grafana` (the ops dashboard: metrics and logs) all point to the VPS. **No AAAA records**: the cluster is IPv4-only, and Let's Encrypt prefers IPv6 when it exists. @@ -145,7 +150,8 @@ encrypting the signing-secret column (we protect the backups instead), OpenAPI t ## Git workflow - `main` is protected: changes only through a PR with 1 approval from the other person, - no force-pushes, no deletion. Once the pipeline exists, it must pass too. + no force-pushes, no deletion, and the required checks `api-test` and `deploy-script-test` + must be green. An approval only counts for the newest push. - Branch names: `feat/…`, `fix/…`, `docs/…`, `infra/…`, `test/…`. - Commit messages: `type: short summary` (for example `docs: add team context`). - Small PRs, squash-merge, delete the branch after merging. @@ -157,13 +163,12 @@ Phase 0 (server foundation) is done: VPS hardened, k3s, cert-manager with Let's issuers, and a temporary hello-world app on https://api.myrelay.tech with a trusted certificate. -Phase 1 (walking skeleton) is nearly done. Merged: HTTP → HTTPS redirect (1.1), DNS for cdn -and grafana (1.2), the API skeleton in `api/` with its Docker image (1.3–1.4, #11), and the -deploy path (1.6, #13): a `deploy` user whose key can only run `relay-deploy`, and a -`deployer` account limited to namespace `relay`. In review, to be merged in this order: the CI -pipeline (1.5, #12), rollback to the last good commit (#14), the API manifests (1.7, #15) and -the deploy job (1.8, #16). Then `api.myrelay.tech` switches from the hello app to the API with -the first deploy through the pipeline. +Phase 1 (walking skeleton) is done (29 Oct). Every merge to `main` is tested, built and +deployed by the pipeline, then checked from outside on `https://api.myrelay.tech/version`. +The API runs as 2 pods with probes, a rolling update and graceful shutdown. Tested live: +76,466 requests from 20 clients during a deploy, 0 failed; a deliberately failed deploy rolled +back to the last good commit with 119,824 requests and 0 failed, and users never saw the bad +version. The hello app is gone. Next: Phase 2, the database foundation. The plan in order: 1 walking skeleton · 2 database foundation · 3 core API · 4 worker · 5 dashboard on cdn (Abdikhani, in parallel) · 6 backups + Terraform (Abdikhani, in parallel) · diff --git a/docs/decisions/0004-public-container-images.md b/docs/decisions/0004-public-container-images.md index 72d6975..57d18e7 100644 --- a/docs/decisions/0004-public-container-images.md +++ b/docs/decisions/0004-public-container-images.md @@ -37,11 +37,11 @@ commit on `main`. so the rule above is a hard rule: every Dockerfile change is reviewed against it. - If an image ever has to contain something that isn't public, this decision is revisited for that image (private, with a pull secret). -- Check that an image is still public (200 = public, 401 or 403 = private): +- Check that the package is still public, without needing a tag that exists. A public package + answers with its tag list; a private (or missing) one answers `DENIED`. Keep the two parts + joined with `&&`, so the second never runs without a token: ```bash -TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:devinder-dev/relay-api:pull&service=ghcr.io" | jq -r .token) -curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" \ - -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json" \ - https://ghcr.io/v2/devinder-dev/relay-api/manifests/ +TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:devinder-dev/relay-api:pull&service=ghcr.io" | jq -r .token) && \ + curl -s -H "Authorization: Bearer $TOKEN" https://ghcr.io/v2/devinder-dev/relay-api/tags/list ``` diff --git a/infra/server/README.md b/infra/server/README.md index b7db845..21b63f1 100644 --- a/infra/server/README.md +++ b/infra/server/README.md @@ -14,8 +14,10 @@ run alone, never inside a pasted block. - **one.com network firewall:** exactly three rules, IPv4 TCP 22, 80 and 443. No `All` rule and nothing for IPv6. ufw can't filter ports that Kubernetes publishes (NodePorts), so this firewall is the only thing blocking them. When changing rules, add the new one before - deleting an old one, so SSH never drops. Check from outside: `nc -z -G 3 30795` - must fail, and 22, 80, 443 must answer. + deleting an old one, so SSH never drops. Check from outside, both ways: 22, 80 and 443 must + answer AND 30795 must not. A check that only expects a failure can't tell a blocked port from + a broken command. macOS: `nc -z -G 3 `; Linux (netcat-openbsd has no `-G`, + so there it fails before connecting): `nc -z -w 3 `. - **get.tech DNS:** A records `api`, `cdn` and `grafana` → the VPS's IPv4 address. No AAAA records: the cluster is IPv4-only, and Let's Encrypt prefers IPv6 when it exists. @@ -49,7 +51,9 @@ run alone, never inside a pasted block. ## 5. GitHub settings (not in git; made with `gh`, stored by GitHub) - Ruleset `protect-main`: PR + 1 approval, approval after the last push, no force-push, no - deletion. Squash merges only. Required check `api-test`. + deletion. Squash merges only. Required checks `api-test` and `deploy-script-test` (since + 29 Oct). "Approval after the last push" is `require_last_push_approval: true`: an old + approval stays visible, but a PR can't merge until the newest push is approved. - Environment `production`: only `main` may use it. Secret `DEPLOY_SSH_KEY`, variables `DEPLOY_HOST` and `DEPLOY_KNOWN_HOSTS`. - GHCR package `relay-api`: public, so the cluster pulls it without a password. @@ -66,4 +70,9 @@ from git and these scripts. Only the database holds data that can't be rebuilt. `unattended-upgrades` installs security updates every day but never reboots by itself (`Automatic-Reboot` stays off, so a demo is never interrupted). After a kernel update (`/var/run/reboot-required` exists), reboot by hand at a quiet time, never in the week of the -defense, and check that `/version` answers again afterwards. +defense. + +- **Before:** check that the running version is the last deploy, so a reboot can't quietly + bring back an old image: `curl -s https://api.myrelay.tech/version` must show the same + commit as `git rev-parse origin/main`. +- **After:** `/version` answers again with that same commit.