spec: the host role cannot provision, and the grant that fixes it can escalate
Switching a deployment on is not just the three CARLOS_STORE_* env vars. region-host's policy has no s3:CreateBucket and none of the iam user lifecycle the provisioner calls, so a console with a backend configured and nothing else fails at its first grant, against live AWS, deep in the walk. Flagship consumes the module from platform main, so the policy lands here. Recording with it the reason that policy is not a one-line addition: EnsureUser sets no permissions boundary, so iam:CreateUser plus iam:PutUserPolicy scoped only by the /carlos-store/ path is a path from the console box to the whole account. The boundary belongs in the same change. Found 2026-09-08 trying to switch the flagship on for an account whose app has been waiting since 2026-09-01.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CfUNFHm89j2mj5q7vBHsfH
1 file changed,
+24
−0
docs/superpowers/specs/2026-09-02-object-storage-self-serve-design.md+24 −0
diff --git a/docs/superpowers/specs/2026-09-02-object-storage-self-serve-design.md b/docs/superpowers/specs/2026-09-02-object-storage-self-serve-design.md| index 0c1a5d262..75c5df2fb 100644 |
| --- a/docs/superpowers/specs/2026-09-02-object-storage-self-serve-design.md |
| +++ b/docs/superpowers/specs/2026-09-02-object-storage-self-serve-design.md |
| @@ -40,6 +40,27 @@ act on neither: |
| needs a person on `CARLOS_STORE_OPERATORS` to run `carlos store grant --app |
| <a>`, once, per app, forever. |
| +**A third blocker, found 2026-09-08 while trying to switch the flagship on for |
| +another account.** Setting the three env vars is necessary but not sufficient: |
| +the console's own instance role cannot provision. `region-host`'s policy grants |
| +Get/Put/Delete on the deployment and data buckets plus a read-only Describe/List |
| +set for the architecture page (`infra/modules/region-host/main.tf:76-140`), |
| +while the provisioner calls `s3:CreateBucket`/`DeleteBucket` and the whole |
| +`iam:CreateUser`/`PutUserPolicy`/`CreateAccessKey` lifecycle |
| +(`internal/objstore/aws.go:31-46`). A deployment that sets |
| +`CARLOS_STORE_BACKEND` and nothing else fails at its FIRST grant, deep in the |
| +walk, against live AWS — the worst place to discover a missing policy. Flagship |
| +consumes the module from `platform.git//infra/modules/region-host?ref=main`, so |
| +that policy lands in this repo before any deployment can be switched on. |
| + |
| +> **The grant carries a privilege escalation that must be closed with it.** |
| +> `EnsureUser` sets no permissions boundary (`aws.go:282-310`), so a role |
| +> holding `iam:CreateUser` and `iam:PutUserPolicy` scoped only by the |
| +> `/carlos-store/` path can mint a principal with any policy at all — a path |
| +> from the console box to the whole deployment account. The module should ship |
| +> a boundary policy (S3 only, scoped to the bucket prefix), condition both |
| +> actions on `iam:PermissionsBoundary`, and `CreateUserInput` should carry it. |
| + |
| This spec fixes (2) and makes (1) visible. (1)'s actual fix is #368 and is a |
| non-goal here (§12). |
| @@ -415,6 +436,9 @@ another name and was ruled out in §3. |
| daily. |
| - **Fixing `CARLOS_STORE_BACKEND` as a root env edit.** That is #368. This spec |
| adds no new root env vars. |
| +- **The host role's provisioning IAM and its permissions boundary.** Rides with |
| + #368's switch-on rather than with this spec — but neither is complete without |
| + it, and until it lands an approved account still cannot get a bucket (§1). |
| - **Quotas or bucket caps.** Ruled out in §3. |
| - **Tearing down estate on decline.** §10. |
| - **A generalised capability-keyed entitlement system.** Email's tier |