Two rulings from Pouya, 2026-09-02.
(1) The section stays generic — "it over-explains technical mechanics that
belong in the evidence file, not in front of an inquirer." Deleted: the
measurement paragraph, the root-credential sentence, the SSO/federated-login
enumeration, the resource-policy clause, the "company that runs a database"
aside, the deploy-credential sentence and the three-copies summary. All of it
stays true and stays measured in AGENTS.md §7 and the evidence file, which now
maps each shipped sentence to what it rests on.
(2) The consent string names the corporation: "I consent to SML Company Ltd
storing and using the information in this form…". docs/05 §Consent text moves
with it, proven byte-identical. Two new §4 rows carry the attestations the copy
rests on.
(3) The §Who can see it approval closes via the page read-through, which is now
blocker 2 in docs/06's callout rather than a checklist line.
Q64 closes MOOT — the paragraph it was about was deleted, so it gates nothing.
The underlying gap is unchanged: §7 records root as held by Pouya, not held only
by Pouya, and nothing about root custody may be published without asking again.
Two sentences were added back under review: the shared-account disclosure, to
§Where it is stored (a storage disclosure, never named in the ruling — without
it no page said the intake sits in a shared account), and one naming SML Company
Ltd in the policy, because a consent naming a company the linked policy never
mentions is an accountability gap.
adversarial-reviewer, two rounds, 14 findings, all resolved, none declined;
nine of round 2's ten were defects in round 1's own repairs. claims-auditor
correctly deferred to cutover per D20.
Gates, exit status read: check 0 · build 0 (23 pages) · check:claims 0
(12 patterns, 33 approved strings) · check:intake 0 · og:proof 0 · lint 0 ·
lighthouse 0, worst of 23 99/100/100/100. Tripwire proven both ways — exit 0 on
the revised page, exit 1 with 5 matches on the bd282aa bytes. Regex untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
536 lines
31 KiB
Markdown
536 lines
31 KiB
Markdown
# 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.** Figures in the ORIGINAL sections were read from AWS on
|
||
**2026-09-01**; everything in the **2026-09-02 addendum** at the foot of this file
|
||
was read on 2026-09-02, and it supersedes the original role screen. Both were run
|
||
read-only as `arn:aws:iam::327082975128:user/pouya` with the commands listed at
|
||
the end.
|
||
No command in this file creates or changes anything. Re-run them rather than
|
||
trusting this file; it is dated for that reason.
|
||
|
||
---
|
||
|
||
## ✅ RULED AND APPLIED — 2026-09-02
|
||
|
||
**Pouya ruled `state the truth`, not `remove the access`** (§9 Q62, ruled
|
||
2026-09-01, applied 2026-09-02). Option 2 below is the one taken; option 1 was
|
||
declined. `lars`'s membership of `admins` is **unchanged**.
|
||
|
||
⚠️ **AND THEN RULED AGAIN THE SAME DAY — §9 Q63, and the second ruling is the
|
||
one this file most needs to carry, because THIS FILE SUPPLIED THE ERROR.** The
|
||
first shipped sentence was *"Two people can"*, and it was read straight off the
|
||
enumeration below. **Pouya's attestation, 2026-09-02:** *"two people is an
|
||
exaggeration… a handful is accurate — the simulation counts identities, not
|
||
humans, and the two are not the same claim."*
|
||
|
||
**The enumeration is exhaustive and the inference off it was not.** Every read
|
||
path terminates at `user/pouya` or `user/lars`; how many **people** can reach
|
||
those two credentials is not something `simulate-principal-policy` can see, so
|
||
the identity count is a **lower bound on people** and the page published it as an
|
||
exact count. **No numeric human headcount may ship.** The identity counts in this
|
||
file are unaffected and stay exactly as measured — this is a correction to what
|
||
may be *concluded* from them, not to any of them.
|
||
|
||
⚠️ **AND RULED A THIRD TIME, LATER THE SAME DAY: THE PAGE STATES WHO, AND THIS
|
||
FILE HOLDS THE METHOD.** Pouya, 2026-09-02: *"the page stays generic. It
|
||
over-explains technical mechanics that belong in the evidence file, not in front
|
||
of an inquirer."* §Who can see it is now **four short statements**. **Deleted from
|
||
the page:** the measurement paragraph, the root-credential sentence, the single-sign-on and federated-login enumeration, the resource-policy clause, the *"company that runs a database"* aside, the deploy-credential sentence and the three-copies summary. ⚠️ **THE SHARED-ACCOUNT CLAUSE WAS CUT WITH THEM AND THEN RESTORED — to §Where it is stored, where it belongs.** It is a storage disclosure rather than mechanics, the ruling did not name it, and without it no page told a reader their intake sits in an account that also runs unrelated systems (`adversarial-reviewer`, round 1). **These lists must stay identical — there were four of them and they named four different sets.** None of that was retracted and none of it is lost — it
|
||
is all still below, unchanged, and **that is now this file's job rather than a
|
||
supporting role.** ⚠️ **THE PAGE NO LONGER CITES THIS FILE'S CONTENT, SO THE
|
||
COMPARISON BELOW IS THE ONLY THING TYING THE TWO TOGETHER. Keep it in sync, and
|
||
do not restore a deleted sentence to the page on the strength of finding it
|
||
here** — the section comment in `src/pages/legal/privacy.astro` carries the same
|
||
bar.
|
||
|
||
**The shipped sentences as at 2026-09-02**, so this file can be compared against
|
||
the live page rather than against a struck one. All four, in order, complete:
|
||
|
||
> The record in the table: me, and the small number of people who administer the
|
||
> account it sits in with me.
|
||
|
||
> The system that receives what you send **can only add a record — it cannot read
|
||
> back what is stored.**
|
||
|
||
> The notification goes to the practice's mailbox, which is read by me and by
|
||
> administrative staff and is hosted on Google Workspace — so Google holds a copy
|
||
> of whatever you send me.
|
||
|
||
> The confirmation that went to you sits with whoever runs your email. That copy
|
||
> is in your hands rather than mine.
|
||
|
||
**What each rests on, because that mapping is the reason this file exists.**
|
||
Sentence 1: the enumeration below, plus Pouya's *"a handful"* attestation for the
|
||
human quantifier — **the measurement gives administrators, the attestation gives
|
||
the number, and neither gives the other.** Sentence 2: `adr-intake-lambda-role`
|
||
holds `PutItem` only, implicitDeny on all six read and modify actions.
|
||
Sentence 3: **an attestation, not a measurement** — `AGENTS.md` §7's
|
||
`info@smlcompany.ca` row; nothing in this repository or in AWS can check it.
|
||
Sentence 4: the handler's second `SendEmailCommand`.
|
||
|
||
**Three things the page deliberately does NOT say, and each was deleted by
|
||
ruling rather than being unsupported.** The **root credential** (§7 records it as
|
||
held by Pouya with no access key and MFA on — ⚠️ *held*, not *held only*, which
|
||
is why publishing it needed a question and why §9 Q64 is closed as **moot** and
|
||
not as answered). The **single-sign-on and resource-policy findings**. And the
|
||
**`33`** — deliberately withheld even while the paragraph stood, because a role
|
||
total moves when AWS creates a service-linked role by itself.
|
||
|
||
**The wording approval Pouya reserved is discharged by the read-through** — his
|
||
ruling, 2026-09-02: *"do not hold anything open waiting on a separate wording
|
||
approval; the read-through is the approval."* Q63(a) had approved a version, and
|
||
the version changed twice after it.
|
||
|
||
⚠️ **AND THE ENUMERATION BELOW WAS NOT ENOUGH TO SUPPORT THE COMPLETENESS CLAIM
|
||
— *"every user and every role in the account was simulated against this table"*,
|
||
which §7 still asserts and the page no longer carries. See the addendum at the
|
||
foot of this file**, which is what that claim actually rests on. Read it before
|
||
citing the five-row table. *(This pointer said "the second of those sentences"
|
||
until 2026-09-02: it indexed the quote block by POSITION, and the block changed
|
||
length under it. Name the claim, not its ordinal.)*
|
||
|
||
---
|
||
|
||
## The claim being checked — ⚠️ STRUCK 2026-09-01, QUOTED HERE AS THE DEFECT
|
||
|
||
This is **no longer on the page.** It is kept because it is the string
|
||
`check-claims.mjs`'s `sole-administrator-q62` pattern permanently bars, and a
|
||
tripwire whose target is not recorded anywhere becomes unmaintainable.
|
||
|
||
`src/pages/legal/privacy.astro`, §Who can see it, **as it stood at `bd282aa`**:
|
||
|
||
> 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 had to happen before `/legal/privacy/` went public — ✅ RESOLVED BY OPTION 2
|
||
|
||
Tracked as `AGENTS.md` §9 **Q62**, **closed 2026-09-02 on option 2.** Kept
|
||
unstruck because the reasoning is what makes the ruling re-readable, and because
|
||
option 1 remains live in one direction: if that access is ever actually removed,
|
||
the page and the tripwire both have to change, and `check-claims.mjs`'s `rule:`
|
||
line carries that instruction.
|
||
|
||
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.**
|
||
|
||
---
|
||
|
||
## ⚠️ ADDENDUM 2026-09-02 — THE ENUMERATION ABOVE WAS INCOMPLETE IN THREE WAYS, AND ITS CONCLUSION SURVIVES ANYWAY
|
||
|
||
**Why this addendum exists.** `/legal/privacy/` published a completeness claim
|
||
about who can read this table — *"every user and every role in the account was
|
||
simulated against this table"* — and, written as it stood, this file did not
|
||
support it. ⚠️ **THAT SENTENCE IS NO LONGER ON THE PAGE**: Pouya ruled later the
|
||
same day that the section states who and not how, and the measurement paragraph
|
||
was deleted (see **The shipped sentences** at the top of this file, which is now
|
||
four statements and does not include it). **This addendum is not thereby
|
||
obsolete — it is now load-bearing in a different place.** `AGENTS.md` §7's
|
||
`Intake table — who can read it` row still asserts the completeness, `docs/06`
|
||
and §12 **R21** still instruct an operator to re-run it before cutover, and the
|
||
page's first sentence still rests on its conclusion even though it no longer
|
||
recites the method. **A claim moved off a public page into a register is still a
|
||
claim.**
|
||
|
||
*(This paragraph quoted a different sentence until 2026-09-02 — round 1's
|
||
**pre-fix** wording, *"every account and role in this infrastructure…"*, which
|
||
the audit then struck. So this file briefly quoted two different sentences as the
|
||
live one, in the two halves of the same document, which defeats the comparison
|
||
R14 exists for. Fixed by pointing at the block above rather than re-quoting it:
|
||
one copy of a fact, in one place. `adversarial-reviewer`, round 2.)* Under R14 the artefact is what a reviewer compares the
|
||
claim against, so a claim stronger than its artefact is unverifiable by
|
||
construction even when it happens to be true.
|
||
|
||
**What was missing.**
|
||
|
||
1. **`aws iam list-role-policies` was never run.** The command block above lists
|
||
only `list-attached-role-policies`, which returns *managed* policies. **23 of
|
||
the 26 non-service-linked roles carry inline policies**, and none of them had
|
||
been read. The screen for "broad policies" could not have seen an inline grant.
|
||
2. **No role was ever simulated against the table.** Access was inferred from
|
||
policy *names* (`AdministratorAccess`) rather than measured as a decision.
|
||
3. **The five users were simulated for reads only** — `GetItem`, `Query`, `Scan`.
|
||
So the page's *(then-shipped; deleted by the mechanics ruling of 2026-09-02 and now §7's alone)* *"the credential that publishes this website has no access to the
|
||
table at all"* covered three read actions and said "at all".
|
||
|
||
**What the measurement found, and it changes the role count.** Simulating all 26
|
||
non-service-linked roles across **seven** actions — `GetItem`, `Query`, `Scan`,
|
||
`BatchGetItem`, `PutItem`, `UpdateItem`, `DeleteItem`:
|
||
|
||
| role | decision on the table | trust |
|
||
|---|---|---|
|
||
| `cdk-hnb659fds-cfn-exec-role-…-ca-central-1` | **allowed on all 7** | `cloudformation.amazonaws.com` only |
|
||
| `cdk-hnb659fds-cfn-exec-role-…-us-east-1` | **allowed on all 7** | `cloudformation.amazonaws.com` only |
|
||
| `cdk-hnb659fds-lookup-role-…-ca-central-1` | **allowed on the 4 READS**, denied on writes | `arn:aws:iam::327082975128:root` |
|
||
| `cdk-hnb659fds-lookup-role-…-us-east-1` | **allowed on the 4 READS**, denied on writes | `arn:aws:iam::327082975128:root` |
|
||
| `adr-intake-lambda-role` | `PutItem` **only**; implicitDeny on the other six | `lambda.amazonaws.com` |
|
||
| the other 21 | implicitDeny on all 7 | — |
|
||
|
||
**So FOUR roles can read the table, not the two this file recorded.** The two
|
||
`lookup` roles were missed by exactly the gap above: their grant is the inline
|
||
`LookupRolePolicy`, and `list-attached-role-policies` returns nothing for them.
|
||
|
||
**Why the published sentence is nevertheless correct.** The question is not how
|
||
many roles exist but which *people* they lead back to.
|
||
|
||
- The two **cfn-exec** roles trust `cloudformation.amazonaws.com` and nothing
|
||
else. No human can assume them; they are reachable only by deploying a
|
||
CloudFormation/CDK stack, which requires a principal who can deploy one.
|
||
- The two **lookup** roles trust the account root, which delegates the decision
|
||
to the caller's own identity policy. Simulated for `sts:AssumeRole` against
|
||
both role ARNs, for all five users, reading `ResourceSpecificResults` (ten
|
||
per-resource decisions, not five — `EvaluationResults` is one entry per
|
||
**action**, and an earlier pass here asserted the wrong count):
|
||
|
||
| principal | assume `lookup-…-ca-central-1` | assume `lookup-…-us-east-1` |
|
||
|---|---|---|
|
||
| `user/pouya` | **allowed** | **allowed** |
|
||
| `user/lars` | **allowed** | **allowed** |
|
||
| `user/adr-sml-deploy` | implicitDeny | implicitDeny |
|
||
| `user/gitea-deploy-meshkinilaw` | implicitDeny | implicitDeny |
|
||
| `user/meshkini-backend-deploy` | implicitDeny | implicitDeny |
|
||
|
||
- The **CloudFormation escalation path this file named and left untested** —
|
||
*"a real path to the table for anyone who can deploy a CDK stack"* — is now
|
||
measured. Simulated for `cloudformation:CreateStack`, `UpdateStack`,
|
||
`CreateChangeSet`, `ExecuteChangeSet`, `iam:PassRole` and `sts:AssumeRole`,
|
||
five users × six actions = **30 decisions**, count asserted:
|
||
|
||
| principal | the CDK / CloudFormation path |
|
||
|---|---|
|
||
| `user/pouya` | **allowed** on all six |
|
||
| `user/lars` | **allowed** on all six |
|
||
| `user/adr-sml-deploy` | implicitDeny on all six |
|
||
| `user/gitea-deploy-meshkinilaw` | implicitDeny on all six |
|
||
| `user/meshkini-backend-deploy` | implicitDeny on all six |
|
||
|
||
This is the finding that mattered most, because `meshkini-backend-deploy` is by
|
||
its name another project's backend-deploy credential and
|
||
`gitea-deploy-meshkinilaw` is held on a **jointly administered** Gitea instance
|
||
(Q23). Either one, had it been able to drive CloudFormation, would have read
|
||
the table without appearing in the five-row table above — and *"Two people
|
||
can"* would have been wrong. Neither can.
|
||
|
||
- **And the seven roles every sweep here had excluded BY CONSTRUCTION are now
|
||
measured too.** Every role loop in this file filters `grep -v
|
||
'^AWSServiceRole'`, and the assertion was written as *"twenty-six role rows"* —
|
||
so a service-linked role was outside the claim rather than inside it, which
|
||
matters because a service-linked role for a backup or migration service can
|
||
read table contents. All **7** (`APIGateway`, `CloudFrontLogger`,
|
||
`InternetMonitor`, `RDS`, `ResourceExplorer`, `Support`, `TrustedAdvisor`) are
|
||
**implicitDeny on all seven actions** — 49 decisions, count asserted
|
||
`[verified 2026-09-02]`. **So the enumeration is 33 of 33 roles, not 26 of
|
||
33**, and the page's *"every user and every role in the account"* is now
|
||
literally true. Raised by `adversarial-reviewer`, round 2.
|
||
|
||
- **There is no federated identity surface at all**: `list-saml-providers` **0**,
|
||
`list-open-id-connect-providers` **0**, `sso-admin list-instances` **0**
|
||
`[verified 2026-09-02]`. So "every user and every role" is not leaving out a
|
||
federated principal, because there is none to leave out.
|
||
|
||
- ⚠️ **AND THE `sso-admin` ZERO NEEDED A SECOND COMMAND TO MEAN ANYTHING, added
|
||
2026-09-02 (round 2).** `list-instances` answers about the account it is called
|
||
in, so a **member of an AWS Organization returns 0 while Identity Center runs in
|
||
the management account** — the zero would have been true and the conclusion
|
||
false. `aws organizations describe-organization` returns
|
||
**`AWSOrganizationsNotInUseException`: "Your account is not a member of an
|
||
organization"** `[verified 2026-09-02]`, so there is no management account above
|
||
this one and the zero is conclusive. Command 8.
|
||
|
||
- **The table carries NO RESOURCE-BASED POLICY OF ITS OWN**, and this is now a
|
||
command rather than an assertion. `aws dynamodb get-resource-policy` returns
|
||
**`PolicyNotFoundException`** `[verified 2026-09-02]` — exit **254**, the error
|
||
on stderr being the result. ⚠️ **A DynamoDB resource policy is invisible to
|
||
`describe-table`**, so no earlier command in this file could have seen one, and
|
||
`/legal/privacy/` publishes the claim (*"the table carries no policy of its own
|
||
granting access to anyone"*). It is the identity-policy enumeration's blind
|
||
spot: every simulation here asks what a **principal** may do, and a resource
|
||
policy grants from the other side. Found unbacked by `adversarial-reviewer`
|
||
round 2, on §10's own precedent — the client-backup bucket, where
|
||
`get-bucket-policy` returning `NoSuchBucketPolicy` was recorded because *"a
|
||
policy read alone could not have established the second half."* Command 7.
|
||
|
||
Every read path therefore terminates at `pouya` or `lars`. ⚠️ **AND THAT IS
|
||
WHERE THIS FILE WENT WRONG, SO THE CORRECTION SITS AT THE SENTENCE THAT CAUSED
|
||
IT.** This read *"The count of people is two, and it is now the result of an
|
||
enumeration rather than of a policy name"* until 2026-09-02, and `/legal/privacy/`
|
||
published that count. **It is a count of IDENTITIES.** Two credentials is a lower
|
||
bound on the number of people who can use them, and Pouya's attestation is that
|
||
*"a handful"* is the true figure (§9 Q63). The enumeration stands exactly as
|
||
measured; **an enumeration of principals is not a census.**
|
||
|
||
**The account root user, recorded because an enumeration that quietly omits it is
|
||
not an enumeration.** Root is not an IAM user and does not appear in
|
||
`list-users`, so it cannot be simulated and no policy constrains it — root can
|
||
always read the table. Two facts bound it: `get-account-summary` reports
|
||
`AccountAccessKeysPresent: 0`, so **there is no programmatic root credential**,
|
||
and `AccountMFAEnabled: 1`. Root access therefore requires the root password and
|
||
its MFA device.
|
||
|
||
⚠️ **THE PAGE SAYS NOTHING ABOUT ROOT, AND THIS PASSAGE HAS NOW BEEN THE REASON
|
||
FOR THAT TWICE ON OPPOSITE GROUNDS.** It first read *"The page does not mention
|
||
root and should not"*, reasoned from its own last clause — *"who holds the root
|
||
credentials is not established in this repository."* **Pouya then established it
|
||
(§9 Q63(c), 2026-09-02): he holds it** `[verified 2026-09-02 — Pouya]`, the page
|
||
published *"has no programmatic key, and I hold it"*, and the gap that opened
|
||
immediately was that *held by* is not *held only by* — a reader takes the
|
||
possessive as sole custody, which nothing measured or attested supports (§9
|
||
**Q64**). **His second ruling that day deleted the sentence** along with the rest
|
||
of the mechanics, so **Q64 is closed as MOOT rather than answered and the
|
||
underlying fact is exactly as unestablished as it was.**
|
||
|
||
**The consequence to carry, because it is not "nothing happened":** root custody
|
||
is now recorded in `AGENTS.md` §7 and nowhere public. ⚠️ **Nothing about it may
|
||
be published without asking him again**, and the question to ask is not *who
|
||
holds root* — that is answered — but *whether anyone else does*. The original
|
||
reasoning still holds and is why the page's first sentence is scoped as it is: an
|
||
account owner's own credential is inherent to every cloud account and is not a
|
||
third party who has been *granted* access, which is why the measured claim was
|
||
always scoped to *"every user and every role"* rather than to a bare "nobody else
|
||
can".
|
||
|
||
**Two claims are supported that were not before. One of them still ships; the
|
||
other was deleted from the page by ruling on 2026-09-02 and is kept here because
|
||
it remains true and remains §7's.**
|
||
|
||
- **SHIPS** — *"The system that receives what you send can only add a record — it
|
||
cannot read back what is stored"* — `adr-intake-lambda-role` returns `allowed`
|
||
for `PutItem` and `implicitDeny` for `GetItem`, `Query`, `Scan`,
|
||
`BatchGetItem`, `UpdateItem` and `DeleteItem`. Previously this rested on
|
||
reading the policy document; it is now the simulator's decision.
|
||
- **NO LONGER ON THE PAGE** — *"the credential that publishes this website has no
|
||
access to the table at all"* — `adr-sml-deploy` is `implicitDeny` on all
|
||
**seven**, so "at all" covers writes and deletes as well as reads. It went with
|
||
the mechanics cut, not because anything about it changed.
|
||
|
||
**And it corroborates §10 from the IAM surface.** Of the 26 non-service-linked
|
||
roles, **9 belong to CDK bootstrap** and **14 to four unrelated production
|
||
systems** in the same account. *(`/legal/privacy/` tells a reader this in as many words —
|
||
*"The table sits in an Amazon Web Services account that also runs systems
|
||
unrelated to this practice"* — in **§Where it is stored**, which is where the
|
||
sentence now lives: the mechanics cut removed it and it was restored there, as a
|
||
storage disclosure rather than a method. §10 is unaffected either way; it never
|
||
depended on the page saying so.)* *(Their role names were listed here until 2026-09-02 and
|
||
are not any more: this is a committed file, they are another project's IAM
|
||
surface, and the count carries the whole of the argument. `adversarial-reviewer`,
|
||
round 2.)*
|
||
|
||
⚠️ **THE INSTRUMENT FAILED FIRST, UNIFORMLY, AND IN THE DIRECTION THAT READS AS
|
||
CLEAN.** The role sweep was first run as `--action-names $ACTS` with the seven
|
||
actions in a shell variable. **zsh does not word-split parameter expansions**, so
|
||
`simulate-principal-policy` received **one** action name — the whole string — and
|
||
answered it: `implicitDeny` for 22 roles, and `allowed` for the four with a `*`
|
||
grant, because `*` matches a bogus action too. Twenty-two clean rows and a
|
||
plausible four. The tell was the shape of the output, not the verdict: one
|
||
decision per role where there should have been seven. **The fix is the assertion,
|
||
not the memory** — the loop now counts `EvaluationResults` per call and refuses a
|
||
row that does not carry exactly seven, and the users' assume check counts
|
||
`ResourceSpecificResults` and refuses a row that does not carry exactly two.
|
||
`CLAUDE.md` records this class five times over; this is the sixth, and it is the
|
||
"uniformly good" half.
|
||
|
||
### Commands — the ones this addendum rests on
|
||
|
||
Read-only, run as `user/pouya` in `ca-central-1`. No stderr suppressed, exit
|
||
status read on every call, and note the **literal** action lists: they are not in
|
||
a variable, which is the whole point above.
|
||
|
||
```bash
|
||
# 1. Inline policies — the command the original block never ran.
|
||
# ⚠️ THE `grep -v` IS THE ORIGINAL DEFECT AND IS KEPT ONLY TO SHOW IT. It
|
||
# excluded the 7 service-linked roles from a claim written as "every role", and a
|
||
# service-linked role for a backup or migration service CAN read table contents.
|
||
# For a full run, DELETE the grep -v — all 33 must be screened, which is what the
|
||
# 2026-09-02 addendum measured and what the page's "every user and every role"
|
||
# now rests on.
|
||
aws iam list-roles --query 'Roles[].RoleName' --output text \
|
||
| tr '\t' '\n' \
|
||
| while IFS= read -r R; do
|
||
aws iam list-role-policies --role-name "$R" --query 'PolicyNames' --output text
|
||
done
|
||
|
||
# 2. Every non-service-linked role, seven actions, decision asserted per row.
|
||
# The count check is what makes a broken call loud instead of clean.
|
||
aws iam simulate-principal-policy \
|
||
--policy-source-arn "arn:aws:iam::327082975128:role/<ROLE>" \
|
||
--action-names dynamodb:GetItem dynamodb:Query dynamodb:Scan \
|
||
dynamodb:BatchGetItem dynamodb:PutItem dynamodb:UpdateItem \
|
||
dynamodb:DeleteItem \
|
||
--resource-arns "arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions" \
|
||
--query 'length(EvaluationResults)' --output text # must print 7
|
||
|
||
# 3. Trust policies of the four roles that can read.
|
||
aws iam get-role --role-name <ROLE> --query 'Role.AssumeRolePolicyDocument'
|
||
|
||
# 4. Who can assume the two lookup roles — per RESOURCE, not per action.
|
||
aws iam simulate-principal-policy \
|
||
--policy-source-arn "arn:aws:iam::327082975128:user/<USER>" \
|
||
--action-names sts:AssumeRole \
|
||
--resource-arns "arn:aws:iam::327082975128:role/cdk-hnb659fds-lookup-role-327082975128-ca-central-1" \
|
||
"arn:aws:iam::327082975128:role/cdk-hnb659fds-lookup-role-327082975128-us-east-1" \
|
||
--query 'EvaluationResults[].ResourceSpecificResults[].{R:EvalResourceName,D:EvalResourceDecision}' \
|
||
--output text # must print 2 rows
|
||
```
|
||
|
||
```bash
|
||
# 5. The CloudFormation / CDK escalation path, per user. Six actions, literal.
|
||
aws iam simulate-principal-policy \
|
||
--policy-source-arn "arn:aws:iam::327082975128:user/<USER>" \
|
||
--action-names cloudformation:CreateStack cloudformation:UpdateStack \
|
||
cloudformation:CreateChangeSet cloudformation:ExecuteChangeSet \
|
||
iam:PassRole sts:AssumeRole \
|
||
--query 'EvaluationResults[].{A:EvalActionName,D:EvalDecision}' --output text
|
||
|
||
# 6. Federated identity surfaces, and root.
|
||
aws iam list-saml-providers --query 'length(SAMLProviderList)'
|
||
aws iam list-open-id-connect-providers --query 'length(OpenIDConnectProviderList)'
|
||
aws sso-admin list-instances --query 'length(Instances)'
|
||
aws iam get-account-summary \
|
||
--query 'SummaryMap.{AccessKeysPresentRoot:AccountAccessKeysPresent,MFA:AccountMFAEnabled}'
|
||
|
||
# 7. The table's OWN policy. ⚠️ NOT VISIBLE IN describe-table — a DynamoDB
|
||
# resource policy needs its own call, and the page publishes a claim about it.
|
||
# A "no policy" answer arrives as a NON-ZERO EXIT with PolicyNotFoundException
|
||
# on stderr, so do not suppress stderr and do not read exit 0 as the result.
|
||
aws dynamodb get-resource-policy \
|
||
--resource-arn 'arn:aws:dynamodb:ca-central-1:327082975128:table/adr-intake-submissions'
|
||
|
||
# 8. Is the account in an AWS Organization? ⚠️ THIS IS WHAT MAKES COMMAND 6's
|
||
# sso-admin ZERO CONCLUSIVE. `list-instances` answers about THIS account, so a
|
||
# member account returns 0 while Identity Center runs in the management
|
||
# account. Not-in-an-org means there is no such management account.
|
||
aws organizations describe-organization
|
||
```
|
||
|
||
**Five user rows and THIRTY-THREE role rows, or the run did not happen.** ⚠️
|
||
**This said "twenty-six" until 2026-09-02, which made an INCOMPLETE sweep pass
|
||
its own acceptance test** — the number matched command 1's `grep -v
|
||
'^AWSServiceRole'`, so an operator following §12 R21's instruction to re-run this
|
||
file would have reproduced the exclusion and got the pass line for it. The page's
|
||
*"every user and every role"* rests on 33 of 33. `adversarial-reviewer`, round 2.
|
||
And for every simulation: **seven decisions per role call, six per CDK-path call,
|
||
two per-resource decisions per assume call** — the counts are the assertion,
|
||
because a call that silently received one bogus action name answers
|
||
`implicitDeny` and reads exactly like a clean row. **Commands 7 and 8 are read by
|
||
their ERROR, not their output**: `PolicyNotFoundException` and
|
||
`AWSOrganizationsNotInUseException` are each the clean result, arriving on stderr
|
||
with a non-zero exit.
|