Skip to content

feat(iam): Claude read-only role for secure AI-native AWS access#76

Merged
dmytrocraft merged 3 commits into
1-implement-per-repository-s3-state-buckets-for-test-prod-aws-environmentsfrom
claude-readonly-aws-access
Jun 13, 2026
Merged

feat(iam): Claude read-only role for secure AI-native AWS access#76
dmytrocraft merged 3 commits into
1-implement-per-repository-s3-state-buckets-for-test-prod-aws-environmentsfrom
claude-readonly-aws-access

Conversation

@dmytrocraft

@dmytrocraft dmytrocraft commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Refs #75.

What

Adds a dedicated, MFA-gated read-only IAM role for AI-assisted DevOps (Claude Code) against AWS, with the security boundary enforced by IAM — not by the agent's own rules.

Base branch note: stacked on 1-implement-… (the active dev line), not main. main diverged from this line at the initial commit and implements the infra differently (no policy_pack/, no scripts/validate_iam_policies.py), so this change cannot be based on main. Reviewers should diff against the dev branch.

Boundary — Pulumi (pulumi/infra/iam/readonly.py)

  • ClaudeReadOnly-<repo>-<env>: trust restricted to configured principals with aws:MultiFactorAuthPresent, max_session_duration=3600.
  • AWS ReadOnlyAccess + explicit Deny on secret/credential reads (secretsmanager:GetSecretValue, kms:Decrypt, ssm:GetParameter*, lambda:GetFunction, ec2:GetPasswordData, *:GetAuthorizationToken, sts:GetSessionToken, cognito-identity:Get*) — read-only without being a secret-exfil primitive (matters here: stack leans on KMS/Pulumi secrets).
  • Opt-in: emitted only when claudeReadonlyPrincipalArns is set. Registered with check-iam (Access Analyzer).

Defense-in-depth — .claude/ (not the boundary)

  • settings.json: read-only profile, plan mode, allow describe-*/get-*/list-*/s3 ls, deny mutations / secret reads / prod profile / pulumi up/destroy / terraform apply.
  • hooks/aws-readonly-guard.sh: PreToolUse backstop that also inspects chained/compound commands the glob matcher misses.

Docs

  • docs/pulumi-claude-readonly.md: laptop assume-role + MFA bridge (kills long-lived prod keys), SSO end-state, attribution/audit.

Verification (local)

ruff format+check, mypy, 89 unit + 29 structural/policy tests pass; readonly.py / config.py / __main__.py at 100% coverage. Guard hook functionally tested (denies mutations/secret-reads/prod-profile/pulumi up/chained terminate; allows reads).

Follow-ups (not in scope)

  • IAM Identity Center (SSO) + per-account split for prod — documented end-state.
  • No AWS MCP server (CLI-only).

Summary by cubic

