无密钥Kubernetes认证服务

MCPIdentity PoC是一个为Kubernetes服务提供无密钥认证的解决方案,通过Keycloak 26.6、SPIRE认证和CIMD技术实现,适用于需要安全、动态客户端认证的场景。

  • by graphsentinel
  • Python
  • 本地部署
  • 3 星标
  • 更新于
在 GitHub 查看 graphsentinel/mcp-agents-identity

使用说明

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.sh checks for k3d, 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.