R2 S3-compatible token setup (per-bucket)
Why programmatic > dashboard
Section titled “Why programmatic > dashboard”| Concern | Dashboard click | API mint (this page) |
|---|---|---|
| Audit trail | None — secret value visible once on screen, then lost | cfat_… value persisted in the PRD’s artefacts/03-mint-r2-token-response.json |
| Replay-ability | Operator must remember which permission groups were attached | 02-mint-r2-token-request.sh is the literal curl, re-runnable |
| Rotation | Guesswork: “did we use Read+Write or just Write?” | Same script, new PRD, new artefact set |
Provenance for wrangler secret put value | Operator-typed | 04-derive-s3-keys.sh is deterministic from the response |
How R2 derives an S3 keypair from a CF API token
Section titled “How R2 derives an S3 keypair from a CF API token”| S3 field | Derivation |
|---|---|
Access Key ID | result.id from POST /accounts/{id}/tokens (32 hex chars) |
Secret Access Key | sha256(result.value) — SHA-256 of the raw cfat_… string (64 hex chars) |
Source of truth: Cloudflare R2 docs — Get S3 API credentials from an API token.
Pinned permission-group UUIDs
Section titled “Pinned permission-group UUIDs”As of 2026-05-05 (re-fetch via GET /accounts/{id}/tokens/permission_groups if older than ~12 months):
| Permission group | UUID |
|---|---|
| Workers R2 Storage Bucket Item Write | 2efd5506f9c8494dacb1fa10a3e7d5b6 |
| Workers R2 Storage Bucket Item Read | 6a018a9f2fc74eb6b293b0c548f38b39 |
Both are required for a bucket whose Worker needs both PutObject and
GetObject (the common case — presigned-URL minters need to read what
they wrote).
The five steps
Section titled “The five steps”- Mint the per-bucket CF API token.
POST /accounts/{id}/tokenswith both perm-group IDs andresourcespinned tocom.cloudflare.edge.r2.bucket.<account_id>_default_<bucket>. Capture the response asdocs/agents/PRD-<N>/artefacts/03-mint-r2-token-response.json. - Derive the S3 keypair.
Access Key ID = result.id,Secret Access Key = sha256(result.value). Script:04-derive-s3-keys.sh. - SigV4 smoke-test.
aws s3api head-bucket --endpoint-url https://<account_id>.r2.cloudflarestorage.com --bucket <bucket>— exit 0 = good. - Push to Worker secrets with
wrangler secret bulkagainst every Worker that needs the keypair (e.g. PRD-223 pushed to bothapi-workerANDn1-mcp). Same JSON, two invocations. - Mirror to local
.dev.varswith a provenance comment block pointing at the per-bucket maintenance playbook.
Mandatory artefact set
Section titled “Mandatory artefact set”Every mint exercise drops the following under docs/agents/PRD-<N>/artefacts/:
| File | Captures |
|---|---|
00-README.md | Critical-facts table (account id, bucket, both perm-group IDs, both keys, parent token id, mint date), index, rotation pointer |
01-cf-permission-groups.json | Pinned permission-group rows |
02-mint-r2-token-request.sh | Verbatim curl to POST /tokens |
03-mint-r2-token-response.json | Verbatim CF API response (contains the cfat_… token VALUE) |
04-derive-s3-keys.sh | Reproducible derivation script |
05-verify-head-bucket.sh | The SigV4 smoke test |
06-wrangler-secret-bulk.json | Verbatim JSON fed to wrangler secret bulk |
07-wrangler-secret-bulk.sh | The two wrangler secret bulk invocations |
The repo owner explicitly authorises plaintext credential storage in
docs/**/*.md (see
docs/cloudflare-account-setup.md
§“Token storage”). The first complete instance is
docs/agents/PRD-223/artefacts/
— copy that file shape verbatim for any future bucket.
Worked example
Section titled “Worked example”The first instance shipped 2026-05-05 alongside PRD-223
(n1-feedback-screenshots). The artefacts contain the live keypair
that api-worker + n1-mcp use today to mint 15-minute presigned-GET
URLs from n1_admin_feedback_list and the admin web UI:
- Account:
dc59d537ca69e32f136f8ab457ce1a7c - Bucket:
n1-feedback-screenshots - Access Key ID:
a9c0e0df8369cb59526e282b69b353ab - Parent CF API token used to mint: the “n1-cloud token”
(
cfat_vfL…91) inpackages/api-worker/.dev.vars— givenAccount API Tokens: Read & Writescope on 2026-05-05.
When to rotate
Section titled “When to rotate”| Trigger | Action |
|---|---|
| Yearly | Re-run §1–§5 with a fresh PRD number; revoke old token via DELETE /accounts/{id}/tokens/{old_id} after ≥ 15 min (wait for in-flight presigned URLs to expire). |
| Suspected leak | Same as yearly, but immediate. The 15-minute wait still applies — already-issued presigned URLs survive past the wrangler-secret swap. |
| Parent CF API token revoked / offboarded | Re-mint from a fresh parent first; only then destroy the old parent. |
Common failures
Section titled “Common failures”| Symptom | Cause | Fix |
|---|---|---|
POST /tokens → 9106 Invalid permission groups | CF rotated the perm-group UUIDs | Re-fetch from GET /accounts/{id}/tokens/permission_groups. |
head-bucket → AccessDenied | resources path doesn’t match the bucket | Literal value: com.cloudflare.edge.r2.bucket.<account_id>_default_<bucket>. |
head-bucket → SignatureDoesNotMatch | Pasted token id where secret should go | Re-derive per Step 2. |
wrangler secret bulk → worker not found | --env=production against a Worker without that env in its wrangler.jsonc | Drop --env. |
Cross-references
Section titled “Cross-references”docs/cloudflare-account-setup.md— parent CF API token storage convention + scope rules.- Cloudflare R2 docs: developers.cloudflare.com/r2/api/tokens
- Cloudflare API reference: developers.cloudflare.com/api/operations/account-api-tokens-create-token