Authored FILES (not applied to AWS — provisioning gated behind the mandatory
GPT-4.1 IAM cross-review + Adam, design §7 B3) for the aws-posture checker's
read-only AWS identity. Decision D5: box stays read-only, auths via IAM Roles
Anywhere short-lived leaf certs from a new internal step-ca; NO long-lived AWS key.
- aws-posture-readonly-policy.json least-privilege read-only (ce:Get*,
cloudwatch:GetMetric*/DescribeAlarms, ec2/elb/rds:Describe*, lambda list +
GetFunctionConfiguration, s3:ListAllMyBuckets/GetBucketLocation). No write,
no iam:* mutation, no s3:GetObject/secrets/kms/logs data reads, no wildcard
actions. Resource:* only where AWS has no resource-level support.
- aws-posture-readonly-policy.rationale.md per-statement least-privilege rationale.
- aws-posture-trust-policy.json pins Roles Anywhere principal + leaf subject CN +
issuer CN + trust-anchor SourceArn (three conditions, all required).
- roles-anywhere-config.json trust anchor (pins step-ca root) + profile (1h session).
- step-ca-config-sketch.md internal CA config + systemd-timer leaf auto-renewal.
- CROSS-REVIEW-PACKET.md end-to-end trust model, blast radius, EXERCISED rollback,
reviewer scrutiny list.
Does NOT build aws-posture.sh, touch checker_coordinator.sh, or requirements.txt.
9.4 KiB
Cross-review packet — R720 aws-posture IAM (step-ca → Roles Anywhere → read-only AWS role)
Audience: the mandatory GPT-4.1 IAM cross-review + Adam.
Status: these are AUTHORED FILES, nothing is applied to AWS. aws-posture (the checker that
uses this role) is hard-gated behind this review (design docs/r720-agent-team-design.md
§7, B3) and is NOT built in this change. Approving this packet unblocks provisioning + building
aws-posture; it does not itself change AWS.
Account: 328440206208 · Region: us-east-1 · Box: the always-on R720 secrev VM (single-user, unattended, currently holds a long-lived read-only GitHub PAT).
Why this exists
aws-posture (design D5 / §4) is a weekly idle/anomalous-spend watch (the design flags ≈$330/mo of waste). It must read Cost Explorer + utilization metrics + an idle-resource inventory from the account. The decision (D5): the box stays read-only, and it authenticates to AWS via IAM Roles Anywhere using short-lived leaf certs issued by a new internal step-ca — NO long-lived AWS access key on the box. The short-lived self-expiring leaf is strictly stronger than the box's existing long-lived PAT.
Files in this packet
| File | What it is |
|---|---|
aws-posture-readonly-policy.json |
The least-privilege permission policy (valid IAM JSON, applyable as-is). |
aws-posture-readonly-policy.rationale.md |
Statement-by-statement least-privilege rationale (IAM JSON can't hold comments). |
aws-posture-trust-policy.json |
The role's trust policy — who may assume it (Roles Anywhere + pinned cert CN/issuer + pinned trust-anchor ARN). |
roles-anywhere-config.json |
The Roles Anywhere trust anchor + profile config (pins the step-ca root; 1h session cap). |
step-ca-config-sketch.md |
The internal CA config + the systemd-timer auto-renewal approach. |
CROSS-REVIEW-PACKET.md |
This document. |
Trust model, end to end
step-ca ROOT cert (CN="Sea Haven Internal CA - R720 Roles Anywhere")
│ pinned as the Roles Anywhere trust anchor (roles-anywhere-config.json)
▼
step-ca issues a SHORT-LIVED leaf (CN="r720-aws-posture", ~24h, auto-renewed hourly by systemd timer)
│ stored mode-600 on the box; private key never leaves the box; no AWS key on disk
▼
box calls AWS via aws_signing_helper credential-process, signing with the leaf
▼
IAM Roles Anywhere trust anchor (r720-aws-posture-step-ca)
│ validates: leaf chains to the pinned root? yes -> emit session tags
│ aws:PrincipalTag/x509Subject/CN = "r720-aws-posture"
│ aws:PrincipalTag/x509Issuer/CN = "Sea Haven Internal CA - R720 Roles Anywhere"
▼
Roles Anywhere profile (r720-aws-posture-readonly, durationSeconds=3600)
│ binds ONLY the one role
▼
sts:AssumeRole on role/r720-aws-posture-readonly
│ trust policy (aws-posture-trust-policy.json) requires ALL of:
│ (1) Principal = rolesanywhere.amazonaws.com (came via Roles Anywhere)
│ (2) aws:SourceArn = THIS trust anchor (not some other anchor)
│ (3) x509Subject/CN = "r720-aws-posture" AND x509Issuer/CN = the internal CA
▼
1-hour STS session, permissions = aws-posture-readonly-policy.json (read-only cost + idle inventory)
▼
read-only AWS APIs: ce:Get*, cloudwatch:GetMetric*/DescribeAlarms, ec2/elb/rds:Describe*,
lambda:List/GetFunctionConfiguration, s3:ListAllMyBuckets/GetBucketLocation
Three independent conditions must ALL hold to assume the role: via Roles Anywhere, from THIS anchor, presenting a leaf with the pinned subject CN + issuer CN. Any one missing → AssumeRole denied.
Least-privilege rationale (summary; full table in the rationale .md)
- Read-only only. No write/modify/delete verb in any service. No
iam:*mutation, no privilege-escalation path, nostsonward-chaining. Resource: "*"only where AWS gives no choice. Cost Explorer, the CloudWatch metric-data calls, and the EC2/ELB/RDSDescribe*list operations do not support resource-level ARNs; least-privilege there is the action allow-list, not the resource. There are no wildcard actions (ce:*,ec2:*) anywhere — every action is an explicit read verb.- No data-plane reads. Deliberately excludes
s3:GetObject,secretsmanager:GetSecretValue,ssm:GetParameter*,kms:Decrypt,logs:GetLogEvents. The role enumerates and prices the account; it cannot read application data, secrets, or logs. - Tighter than the AWS-managed
ReadOnlyAccess/ViewOnlyAccess(those include object reads, table reads, etc.) — this is the small cost+inventory subset aws-posture actually queries.
Blast radius if the leaf (or its private key) is compromised
- Ceiling = read-only enumeration + pricing of account 328440206208 for ≤1 hour per session
(STS
durationSeconds=3600), and only while a valid unexpired leaf exists (~24h leaf life). - Cannot: read S3 object data, read secrets/SSM params, read CloudWatch logs, modify or delete any resource, touch IAM, assume any other role, or act in any other account/region scope beyond what read-only describe calls expose.
- Containment levers, fastest first:
- Disable the Roles Anywhere profile or trust anchor (
enabled:false) → immediately stops all new credential vending, regardless of leaf validity. (seconds) - Detach/empty the role's permission policy → any still-live session loses all access at the next AWS authz check. (seconds)
- Revoke at the CA / rotate the leaf → step-ca stops renewing; the leaf self-expires within its ≤24h window even with no action.
- Disable the Roles Anywhere profile or trust anchor (
- The self-expiring leaf means even a "do nothing" outcome bounds exposure to the leaf lifetime — unlike the box's current long-lived PAT, which would persist until manually rotated.
Rollback (EXERCISED, not just written — design §7 "exercised, not merely written")
Teardown order is the reverse of provisioning; each step is independently sufficient to cut access. Tested via a simulated teardown/re-provision against throwaway names (see "Exercise" below) before this packet is accepted.
ACC=328440206208 ; REG=us-east-1
TA_ID=REPLACE_TRUST_ANCHOR_ID ; PROF_ID=REPLACE_PROFILE_ID
ROLE=r720-aws-posture-readonly ; POLICY=r720-aws-posture-readonly
# 1) Stop credential vending FIRST (fastest cut): disable then delete the profile + trust anchor.
aws rolesanywhere disable-profile --profile-id "$PROF_ID" --region "$REG"
aws rolesanywhere disable-trust-anchor --trust-anchor-id "$TA_ID" --region "$REG"
aws rolesanywhere delete-profile --profile-id "$PROF_ID" --region "$REG"
aws rolesanywhere delete-trust-anchor --trust-anchor-id "$TA_ID" --region "$REG"
# 2) Remove the role + its permission policy.
POLICY_ARN="arn:aws:iam::$ACC:policy/$POLICY"
aws iam detach-role-policy --role-name "$ROLE" --policy-arn "$POLICY_ARN"
aws iam delete-role --role-name "$ROLE"
aws iam delete-policy --policy-arn "$POLICY_ARN"
# 3) Remove the internal CA + the leaf on the box (no AWS state involved).
sudo systemctl disable --now aws-posture-cert-renew.timer step-ca.service
sudo rm -f /etc/aws-posture/leaf.crt /etc/aws-posture/leaf.key
sudo rm -rf /etc/step-ca # destroys root/intermediate/keys -> no further leaves issuable
# (optional) remove the [profile r720-aws-posture] block from ~/.aws/config
Re-provision = re-run step ca init (step-ca-config-sketch.md) → re-create role + policy +
trust anchor + profile → drop in the new trust-anchor/profile ARNs. A revert point (VM snapshot
per feedback_ec2_replacement_snapshot) is taken before provisioning so the whole change is one
snapshot-restore away from gone.
Exercise log (to be completed before acceptance)
Run the provision → assume-once (confirm read-only works, confirm a write is denied) → run the rollback above against throwaway-suffixed names → confirm AssumeRole now fails and the CA is gone. Paste the transcript here. Until this is filled in, the rollback is "written, not exercised" and the phase is NOT accepted (design §7).
Specific things for the cross-reviewer to scrutinize
- Trust-policy condition completeness. Are
aws:PrincipalTag/x509Subject/CN+aws:PrincipalTag/x509Issuer/CN+ArnEquals aws:SourceArn(the trust anchor) sufficient to prevent any other cert (or another trust anchor in the account) from assuming the role? Is there a confused-deputy gap I should also pin (e.g. should I addaws:SourceAccount)? Resource: "*"statements. Confirm each is an API that genuinely has no resource-level support, and that no statement could be tightened with a condition (e.g.aws:RequestedRegion= us-east-1) without breaking the checker.- Action allow-list. Anything in here that is NOT needed for idle/anomalous-spend (i.e. over-grant), or any read verb that leaks data we don't want (the intent is pricing + inventory, no object/secret/log data).
- Session duration vs leaf lifetime. 1h STS session + ~24h leaf — acceptable blast window?
- Rollback ordering. Is disabling the profile/anchor first the correct fastest-cut order, and does step 2/3 leave any orphaned grant?
Process note
Per global instructions this IAM change ALSO requires the GPT-4.1 cross-family review run via
python3 ~/Documents/repositories/orchestrator/run.py "<diff/describe>" (it is an IAM
role/policy + trust-anchor change). This packet is the input to that review; aws-posture is not
built until the review is recorded.