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