# Defense-in-Depth Score: AWS API MCP Server

**Repo:** https://github.com/awslabs/mcp (`src/aws-api-mcp-server`) · **Commit:** `93991b85acfe8d139abc069547973241f88c12e2` · **Reviewed:** 2026-10-03
**What it is:** MCP server letting agents run arbitrary AWS CLI commands
**Category:** Infrastructure & Ops
**Scored configuration:** Local stdio server with default environment: READ_OPERATIONS_ONLY=false, REQUIRE_MUTATION_CONSENT=false, file access 'workdir', telemetry on, agent scripts off, ambient AWS credential chain.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory no · untrusted input yes · third party extensions no · sub agents no · external communication yes

## Score: 4.5 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L2 | L3 | L0 | L0 | 0.38 | G1 | **0.38** (alt) | High |
| C2 | Approval gates | L3 | L3 | L0 | L0 | 0.45 | G1 | **0.45** (alt) | High |
| C3 | Tool & action scoping | L2 | L3 | L1 | L0 | 0.42 | — | **0.42** | High |
| C4 | Code-execution isolation | L0 | L0 | L0 | L0 | 0.00 | — | **0.00** | High |
| C5 | Untrusted input blast radius | L2 | L2 | L3 | L0 | 0.45 | C5-WORSTCASE | **0.25** | High |
| C6 | Memory, context & configuration integrity | SA | SA | SA | SA | 1.00 | — | **1.00** (SA) | High |
| C7 | Third-party extensions | SA | SA | SA | SA | 1.00 | — | **1.00** (SA) | High |
| C8 | Secrets & sensitive-data protection | L1 | L1 | L1 | L0 | 0.20 | — | **0.20** | High |
| C9 | Audit & traceability | L1 | L2 | L2 | L1 | 0.38 | — | **0.38** | Medium |
| C10 | Limits & kill switch | L2 | L2 | L2 | L1 | 0.45 | — | **0.45** | High |

Controls where a risk surface exists: 2.58 / 8.0 (32%); 2 criteria scored SA (surface absent).

As shipped, this server runs any AWS API the model asks for, with the operator's own AWS credentials and no confirmation, including IAM changes, deletions and remote shell commands through SSM. AWS IAM is the only real limit, so deploy it with a scoped-down role. The opt-in read-only mode and per-write consent are well built, mapping every request to AWS's own permission data and failing closed, but both are off by default. File access is confined to a working directory by default, and there is no local shell.

## Critical gaps
- Default policy allows every AWS operation, so the model can modify the server's own IAM permissions or mint new access keys (C1-SELFESC). (ASI03, T3; C1) — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:108-110](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L108-L110)
- The server acts with the operator's full ambient AWS authority, unscoped (C1 blast radius L0). (ASI03; C1) — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:31-37](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L31-L37)
- Model-written code reaches remote execution (SSM RunCommand, CloudFormation deploy, Lambda) with the operator's credentials and no isolation (C4 blast radius L0). (ASI05, T11; C4) — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py:82-87](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py#L82-L87); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:31-37](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L31-L37)
- A hijacked session can exfiltrate data through AWS and take irreversible actions with no human step in the default configuration (C5-WORSTCASE). (ASI01, LLM01; C5) — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:174](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L174)

## Criterion details

### C1 Identity & least privilege — 0.38 (high)

The server runs every AWS call with the operator's ambient credentials (the boto3 default chain or a configured profile), and the model may switch to any other locally configured profile with --profile. By default there is no authorization layer: every operation, including IAM changes to the server's own identity, is allowed. An opt-in read-only mode maps each request to AWS's own Service Authorization data and denies writes, failing closed on unknown services or fetch errors, but it still lets the model mint EKS, ECR and RDS tokens and pick a more privileged profile. IAM policy on the operator's credentials is the only real bound.

- **default configuration** (default; raw 0.00, cap C1-SELFESC → 0.00)
  - **S L0:** Requests use the operator's ambient AWS credentials from the default chain or AWS_API_MCP_PROFILE_NAME, with no narrowing, and the model may pick any local profile. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:31-37](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L31-L37); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:108-110](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L108-L110) (verified)
    - *To reach the next level:* No deterministic per-request authorization in the default configuration; nothing narrows the credential.
  - **C L0:** In the default configuration no tool path checks authorization; the policy falls through to ALLOW for every operation. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138) (verified)
    - *To reach the next level:* No authorization layer applied to tool paths by default.
  - **D L0:** The default install runs with whatever the operator's credentials permit, often admin; read-only mode is opt-in. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:171](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L171); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138) (verified)
    - *To reach the next level:* Default is not read-only; least privilege needs manual IAM hardening or READ_OPERATIONS_ONLY.
  - **B L0:** A hijacked server holds the operator's full AWS authority, including IAM writes, across every enabled region. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:31-37](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L31-L37); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py:287-295](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py#L287-L295) (verified)
    - *To reach the next level:* No scoping of the credential to a project, tenant or read-only role.
- **opt-in READ_OPERATIONS_ONLY authorization gate** (alt; raw 0.38, cap G1 → 0.38) ← counted
  - **S L2:** Each request is mapped to AWS's own permission model (embedded metadata, then the official Service Authorization Reference IsWrite flag over HTTPS) and writes are denied before credentials are attached; unknown services and fetch errors fail closed. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py:79-93](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py#L79-L93); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py:95-106](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py#L95-L106); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py:22](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py#L22); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py:25-44](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py#L25-L44); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:329-335](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L329-L335); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:108-110](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L108-L110); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py:132-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py#L132-L138) (verified)
    - *To reach the next level:* The model can still pick any local profile with --profile, and token-minting operations (eks get-token, ecr get-login-password, rds generate-db-auth-token) are allowlisted as read-only, so the bound leaks credentials for other systems.
  - **C L3:** Every call_aws path (service operations, CLI customizations and help) passes check_security_policy before execution; suggest_aws_commands uses no AWS credentials. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:337-344](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L337-L344); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:346-362](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L346-L362) (verified)
    - *To reach the next level:* No per-principal authorization: in HTTP mode one credential serves every client.
  - **D L0:** Read-only mode is off unless the operator sets READ_OPERATIONS_ONLY. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:171](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L171) (verified)
    - *To reach the next level:* Not on by default.
  - **B L0:** If the read-only gate is bypassed the operator's full ambient credentials remain usable. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:31-37](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L31-L37) (verified)
    - *To reach the next level:* No narrower credential behind the gate.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.

### C2 Approval gates — 0.45 (high)

Everything goes through one call_aws tool that mixes reads and writes; by default it is simply annotated destructive, so the host must gate every call because it can't tell reads from writes. The server offers an opt-in consent mode that, for every operation AWS classes as a write, shows the user the exact CLI command through MCP elicitation and refuses if the client can't ask. It is off by default, and when it is off every AWS action, including deletes and terminations, runs without any server-side confirmation. AWS actions cannot be rolled back by the server.

- **default configuration** (default; raw 0.33 → 0.33)
  - **S L1:** Risk signalling is a single destructiveHint on the one call_aws tool that performs both reads and writes; there are no separate read and write tools. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:242-247](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L242-L247) (verified)
    - *To reach the next level:* Split read and write operations into separately annotated tools or modes.
  - **C L2:** All tools carry accurate annotations (suggest_aws_commands readOnly, call_aws destructive), so a host gating on hints covers every path. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:242-247](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L242-L247); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:147-149](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L147-L149) (verified)
    - *To reach the next level:* No server-side gate covering every path in the default configuration.
  - **D L2:** Annotations are fixed in code and only flip to readOnly when read-only mode is enabled. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:242-247](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L242-L247); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:174](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L174) (verified)
    - *To reach the next level:* No server-enforced confirmation is on by default.
  - **B L0:** With no gate, a wrongly approved call can make irreversible AWS changes (delete buckets, terminate instances, drop databases) with no undo. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138) (verified)
    - *To reach the next level:* No dry-run, rollback or quantity bounds on AWS actions.
- **opt-in REQUIRE_MUTATION_CONSENT elicitation** (alt; raw 0.45, cap G1 → 0.45) ← counted
  - **S L3:** Each non-read-only operation (per AWS's own classification) triggers an elicitation that shows the exact CLI command; rejection, or a client without elicitation, aborts the call. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py:52-66](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py#L52-L66); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:134-135](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L134-L135) (verified)
    - *To reach the next level:* No argument-level allow/deny/escalate policy; decisions are per operation only.
  - **C L3:** Every call_aws command, including CLI customizations and each region of a --region * fan-out, passes the policy before execution, and auto-approved calls are AWS-classified read-only operations. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:337-344](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L337-L344); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py:79-93](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py#L79-L93); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py:287-295](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py#L287-L295) (verified)
    - *To reach the next level:* Read-only auto-approval still includes credential-minting and local-file-writing operations; no rejection of unknown commands beyond non-read-only classification.
  - **D L0:** Consent mode is off unless the operator sets REQUIRE_MUTATION_CONSENT. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:174](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L174) (verified)
    - *To reach the next level:* Not on by default.
  - **B L0:** A wrongly approved AWS action is still irreversible and unbounded. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138) (verified)
    - *To reach the next level:* No rollback, dry-run or quantity limits.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.

### C3 Tool & action scoping — 0.42 (high)

call_aws is a general tool: any AWS API in any region with the operator's credentials. Inputs are parsed as AWS CLI syntax, not passed to a shell. The parser checks parameters against the service schema, rejects --debug, --no-verify-ssl and --no-sign-request, only allows loopback --endpoint-url values, and limits CLI customizations to an allowlist that spawns no subprocesses. Local file arguments are confined by default to a working directory using resolved paths. Files referenced indirectly (for example artefacts listed inside a CloudFormation template) are not confined, and nothing limits which AWS resources or quantities a call may touch.

- **S L2:** Typed parsing and schema validation with real containment for direct file paths and loopback-only endpoints, but the tool accepts any AWS operation and the path check misses nested file references. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/lexer.py:38-52](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/lexer.py#L38-L52); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py:674-684](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py#L674-L684); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py:853-861](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py#L853-L861); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/file_system_controls.py:155-168](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/file_system_controls.py#L155-L168); [src/aws-api-mcp-server/README.md:211](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/README.md#L211) (verified)
  - *To reach the next level:* No allowlist of operations or resources; nested file references escape the working-directory check.
- **C L3:** All built-in tools validate input: call_aws through the shared parser, suggest_aws_commands with a 2000-character limit, and the opt-in plan tool with a dictionary lookup. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/lexer.py:38-52](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/lexer.py#L38-L52); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:152-157](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L152-L157) (verified)
  - *To reach the next level:* No central policy layer that new tools inherit automatically.
- **D L1:** All AWS operations, including writes, are enabled by default; read-only mode, no-access file mode and a user denylist can disable them. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:171](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L171); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:75](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L75); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:56-65](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L56-L65) (verified)
  - *To reach the next level:* Default tool set is not read-only.
- **B L0:** A misused call_aws can hit any API in the operator's AWS account across all enabled regions. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py:287-295](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py#L287-L295); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138) (verified)
  - *To reach the next level:* No scoping to a project, resource set or quantity bound.
- **Cap:** none

### C4 Code-execution isolation — 0.00 (high)

The server has no local shell or eval: model output is parsed into AWS API calls, and the CLI customizations it allows spawn no subprocesses. It does, however, forward model-written code for execution in the operator's AWS account (SSM RunCommand shell scripts, CloudFormation deploys, Lambda code, EC2 user data), all allowed by default with no isolation boundary and the operator's credentials. The opt-in unrestricted file-access mode also does not confine local execution.

- **S L0:** Model-authored scripts and templates are forwarded to SSM, CloudFormation and Lambda with the operator's credentials; there is no sandbox primitive. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py:82-87](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py#L82-L87); searched `rg -n -i 'SendCommand|RunShellScript|send-command' --glob '!*.json'` in `src/aws-api-mcp-server/awslabs` → 0 hits (No code path denies or gates SSM SendCommand / RunShellScript; it is an ordinary allowed operation.); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py:68-76](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py#L68-L76) (verified)
  - *To reach the next level:* No isolation or restriction on remote-execution APIs.
- **C L0:** No execution path is isolated; there is no local exec path (negative search), and the remote execution paths are unrestricted. — searched `rg -n 'subprocess|os\.system|eval\(|exec\(|Popen|pickle'` in `src/aws-api-mcp-server/awslabs` → 1 hits (Only hit is parser.py:83, a comment stating allowed customizations do not spawn subprocesses.); searched `rg -n -i 'SendCommand|RunShellScript|send-command' --glob '!*.json'` in `src/aws-api-mcp-server/awslabs` → 0 hits (No code path denies or gates SSM SendCommand / RunShellScript; it is an ordinary allowed operation.) (verified)
  - *To reach the next level:* No path goes through an isolation boundary.
- **D L0:** Nothing restricts remote-execution operations by default; only opt-in read-only or consent modes would stop them. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:171](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L171); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:174](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L174) (verified)
  - *To reach the next level:* No default restriction on remote code execution.
- **B L0:** Remote commands run on the operator's instances and functions with their attached roles, and the server process holds the operator's AWS credentials. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py:31-37](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/driver.py#L31-L37); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py:82-87](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/parser.py#L82-L87) (verified)
  - *To reach the next level:* No credential-free, network-restricted execution environment.
- **Cap:** none
- **Notes:** Local code execution is structurally absent in the default workdir file mode (NO_EXEC search). The opt-in unrestricted file-access mode does not confine local execution.

### C5 Untrusted input blast radius — 0.25 (high)

The server returns AWS data, including content anyone with write access to the account's resources can control (S3 objects, log events, tags, queue messages), as structured JSON with the command, service, operation and region attached, but with nothing marking it as untrusted. In the default configuration a hijacked model can both read sensitive data and act on it: copy data to an external bucket or topic, mint credentials, or delete resources, all without a human step. The opt-in read-only and consent modes narrow this, but they are off by default.

- **S L2:** Outputs are structured: cli_command, response JSON, status and error codes, and service/operation/region metadata, kept separate from instructions. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py:214-219](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py#L214-L219); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py:48-54](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py#L48-L54) (verified)
  - *To reach the next level:* No provenance or untrusted flag on the returned AWS content.
- **C L2:** Every call_aws result uses the structured model; suggest_aws_commands passes the remote endpoint's JSON through unchanged. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py:214-219](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py#L214-L219); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:168-180](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L168-L180) (verified)
  - *To reach the next level:* suggest_aws_commands output and the opt-in plan text are passed through raw.
- **D L3:** The response structure is fixed in code and cannot be disabled by configuration or content. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py:214-219](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py#L214-L219); searched `rg -n -i 'untrusted|provenance' --glob '!*.json'` in `src/aws-api-mcp-server/awslabs` → 0 hits (verified)
  - *To reach the next level:* No untrusted-content signalling to keep on; the read-only and consent modes that would drop a Rule-of-Two leg are opt-in.
- **B L0:** In the default configuration a hijacked model can read secrets or data and exfiltrate them through AWS (s3 cp to a foreign bucket, SNS publish) and delete resources, with no human step. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:174](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L174) (verified)
  - *To reach the next level:* No default mode that removes egress or state change once untrusted AWS data has been read.
- **Cap:** C5-WORSTCASE — Worst case (B L0): a hijacked agent can leak data and take irreversible actions unattended.

### C6 Memory, context & configuration integrity — 1.00 (high)

The server keeps no memory and loads no context from the working directory. Its only configuration files are environment variables and a user-scope policy file in ~/.aws/aws-api-mcp, which the model cannot write in the default working-directory file mode because of resolved-path containment. Opt-in unrestricted file access would let the model overwrite that file.

- **Structural absence:** searched `rg -n -i 'load_dotenv|dotenv|vector|embedding|remember|save_memory' --glob '!*.json' --glob '!*.md'` in `src/aws-api-mcp-server/awslabs` → 0 hits; [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:56-65](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L56-L65); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/file_system_controls.py:155-168](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/file_system_controls.py#L155-L168)

### C7 Third-party extensions — 1.00 (high)

The server loads no plugins, MCP servers, packages or model files at runtime. Remote data it fetches (AWS service-reference documents and command suggestions from an AWS endpoint) is data, not code. Opt-in agent scripts are markdown read from a directory the operator chooses. The AWS CLI's own [plugins] config section in ~/.aws/config is user scope and outside this server's code.

- **Structural absence:** searched `rg -n 'entry_points|import_module|pip install|npx|plugin' --glob '!*.json' --glob '!*.md'` in `src/aws-api-mcp-server/awslabs` → 0 hits; searched `rg -n 'subprocess|os\.system|eval\(|exec\(|Popen|pickle'` in `src/aws-api-mcp-server/awslabs` → 1 hits (Only hit is parser.py:83, a comment stating allowed customizations do not spawn subprocesses.)

### C8 Secrets & sensitive-data protection — 0.20 (high)

The server never puts its own AWS keys into model context or logs, and its execution log deliberately omits command parameters. But credentials are held in a plain model with no masking, the suggest tool logs the user's query verbatim, and AWS responses go to the model unredacted, including secrets the model asks for (Secrets Manager values, SSM SecureString parameters, new IAM access keys, which are allowed by default). Telemetry is on by default. It sends no content, only the MCP client name, version and configuration flags in the AWS User-Agent.

- **S L1:** Credentials come from the ambient chain into a plain pydantic model; the only masking is omitting parameters from the execution log line. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py:74-82](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/models.py#L74-L82); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:321-325](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L321-L325) (verified)
  - *To reach the next level:* No type-level masking or redaction filters; no redaction of secrets in AWS responses.
- **C L1:** Only the main execution log path is protected; the suggest query is logged verbatim and model-bound AWS responses are unredacted. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:321-325](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L321-L325); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:162](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L162) (verified)
  - *To reach the next level:* No redaction on model-bound messages, errors or other log lines.
- **D L1:** Telemetry is on by default (AWS_API_MCP_TELEMETRY=true) and content-free, adding client name/version and config flags to the AWS User-Agent. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:172](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L172); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:106-116](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L106-L116) (verified)
  - *To reach the next level:* Telemetry should be opt-in.
- **B L0:** With the default ALLOW policy the model can mint long-lived high-privilege keys (iam create-access-key) or read stored secrets into its context. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py:131-138](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/security/policy.py#L131-L138); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py:25-44](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/metadata/read_only_operations_list.py#L25-L44) (verified)
  - *To reach the next level:* No gate or redaction keeping long-lived secrets out of model reach by default.
- **Cap:** none

### C9 Audit & traceability — 0.38 (medium)

The server writes a loguru log to ~/.aws/aws-api-mcp/aws-api-mcp-server.log (10 MB rotation, 7-day retention), outside the working directory. Each executed command is recorded only as service and operation; parameters are deliberately left out. Policy denials and user consent decisions go back to the client as errors, not into the server log. Nothing records which user or client made a call, and nothing protects the log from tampering, so an incident can be traced to operations but not to the exact resources touched or who approved them.

- **S L1:** Unstructured text log lines record service and operation with parameters redacted, plus timing lines. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:321-325](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L321-L325); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:67-73](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L67-L73) (verified)
  - *To reach the next level:* No structured per-call record with arguments, result status and actor.
- **C L2:** All tools log their invocation (call_aws, suggest_aws_commands, get_execution_plan). — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:321-325](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L321-L325); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:162](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L162); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:421](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L421) (verified)
  - *To reach the next level:* Policy denials and consent decisions are sent to the client only, not logged.
- **D L2:** On by default, written to ~/.aws/aws-api-mcp outside the working directory, but the server process (and its log level env var) can alter it. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:67-73](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L67-L73); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/file_system_controls.py:155-168](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/file_system_controls.py#L155-L168) (verified)
  - *To reach the next level:* Log not written by a component the server process cannot modify.
- **B L1:** Logging is best-effort via loguru; a logging failure does not block an action. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:67-73](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L67-L73) (inferred)
  - *To reach the next level:* Errors are not surfaced and records are not guaranteed per action.
- **Cap:** none

### C10 Limits & kill switch — 0.45 (high)

The server caps a batch at 20 commands and sets 10-second connect and 60-second read timeouts with three retries on each AWS call. Pagination is unbounded unless the caller passes max_results, and --region * multiplies each command across every enabled region. There is no rate limit, no session budget, no limit on how many resources a call creates, and no way to cancel an in-flight call.

- **S L2:** Server-enforced batch size, per-request timeouts and retry count exist on the main path. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:188-191](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L188-L191); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:263-266](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L263-L266); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py:58-64](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/parser/interpretation.py#L58-L64) (verified)
  - *To reach the next level:* No caps on pagination volume or region fan-out, and no rate or concurrency limits.
- **C L2:** Timeouts apply to every boto3 call; pagination and region fan-out are unbounded in aggregate. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py:199-203](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py#L199-L203); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py:287-295](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/service.py#L287-L295); searched `rg -n -i 'rate.?limit|semaphore|cancel|concurren' --glob '!*.json' --glob '!*.md'` in `src/aws-api-mcp-server/awslabs` → 0 hits (verified)
  - *To reach the next level:* No aggregate bound per tool call or session.
- **D L2:** Batch and timeout values are constants the model cannot change, but full pagination is the default. — [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py:188-191](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/common/config.py#L188-L191); [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py:199-203](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/core/aws/services.py#L199-L203) (verified)
  - *To reach the next level:* Default pagination is unlimited; no hard ceilings.
- **B L1:** A runaway host loop can keep creating or deleting resources with no server ceiling, and nothing cancels in-flight calls. — searched `rg -n -i 'rate.?limit|semaphore|cancel|concurren' --glob '!*.json' --glob '!*.md'` in `src/aws-api-mcp-server/awslabs` → 0 hits; [src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py:263-266](https://github.com/awslabs/mcp/blob/93991b85acfe8d139abc069547973241f88c12e2/src/aws-api-mcp-server/awslabs/aws_api_mcp_server/server.py#L263-L266) (verified)
  - *To reach the next level:* No session-level ceiling or cancellation.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: AWS resource content (S3 objects, logs, tags, SQS messages) returned by call_aws (server.py:357-362) · [B] sensitive data/systems: operator's ambient AWS credentials and account data (core/aws/driver.py:31-46) · [C] state change / egress: any AWS write API allowed by default (core/security/policy.py:137-138) · Same default session? Yes

## Highest-impact improvements
1. Turn REQUIRE_MUTATION_CONSENT on by default so every AWS write needs per-call human consent showing the exact command. — C2 D L0→L3, +0.150 before caps (Playbook 5)
2. Make READ_OPERATIONS_ONLY the default and require an explicit operator flag for writes. — C1 D L0→L3, +0.150 before caps (Playbook 4)
3. Ignore model-supplied --profile (or restrict it to an operator allowlist) and remove token-minting operations from the read-only allowlist. — C1 S L2→L3, +0.075 before caps (Playbook 4)
4. Write a structured per-call audit record (full redacted arguments, result status, policy decision, consent outcome, client identity). — C9 S L1→L2, +0.075 before caps
5. Make telemetry opt-in. — C8 D L1→L2, +0.050 before caps

## Re-audit log
- No changes.

## Limitations
- Static source review of src/aws-api-mcp-server at the pinned commit only; nothing was installed or run.
- Behaviour of awscli/botocore internals (CloudFormation package artefact resolution, loguru flushing, awscli [plugins] loading) is inferred from library knowledge, not read in this repo.
- HTTP transport (streamable-http with no-auth or OAuth) and Bedrock AgentCore deployment were not scored; the README leads with local stdio.
- The opt-in experimental agent scripts were reviewed only briefly; one bundled script instructs the model to run IAM attach-role-policy/put-user-policy.
- No reviewer-targeted prompt injection was found in the subpath.
