IdP
Okta / Entra. Human authentication always happens here; we only consume assertions.
Integration · primary path
This is V1's primary integration path: the agent reaches a remote / internal MCP server through your existing gateway, and AgentPAM plugs in as the external Authorization Server + PDP + credential broker. (The local laptop / stdio scenario is out of current scope — V2+ roadmap.)
Deployment topology
The IdP is yours, the MCP gateway is yours, the MCP servers / APIs are yours. We provide only the control-plane container group in the middle — running on your own Linux, air-gap capable, with no outbound callback.
+-- Your own Linux environment (air-gap capable, no outbound callback) --------+ | | | +------------+ OIDC federation +------------------------------+ | | | Your IdP |<----------------------| AgentPAM control plane | | | | Okta/Entra | (consumes assertions only) | /authorize /token AS+STS | | | +-----+------+ | PDP AuthZEN / COAZ | | | | human SSO (Tu) | Credential broker RFC 8693 | | | +-----v------+ ext_authz / AS | Audit sink | | | | Your MCP |<--------------------->| JWKS (independent HA) | | | | gateway | inject cred (Tup) +------------------------------+ | | | (RS) | | | +-----+------+ | | | inject short-lived credential | | +-----v------+ | | | Sensitive | <- remote / internal, NOT a laptop | | | API/server | | | +------------+ | +------------------------------------------------------------------------------+ ^ agent holds Ts (session token, DPoP, 5-15 min) +----+-----+ | AI Agent | managed workstation / VDI (agent is on the endpoint, | Claude | but the MCP server is not) +----------+
Okta / Entra. Human authentication always happens here; we only consume assertions.
It is the Resource Server. We plug into it; we do not replace it.
Remote or internal, not a laptop. Credentials are broker-injected; the agent never sees them.
Connect the IdP
AgentPAM does not authenticate humans (constitution C-02). Human authentication happens at your IdP; we trust its assertions over OIDC federation. We do not store human passwords and we do not do MFA.
/callback address.sub, email, groups / roles).identity_providers:
- id: corp-okta
issuer: https://corp.okta.internal # your IdP, internal
jwks_uri: https://corp.okta.internal/oauth2/v1/keys
audience: agentpam # we are the audience
claim_mapping:
subject: sub
email: email
groups: groups| IdP | Method | Notes |
|---|---|---|
| Okta | OIDC + ID-JAG / Cross-App Access | Preferred. EMA (Enterprise-Managed Auth) is stable — moves consent and policy to the IdP, no consent screen |
| Microsoft Entra | OIDC + OBO (On-Behalf-Of) | We consume Entra Agent ID facet claims directly (xms_act_fct / xms_sub_fct) |
| Google Workspace | OIDC | Standard OIDC federation |
| Keycloak / self-hosted | OIDC + RFC 8693 | Common with air-gapped customers; Keycloak supports token exchange natively |
| SAML-only legacy IdP | SAML assertion → we convert to OIDC | Fallback path |
Connect the gateway
After the DR-001 correction: there is no “universal” path (we already retracted that claim). Pick one by your gateway type. The third is a fallback.
| Gateway | Path | What you change |
|---|---|---|
| agentgateway / ContextForge / Kong EE / AWS AgentCore | Path B: we are the external AS | One config block pointing at us |
| Envoy/Istio / Kong OSS / Pomerium / Obot / anything with a PDP hook | Path A: we are the external PDP | ext_authz / OPA pointing at us |
| Docker MCP Gateway / no hooks | Path C: as an upstream MCP proxy (fallback) | The gateway treats us as an upstream server |
The three paths enforce at different moments, which decides the granularity they can reach. Parameter-level policy (e.g. refund.amount ≤ 500) — our headline differentiator — is enforced by nobody on pure Path B: a generic gateway only validates the token's sig + audience + scope, does not interpret our custom RAR predicate, and does not pass tool arguments to us.
| Capability | Path A (per-call PDP) | Path B (token-carried) | Path C (proxy) |
|---|---|---|---|
| Enforcement moment | every tools/call | at Ts-mint (once) | every tools/call |
| Who is the PEP | us (sees tool+args) | the gateway (token only) | us |
| Server-level | ✅ | ✅ | ✅ |
| Tool-level | ✅ | ✅ (within token scope) | ✅ |
Parameter-level amount ≤ 500 | ✅ | ❌ nobody enforces | ✅ |
| Per-call revocation | ✅ | ❌ (until Ts expires) | ✅ |
Rule (no exceptions): parameter-level ⇒ requires a per-call hook (Path A / ext_authz, or Path B + a per-call callback). Pure Path B is server / tool coarse-grained only. We do not claim "config-only gets you parameter-level" — Path B is "config-only integration, parameter-level needs a per-call hook."
# 1) the gateway publishes protected resource metadata (RFC 9728) # GET /.well-known/oauth-protected-resource -> mcpAuthentication: issuer: https://agentpam.customer.internal # points at us audiences: [ "https://mcp.customer.internal" ] # * MANDATORY in agentgateway jwksUri: https://agentpam.customer.internal/.well-known/jwks.json # 2) upstream credential injection: the gateway exchanges for Tup, the agent never sees it backendAuth: oauthTokenExchange: path: /token # * local YAML key is 'path', NOT tokenEndpointPath grantType: urn:ietf:params:oauth:grant-type:token-exchange
Keys corrected against the G-04 source review (resource.proto): the local YAML key is path, not tokenEndpointPath (the latter is the xDS/proto field name and will fail to load in a config file). audiences is structurally mandatory in agentgateway — omit it and the config fails to load, which is the safe default. ⚠️ Note this backendAuth exchange / injection path is config-level understanding, not runtime-verified against a live backend (G-22).
# Envoy ext_authz -> our PDP http_filters: - name: envoy.filters.http.ext_authz typed_config: grpc_service: envoy_grpc: { cluster_name: agentpam_pdp } # points at our PDP with_request_body: # * pass body so we see MCP method/tool max_request_bytes: 8192 allow_partial_message: true --- our PDP interface (AuthZEN / COAZ-MCP compatible) --- POST /pdp/evaluate { "subject": { "id": "u_dana", "type": "human" }, # the human "context": { "agent": "agent_support_7f3a", # agent, 2nd dimension (COAZ) "task": "task_88213" }, "action": { "name": "tools/call", "tool": "payments.refund" }, "resource": { "server": "payments", "args": { "amount": 120 } } } -> 200 { "decision": true, "obligations": ["audit:high"], "headers": { "Authorization": "Bearer <Tup short-lived>" } } # injected on allow
The key is letting the PDP mutate headers on allow (the credential-injection primitive), and passing the request body so we can see the MCP method / tool. Our PDP interface is AuthZEN / COAZ-MCP compatible: subject is the human, context.agent is the agent's second dimension.
When the gateway has no hooks at all (e.g. Docker MCP Gateway): the gateway configures us as an upstream MCP server, and we act as the policy-enforcement proxy in front of the real server. Cost: an extra hop, and we only see traffic routed to us. Benefit: zero requirements on your gateway. For Docker MCP Gateway this is the only viable path — its before:http: hook cannot mutate headers, so Path A does not work either.
mcpAuthentication block publishes us as the AS and issues an RFC 9728 challenge). The backendAuth.oauthTokenExchange path that actually mints and injects Tup is not yet runtime-verified against a live backend (G-22) — it is config-level. The other three Path B gateways and every Path A snippet are likewise documentation-grade, not runtime-verified. We will not write these up as “verified”; the full matrix is on the architecture page.
End to end
[1] Human SSO -> your IdP -> Tu (OIDC, ~1h) [2] agent -> AgentPAM /token (RFC 8693) subject_token=Tu, actor_token=agent instance assertion authorization_details=[{ type: agent_task, task_id, purpose, sensitivity }] AgentPAM verifies Tu (against your IdP JWKS) -> issues Ts Ts: sub=human, act={sub: agent_instance}, cnf.jkt (DPoP), 5-15 min [3] agent calls your MCP gateway with Ts gateway verifies Ts (against OUR JWKS) + audience [4] pure path B: gateway enforces token (server / tool scope, COARSE) parameter-level requires a per-call hook (path A / ext_authz) -> per tools/call PDP decision with args [5] allow -> gateway exchanges at AgentPAM /token for Tup (upstream cred, 60-300s) ** the agent never touches Tup ** [6] gateway injects Tup and calls the upstream API / MCP server [7] AgentPAM writes the audit event (human/agent/task/tool/resource/decision/traceparent) [8] task ends or CAEP revocation -> Ts invalidated immediately (no wait for TTL)
The whole chain is delegation, never impersonation. sub is always the human; act carries the agent instance. After step 5, the agent never touches Tup (the upstream credential).
One-day POC
| # | Action | Who | Verify |
|---|---|---|---|
| 1 | docker load the AgentPAM container group image (offline media) | Your ops | docker images |
| 2 | Fill agentpam.yaml: IdP issuer / jwks, audience | Your ops | Startup logs clean |
| 3 | Register AgentPAM as an RP in the IdP | Your IdP admin | Discovery reachable |
| 4 | Point the gateway at us (Path B metadata + token endpoint, or Path A ext_authz) | Your platform team | curl .well-known/oauth-protected-resource returns us |
| 5 | Register the first AgentBlueprint + AgentIdentity (with a human sponsor) | Your platform team | Shows in the Portal |
| 6 | Define policy (server / tool / parameter, three levels) | Your security team | Policy in force |
| 7 | Run one agent call | Your developer | See acceptance below |
| # | Check | Expected |
|---|---|---|
| 1 | Agent calls a permitted tool | Succeeds; the upstream log shows a short-lived credential |
| 2 | Agent calls a tool the policy denies | The request never reaches upstream; the denial is in the audit log |
| 3 | Parameter over the limit (refund $2500 > $500) | Parameter-level denial |
| 4 | Dump the agent session and search for the upstream credential | Tup not found (only Ts) |
| 5 | Audit event | Carries human + agent + task, correlatable via traceparent |
| 6 | Mark the task complete, then call again | Immediately denied (even though Ts has not expired) |
| 7 | Disconnect AgentPAM, then restart the gateway (HA test) | ⚠️ See “Two honest disclosures” below — this is a coupling we must tell you about |
Bridge, not bucket
The previous section admitted we are a high-value target. This section is its architectural answer: a broker that stores every customer's credentials has merely concentrated the risk (Salesloft Drift fell exactly this way). We do not build that bucket.
No long-lived credential that can directly access a customer's upstream API exists in cleartext anywhere in AgentPAM's persistent storage.
| Key material | What it is | Can it be a “bridge”? | How it is handled |
|---|---|---|---|
| ① Upstream API credentials | The customer's GitHub / DB / cloud access credential (the source of Tup) | ✅ Yes | Not stored. Fetched from the customer vault / cloud IAM per request, injected, then discarded |
| ② Customer IdP token (Tu) | The human SSO token | ✅ Yes | Not stored. Validated in one token exchange, never persisted |
| ③ Our signing key | The key that signs Ts / JWKS | ❌ Must exist somewhere | In an HSM / KMS — generated in hardware, never exportable, sign-only |
| ④ Secret zero | The bootstrap credential we use to reach the customer vault | ⚠️ The hardest | Prefer no long-lived secret: projected SA token / workload identity; bare-metal falls back to rotatable mTLS |
| Mode | Mechanism | What we store |
|---|---|---|
| A. Vault passthrough | Customer runs HashiCorp Vault / Infisical. We call its dynamic secret engine per request for a short-lived credential, inject, discard | Nothing — even the reference is a path into the customer's vault |
| B. Cloud STS exchange | Customer runs cloud IAM. We use workload identity to exchange for a short-lived STS credential (e.g. AWS AssumeRole 15min), inject, discard | Only a role ARN reference |
| C. Upstream OAuth client | Upstream is SaaS (GitHub / Slack). The refresh token lives in the customer's vault, not ours; we fetch it per request to mint an access token, inject, discard | Only a vault reference |
| Bucket (Salesloft model) | Bridge (AgentPAM) | |
|---|---|---|
| Directly obtained | Every customer's upstream credentials, in cleartext | A pile of vault path / ARN references + a non-exportable HSM handle |
| Redeeming still requires | Nothing — use them directly | Passing through secret zero → the customer vault's own policy and audit |
| Each redemption | Leaves no trace | Logged, rate-limited, revocable by the customer's vault |
| Blast radius | Every customer (multi-tenant SaaS) | One customer (private, C-13) |
| What the customer can do | Depend on the SaaS vendor | Rotate vault credentials, revoke secret zero, stop the container — the customer holds the controls |
A customer runs these six checks. Checks 1 and 6 are the core — they empirically prove BNB-0: there are no eggs in our storage, and you hold the power over the credentials.
| # | Check | Expected |
|---|---|---|
| 1 | Dump our persistent storage (DB / disk) and grep for upstream credential signatures (ghp_ / sk- / AKIA / DB passwords) | Zero hits — only path / ARN references |
| 2 | Inspect the HSM / KMS config; confirm the signing key is non-exportable | CKA_EXTRACTABLE=false |
| 3 | Cut our connection to your vault, then watch an agent call | Fails — proving credentials are fetched live, not stored by us |
| 4 | Audit our outbound network (should be zero when air-gapped) | No “phone home” connections (C-15) |
| 5 | Verify the image signature + SBOM | Signature valid, dependencies auditable |
| 6 | Revoke one credential in your vault, then immediately retry an agent call | Fails immediately — proving we neither cache nor retain |
Ts / JWKS need to be signed with our private key, and that key must exist somewhere — we use an HSM to make it “stored but unextractable”, not “unstored.” Residual risk: a signing oracle — after RCE an attacker cannot take the key, but might induce the HSM to legitimately sign a malicious token. The HSM cannot stop that; policy-side defenses do (issued-content templates + rate limiting + anomaly detection).
K8s (projected SA token) and cloud (workload identity federation) achieve no long-lived secret; but bare-metal air-gap's best case is a rotatable mTLS client certificate, not zero secret. Even if it leaks, the attacker gains “access to your vault,” and must still pass through your vault's own policy, audit, and revocation.
Even on the bridge, a credential appears in memory for ≤ a second between fetch and inject. Mitigations: zeroize immediately after injection, keep it out of logs and long-lived objects, and do not cache across requests by default (trading a little latency for minimal exposure). We do not pretend this window is zero.
Two honest disclosures
agentgateway fetches JWKS eagerly at startup and fails closed — if AgentPAM is unreachable at that moment, the gateway may not come up. Mitigation: deploy the JWKS endpoint in independent HA, isolated from the policy engine's failure domain, and support customer-side static JWKS hosting. This must be in the SLA and deployment docs; we do not hide it.
audiences silently disables the checkagentgateway structurally requires audiences (omit it and the config fails to load — safe). But with Envoy, omitting audiences silently disables audience validation — tokens can be replayed across audiences. A deployment check must actively probe this rather than assume the default is safe.
Mitigation: see “A bridge, not a bucket” above — our persistent storage holds no cleartext credential that can directly reach an upstream API (invariant BNB-0). Plus root keys in your KMS / HSM, single-tenant isolation by construction, and hash-chained tamper-evident audit. Bridge + private = double containment: the blast radius is bounded by your own failure domain, and a breach yields no offline-usable credential store — only a logged, revocable, single-tenant live oracle (honest limits in the “three places we cannot be a perfect bridge” below).