secu-mcp
The public lab of pythagoras-sec: a generic MCP reverse proxy that refuses any server unable to prove its workload identity, and enforces default-deny on everything an authenticated server can do.
- Status
- Active
- Started
- Sept 2026
- Stack
- Go · SPIFFE/SPIRE · mTLS · MCP · k3s · GCP
- Code
Problem. Standard MCP implementations trust a server once it is configured. If an agent or a server is compromised, nothing stands between it and tool execution. secu-mcp makes the answer public as a standalone, reusable proxy.
Architecture. Open-source MCP reverse proxy, over HTTP or stdio, that fails closed:
- Identity: SPIFFE ID checked over mTLS, or a service-mesh sidecar assertion.
- Tool policy: default deny; a refused call never reaches the upstream server. Sensitive calls wait for human approval (webhook, or a signed JWT bound to the exact call).
- Tool pinning: SHA-256 of each tool’s name, description and schema; a tool that drifts is removed and quarantined.
- Untrusted data: responses are redacted and wrapped in
<untrusted_data>. - Auditability: HMAC-chained JSONL log.
Validation. go test mints real SVIDs in memory and runs real TLS handshakes and MCP calls, with no cluster needed: the rogue server’s handler never runs. The full lab runs on k3s and SPIRE on GCP.
What it is
secu-mcp is the open part of pythagoras-sec, published on 4 September 2026. It packages the four locks as a reverse proxy (cmd/mcp-proxy) that sits in front of any MCP server, over HTTP or stdio. Every mechanism fails closed when it is not configured.
Proof without infrastructure
The security logic is proven by the test suite, independently of any cluster: internal/testca mints real SVIDs in memory, and the tests go through real TLS handshakes and real MCP calls.
go test -v ./...
Among other things, the suite shows that the rogue server’s handler never runs.
What the proxy enforces
| Mechanism | Package | Behaviour |
|---|---|---|
| Identity | internal/identity | IDENTITY_MODE=spiffe terminates mTLS and checks the SPIFFE ID; mesh trusts an Istio/Envoy sidecar and verifies the X-Forwarded-Client-Cert assertion; none exists for development and says so loudly. |
| Tool policy | internal/toolpolicy | Default deny. A refused call never reaches the upstream session. |
| Human approval | internal/approval | A real-time webhook pauses the JSON-RPC request until Slack, Jira or ServiceNow answers, or an operator-signed JWT (TTL of 15 min or less) bound to the exact call by sha256(tool + args). Every decision goes to an HMAC-chained JSONL audit log. |
| Tool pinning | internal/toolpin | SHA-256 of each tool’s name, description and schema in tools.lock.json. A tool whose hash drifts is removed and quarantined (ERR_TOOL_SCHEMA_MUTATED). |
| Untrusted data | internal/envelope, internal/scrub | Responses are redacted (tokens, secrets, emails, routable IPs) and sealed in <untrusted_data>. Tool descriptions are redacted too: a hidden instruction there is the same injection, one message earlier. |
| JSON-RPC surface | internal/proxy | Any method outside the allowed matrix is refused. Server-initiated sampling and elicitation are dropped by default. |
stdio transport
On a desktop host, an MCP server is a subprocess: no network, so no handshake to check. The risk moves to the integrity of the binary. mcp-proxy -stdio -- <command> wraps the server and UPSTREAM_SHA256 pins the binary’s hash before it is spawned.