使用说明
MCPIdentity PoC
Keyless MCP authentication for Kubernetes services with Keycloak 26.6
Two MCP agents in two namespaces talking to two MCP servers in two more namespaces. Zero pre-registered Keycloak clients. Zero credential files on any pod. SPIRE attestation + CIMD + Federated Client Authentication + DPoP + OPA trust matrix.
Reference deployment for the KeycloakCon @ KubeCon Japan 2026 talk. This is the
generic, domain-agnostic version. Healthcare-themed variants live on the
health-svid-spiffe-keyed (PoC 1) and health-svid-spiffe-bound (PoC 2)
branches — same architecture, different names.
1. Prerequisites
| Tool | Why it is needed | Quick check |
|---|---|---|
| Docker | k3d runs the cluster inside Docker; the daemon must be running | docker info |
| k3d (≥ 5) | creates the local Kubernetes cluster | k3d version |
| kubectl | talks to the cluster | kubectl version --client |
| Podman | builds the unified MCP image, imported into k3d via a saved tarball | podman --version |
| Python 3.12 | only for the test suite (optional) | python3 --version |
Two runtimes on purpose: k3d runs on Docker, but the container image is built with Podman and imported as a tarball. Both must be installed.
scripts/setup-all.shchecks fork3d,kubectl,docker, and a running Docker daemon before it does anything.
2. Demo environment (k3d cluster)
All cluster settings — name, host ports, image tag — live in scripts/cluster.env
and can be overridden from the shell (e.g. CLUSTER_NAME=my-name ./scripts/setup-all.sh).
Bring everything up
./scripts/setup-all.sh
Runs, in order:
| # | Script | What it does |
|---|---|---|
| 1 | 01-create-cluster.sh |
k3d cluster (1 server + 2 agents, Traefik disabled) + 4 MCP namespaces |
| 2 | 02-deploy-spire.sh |
SPIRE server/agent + role-aware registration entries |
| 3 | 03-deploy-keycloak.sh |
Keycloak 26.6 (--features=cimd) + PostgreSQL 16 backend + mcp-identity realm (zero pre-registered agents) |
| 4 | 04-deploy-opa.sh |
OPA + the trust-matrix.rego policy |
| 5 | 05-build-image.sh |
build mcp-identity:latest with Podman, import into k3d |
| 6 | 06-deploy-mcp-pods.sh |
2 agents + 2 servers across 4 namespaces |
Tear everything down
./scripts/remove-all.sh # reverse of setup-all, in order (asks to confirm)
./scripts/remove-all.sh --yes # no prompt (CI / scripted teardown)
Or drop individual layers with the matching NN-delete-*.sh (all idempotent):
./scripts/06-delete-mcp-pods.sh # MCP pods only, keep infra
./scripts/03-delete-keycloak.sh # realm + Keycloak (also removes its PVC)
./scripts/02-delete-spire.sh # SPIRE + registration entries
./scripts/01-delete-cluster.sh # the whole k3d cluster + MCP namespaces
Reset to a clean "Demo 1" state (keep the cluster)
./scripts/reset-cimd-clients.sh # delete all CIMD clients + restart pods
./scripts/reset-cimd-clients.sh --no-restart # delete clients only
Removes every URL-based client_id from the realm, flushes Keycloak's JWKS
cache, and restarts the pods so they self-bootstrap fresh.
3. Run the demo, step by step
Each script is short and self-contained. Run them in order:
./demo/01-no-keys-no-clients.sh # prove the absence: no keys on pods, no MCP clients in the realm
./demo/02-discovery.sh # RFC 9728 protected-resource discovery
./demo/03-svid.sh # SPIRE SVID + CIMD document + JWKS on the pod
./demo/04-cimd-auth.sh # full round-trip: CIMD register → DPoP-bound token → tool call
./demo/05-replay-attack.sh # DPoP replay defence (stolen token / proof rejected)
./demo/06-trust-matrix.sh # OPA: six scenarios — four allow, two deny
./demo/07-add-client-live.sh # add a new agent at runtime — no realm edit
Optional / adversarial:
./demo/08-amplification-attack.sh # CIMD /auth amplification (the honest-limit demo)
Or run demos 1–7 interactively:
./demo/run-all.sh
Speaker-facing walkthrough: demo/DEMO_SCENARIOS.md.
Inspect the running cluster:
kubectl get pods --all-namespaces -l app.kubernetes.io/component=mcp-agent
kubectl get pods --all-namespaces -l app.kubernetes.io/component=mcp-server
kubectl logs -n keycloak deployment/keycloak | grep -i cimd
Topology
ns: mcp-agents ns: mcp-agents-restricted
┌─────────────────────────┐ ┌─────────────────────────┐
│ mcp-agent-alpha │ │ mcp-agent-beta │
│ SA: alpha-sa │ │ SA: beta-sa │
│ Trust: trusted │ │ Trust: semi-trusted │
│ SPIFFE: .../role/agent │ │ SPIFFE: .../role/agent │
│ /.well-known/oauth-cli. │ │ /.well-known/oauth-cli. │
│ /.well-known/jwks.json │ │ /.well-known/jwks.json │
└─────────────┬───────────┘ └─────────────┬───────────┘
│ POST /token (CIMD client_id + private_key_jwt + DPoP proof)
▼ ▼
┌────────────────────────────────────────┐
│ Keycloak 26.6 (--features=cimd) │
│ Federated Client Authentication │
│ Realm: ZERO pre-registered MCP agents │
└────────────┬───────────────────────────┘
│ DPoP-bound access token → tool call (Authorization: DPoP <token>)
▼ ▼
ns: mcp-servers-data ns: mcp-servers-actions
┌─────────────────────────┐ ┌─────────────────────────┐
│ mcp-server-data │ │ mcp-server-actions │
│ query_database │ │ send_notification │
│ list_tables │ │ create_ticket │
│ describe_schema │ │ lookup_user │
└─────────────┬───────────┘ └─────────────┬───────────┘
└──────────────┬───────────────────┘
▼
┌────────────────────┐
│ ns: opa-system │ Trust matrix (role × tier × tool)
└────────────────────┘
ns: spire-system — SPIRE Server + Agent DaemonSet
→ attests every MCP pod via the Workload API (the app never reads a SA token)
Trust matrix
| mcp-server-data (trusted) | mcp-server-actions (semi-trusted) | |
|---|---|---|
| mcp-agent-alpha (trusted) | query_database, list_tables, describe_schema | send_notification, create_ticket, lookup_user |
| mcp-agent-beta (semi-trusted) | list_tables, describe_schema (no query_database) | lookup_user (no send_notification, no create_ticket) |
Enforced by policies/trust-matrix.rego on every tool call. Six scenarios —
four allows, two denies — are shown by demo/06-trust-matrix.sh.
What is keyless about this?
| Concern | Traditional approach | MCPIdentity |
|---|---|---|
| Per-client credential | client_secret in env or file |
none — SPIRE issues a JWT-SVID in memory |
| Pre-registered Keycloak client | one per agent | zero — CIMD self-describes via URL |
| ServiceAccount token use | mounted at /var/run/secrets/... |
automountServiceAccountToken: false |
| Private key file | mounted volume | none — an EC keypair is generated in memory |
| Token replay | possible if token leaks | DPoP-bound to the in-memory key, replay impossible |
| Adding a new client | manual realm change | deploy a pod, done |
Repository layout
mcp-identity-poc/
├── manifests/ namespaces, Keycloak (+Postgres), OPA, SPIRE, MCP agents/servers
├── policies/ trust-matrix.rego (role × tier × tool)
├── src/ shared/ (SPIFFE, JWKS, DPoP, CIMD, federated auth, OPA, MCP base),
│ mcp_agent/, mcp_server_data/, mcp_server_actions/, Dockerfile
├── scripts/ 01..06 deploy + matching delete, setup-all.sh, remove-all.sh,
│ reset-cimd-clients.sh, cluster.env
├── demo/ 01..08 step scripts, run-all.sh, DEMO_SCENARIOS.md
├── tests/ pytest suite
└── README.md
Tests
pip install -r requirements-test.txt
pytest -v
Covers the shared library (SpiffeJWKS, DPoP signer/verifier, CIMD, protected resource), the OPA trust matrix (all six scenarios), and MCP agent/server app construction — no live cluster required.
Keycloak backend
Keycloak runs against a PostgreSQL 16 backend (not the default in-memory H2)
so the realm, client policies, and dynamically registered CIMD clients survive
pod restarts during the demo. 03-delete-keycloak.sh removes the PVC so re-runs
start clean.
License
Apache 2.0 — see LICENSE.
