carlos / platform Public

README: the mail user boundary rollout, in order

Set CARLOS_MAIL_USER_BOUNDARY everywhere first, IAM step 1, roll, run
the sweep, check every /carlos-mail/ user, then IAM step 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev fba9c6c0eb0ddc7a155bc1e257559b570805c4a0 parent fedae5a
1 file changed, +10 −4
  • README.md +10 −4
diff --git a/README.md b/README.md
index 940768627..497ee1b62 100644
--- a/README.md
+++ b/README.md
@@ -500,10 +500,16 @@ This release stops every box and the console from reading the whole deployment b
This release creates every email sending user (IAM path `/carlos-mail/`) under a permissions boundary, the way store users already are. Without one, anything that can act as the console's role can create a user on that path, give it an administrator policy and mint it a key.
-- a console with `CARLOS_MAIL_REGION` set now refuses to start without `CARLOS_MAIL_USER_BOUNDARY`. set it to the ARN of an IAM managed policy that allows `ses:SendRawEmail` and nothing else.
-- create that policy and give the console role `iam:PutUserPermissionsBoundary` on `user/carlos-mail/*` before this rolls. condition it on `iam:PermissionsBoundary` equal to the boundary ARN, or the role can set any boundary it likes. never grant `iam:DeleteUserPermissionsBoundary`. nothing calls it.
-- existing users are fixed lazily. a user without the boundary, or with a different one, gets the configured one the next time the console ensures it: `carlos email enable` for the app, a new standalone credential, or a domain change that refreshes the app's policies. until then it stays unbounded. list the ones still missing it with `aws iam get-user` over `aws iam list-users --path-prefix /carlos-mail/`.
-- once every console runs this release and every existing user carries the boundary, condition the role's `iam:CreateUser` and `iam:PutUserPolicy` on `iam:PermissionsBoundary` too. the console sets the boundary before it writes the policy, so the condition does not block the backfill.
+A console with `CARLOS_MAIL_REGION` set refuses to start on this release without `CARLOS_MAIL_USER_BOUNDARY`. Its value is the ARN of a customer managed policy in the same account, `arn:aws:iam::<12-digit account>:policy/<name>`, that allows `ses:SendRawEmail` and nothing else. A console without `CARLOS_MAIL_REGION` needs nothing.
+
+Roll it in this order:
+
+1. create the boundary policy. set `CARLOS_MAIL_USER_BOUNDARY` to its ARN in every console's environment before any console gets this release. an older console ignores the variable. this release refuses to start without it.
+2. apply IAM step 1 to the console role. add `iam:PutUserPermissionsBoundary` on `user/carlos-mail/*`, conditioned on `iam:PermissionsBoundary` equal to the boundary ARN. without that condition the role can set any boundary it likes. add `iam:ListUsers` for the sweep in step 4. AWS does not scope `ListUsers` to a path, so it is granted on `Resource "*"`. it only reveals user names and paths, and the sweep asks only for `/carlos-mail/`. the role already has `iam:GetUser` on the path. never grant `iam:DeleteUserPermissionsBoundary`. nothing calls it.
+3. roll the console. from now on every new sending user gets the boundary. an existing user gets it the next time the console touches it: enable, a standalone credential, a domain change, and key rotation too, since no key is minted for a user without the boundary.
+4. run the sweep, which also reaches users nothing will touch again. set `CARLOS_MAIL_BOUNDARY_SWEEP=1` on one console and restart it. it logs `set the boundary on <user>` for each user it fixes. its last line is either `all N mail users bounded` or the users that are not, with why. fix any failure and run it again. then remove the variable.
+5. check it yourself. for every user in `aws iam list-users --path-prefix /carlos-mail/`, `aws iam get-user --user-name <user>` must show `PermissionsBoundary` with the boundary ARN.
+6. apply IAM step 2. condition the role's `iam:CreateUser` and `iam:PutUserPolicy` on `iam:PermissionsBoundary` equal to the boundary ARN. do this only after every console runs this release, because an older console creates users without the boundary and step 2 refuses them.
## App config & secrets