# Who can read `adr-intake-submissions` — verification extract **Why this file exists.** `/legal/privacy/` makes a statement to the public about who can see the contents of the intake table. `AGENTS.md` §4 admits no factual claim that cannot be traced, and `CLAUDE.md`'s R14 rule is that anything a spec claims about must be reachable from the repository — *"if the artefact lives only in a console, no reviewer can compare the claim against it and the claim is unverifiable by construction."* Until this file existed, that sentence was the one claim on the site whose subject was entirely outside the repo. Raised by `claims-auditor` in the D20 cutover audit, 2026-09-01, finding 8. **Provenance.** Every figure below was read from AWS on **2026-09-01** with the commands listed at the end, run read-only as `arn:aws:iam::327082975128:user/pouya`. No command in this file creates or changes anything. Re-run them rather than trusting this file; it is dated for that reason. --- ## The claim being checked `src/pages/legal/privacy.astro`, §Who can see it: > The table is reachable by the function that writes to it and by one > administrative account, which is mine — nobody else has access to the table. > There is no team, no assistant and no external administrator. ## The finding: it is false The account has an IAM group **`admins`** carrying the AWS managed policy **`AdministratorAccess`**, and it has **two members: `pouya` and `lars`.** `iam simulate-principal-policy` for `dynamodb:GetItem`, `dynamodb:Query` and `dynamodb:Scan` against `arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions`, across all five IAM users in the account: | principal | GetItem | Query | Scan | |---|---|---|---| | `user/pouya` | **allowed** | **allowed** | **allowed** | | `user/lars` | **allowed** | **allowed** | **allowed** | | `user/adr-sml-deploy` | implicitDeny | implicitDeny | implicitDeny | | `user/gitea-deploy-meshkinilaw` | implicitDeny | implicitDeny | implicitDeny | | `user/meshkini-backend-deploy` | implicitDeny | implicitDeny | implicitDeny | `lars` holds exactly the access `pouya` holds, by the same route: membership of `admins`. The user's own attachments are only `IAMUserChangePassword`, so the group is the whole of it. So the published sentence is wrong on both of its halves — a second account has access, and it belongs to a second administrator of a shared account. ## The rest of the surface, recorded so the check is complete rather than partial - **5 IAM users**: `adr-sml-deploy`, `gitea-deploy-meshkinilaw`, `lars`, `meshkini-backend-deploy`, `pouya`. The three deploy users are all `implicitDeny` above. `adr-sml-deploy`'s scope is S3 + CloudFront and touches no table (`docs/reference/deploy-credential-verification.md`). - **1 IAM group**: `admins` — `AdministratorAccess` and `Billing`, two members. - **33 IAM roles**, 26 of them not service-linked. Two carry `AdministratorAccess`: `cdk-hnb659fds-cfn-exec-role-327082975128-ca-central-1` and `…-us-east-1`. These are **AWS CDK bootstrap CloudFormation execution roles**, assumable by CloudFormation for stack deployment. They are a real path to the table for anyone who can deploy a CDK stack in this account — which is the two administrators above — rather than a third party. - **`adr-intake-lambda-role`** is the writing principal: `dynamodb:PutItem` on this table, `ses:SendEmail`/`SendRawEmail`, plus `AWSLambdaBasicExecutionRole`. **`PutItem` only — it cannot read the table**, which is worth stating because it is a stronger fact than the page currently claims and it is the part of the sentence that is true. - **No resource-based policy on the table.** DynamoDB supports one; this table has none, so access is governed entirely by identity policies. - The account is **not single-project** (`AGENTS.md` §10). `lars` and the two `meshkini*`/`gitea*` users are evidence of that on the IAM surface, not just in the S3 bucket listing §10 describes. ## What has to happen before `/legal/privacy/` goes public Tracked as `AGENTS.md` §9 **Q62**. It is one of two things and both are Pouya's: 1. **Remove the access** — take `lars` out of `admins`, or replace that membership with a policy that denies DynamoDB on this table — and then this sentence becomes true. Note the likely collision: `AGENTS.md` Q23 records the Gitea instance as *jointly administered* and blocked on *"its second administrator"*, so this account is probably not the only thing that access is for. 2. **Correct the sentence** to what is true. It is a privacy policy, so the honest version is short and specific — the number of people with administrative access, and that the function that writes cannot read. **Do not resolve it by softening.** "Access is limited to authorised administrators" is the shape §4 exists to bar: defensible, uninformative, and it would replace a false specific with a true vacancy on the one page where a reader is entitled to the specific. --- ## Commands Run as `user/pouya`, `ca-central-1`, all read-only. Exit status read on each; no stderr suppressed anywhere. ```bash aws iam list-users --query 'Users[].UserName' aws iam list-groups --query 'Groups[].GroupName' aws iam get-group --group-name admins --query 'Users[].UserName' aws iam list-attached-group-policies --group-name admins --query 'AttachedPolicies[].PolicyName' aws iam list-group-policies --group-name admins --query 'PolicyNames' aws iam list-groups-for-user --user-name lars --query 'Groups[].GroupName' aws iam list-attached-user-policies --user-name lars --query 'AttachedPolicies[].PolicyName' aws iam list-user-policies --user-name lars --query 'PolicyNames' TARN="arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions" for U in pouya lars adr-sml-deploy gitea-deploy-meshkinilaw meshkini-backend-deploy; do aws iam simulate-principal-policy \ --policy-source-arn "arn:aws:iam::327082975128:user/${U}" \ --action-names dynamodb:GetItem dynamodb:Query dynamodb:Scan \ --resource-arns "$TARN" \ --query 'EvaluationResults[].{A:EvalActionName,D:EvalDecision}' --output text done # Roles: 33 total, 26 non-service-linked; screened for broad policies. for R in $(aws iam list-roles --query 'Roles[].RoleName' --output text \ | tr '\t' '\n' | grep -v '^AWSServiceRole'); do aws iam list-attached-role-policies --role-name "$R" \ --query 'AttachedPolicies[].PolicyName' --output text done aws iam get-role-policy --role-name adr-intake-lambda-role \ --policy-name adr-intake-lambda-inline --query PolicyDocument aws iam list-attached-role-policies --role-name adr-intake-lambda-role ``` ⚠️ **`simulate-principal-policy` was the tool that produced a false negative on this project once already** — `AGENTS.md` Q22, where eight checks returned empty because `2>/dev/null` was hiding an `InvalidInput` error caused by a zsh parameter-expansion bug (`$ACCT:user/` parses `:u` as a history modifier). The loop above brace-quotes `${U}` for that reason, prints one line per principal so a silently-skipped iteration is visible as a missing row, and suppresses nothing. **Five rows, or the run did not happen.**