* feat: add router-less cross-family reviewer CLI (cross_review.py)
Re-homes the archived orchestrator repo's GPT-4.1 cross_reviewer as a
direct OpenAI SDK CLI: verbatim system prompt, same model id, ported
3-attempt exponential-backoff retry. Reads OPENAI_API_KEY from the
environment or the gitignored repo-root .env. Lazy openai import so
--help works without the package.
* feat: create machine-level suppressions dir on --global hook install
install-hooks.sh --global now mkdir -p's
${SH_SECURITY_SUPPRESSIONS_DIR:-~/.config/sea-haven/security-review} and
states the convention: machine-level <dir>/<repo-basename>/suppressions.json
is preferred over repo-local .security-review/suppressions.json, with
review.sh merging both when run without --suppressions.
* docs: describe sh-build-review as cross-family GPT-4.1 pass via cross_review.py
* chore: retire Path B VM host artifacts, repoint xmodel hook to cross_review.py
- delete systemd/ units and DEPLOY-R720.md (VM destroyed; recoverable
from git history)
- nightly_sweep.sh: retained for reference — header notes the VM-based
Path B sweep is retired; ENABLE_XMODEL_HOOK now calls this repo's
cross_review.py instead of the archived orchestrator's run.py
- sweep-targets.txt: drop the stale ~/orchestrator warning block
- README: document the Claude Code web cloud routines
(repo-scanner-nightly-sweep 08:00 ET, repo-checkers-plane1 07:30 ET,
ALARM-only to #repo-scanner) and add the cross_review.py section
* docs(iam): repoint cross-family review invocations to cross_review.py
* fix(ci): exclude canary corpus from ruff, add import smoke test
CI has been red repo-wide: the latest ruff wants to reformat the
intentionally-vulnerable canary fixtures (whose line numbers are keyed
in canary-meta/KEY.md), and pytest --collect-only exits 5 with zero
tests. Exclude canary/ via ruff.toml and add a root-level import smoke
test so collection is non-empty and imports cross_review.py.
11 KiB
Cross-review packet — R720 aws-posture IAM (step-ca → Roles Anywhere → read-only AWS role)
GPT-4.1 cross-review 2026-06-18: APPROVE, no BLOCKs; FIXes applied (
aws:SourceAccountadded to the trust policy;ec2:DescribeImagesremoved from the permission policy). NIT answers recorded inaws-posture-readonly-policy.rationale.md: snapshots = account-owned idle-spend signal (kept);s3:ListAllMyBuckets= names-only inventory, no object data (kept); nologs:*needed;aws:RequestedRegiondeliberately SKIPPED (global-endpointce:*/s3:ListAllMyBucketscould be DENYed by a blanket region pin).
Audience: the mandatory GPT-4.1 IAM cross-review + Adam. Status: these are AUTHORED FILES, nothing is applied to AWS. The review has now PASSED (APPROVE, no BLOCKs), which unblocks building aws-posture (done in this Phase-3 change set, PROVISIONING-GATED — the checker makes no AWS call until step-ca + Roles Anywhere are stood up). Approving this packet unblocks provisioning; 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)
│ (2b) aws:SourceAccount = 328440206208 (confused-deputy guard, added in cross-review)
│ (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 (in THIS account, via the aws:SourceAccount guard added in cross-review), 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 (with resolutions)
- 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)? → RESOLVED (FIX applied): addedaws:SourceAccount = 328440206208to the StringEquals condition. The trust now pins anchor (SourceArn) and account (SourceAccount) plus the cert CN/issuer — closing the confused-deputy gap the reviewer raised. 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. → RESOLVED (NIT, SKIPPED with rationale): everyResource:"*"statement is an API family without resource-level ARNs (ce, the cloudwatch metric-data calls, ec2/elb/rdsDescribe*).aws:RequestedRegionis NOT applied —ce:*ands3:ListAllMyBucketsare global-endpoint services that a blanket region condition could DENY. Full reasoning inaws-posture-readonly-policy.rationale.md("Cross-review NIT answers").- 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).
→ RESOLVED (FIX applied): removed
ec2:DescribeImages(AMIs are not an idle-spend signal).ec2:DescribeSnapshotskept (orphan-snapshot waste, account-owned metadata only);s3:ListAllMyBucketskept (bucket names only, nos3:GetObject); nologs:*granted. - Session duration vs leaf lifetime. 1h STS session + ~24h leaf — acceptable blast window? → Accepted as-is (no change requested).
- Rollback ordering. Is disabling the profile/anchor first the correct fastest-cut order, and does step 2/3 leave any orphaned grant? → Accepted as-is (no change requested).
Process note
Per global instructions this IAM change ALSO requires the GPT-4.1 cross-family review run via
python3 ~/Documents/repositories/seahaven/security-review/cross_review.py "<task>" (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.