Skip to content

Demo scenarios

make demo runs four scenarios against a real SPIRE 1.15.3 stack. Policy for all of them is deploy/policy.yaml.

Workloads

SPIFFE ID Container label org.example.svid.workload In policy
spiffe://example.org/authz authz n/a
spiffe://example.org/mcp/notes-a notes-a n/a
spiffe://example.org/mcp/notes-b notes-b n/a
spiffe://example.org/agent/research agent-research notes-a: notes:read notes:write; notes-b: notes:read
spiffe://example.org/agent/intruder agent-intruder No

Registration entries use docker:label:org.example.svid.workload:<name> selectors, a JWT-SVID TTL of 300s and -jwtSVIDIncludeJTI, so each SVID carries a jti. authz runs with --svid-replay=allow-reuse-within-lifetime: the SPIRE 1.15.3 agent re-serves one cached SVID per audience, so scenarios 2 and 4 present the same SVID and jti as scenario 1. See JWT-SVID replay tracking. The SPIRE agent reads labels through the Docker socket; see Docker socket exposure. Workloads still run as distinct non-root UIDs, but the UID no longer decides the identity.

Scenarios

# Caller Target Expected
1 agent/research notes-a token with notes:read notes:write, both tools allowed
2 agent/research notes-b token with notes:read, notes.write gets 403 insufficient_scope
3 agent/intruder notes-a valid SVID, not in policy.yaml, token request fails with unauthorized_client
4 agent/research token for notes-a sent to notes-b 401 invalid_token, audit reason InvalidAudienceError

Commands, as run by deploy/demo.sh. Each run also passes --trusted-issuer http://authz:8100, omitted below:

# 1
mcp-svid-agent --resource http://notes-a:8101/mcp --scope "notes:read notes:write" \
  --spiffe-id spiffe://example.org/agent/research
# 2
mcp-svid-agent --resource http://notes-b:8102/mcp --scope notes:read
# 3 (in the agent-intruder container)
mcp-svid-agent --resource http://notes-a:8101/mcp --scope notes:read
# 4
mcp-svid-agent --resource http://notes-a:8101/mcp --scope "notes:read notes:write" \
  --steal-token http://notes-b:8102/mcp

In each run the agent calls notes.search and then notes.write, and prints one JSON event per step (token, replay, call, token_denied, or issuer_refused).

Result

Item State
make demo against SPIRE 1.15.3 Pass on 2026-09-27, Docker Desktop 29.6.1 on macOS, docker attestor, --trusted-issuer, --svid-replay=allow-reuse-within-lifetime. All four scenarios behave as in the table above. The three authz allow lines carry the same svid_jti, which shows the agent SVID cache.
make demo with --svid-replay reject Fails as expected on 2026-09-27: scenario 1 passes, scenarios 2 and 4 get 401 invalid_client: client assertion replayed. Without -jwtSVIDIncludeJTI every request gets 401 invalid_client (no jti).
Offline tests Pass (make check)

Audit line format:

{"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"}

Offline equivalents

The same behaviour is covered without SPIRE in tests/, using LocalSvidIssuer:

Test file Covers
test_end_to_end.py Scenarios 1 to 4, Protected Resource Metadata, missing-token challenge
test_authz.py SVID validation, policy grants, trust domain, client_id match, metadata, audit
test_authz_hardening.py iat and SVID lifetime, fixed error strings, 503 on bundle failure, scope parsing, repeated parameters
test_resource_server.py Expiry, issuer, typ, ES256 pin, unknown tool, bad and oversized bodies, every tool declares a scope
test_stdio_wrapper.py Token file mode, refresh, fail closed, env export opt-in, cleanup
test_jwks_fetcher.py Async JWKS fetch, one refetch on unknown kid at most every 10s, fail closed when the fetch fails
test_authz_replay.py --svid-replay modes: jti required and single use in reject, reuse and missing jti accepted in allow-reuse-within-lifetime, malformed jti refused in both, full cache fails closed, demo sets the mode explicitly
test_issuer_allowlist.py Trusted issuer allowlist: unknown and lookalike issuers refused before any SVID fetch, refusal event
test_no_identifiers.py Optional anonymization denylist