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

20 KiB

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

$ 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

$ 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

$ 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 itAGENTS.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

$ 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.

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:

$ 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:

$ 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

{
    "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

{
    "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

{
    "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

{
    "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

{
    "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

{
    "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

{
    "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

{
    "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

{
    "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.