diff --git a/SUBSTRATE.md b/SUBSTRATE.md index 2f8db03..1c5fe22 100644 --- a/SUBSTRATE.md +++ b/SUBSTRATE.md @@ -72,10 +72,28 @@ consulted, so a perfect service presents as "the app is broken." All in the fleet lockbox, injected by env, **never committed**. `make check` fails on any `cfat_` / `cfut_` / `gh[pousr]_` / `et_plt_` literal in the tree. -⚠️ The R2 credential should be **scoped to this cell**. The sites cell ended up -holding the account-wide god token because v4 R2 object endpoints reject -restricted tokens. Whichever we end up with, record it here — an account-wide -token is an acceptable named debt and an unacceptable invisible one. +### ⚠️ NAMED DEBT — the R2 credential is account-wide + +**As of 2026-08-11 this cell holds the Cloudflare god token as its R2 +credential.** R2's S3 credentials are derived from an API token (access key id = +the token's id, secret = SHA-256 of its value), and **no token available to this +session has permission to mint a new one** — creating tokens is dashboard-only +or needs a token-creating token. So the wiring was proven with the god token +rather than blocked on it. + +This is recorded, not hidden, because an account-wide token is an acceptable +named debt and an unacceptable invisible one. + +**GATE: this must be replaced with a scoped R2 token BEFORE strand G7 lands +CI runners on this host.** I-5 says runners execute untrusted code and must not +share a kernel with credentials scoped beyond their own job; a god token with +R2 + Workers + Pages + WAF + SSL rights sitting on the same box as a runner is +exactly the thing I-5 exists to prevent. + +Minting one is a two-minute job in the Cloudflare dashboard: **R2 → Manage R2 +API Tokens → Create → Object Read & Write, scoped to the three `windy-git-*` +buckets.** Then set `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` in +`/srv/windygit/src/.env` and redeploy. ⚠️ The Cloudflare **god token has Zone:Read but no DNS:Edit.** Use the DNS:Edit token for record creation. diff --git a/docker-compose.yml b/docker-compose.yml index 7a93fa5..ab3caae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,22 @@ services: # G2.2 — OIDC only. No local password login, no self-registration. GITEA__service__DISABLE_REGISTRATION: "true" GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true" - GITEA__lfs__PATH: /data/lfs + # G4.3 — heavy bytes to R2 at ZERO egress. Git object databases stay on + # local NVMe (I-3); this covers LFS, attachments, packages, avatars and + # Actions artifacts, which is where GitHub's painful bills actually come + # from and where R2's free egress is a structural, permanent advantage. + GITEA__storage__STORAGE_TYPE: minio + GITEA__storage__MINIO_ENDPOINT: ${R2_ACCOUNT_ID}.r2.cloudflarestorage.com + GITEA__storage__MINIO_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID} + GITEA__storage__MINIO_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY} + GITEA__storage__MINIO_BUCKET: ${R2_BUCKET_LFS:-windy-git-lfs} + GITEA__storage__MINIO_LOCATION: auto + GITEA__storage__MINIO_USE_SSL: "true" + # ⚠️ THE R2 TRAP. R2 rejects the checksum algorithm S3 clients send by + # default; without this, uploads fail with an opaque checksum error that + # reads like a credential problem and is not one. + GITEA__storage__MINIO_CHECKSUM_ALGORITHM: md5 + GITEA__lfs__STORAGE_TYPE: minio volumes: # I-3: git object databases on a POSIX filesystem. Never object storage. - ${GIT_DATA_ROOT:-./data/gitea}:/data