Files
adr-sml/docs/reference/deploy-credential-verification.md
Pouya LajevardiandClaude Opus 5 fd5f610982 feat: D19 bounds the review loop; apply nine rulings; close Q46(a) and Q48–Q53
D19 into §3 and swept to all six destinations Pouya named: both agent briefs
(scope + severity filter), /build Phases 2–4 (non-gating questions, scope, stop
signal, two-round cap), /wrap, and CLAUDE.md (comments record decisions, not
history). Sweep verified by command, not recalled.

Rulings applied:
  Q46(a) PUBLISHABLE — three §4 Offerings rows flipped; gate 1 records
         [Pouya's stated basis], never [verified]. Step 5 unblocked.
  Q48    closed, not site-relevant.
  Q49    one §4 row, "Mediator"; read as declining Q49(b), so worksFor stays out.
  Q50    DEVIATES — ships as name + slogan, not the concatenation. Flagged.
  Q51    OBA sections stay; the regulator/voluntary distinction recorded.
  Q52    docs/reference/deploy-credential-verification.md — 18 read-only AWS
         calls, re-run rather than transcribed, access key ID redacted.
  Q53    memberOf emitted on /about/'s Person node.

Two review rounds. The headline finding was this session's own: the Q53 sweep
was asserted and never run, leaving six in-scope records saying memberOf was
withheld — including §12 R10, which is read aloud every session. Round 2 then
found that round 1's simplification had put memberOf on / as well; the per-page
opt-in is restored, because Pouya's ruling turns on /about/'s visible HTML.

Also fixed: MEMBERSHIP_ORGS had orphaned BOUTIQUE's D16 JSDoc; /'s title now
derives from the constants; §7's deploy row stated and retracted three facts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148NztQskLKKApP5SzAA78e
2026-08-28 15:52:52 -04:00

560 lines
20 KiB
Markdown

# Deploy credential — verification output
**What this is.** The tool output behind `AGENTS.md` §7's *Deploy credential —
PROVISIONED* row and the two risk downgrades in `AGENTS.md` §10. It exists because the row
was written on evidence that lived only in a terminal, which is the
**Q24 / Q32 shape** R14 exists for: a claim whose supporting artefact is
unreachable is unverifiable by construction, not merely unverified.
`AGENTS.md` **Q52**, ruled 2026-08-28: *"YES — commit the simulate results, the
inline policy, and the `NoSuchBucketPolicy` response, access key ID redacted."*
## Provenance
| | |
|---|---|
| Subject | IAM user `adr-sml-deploy`, AWS account `327082975128` |
| Retrieved | **2026-08-28**, re-run from the repository root |
| Calling identity | `arn:aws:iam::327082975128:user/pouya` — the broadly-permissioned personal user, **at an interactive keyboard**. `AGENTS.md` §7 and §10: acceptable interactively, **never** as a CI credential |
| Calls | 18 — all **read-only**. No `create`, `put`, `attach`, `delete`, or `update` |
| Redaction | The access key ID is replaced with `AKIA…REDACTED` throughout. Nothing else is redacted; `UserId` (`AIDA…`) is retained because it is the durable principal identifier that appears in CloudTrail and grants nothing. The **secret** access key was never requested and cannot be retrieved from the API at all |
| Method | `aws-cli/2.34.53`. Every command's exit status was read, and **no `2>/dev/null` anywhere** — see the `NoSuchBucketPolicy` section, where the error *is* the result |
**Re-run it.** Every command below is copy-pasteable. Nothing here is
transcribed from a session; this file was generated from the captured output.
---
## 1. The user
```console
$ aws iam get-user --user-name adr-sml-deploy
{
"User": {
"Path": "/",
"UserName": "adr-sml-deploy",
"UserId": "AIDAUYJ5KD6MHAAC4BOKD",
"Arn": "arn:aws:iam::327082975128:user/adr-sml-deploy",
"CreateDate": "2026-08-26T15:45:18+00:00"
}
}
```
Created **2026-08-26T15:45:18+00:00**, path `/`, no tags.
## 2. Nothing attached, no groups, one inline policy
```console
$ aws iam list-attached-user-policies --user-name adr-sml-deploy
{
"AttachedPolicies": []
}
$ aws iam list-groups-for-user --user-name adr-sml-deploy
{
"Groups": []
}
$ aws iam list-user-policies --user-name adr-sml-deploy
{
"PolicyNames": [
"adr-sml-deploy-minimal"
]
}
```
**This is the part policy-reading alone gets wrong.** An empty
`AttachedPolicies` proves nothing on its own — permissions can arrive through a
group, or through a resource-based policy that is invisible from the IAM side.
Both are checked: `Groups` is empty here, and **§6 below** checks the resource
side — §5 is the identity-side simulation and does not reach a bucket policy.
## 3. The inline policy — `adr-sml-deploy-minimal`
```console
$ aws iam get-user-policy --user-name adr-sml-deploy \
--policy-name adr-sml-deploy-minimal
{
"UserName": "adr-sml-deploy",
"PolicyName": "adr-sml-deploy-minimal",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListSiteBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::adr-smlcompany-site"
},
{
"Sid": "WriteSiteObjects",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::adr-smlcompany-site/*"
},
{
"Sid": "InvalidateOneDistribution",
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA"
}
]
}
}
```
Four actions, one bucket, one distribution. **Never widen it**`AGENTS.md` §10
records why
in terms: this is the control standing between a shared Gitea instance and an
AWS account that also holds `mlp-clientdb-prod-backups-327082975128`. Treat any
request to widen it as a security decision, not a convenience one.
## 4. The access key — issued, never used
```console
$ aws iam list-access-keys --user-name adr-sml-deploy
{
"AccessKeyMetadata": [
{
"UserName": "adr-sml-deploy",
"AccessKeyId": "AKIA…REDACTED",
"Status": "Active",
"CreateDate": "2026-08-26T15:45:19+00:00"
}
]
}
$ aws iam get-access-key-last-used --access-key-id AKIA…REDACTED
{
"UserName": "adr-sml-deploy",
"AccessKeyLastUsed": {
"ServiceName": "N/A",
"Region": "N/A"
}
}
```
One key, `Active`, created **2026-08-26T15:45:19+00:00**.
⚠️ **A correction to `AGENTS.md` §7's wording, found by re-running the call.**
That row said
*"one active access key, `LastUsed` null"*. The API does not return null — it
returns `AccessKeyLastUsed` with **`ServiceName: "N/A"` and `Region: "N/A"`**,
and there is no `LastUsedDate` key at all. Same conclusion (the key has never
authenticated a request), different field shape, and a reader looking for a
literal `null` would not find one.
**Rotation is due 2026-11-26** — key created 2026-08-26, D3 commits to
quarterly. `AGENTS.md` §12 R17 carries the date; `docs/06-deployment.md` §Key rotation
carries the procedure. Create the second key, update the Gitea secrets, verify a
deploy, **then** delete the first.
## 5. `simulate-principal-policy` — nine checks, verified by execution
The point of this section is that it tests the **negative** half. A policy that
grants four actions is only a control if it can be shown not to grant the rest.
```bash
PRINCIPAL=arn:aws:iam::327082975128:user/adr-sml-deploy
sim() { aws iam simulate-principal-policy \
--policy-source-arn "$PRINCIPAL" --action-names "$1" --resource-arns "$2" ; }
```
| # | Action | Resource | `EvalDecision` |
|---|---|---|---|
| 1 | `s3:PutObject` | `arn:aws:s3:::adr-smlcompany-site/index.html` | **allowed** |
| 2 | `s3:DeleteObject` | `arn:aws:s3:::adr-smlcompany-site/old.html` | **allowed** |
| 3 | `s3:ListBucket` | `arn:aws:s3:::adr-smlcompany-site` | **allowed** |
| 4 | `cloudfront:CreateInvalidation` | `arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA` | **allowed** |
| 5 | `s3:ListBucket` | `arn:aws:s3:::mlp-clientdb-prod-backups-327082975128` | **implicitDeny** |
| 6 | `s3:GetObject` | `arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything` | **implicitDeny** |
| 7 | `s3:DeleteBucket` | `arn:aws:s3:::adr-smlcompany-site` | **implicitDeny** |
| 8 | `s3:PutObject` | `arn:aws:s3:::meshkinilaw.ca/anything` | **implicitDeny** |
| 9 | `iam:CreateUser` | `*` | **implicitDeny** |
Four `allowed`, five `implicitDeny`. `implicitDeny` rather than `explicitDeny`
is the correct and expected shape: the policy contains no `Deny` statement, so
everything outside its three `Allow` statements falls through to IAM's
default-deny. An `explicitDeny` here would mean a *different* policy was also in
play.
Raw output for each check, including the full `MatchedStatements` and
`MissingContextValues`, follows in **§7 of this file**. *(Bare `§n` here means a
section of this document; references to the working file are written
`AGENTS.md` §n.)*
## 6. The resource side — `NoSuchBucketPolicy`, and why the contrast call matters
IAM simulation covers identity-based policy. It does **not** cover a
resource-based grant on the backup bucket, which would be invisible from the
principal's side. So:
```console
$ aws s3api get-bucket-policy --bucket mlp-clientdb-prod-backups-327082975128
# exit status: 254
# stdout: 0 bytes
# stderr:
# aws: [ERROR]: An error occurred (NoSuchBucketPolicy) when calling the GetBucketPolicy operation: The bucket policy does not exist
```
**An error is the result here, and that is only legible because stderr was not
suppressed.** `CLAUDE.md`'s convention, from Pouya's own 2026-08-28 correction:
`2>/dev/null` converts *"it failed"* into *"it found nothing"*, and those are
opposite results. The exit status is **254** and stdout is **empty** — read the
status, not just stdout.
**And an empty result needs a working instrument.** The same command against the
site bucket, to prove the call itself resolves a policy when one exists:
```console
$ aws s3api get-bucket-policy --bucket adr-smlcompany-site
{
"Policy": "{\"Version\":\"2008-10-17\",\"Id\":\"PolicyForCloudFrontPrivateContent\",\"Statement\":[{\"Sid\":\"AllowCloudFrontServicePrincipal\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cloudfront.amazonaws.com\"},\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::adr-smlcompany-site/*\",\"Condition\":{\"StringEquals\":{\"AWS:SourceArn\":\"arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA\"}}}]}"
}
```
Exit **0**, a real policy returned — the CloudFront OAC grant, scoped to
distribution `E1OK7G98KNKUTA`. So `NoSuchBucketPolicy` on the backup bucket is a
**genuine absence**, not a command that failed to run. That contrast is the
whole reason this call is in the file: without it, an empty result from a broken
command looks exactly like an empty result from a bucket with no policy.
*(This contrast call was **not** part of the original Q22 verification. It was
added on re-run, and it closes the gap that made the original evidence weaker
than it read.)*
## 7. Raw `simulate-principal-policy` responses
### Check 1
```json
{
"EvaluationResults": [
{
"EvalActionName": "s3:PutObject",
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/index.html",
"EvalDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 6,
"Column": 55
},
"EndPosition": {
"Line": 9,
"Column": 57
}
}
],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/index.html",
"EvalResourceDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 6,
"Column": 55
},
"EndPosition": {
"Line": 9,
"Column": 57
}
}
]
}
]
}
]
}
```
### Check 2
```json
{
"EvaluationResults": [
{
"EvalActionName": "s3:DeleteObject",
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/old.html",
"EvalDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 6,
"Column": 55
},
"EndPosition": {
"Line": 9,
"Column": 57
}
}
],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site/old.html",
"EvalResourceDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 6,
"Column": 55
},
"EndPosition": {
"Line": 9,
"Column": 57
}
}
]
}
]
}
]
}
```
### Check 3
```json
{
"EvaluationResults": [
{
"EvalActionName": "s3:ListBucket",
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
"EvalDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 3,
"Column": 17
},
"EndPosition": {
"Line": 6,
"Column": 55
}
}
],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
"EvalResourceDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 3,
"Column": 17
},
"EndPosition": {
"Line": 6,
"Column": 55
}
}
]
}
]
}
]
}
```
### Check 4
```json
{
"EvaluationResults": [
{
"EvalActionName": "cloudfront:CreateInvalidation",
"EvalResourceName": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA",
"EvalDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 9,
"Column": 57
},
"EndPosition": {
"Line": 12,
"Column": 83
}
}
],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:cloudfront::327082975128:distribution/E1OK7G98KNKUTA",
"EvalResourceDecision": "allowed",
"MatchedStatements": [
{
"SourcePolicyId": "user_adr-sml-deploy_adr-sml-deploy-minimal",
"SourcePolicyType": "IAM Policy",
"StartPosition": {
"Line": 9,
"Column": 57
},
"EndPosition": {
"Line": 12,
"Column": 83
}
}
]
}
]
}
]
}
```
### Check 5
```json
{
"EvaluationResults": [
{
"EvalActionName": "s3:ListBucket",
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128",
"EvalDecision": "implicitDeny",
"MatchedStatements": [],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128",
"EvalResourceDecision": "implicitDeny"
}
]
}
]
}
```
### Check 6
```json
{
"EvaluationResults": [
{
"EvalActionName": "s3:GetObject",
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything",
"EvalDecision": "implicitDeny",
"MatchedStatements": [],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:s3:::mlp-clientdb-prod-backups-327082975128/anything",
"EvalResourceDecision": "implicitDeny"
}
]
}
]
}
```
### Check 7
```json
{
"EvaluationResults": [
{
"EvalActionName": "s3:DeleteBucket",
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
"EvalDecision": "implicitDeny",
"MatchedStatements": [],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:s3:::adr-smlcompany-site",
"EvalResourceDecision": "implicitDeny"
}
]
}
]
}
```
### Check 8
```json
{
"EvaluationResults": [
{
"EvalActionName": "s3:PutObject",
"EvalResourceName": "arn:aws:s3:::meshkinilaw.ca/anything",
"EvalDecision": "implicitDeny",
"MatchedStatements": [],
"MissingContextValues": [],
"EvalDecisionDetails": {},
"ResourceSpecificResults": [
{
"EvalResourceName": "arn:aws:s3:::meshkinilaw.ca/anything",
"EvalResourceDecision": "implicitDeny"
}
]
}
]
}
```
### Check 9
```json
{
"EvaluationResults": [
{
"EvalActionName": "iam:CreateUser",
"EvalResourceName": "*",
"EvalDecision": "implicitDeny",
"MatchedStatements": [],
"MissingContextValues": []
}
]
}
```
---
## What this does and does not establish
**Establishes.** The user exists with the creation date `AGENTS.md` §7 records; it holds one
inline policy and nothing else; the policy grants four actions on one bucket and
one distribution; the four intended actions are `allowed`; five representative
out-of-scope actions — including two against the client-database backup bucket —
are denied; the backup bucket carries no resource-based policy that could grant
around IAM; and one key exists that has never authenticated a request.
**Does not establish.** That the key works (it has never been used, and testing
it would mean putting a real object in the bucket). That Gitea holds it —
`AGENTS.md` §7 records **`GITEA ACTIONS SECRETS: UNSET`**, and Q23 is open on whether a runner
exists at all. That the policy is sufficient for a deploy in practice: the
`s3:AbortMultipartUpload` omission is covered by asset sizes rather than by a
lifecycle rule, and `docs/06-deployment.md` records that with a revisit trigger.
**Not in scope of this file, and deliberately so.** The secret access key. It
must never reach the repository — D3, and `AGENTS.md` §10 on the jointly administered Gitea
instance, where an instance administrator can generally reach repository secrets
or register a runner that receives them.