Skip to content

ADR 0007 least privilege iam

wiki-sync[bot] edited this page Jul 23, 2026 · 1 revision

ADR-0007: Least-privilege IAM — per-stage roles, read-only in the reviewed account

Status: Accepted (2026-07-23)

Context

This is the keystone security decision; it binds together earlier ADRs:

  • The agent runs in a tooling account (ADR-0001) and reviews workloads that realistically live in other accounts.
  • The scanners need broad read-only access to the target account (ADR-0004).
  • The only write in the whole system is to the automation's own Well-Architected workload (ADR-0006), plus its report topic and log bucket.
  • The Reconcile stage processes untrusted input (HCL, tfstate, findings), so a prompt-injection there must not be able to reach scan or write credentials.
  • ADR-0002 already forbids long-lived API keys.

The headline the IAM design must deliver: the agent can read anything in the reviewed account but write nothing there — the only writes anywhere are to resources the automation owns.

Decision drivers

  • Read-anything / write-nothing in the reviewed account, enforced structurally, not by convention.
  • Per-stage blast-radius confinement — each stage holds only its own scope.
  • No long-lived credentials — everything STS-temporary.
  • Confused-deputy protection and complete audit.

Decision

Cross-account access via per-stage role assumption; write is structurally impossible in the reviewed account.

Cross-account model

The tooling-account CodeBuild role sts:AssumeRoles into a read-only role deployed in each target account. That role's trust policy trusts the CodeBuild role ARN plus an ExternalId (confused-deputy protection) and is deployed by the account owner via a shared StackSet / IaC module. Single-account review is the degenerate case (assume within the same account).

Per-stage roles (least-privilege core)

The CodeBuild base role is near-empty: sts:AssumeRole to the specific purpose roles, bedrock:InvokeModel on the two model ARNs (ADR-0002), and CloudWatch Logs for its own log stream — nothing else. Each stage assumes only its role; temporary credentials are dropped after the stage.

Stage Assumes Scope
Scan — live cross-account read-only role SecurityAudit + ViewOnlyAccess as the ceiling, with an explicit Deny on (a) all write/create/update/delete/put actions and (b) data-plane reads — s3:GetObject, secretsmanager:GetSecretValue, kms:Decrypt, DynamoDB item reads. Control-plane Describe*/List*/Get*-config only (composes with ADR-0001's no-VPC posture).
Scan — code tfstate reader s3:GetObject on the state bucket/prefix + kms:Decrypt on that one state key only.
Reconcile base role bedrock:InvokeModel on the two model ARNs only — no AWS-resource access.
Scorer WA-write role wellarchitected:UpdateAnswer + CreateMilestone scoped to the automation workload ARN (ADR-0006) + read APIs; sns:Publish to the report topic ARN; s3:PutObject to the logs prefix.

Guardrails (defence-in-depth)

  • Permissions boundary on every role; an explicit Deny on all mutating actions in the target account makes the review tool structurally incapable of changing what it reviews (reinforces ADR-0008).
  • No long-lived credentials — all STS-temporary, session duration capped to the build length.
  • Scoped ARNs everywhere (models, workload, topic, bucket, state key) — never * on resource.
  • CloudTrail on every AssumeRole and API call — complete audit trail (composes with ADR-0002).

Consequences

  • Write is structurally impossible in the reviewed account — the strongest possible statement for a review/governance tool, and the IAM backing for ADR-0008's no-auto-remediation stance.
  • Prompt-injection is contained — a compromise of the Reconcile stage reaches neither the read-only scan role nor the WA-write scope; those creds never exist in that stage.
  • Broad read stays bounded — the scan role is wide but read-only, and the explicit data-plane Deny keeps object/secret/key values out of reach even though config is readable.
  • No secrets to rotate or leak — consistent with ADR-0002.
  • Cost: operational complexity — per-stage credential management in the buildspec, and a StackSet the target-account owners must deploy. Accepted as the price of the isolation and the cross-account trust model.

Alternatives considered

Option Verdict Why
Per-stage roles + cross-account read-only Chosen Confines each capability to its stage; write structurally impossible in the reviewed account.
Single CodeBuild role, union of permissions Rejected The Reconcile stage (untrusted input) would hold the WA-write and scan scopes — large blast radius. Fallback only with strong compensating controls.
Same-account only (no cross-account) Kept The single-account degenerate case, not the general multi-account model.
Long-lived IAM access keys Rejected Rotation and leak risk; contradicts ADR-0002's no-key posture.
Broad read including data-plane Rejected tfstate/secret/object values are sensitive; config-only read is sufficient for the scanners.

Revisit trigger

  • If per-target StackSet onboarding friction is too high at scale, consider an AWS Organizations delegated-admin / org-wide read-only role.
  • If per-stage credential management proves too heavy for a simple single-account deployment, fall back to the union role with the permissions-boundary write-deny retained as the non-negotiable control.

References

  • ADR-0001 (compute — the CodeBuild base role; control-plane-only, no VPC)
  • ADR-0002 (LLM provider — no API keys; STS-temporary throughout)
  • ADR-0003 (per-stage pipeline — the isolation unit)
  • ADR-0004 (scanner read breadth this scopes)
  • ADR-0006 (the single WA-Tool write scope)
  • ADR-0008 (no auto-remediation — write is structurally impossible here)

Clone this wiki locally