Central deployment
One server for a whole team: SSO, roles from group membership, per-user PCE credentials.
- The decision that shapes everything
- Two constraints to design around
- Request path
- Configuration
- Roles
- Onboarding a user (per_user mode)
- Operational checklist
- Further reading
The decision that shapes everything
MCP_PCE_MODE determines what your PCE audit log is worth. It is not a tuning knob.
per_user (default) | shared | |
|---|---|---|
| PCE credentials | Each human registers their own, AES-256-GCM encrypted at rest | One service account from PCE_* env |
| PCE-side audit shows | The actual human | The service account, for everyone |
| PCE RBAC applies | Per human | Uniformly, at service-account level |
| Onboarding | User visits /setup once | None |
Requires MCP_KEK | Yes | No |
per_user is the reason this transport exists — it makes “alex@example.com deleted that ruleset” visible in Illumio’s own logs rather than “the MCP service account did it”. Choose shared only if you accept that attribution collapses. Either way, SSO, role enforcement and the server-side audit log still record the real human.
Two constraints to design around
The server cannot bind a public interface with authentication enabled.
serve_http(host="0.0.0.0")exits with “Refusing to bind ‘0.0.0.0’ without MCP_DEV_INSECURE=1”, and that flag forces the unauthenticated path — no OAuth, no keystore, admin role for everyone, no confirmation gate.So the TLS terminator must share a network namespace with the app: nginx or Envoy on the same host, or a sidecar in the same Kubernetes pod, reaching it over
127.0.0.1. A reverse proxy on a different host cannot reach it.
It is a single-instance service. Three SQLite files hold state:
File Contents Variable keys.dbPer-user PCE credentials MCP_KEYSTORE_PATHaudit.dbWho ran which tool MCP_AUDIT_LOG_PATHjtitableConfirmation-token replay guard MCP_CONFIRM_JTI_PATHThe MCP layer is stateless, so no session affinity is needed — the constraint is purely this state. Two replicas behind a load balancer would split the credential store and, worse, split the replay guard: a token spent on replica A could be replayed on replica B. Run one instance with a persistent volume.
Request path
MCP client
→ TLS terminator (same host or pod) HTTPS ends here
→ 127.0.0.1:8080
RequestIdMiddleware X-Request-Id
JWTAuthMiddleware RS256/ES256 against JWKS
requires exp, iat, iss, aud, sub
audience must equal this resource
dispatcher groups → role → role gate
→ PCE gate → confirmation gate
→ PCE
Unauthenticated routes: /healthz, /readyz, /.well-known/oauth-protected-resource. Everything else needs a bearer token.
Configuration
# OAuth resource server — all required
MCP_OAUTH_ISSUER=https://login.microsoftonline.com/<tenant>/v2.0
MCP_OAUTH_JWKS_URL=https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys
MCP_OAUTH_AUDIENCE=api://illumio-mcp
MCP_PUBLIC_URL=https://mcp.example.com # must match what clients use
MCP_OAUTH_REQUIRED_SCOPE=illumio-mcp.use
# Secrets — from a secrets manager, not env files
MCP_KEK=<32 bytes hex> # per_user mode only
MCP_CONFIRM_HMAC_KEY=<32 bytes hex> # required whenever auth is on
MCP_PCE_MODE=per_user
MCP_ROLE_GROUPS_ADMIN=illumio-mcp-admins
MCP_ROLE_GROUPS_OPERATOR=illumio-mcp-operators
MCP_ROLE_GROUPS_READER=illumio-mcp-readers
# MCP_ROLE_DEFAULT= # leave unset: no group = no access
MCP_HTTP_HOST=127.0.0.1 # see constraint above
MCP_LOG_LEVEL=INFO # DEBUG echoes full tool arguments
MCP_KEYSTORE_PATH=/var/lib/illumio-mcp/keys.db
DOCKER_CONTAINER=true
The server fails closed on all of these: missing OAuth configuration or MCP_CONFIRM_HMAC_KEY is a startup error, not a silent degrade.
Full variable list: Configuration.
Roles
Roles come from the JWT groups (or roles) claim, highest wins — admin > operator > reader.
| Tools | Minimum role |
|---|---|
| 22 read-only queries and reports | reader |
| 20 mutating operations | operator |
| 2 credential self-service | reader |
2 confirmation-gated (provision-policy, ringfence-batch) | admin |
Leave
MCP_ROLE_DEFAULTunset so an authenticated user with no mapped group gets nothing. Setting it toreadersilently grants everyone in your directory read access to your segmentation topology.
Onboarding a user (per_user mode)
- Add the user to the appropriate IdP group.
- They open
https://mcp.example.com/setup, authenticate via SSO, and paste their own PCE API key and secret. It is stored envelope-encrypted underMCP_KEK, bound to theirsubandiss. - They point their MCP client at
https://mcp.example.com/mcp.
Before step 2, their tool calls return a clear no_credentials error. check-pce-credentials-status is the self-service diagnostic.
Operational checklist
- Back up
keys.db. Losing it means every user re-registers. LosingMCP_KEKmakes the backup unreadable — keep them in different failure domains. - The
jtitable grows unboundedly.purge_expiredexists but is never called. Schedule it or accept the growth. - Ship
audit.dbto your SIEM. It is the record of who ran what. - Keep
MCP_LOG_LEVEL=INFO. AtDEBUGthe handlers echo full tool arguments; secrets are redacted, but the remainder describes your segmentation topology. - Scope the PCE service account (shared mode) to the minimum RBAC the tools need.