Adds an MFA-gated AWS read-only IAM role for Claude Code with explicit blocks on secret/credential reads, plus guardrails and docs for secure usage. The role is created only when claudeReadonlyPrincipalArns is set and its ARN is exported for clients (Refs #75).

  • New Features

    • Provision ClaudeReadOnly-<repo>-<env> in pulumi/infra/iam/readonly.py: trust only listed principals with aws:MultiFactorAuthPresent, 1h session, attach AWS ReadOnlyAccess, and explicitly deny secret/credential reads (e.g., secretsmanager:GetSecretValue, kms:Decrypt, ssm:GetParameter*, lambda:GetFunction*, ec2:GetPasswordData, *:GetAuthorizationToken, sts:GetSessionToken, cognito-identity:Get*).
    • Conditional wiring in pulumi/__main__.py driven by claudeReadonlyPrincipalArns; exports claudeReadonlyRoleArn. Config helpers in pulumi/infra/config.py validate ARNs and build a bounded role name.
    • Defense-in-depth guardrails in .claude: settings.json runs in plan mode with allow/deny lists (cleaned up redundant allows). hooks/aws-readonly-guard.sh now matches aws as a real command token (handles wrappers and /usr/bin/aws, ignores substrings), and blocks prod profiles, mutating commands, secret reads, and destructive IaC.
    • Documentation and validation: docs/pulumi-claude-readonly.md; register the deny policy in scripts/validate_iam_policies.py; tests cover the role, policies, config, and stack wiring (tightened to assert the full deny list).
  • Migration

    • Set claudeReadonlyPrincipalArns[] in Pulumi config and run pulumi up to create and export claudeReadonlyRoleArn.
    • Update local AWS profiles to assume the role with MFA; remove long‑lived prod keys. Optionally add .claude/settings.local.json to switch AWS_PROFILE for prod reads.

Written for commit d9db130. Summary will update on new commits.

Review in cubic

Provision ClaudeReadOnly-<repo>-<env>, an IAM role assumable only with MFA by
the principals listed in the new claudeReadonlyPrincipalArns config. It grants
the AWS-managed ReadOnlyAccess policy PLUS an explicit Deny on the sensitive
reads ReadOnlyAccess would otherwise allow (secretsmanager:GetSecretValue,
kms:Decrypt, ssm:GetParameter*, lambda:GetFunction, ec2:GetPasswordData,
*:GetAuthorizationToken, sts:GetSessionToken, cognito-identity:Get*), so a
read-only AI agent cannot exfiltrate secrets. Creation is gated: the component
is only emitted when at least one principal ARN is configured.

Add Claude Code guardrails as defense-in-depth (not the boundary): a committed
.claude/settings.json that pins a read-only profile, runs in plan mode, and
allow/deny-lists AWS verbs; plus a PreToolUse hook that also inspects
chained/compound commands. Register the deny policy with the IAM Access
Analyzer validation set, and document the laptop assume-role + MFA setup.

Tests cover the policies, the component (success + guard), config helpers, and
the stack wiring; new code is at 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81bb44d3-db74-48e0-a72d-b5b80a451a47

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 13 files

Confidence score: 3/5

  • In .claude/hooks/aws-readonly-guard.sh, the wrapper-command regex for xargs/timeout misses cases with intermediate flags, so commands like xargs -I {} aws ... can evade mutating-verb detection and potentially run writes under a “readonly” guard — update the pattern to handle wrapper arguments (and include parallel if intended) before merging.
  • In .claude/settings.json, Bash(aws logs start-query:*) is currently ineffective because it is overridden by Bash(aws * start-*:*), which can block expected CloudWatch Logs query workflows and create confusing policy behavior — carve out start-query from the deny rule or remove the dead allow entry.
  • In tests/unit/test_policies.py, test_readonly_deny_policy_blocks_secret_and_credential_reads validates only part of _DENIED_SENSITIVE_ACTIONS, leaving sensitive actions untested and increasing regression risk in policy coverage — extend assertions to all listed denied actions before merge to lock intended protections.
Architecture diagram
sequenceDiagram
    participant Claude as Claude Code Agent
    participant GuardHook as .claude/hooks/aws-readonly-guard.sh (PreToolUse)
    participant Permissions as .claude/settings.json (Permission Rules)
    participant AWS_CLI as AWS CLI (local)
    participant STS as AWS STS
    participant IAM as AWS IAM
    participant SecretStore as AWS Secrets/SSM/KMS

    Note over Claude,SecretStore: NEW: Read-only AWS access for AI-native DevOps

    Claude->>Permissions: Check command against allow/deny globs
    alt Command allowed by glob (e.g., describe-*, get-*, list-*)
        Permissions-->>Claude: Allow (plan mode)
    else Command denied by glob (e.g., create-*, delete-*, pulumi up)
        Permissions-->>Claude: Deny
        Claude->>Claude: Block execution
    end

    Note over Claude,GuardHook: Defense-in-depth: catches chained/compound commands

    Claude->>GuardHook: PreToolUse hook triggers on Bash commands
    GuardHook->>GuardHook: Parse command with regex
    alt Prod profile detected
        GuardHook-->>Claude: Deny - "prod AWS profile out of scope"
    else AWS mutating verb detected (create, delete, terminate, etc.)
        GuardHook-->>Claude: Deny - "AWS mutating command blocked"
    else Secret/credential read detected (get-secret-value, kms decrypt, etc.)
        GuardHook-->>Claude: Deny - "reads secret/credential material"
    else Destructive IaC detected (pulumi up, terraform apply)
        GuardHook-->>Claude: Deny - "destructive IaC operation"
    else Safe command
        GuardHook-->>Claude: Allow
    end

    Note over Claude,AWS_CLI: Boundary: IAM role enforces read-only with MFA

    Claude->>AWS_CLI: Execute safe AWS CLI command
    AWS_CLI->>STS: sts:AssumeRole with MFA token
    alt MFA not provided
        STS-->>AWS_CLI: AccessDenied - aws:MultiFactorAuthPresent required
        AWS_CLI-->>Claude: Error
    else MFA provided
        STS-->>AWS_CLI: Short-lived credentials (ClaudeReadOnly-* role, 1h)
        AWS_CLI->>IAM: API call with role credentials
        IAM->>IAM: Check ReadOnlyAccess policy
        alt Read-only action (describe, list, get)
            IAM->>IAM: Check explicit Deny policy
            alt Secret/credential read (secretsmanager:GetSecretValue, kms:Decrypt, etc.)
                IAM-->>AWS_CLI: Deny - explicit Deny policy
            else Safe read
                IAM-->>AWS_CLI: Allow
            end
        else Mutating action
            IAM-->>AWS_CLI: Deny - ReadOnlyAccess does not grant
        end
        AWS_CLI-->>Claude: Command output
    end

    Note over AWS_CLI,SecretStore: CloudTrail attribution (agent-assumed sessions)

    Claude->>Claude: Logs in CloudTrail as AssumedRole for ClaudeReadOnly-*
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .claude/settings.json Outdated
Comment thread tests/unit/test_policies.py
Comment thread .claude/hooks/aws-readonly-guard.sh Outdated
ty types _sync_await(future_output(...)) as Unknown | None; assert is-not-None
before endswith/in/subscript, matching the existing component tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

Re-trigger cubic

- settings.json: drop dead 'aws logs start-query' allow (shadowed by the
  'aws * start-*' deny; deny wins in Claude Code).
- hook: match aws as a real command token via a non-identifier boundary so
  wrapper invocations with intervening flags (timeout 5 aws, xargs -I{} aws)
  and full paths (/usr/bin/aws) are caught, while substrings like myaws are not.
- test: assert the deny policy equals the full _DENIED_SENSITIVE_ACTIONS list
  (was only spot-checking 8 of 20 actions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 3 files (changes from recent commits).

Re-trigger cubic

@dmytrocraft dmytrocraft merged commit eac48fd into 1-implement-per-repository-s3-state-buckets-for-test-prod-aws-environments Jun 13, 2026
37 of 38 checks passed
@dmytrocraft dmytrocraft deleted the claude-readonly-aws-access branch June 13, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant