Skip to content

Require AGENT_MCP_TOKEN for non-loopback MCP binds#15

Merged
lai3d merged 1 commit into
mainfrom
claude/agent-mcp-auth
May 19, 2026
Merged

Require AGENT_MCP_TOKEN for non-loopback MCP binds#15
lai3d merged 1 commit into
mainfrom
claude/agent-mcp-auth

Conversation

@lai3d
Copy link
Copy Markdown
Owner

@lai3d lai3d commented May 19, 2026

Summary

Closes the last gap from the AI-features review: the MCP server's only trust boundary was the bind address. Flipping AGENT_MCP_BIND from 127.0.0.1 to 0.0.0.0 silently exposed every tool — allocate_ports, query_envoy_routes, agent_check_update --force — to anyone who could reach the agent, with no per-request auth.

How the fix works

AGENT_MCP_BIND AGENT_MCP_TOKEN Behaviour
loopback (127/8, ::1, localhost) unset enabled, no auth (today's default — unchanged)
loopback set enabled, token enforced (defence in depth)
non-loopback unset serve_mcp refuses to start; loud error! log; agent keeps running with MCP disabled
non-loopback set enabled, token enforced

When a token is configured, every request must carry Authorization: Bearer <token>. Comparison is constant-time (length-stable XOR loop). Missing/invalid → HTTP 401.

Why "refuse to start" instead of "auto-generate a token"

Generating a token silently would still ship a secret out-of-band that the operator might not notice. Loud refusal forces the operator to make an explicit decision. The agent itself stays up — only MCP is disabled — so heartbeat / xDS / metrics keep working.

Loopback detection

is_loopback_bind recognises:

  • IPv4 127.0.0.0/8 (via Ipv4Addr::is_loopback)
  • IPv6 ::1 (handles [::1]:port bracket form)
  • Literal localhost (case-insensitive)

Anything else — 0.0.0.0, ::, public IPs, DNS names, unparseable strings — is treated as non-loopback (fail-closed).

Tests (5 new, all passing)

```
running 5 tests
test mcp::tests::constant_time_eq_matches_string_eq ... ok
test mcp::tests::loopback_recognises_localhost ... ok
test mcp::tests::loopback_recognises_ipv4_127 ... ok
test mcp::tests::loopback_recognises_ipv6 ... ok
test mcp::tests::loopback_rejects_wildcards_and_externals ... ok

test result: ok. 5 passed; 0 failed
```

Docs

  • sigma-agent/README.md — new env-var row, expanded "Security defaults" section with a copy-paste example for off-loopback deployment.
  • sigma-agent/CLAUDE.md — config table + MCP security contract updated.
  • docs/ai-triage.{en,zh}.md — Component 1 (agent) security-model bullets updated.

Test plan

  • cargo test --bin sigma-agent mcp:: — 5/5 green locally
  • Manual smoke: AGENT_MCP_ENABLED=true AGENT_MCP_BIND=0.0.0.0:9103 (no token) → agent logs the refusal and continues; MCP port not bound
  • Manual smoke: same bind with AGENT_MCP_TOKEN=... → MCP serves; request without Authorization returns 401; request with correct token returns 200
  • Manual smoke: default loopback bind with no token still works (no regression)

🤖 Generated with Claude Code

The MCP server's only trust boundary was the bind address: flipping
AGENT_MCP_BIND from 127.0.0.1 to 0.0.0.0 silently exposed every tool —
allocate_ports, query_envoy_routes, agent_check_update --force — with
no per-request auth.

Changes:
- Add AGENT_MCP_TOKEN (--mcp-token) config option.
- serve_mcp refuses to start when the bind is non-loopback and no token
  is configured. The rest of the agent keeps running; operator gets an
  error! log and chooses to set the token or revert the bind.
- mcp_handler validates Authorization: Bearer <token> against the
  configured token in constant time; returns 401 on missing/invalid.
- Setting the token on a loopback bind is allowed and stacks for
  defence in depth.
- is_loopback_bind recognises 127.0.0.0/8, ::1, and `localhost`; treats
  unparseable strings as non-loopback (fail-closed).

5 unit tests cover loopback detection (IPv4, IPv6, localhost,
wildcards, garbage) and the constant-time comparison helper.

Docs updated: sigma-agent README + CLAUDE.md, and docs/ai-triage.{en,zh}.md
security-model sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lai3d lai3d merged commit 09b9e0d into main May 19, 2026
1 check passed
@lai3d lai3d deleted the claude/agent-mcp-auth branch May 19, 2026 13:35
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