mcp-svid-notes
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-svid-notessearch my notes for the SPIFFE demo"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-svid-auth
A checked proof of concept: an AI agent exchanges its SPIFFE workload identity for a short-lived, audience-bound MCP access token. The demo shows both enforced security boundaries and the failures identity alone cannot prevent.
Documentation: https://tanya-ok.github.io/mcp-svid-auth/
Problem
MCP client configs often hold long-lived static keys:
{ "env": { "NOTES_API_KEY": "sk-live-..." } }Issue with static keys | Effect |
Long-lived | A leaked key works until someone rotates it by hand |
Not bound to a workload | Any process that reads the file can use it |
Not bound to a server | One key often opens several servers |
No per-caller identity | Audit logs cannot say which agent acted |
Related MCP server: MCP Credentials Broker
What this POC shows
Claim | Where |
An agent gets a short-lived access token using only its JWT-SVID |
|
Tokens are bound to one MCP server via the RFC 8707 |
|
A token issued for server A is rejected by server B |
|
Scopes are enforced per tool, with a 403 |
|
Decisions are hash-chained; a separately trusted terminal checkpoint detects suffix deletion and chain replacement. Without a checkpoint, |
|
The MCP server never forwards the incoming token. With an upstream it uses its own token |
|
A stdio MCP server can start with a refreshed short-lived token instead of a static key |
|
Targets the MCP authorization spec, revision 2026-07-28 (checked 2026-10-04): Protected Resource Metadata (RFC 9728), resource indicators (RFC 8707), audience validation, no token passthrough.
Client authentication implements the JWT-SVID exchange from draft-ietf-oauth-spiffe-client-auth-02: assertion type urn:ietf:params:oauth:client-assertion-type:jwt-spiffe, advertised as spiffe_jwt in token_endpoint_auth_methods_supported. This is a supported subset, with a deliberate local Workload API bundle-lookup deviation. See the compatibility matrix for draft status, generic OAuth differences and the independently checked TypeScript adapter.
Architecture
sequenceDiagram
participant SA as SPIRE agent<br/>(Workload API)
participant AG as agent/research
participant AZ as authz
participant A as notes-a (MCP)
participant B as notes-b (MCP)
AG->>A: GET /.well-known/oauth-protected-resource/mcp
A-->>AG: resource, authorization_servers
AG->>AZ: GET /.well-known/oauth-authorization-server
AZ-->>AG: issuer, token_endpoint
AG->>SA: FetchJWTSVID(aud = issuer)
SA-->>AG: JWT-SVID (60s demo; max 5 min)
AG->>AZ: POST /token client_credentials<br/>client_assertion = JWT-SVID<br/>resource = notes-a
AZ->>SA: FetchJWTBundles
AZ->>AZ: verify SVID, check policy.yaml
AZ-->>AG: access token (aud = notes-a, 5 min)
AG->>A: tools/call + Bearer token
A->>A: verify sig, iss, exp, aud, tool scope, write audit line
A-->>AG: result
AG-->>B: same token replayed
B-->>AG: 401 invalid_token (aud mismatch)Components
Module | Command | Role |
|
| Token endpoint. JWT-SVID client assertion in, audience-bound JWT access token out. Publishes JWKS and RFC 8414 metadata. |
|
| MCP server over Streamable HTTP. Tools |
|
| Discovers the authorization server, refuses it unless it is on the |
|
| Starts a local stdio MCP server with |
|
| SPIRE server and agent 1.15.3 (pinned by digest), registration entries, the services, eighteen asserted Kubernetes scenarios and a safe verdict report. |
Token request, as sent by the agent:
Parameter | Value |
|
|
|
|
| JWT-SVID, |
| the SPIFFE ID (optional, must match the SVID |
| canonical URI of the MCP server (required) |
| required, single-space separated, must be a subset of the policy grant |
Repeated parameters are rejected with invalid_request. Error descriptions are fixed strings; audit diagnostics retain exception class names, not exception text. Unverified subjects and SVID IDs are hashed before logging.
The MCP server accepts only access tokens with header typ at+jwt, algorithm ES256, and claims iss, sub, aud, exp, iat, scope. Request bodies are capped at 1 MiB (413). Bodies that are not valid JSON-RPC get 400.
Quickstart
The single-repository path needs only Python 3.12+ and uv, plus Node 26+ and npm for the interoperability check. It uses runtime local keys instead of SPIRE and contacts no external service after dependencies are installed.
uv sync --locked
make check # ruff, mypy strict, pytest (offline, no SPIRE needed)
(cd examples/typescript-client && npm ci --ignore-scripts --no-audit --no-fund)
make interop # local authz and notes server on loopback, driven by the TypeScript MCP SDK
make package # local wheel + source archive; no publicationmake interop is the local end-to-end demo without the lab: discovery, JWT-SVID exchange, MCP initialization, scoped tool listing, an allowed read and a denied write. Offline tests use a local key as a stand-in for SPIRE (LocalSvidIssuer). mcp-svid-authz --static-jwks accepts a JWKS file for the same purpose.
Optional local anonymization denylist: put one term per line in tests/denylist.txt (git-ignored). tests/test_no_identifiers.py then fails on any match in the tree or history.
Full Kubernetes lab (requires unpublished components)
The integrated lab is not reproducible from this repository alone. It needs a sibling checkout of mcp-spiffe-bridge, which is not yet published, and of hipaa-fhir-mcp. On macOS it also needs Lima 2.2.1+. The demo scenarios below were recorded in that lab. See shared security lab and CNCF runtime.
make runtime-up # dedicated Lima Kubernetes/containerd VM: 2 CPUs, 4 GiB memory
make demo # fresh lab stack; includes real expiry and negative cases
make down
make runtime-stop # release VM CPU/memory only when no sibling project needs itFor the lab's read-only interface, run make ui and open Security lab. It shows services, trust relationships, permissions, safe audit outcomes and the latest demo verdicts. See Headlamp interface for access limits and shutdown commands.
make down removes only owned Security Lab resources. If another project shares
this VM, retain the VM instead of running make runtime-stop. IT GRC (it-grc,
not yet public) runs CISO Assistant Community edition in its own Lima VM, not in
this lab, and takes the lab's demo report as its input.
Demo scenarios
Each scenario asserts its outcome. An unexpected exit, missing evidence or wrong result makes make demo fail. It writes a credential-free report to ignored deploy/.data/demo-report.json; the expiry probe uses 60-second SVIDs and includes the 30-second validation leeway. See demo details.
# | Caller | Target | Expected |
1 |
| notes-a | token with |
2 |
| notes-b | token with |
3 |
| notes-a | valid SVID, not in |
4 |
| token for notes-a sent to notes-b | 401 |
5 |
| notes-a | gets the intruder identity; token request denied |
6 |
| notes-rogue, whose PRM names |
|
7 |
| authz | two tokens under |
8 |
| notes-a, which calls notes-b | notes-b audits |
9 |
| notes-b |
|
10 | Deterministic injected write request | notes-a | read-only token denied; read-write token succeeds: identity does not determine intent |
11 |
| notes-a | restart key accepted, then prepublished rotation preserves old and new tokens during overlap (LS-10) |
12 | Research workload through initial SVID expiry | authz and notes-a | renewed SVID remains usable; expired original assertion refused |
13 | One SVID used twice under default strict mode | authz-strict | first token issued, second exchange denied |
14 | Audit capture, then delete its final record | local verifier | complete chain passes; deletion fails against saved checkpoint |
15 | Research workload with SPIRE mTLS and OAuth | FHIR service | Three tools succeed; wrong audience, missing bearer and bad/unknown requests denied |
16 | Intruder with valid SPIRE certificate | FHIR bridge | SPIFFE caller allowlist denies access |
17 | Workload without client certificate | FHIR bridge | TLS handshake denied |
18 | FHIR audit checker | Service log | One record per invocation, verified caller, data minimization |
Audit line example:
{"record_id":"5f0c9a53-2a8e-4d0b-9a57-0f3e2f6d1c44","parent_record_id":"b1d7e0a2-6c1f-4e89-8f0e-2d4c7a9b3e51","prev_hash":"3b9f0c2e8d7a41f6b5e0c9d8a7f6e5d4c3b2a1908f7e6d5c4b3a29180f7e6d5c","timestamp":"2026-09-26T18:07:11.742+00:00","component":"mcp_server:notes-b","spiffe_id":"spiffe://example.org/agent/research","tool":"notes.write","decision":"deny","reason":"insufficient_scope: needs notes:write"}Denials record unverified subjects as unverified:sha256:<digest prefix>. A checkpoint must be retained independently of the log writer; see audit integrity.
Threat model notes
Threat | Mitigation here | Gap |
Static key leak | No static keys. Access tokens live 5 min; SVID maximum is 5 min, with 60s SVIDs in the demo. | A stolen access token works until expiry. |
Token replay to another server |
| None within one issuer. |
JWT-SVID replay to authz | SVID | The container demo runs |
SVID harvesting by a malicious resource | The agent mints SVIDs only for issuers on its | The allowlist is per process, not per resource. |
Discovery SSRF | Every URL the agent fetches or sends a credential to (resource, issuer, |
|
Workload impersonation | SPIRE Kubernetes attestor binds identity to namespace and ServiceAccount. Copied labels do not suffice. | Cluster operators and anyone allowed to use that ServiceAccount can assume its identity. |
Over-broad access | Allowlist per SPIFFE ID, resource and scope. Per-tool scope check. Policy reloaded on SIGHUP or file change. | Policy is a local file. No versioning or review flow. Issued tokens outlive a revoked grant by up to 300s. |
Confused deputy | Server never forwards the incoming token. An upstream call uses the server's own SVID-based token ( | No delegation chain: the upstream sees the relay, not the original caller. |
Algorithm confusion | SVIDs: asymmetric algorithms only, | |
Stale token in a stdio child | Token passed by 0600 file, refreshed. Wrapper deletes it and stops the child on expiry. With |
|
Supply chain | Dependency majors bounded, images pinned by digest, |
|
Non-goals
Not production software. No HA or distributed rate limits; planned in-process key rotation and local cost budgets only. Plain HTTP inside the demo network.
Not a replacement for an MCP gateway such as agentgateway. It shows the token flow a gateway could implement.
No user delegation. The agent acts as itself (
client_credentials). No token exchange oractclaim chain yet.The authorization server accepts JWT-SVID client assertions only. The shared FHIR transport also uses X.509-SVID mTLS. No WIT-SVID client authentication.
The default Kubernetes lab is single-node and synthetic. Podman compatibility commands were retired on 2026-10-13. See shared lab limits.
No
jtireplay tracking for access tokens.The container demo accepts JWT-SVID reuse within the SVID lifetime (
--svid-replay=allow-reuse-within-lifetime). The SPIRE 1.15.3 agent returns the same cached SVID, with the samejti, on repeated fetches before renewal.
Status
The dated publication validation records the checks, artifacts and remaining proof-of-concept limits. Validation runs locally; no release, package upload or deployment is part of it.
Item | State |
Offline checks | Pass on 2026-10-13: 537 tests on Python 3.12 and 3.13 after retiring the Podman backend and adding runtime migration checks, lint, formatting, strict types, generated docs and strict documentation build; source/history credential checks have no unresolved findings; see the exact packet-hash false positive in the portfolio readiness record |
Kubernetes/containerd integration | Pass on 2026-10-13: fresh rebuild, all 18 scenarios and final scoped teardown. Planned signing rotation preserved old/new tokens; three real SPIRE fetches returned one cached assertion with jti. CISO namespace UID and shared VM preserved. See rotation evidence |
Headlamp interface | Pass: five protected desktop views, 390 x 844 layout with visible Refresh, opaque-session logout, idempotent start, shutdown on identity backend loss and recovery. Hosted Login V2 reached the passkey screen without a password form. See interface and boundaries |
Optional ZITADEL identity | Pass: 11 real synthetic WebAuthn/OIDC checks, 4 closed Google setup checks and full scoped lifecycle on 2026-10-06. The recovery cycle sampled at most 1858.1 MiB guest use including native startup. Real Google login and physical enrollment remain unverified. See identity boundaries |
Historical Podman validation | Retired on 2026-10-13; the 2026-10-05 result remains historical evidence only |
Default strict replay mode | Live comparison: first exchange succeeds, second is denied. Normal demo explicitly accepts cached SVID reuse |
Independent TypeScript MCP SDK | SDK 1.32.1 with explicit JWT-SVID adapter: discovery, exchange, initialize, scoped listing and read verified; added write denial checked separately |
Audit completeness | Terminal checkpoint supported; its protection requires separate trusted storage |
Publication packages | Local wheel and source archive, license and contents verified; never uploaded |
License
Apache-2.0. See LICENSE and NOTICE.
Development review orchestration
The local Codex and Claude review workflow records source snapshots, independent research, objections and explicit task selection through MCP. A synthetic walkthrough demonstrates the process without model calls. Model agreement is a proposal, not proof of correctness.
This server cannot be deployed
Maintenance
Related MCP Connectors
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
One MCP endpoint for your company's tools, with per-person sign-in, policy and audit.
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
Prove end users to agents and apps: login-links, OIDC clients, and API keys over remote MCP
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceAn MCP server for OAuth 2.0 authentication supporting Device Code and Client Credentials flows, enabling secure token management for MCP applications.-
- AlicenseAqualityDmaintenanceProvides secure OAuth2-based credential management for MCP servers, allowing agents to obtain short-lived token references without exposing raw secrets.73 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables clients to access multiple backend MCP servers through a single endpoint, with OAuth 2.1 authorization, namespaced tools, and secure credential management.2MIT
- AlicenseNot gradedqualityBmaintenanceEnables teams to securely store and retrieve credentials, view decrypted secrets or live OAuth tokens, and connect to third-party MCP servers through a unified MCP endpoint.MIT