feat(iam): Claude read-only role for secure AI-native AWS access#76
Conversation
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 reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
There was a problem hiding this comment.
3 issues found across 13 files
Confidence score: 3/5
- In
.claude/hooks/aws-readonly-guard.sh, the wrapper-command regex forxargs/timeoutmisses cases with intermediate flags, so commands likexargs -I {} aws ...can evade mutating-verb detection and potentially run writes under a “readonly” guard — update the pattern to handle wrapper arguments (and includeparallelif intended) before merging. - In
.claude/settings.json,Bash(aws logs start-query:*)is currently ineffective because it is overridden byBash(aws * start-*:*), which can block expected CloudWatch Logs query workflows and create confusing policy behavior — carve outstart-queryfrom the deny rule or remove the dead allow entry. - In
tests/unit/test_policies.py,test_readonly_deny_policy_blocks_secret_and_credential_readsvalidates 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-*
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
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>
There was a problem hiding this comment.
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>
eac48fd
into
1-implement-per-repository-s3-state-buckets-for-test-prod-aws-environments
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.
Boundary — Pulumi (
pulumi/infra/iam/readonly.py)ClaudeReadOnly-<repo>-<env>: trust restricted to configured principals withaws:MultiFactorAuthPresent,max_session_duration=3600.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).claudeReadonlyPrincipalArnsis set. Registered withcheck-iam(Access Analyzer).Defense-in-depth —
.claude/(not the boundary)settings.json: read-only profile,planmode, allowdescribe-*/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__.pyat 100% coverage. Guard hook functionally tested (denies mutations/secret-reads/prod-profile/pulumi up/chainedterminate; allows reads).Follow-ups (not in scope)
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
claudeReadonlyPrincipalArnsis set and its ARN is exported for clients (Refs #75).New Features
ClaudeReadOnly-<repo>-<env>inpulumi/infra/iam/readonly.py: trust only listed principals withaws:MultiFactorAuthPresent, 1h session, attach AWSReadOnlyAccess, and explicitly deny secret/credential reads (e.g.,secretsmanager:GetSecretValue,kms:Decrypt,ssm:GetParameter*,lambda:GetFunction*,ec2:GetPasswordData,*:GetAuthorizationToken,sts:GetSessionToken,cognito-identity:Get*).pulumi/__main__.pydriven byclaudeReadonlyPrincipalArns; exportsclaudeReadonlyRoleArn. Config helpers inpulumi/infra/config.pyvalidate ARNs and build a bounded role name..claude:settings.jsonruns in plan mode with allow/deny lists (cleaned up redundant allows).hooks/aws-readonly-guard.shnow matchesawsas a real command token (handles wrappers and/usr/bin/aws, ignores substrings), and blocks prod profiles, mutating commands, secret reads, and destructive IaC.docs/pulumi-claude-readonly.md; register the deny policy inscripts/validate_iam_policies.py; tests cover the role, policies, config, and stack wiring (tightened to assert the full deny list).Migration
claudeReadonlyPrincipalArns[]in Pulumi config and runpulumi upto create and exportclaudeReadonlyRoleArn..claude/settings.local.jsonto switchAWS_PROFILEfor prod reads.Written for commit d9db130. Summary will update on new commits.