Configuration reference¶
All commands are installed by uv sync as console scripts. --socket defaults to the SPIFFE_ENDPOINT_SOCKET environment variable.
The flag tables, fixed values and policy schema below are generated from the code by scripts/gen_config_docs.py. CI fails if they drift.
mcp-svid-authz¶
JWT-SVID to access token bridge (POC)
| Flag | Default | Meaning |
|---|---|---|
--issuer |
required | Public base URL, used as iss and as the required SVID aud; trailing slash removed |
--policy |
required | Path to the policy YAML file; reloaded on SIGHUP and when its content changes |
--policy-poll-seconds |
5.0 |
How often to check the policy file for changes; 0 disables polling (SIGHUP only) |
--token-exchange |
off | Enable RFC 8693 on-behalf-of exchange for tokens this server issued; off by default |
--host |
127.0.0.1 |
Listen address |
--port |
8100 |
Listen port |
--static-jwks |
none | Test mode: JWT-SVID bundle as a JWKS file instead of the Workload API |
--socket |
none | Workload API socket for JWT bundles, default SPIFFE_ENDPOINT_SOCKET |
--audit-log |
none | Audit log file (JSON lines, appended), default stderr |
--max-svid-lifetime |
300 |
Reject JWT-SVIDs whose exp - iat exceeds this many seconds |
--svid-replay |
reject |
Reject: require a JWT-SVID jti and accept each (sub, jti) once; allow-reuse-within-lifetime: jti optional, a reused SVID is accepted until it expires |
--replay-cache-size |
10000 |
Global live assertion cache limit; refuses when full |
--replay-subject-limit |
1000 |
Maximum live assertion entries per verified subject |
--token-requests-per-second |
50 |
Per-process token endpoint requests per fixed one-second window |
--subject-requests-per-second |
10 |
Exchanges per authorized subject per fixed one-second window |
--signing-rotation-seconds |
3600 |
Planned signing rotation interval; zero disables, minimum 60 seconds |
Fixed values:
| Setting | Value |
|---|---|
| Token form byte limit | 16384 |
| Signing key prepublication | 60s |
| Retired signing keys | at most 6 |
| Access token TTL | 300s |
| Access token algorithm | ES256 |
| SVID clock leeway | 30s |
| Trust bundle cache (Workload API) | 30s |
mcp-svid-notes¶
Notes MCP server (POC)
| Flag | Default | Meaning |
|---|---|---|
--name |
notes |
Server name, also used in the audit component (mcp_server:<name>) |
--resource |
required | Canonical URI, e.g. http://host:8101/mcp; must equal the token aud |
--issuer |
required | Authorization server issuer URL |
--jwks-uri |
none | JWKS location, default <issuer>/jwks.json |
--host |
127.0.0.1 |
Listen address |
--port |
8101 |
Listen port |
--audit-log |
none | Audit log file (JSON lines, appended), default stderr |
--upstream-resource |
none | Upstream MCP server for notes.search_upstream, called with this server's own token |
--upstream-scope |
notes:read |
Scope requested for the upstream token |
--trusted-issuer |
none | Authorization server allowed for the upstream token; required with an upstream |
--socket |
none | Workload API socket for the upstream SVID, default SPIFFE_ENDPOINT_SOCKET |
--allow-http |
off | Dev only: accept plain http URLs for discovery, token requests and tool calls |
--allow-private-network |
off | Dev only: accept hosts that resolve to private, loopback or link-local addresses |
Fixed values:
| Setting | Value |
|---|---|
| Tools (scope) | notes.search (notes:read), notes.write (notes:write) |
| JWKS cache | 60s |
Unknown kid refetch |
at most every 10s |
| Token verification budget | 256/second/process |
| Access token byte limit | 8192 |
| JWKS decoded byte limit | 65536 |
| JWKS key count limit | 16 |
| Request body limit | 1048576 bytes |
Accepted token typ |
at+jwt |
| Accepted token algorithms | ES256 |
mcp-svid-agent¶
Agent that authenticates with its JWT-SVID
| Flag | Default | Meaning |
|---|---|---|
--resource |
required | MCP server canonical URI |
--scope |
required | Space separated scopes, e.g. 'notes:read notes:write' |
--trusted-issuer |
required | Authorization server issuer the agent may mint SVIDs for; repeatable, exact match |
--allow-http |
off | Dev only: accept plain http URLs for discovery, token requests and tool calls |
--allow-private-network |
off | Dev only: accept hosts that resolve to private, loopback or link-local addresses |
--spiffe-id |
none | Sent as client_id |
--socket |
none | Workload API socket, default SPIFFE_ENDPOINT_SOCKET |
--call |
none | Tool to call with JSON arguments; repeatable; default notes.search then notes.write |
--steal-token |
none | Demo: send the token issued for --resource to OTHER_RESOURCE instead |
mcp-svid-stdio¶
Run a stdio MCP server with a short-lived token from the Workload API
| Flag | Default | Meaning |
|---|---|---|
--resource |
required | Upstream resource the child calls |
--scope |
required | Space separated scopes |
--trusted-issuer |
required | Authorization server issuer the wrapper may mint SVIDs for; repeatable, exact match |
--allow-http |
off | Dev only: accept plain http URLs for discovery, token requests and tool calls |
--allow-private-network |
off | Dev only: accept hosts that resolve to private, loopback or link-local addresses |
--socket |
none | Workload API socket, default SPIFFE_ENDPOINT_SOCKET |
--refresh-margin |
60 |
Seconds before expiry to refresh |
--export-token-env |
off | Also set MCP_ACCESS_TOKEN (weaker: visible in the environment); the child is stopped when that token expires |
-- command [args...] |
required | Child command, after -- |
Fixed values:
| Setting | Value |
|---|---|
| Exit code when the token cannot be refreshed | 75 |
| Grace between SIGTERM and SIGKILL for the child on expiry | 5s |
mcp-svid-audit-verify¶
Verify the hash chain of audit log files written with --audit-log
| Flag | Default | Meaning |
|---|---|---|
files |
required | Audit log files, one chain each |
--checkpoint |
none | Verify one complete log against a separately trusted terminal checkpoint |
--write-checkpoint |
none | Create a new terminal checkpoint for one valid log; refuses to overwrite |
Fixed values:
| Setting | Value |
|---|---|
| Chain | prev_hash = SHA-256 of the JCS form of the previous record, hex |
Policy file¶
Example from deploy/policy.yaml:
# Which SPIFFE IDs may get access tokens for which MCP servers, with which scopes.
# Anything not listed is denied.
trust_domain: example.org
clients:
- spiffe_id: spiffe://example.org/agent/research
resources:
http://notes-a:8101/mcp: [notes:read, notes:write]
http://notes-b:8102/mcp: [notes:read]
# notes-a calls notes-b with its own token for notes.search_upstream.
- spiffe_id: spiffe://example.org/mcp/notes-a
resources:
http://notes-b:8102/mcp: [notes:read]
# spiffe://example.org/agent/intruder has a valid SVID but is deliberately not listed.
| Key | Type | Meaning |
|---|---|---|
trust_domain |
string, required | Only SVIDs from this trust domain are accepted |
clients |
list | One entry per SPIFFE ID |
clients[].spiffe_id |
string, required | Exact SPIFFE ID (SVID sub) |
clients[].resources |
map | Resource URI to list of allowed scopes. Trailing slashes are ignored when matching. |
Behaviour notes¶
mcp-svid-agentcallsnotes.searchthennotes.write. It exits with code 2 and prints atoken_deniedevent when the token request fails.mcp-svid-stdiois described in stdio wrapper.
Policy decisions¶
The policy is an allowlist of which SPIFFE IDs may get access tokens for which MCP servers, with which scopes. Anything not listed is denied.
Decision order:
| Condition | Error |
|---|---|
| SPIFFE ID not listed | unauthorized_client |
| Resource not listed for that ID | invalid_target |
| Requested scopes not a subset of the listed scopes | invalid_scope |
The token carries exactly the requested scopes, not the full grant.