pythagoras-sec
Most MCP setups rely on static, implicit trust. pythagoras-sec replaces static config with cryptographic identity (SPIFFE/SPIRE) at handshake, then enforces runtime policy on every tool execution.
- Status
- Active
- Started
- Jul 2026
- Stack
- Go · SPIFFE/SPIRE · mTLS · k3s · NetworkPolicy · GCP · Python · Mistral




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.
Architecture. A policy engine between agents and MCP tools:
- Identity: SPIFFE/SPIRE workload identity, verified over mTLS at the handshake (public lab: secu-mcp).
- Runtime policy: every tool call is evaluated against versioned directives before it runs; actions above authorization thresholds pause for human approval.
- Auditability: every decision is written to a tamper-evident, hash-chained journal that can be replayed and verified offline.
Validation. Synthetic banking scenario: a loan approval runs in 12 calls. With directive D-009 enabled, the action is intercepted, paused for human validation and completes in 14 calls. Both audit chains pass offline verification.
The problem
An MCP client connects to servers listed in local configuration files. That configuration is saved once and never validated at runtime. Any process responding on the expected address with a matching tool name gains agent trust and receives sensitive API credentials.
The lab demonstrates this vulnerability directly. The network-config service (legitimate) and rogue service (malicious) both expose the tool name get_file. With TRUST_MODE=naive, the agent executes calls against the rogue server, leaking credentials. With TRUST_MODE=zerotrust, the rogue connection is terminated before any handler executes.
Two halves of the threat model
| Dimension | Question | Nature | Treatment |
|---|---|---|---|
| Identity | Who are you? | Binary, provable | Solved via SPIFFE and mTLS |
| Behavior | What are you doing? | Continuous, unpredictable | Contained through strict execution bounds |
An authenticated server can still suffer prompt injection or drift. Text filters applied to inputs eventually fail against obfuscation, uncommon languages, or Base64 encoding. The architecture assumes filters will be bypassed and focuses on containing the blast radius.
Four locks
| Lock | Location | Guarantee |
|---|---|---|
| Lock 0 Identity | internal/identity | Peers prove SPIFFE ID during mTLS handshake. Without an attested SVID, connection drops. The rogue pod has a ServiceAccount but no SPIRE entry, making identity attestation impossible. |
| Lock 1 Egress | NetworkPolicies | Default-deny rules enforced at kernel level. Exfiltration attempts fail immediately. |
| Lock 2 Execution | internal/toolpolicy | Capability allowlists evaluated before calls leave process context. Read and write actions are segregated; write actions require explicit human approval with target binding. |
| Lock 3 Data isolation | internal/envelope | Tool responses are encapsulated inside <untrusted_data id="nonce"> tags using fresh 16-byte nonces, marked inert in system prompts. |
Locks 1 to 3 remain effective even if prompt injection succeeds against the model.
Lab setup
GCP e2-standard-2 running single node k3s with kube-router. SPIRE server operates as root CA (trust domain mcp.gcp.lab, 1 hour SVID TTL), with SPIRE agent running as DaemonSet using k8s_psat attestation. Prometheus, Alertmanager, and Grafana sit behind an obs-bridge proxy that holds the Grafana API key. The monitoring agent receives metrics without accessing credentials.
Engine design
The control point evolved into a standalone policy engine. Every tool call is evaluated against versioned directives, logged to a hash-chained audit journal, and reproducible offline. Actions exceeding authorization thresholds pause for explicit human approval.
In a test run using synthetic banking data, a standard loan approval executed in 12 calls. Enabling directive D-009 (internal rate checked without official rate reference) caused D-009 to intercept the action, pause execution for human validation, and complete in 14 calls. Both audit chains passed full offline cryptographic verification.
Known limits
- NetworkPolicy start-up race condition. iptables rules apply shortly after pod IP allocation. Measured on cluster: at t+0 s a
wgetcommand reaches external IPs; at t+20 s rules take effect and block egress. The egress lock protects against compromised workloads at runtime, but not against exfiltration within the first few seconds of pod creation. Mitigating this requires policy enforcement prior to pod startup via specialized CNI or mandatory egress proxies. - Deterministic triage without external LLM calls. Under default-deny, querying an external model requires opening egress paths, which defeats the principle of minimal network exposure.
- Scoped shortcuts. PromQL validation uses syntax parsing, data redaction relies on pattern matching rather than full classification, and SPIRE agent uses
insecure_bootstrap. The lab models rogue MCP servers rather than a compromised SPIRE control plane.
Development model
The engine was developed by coordinating multiple coding agent sessions in parallel against immutable interface contracts. I define the contracts, write specifications, review pull requests, and merge code only when go vet, go test -race, and journal replay tests pass cleanly.
Next steps
Implementing segmented controls: replacing manual rule definition with chained binary classifiers. Decisions combine deterministically so that no individual model evaluation can relax an existing policy constraint.
