Skip to main content

Agent Identity

Self-Hosted Kubernetes

Identity patterns for AI agents in Kubernetes: a cryptographic workload identity unique to each agent class, a compound dual identity that ties the agent to the human who launched it, and identity for the raw HTTP(S) calls an agent process makes outside the MCP protocol.

Using Microsoft Entra for autonomous agents?

Follow Set up Microsoft Entra agent identity to create native agent identities, configure separate certificate credentials, assign read/write roles, and validate MCP access through AIControls. That flow uses Entra-issued access tokens, not Kubernetes ServiceAccount tokens or human console SSO.

Why identity matters​

Without verified identity, every agent looks identical in the audit log and every policy applies to all agents equally. With identity, AIControls can:

  • Attribute every tool call and LLM request to a specific agent class in the audit log
  • Enforce per-agent policies: tool allowlists, budget limits, HITL approval rules
  • Detect behavioral anomalies per workload: deny-rate spikes, tool-call frequency deviations
  • Answer compliance questions like "Which agent made this production change and who authorized it?"

Scenario 1: Unique agent identity via ServiceAccount + OIDC​

Each agent Deployment gets a dedicated Kubernetes ServiceAccount. AIControls validates the projected SA token against the cluster's OIDC issuer and resolves namespace/serviceaccount as the agent's identity in every audit event and CEL policy.

How tokens are verified

AIControls verifies ServiceAccount tokens against the cluster's OIDC issuer; it doesn't call the Kubernetes TokenReview API. For a verified identity, set identity.validationMode: oidc with clusterOIDCIssuer as shown below.

Step 1 — Create a dedicated ServiceAccount per agent class​

# agents/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: agents
labels:
aicontrols.nirmata.io/govern-agents: "true"
---
# One ServiceAccount per logical agent type
apiVersion: v1
kind: ServiceAccount
metadata:
name: claude-code-agent
namespace: agents
labels:
aicontrols.nirmata.io/governed: "true"
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-agent
namespace: agents
labels:
aicontrols.nirmata.io/governed: "true"

Step 2 — Find your cluster's OIDC issuer​

aws eks describe-cluster \
--name YOUR_CLUSTER_NAME \
--query 'cluster.identity.oidc.issuer' \
--output text
# → https://oidc.eks.us-east-1.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Managed clusters with a public issuer (Amazon EKS, Google GKE, and Azure AKS created with --enable-oidc-issuer) serve their keys from a public URL. kind, kubeadm, and DigitalOcean (DOKS) clusters use the in-cluster API server as the issuer instead; see In-cluster API server issuer.

In-cluster API server issuer​

When AIControls runs in the same cluster and clusterOIDCIssuer is that cluster's API server, AIControls fetches the issuer's discovery document and signing keys (JWKS) as an in-cluster client:

  • It verifies the API server's certificate against the cluster CA that Kubernetes mounts into the pod at /var/run/secrets/kubernetes.io/serviceaccount/ca.crt. TLS verification stays on; the public trust store isn't used for this issuer.
  • It authenticates with its own ServiceAccount token. The default system:service-account-issuer-discovery ClusterRoleBinding lets every ServiceAccount read the keys, so you don't add any RBAC.
  • It connects only to the address in KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT, and reads the keys from the API server's /openid/v1/jwks path.

AIControls treats the issuer as the in-cluster API server only when all of the following are true:

  • AIControls runs in a pod, so KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT are set.
  • The issuer is an https URL with no path, and its host is kubernetes.default.svc, kubernetes.default.svc.<cluster domain> (for example, kubernetes.default.svc.cluster.local), or the KUBERNETES_SERVICE_HOST address.
  • The issuer's port, 443 when omitted, matches KUBERNETES_SERVICE_PORT.
  • The issuer is spelled exactly as Kubernetes publishes it: lowercase, with no trailing slash and no explicit :443. Copy the issuer value from kubectl get --raw /.well-known/openid-configuration.

OIDC issuer checks are exact string comparisons, so a spelling such as https://kubernetes.default.svc.cluster.local/ or https://kubernetes.default.svc.cluster.local:443 can never verify a token. Save & verify rejects it and names the value to use; a value set in values.yaml shows the same message in the startup check and in the proxy log. For any issuer, Save & verify and the startup check also fail when the discovery document's issuer differs from the configured value, and the message shows both values.

The issuer host resolves to the kubernetes Service's private ClusterIP, which the outbound address check normally blocks (see Private identity endpoints). AIControls exempts only this issuer from that check, because the connection can reach only the cluster's own API server and must present a certificate signed by the cluster CA. You don't add the ClusterIP to proxy.ssrfAllowlist. Any other private issuer, including another Service in the cluster, still needs an allowlist entry.

Keep the ServiceAccount token mounted in the AIControls pods (the chart default). If you set automountServiceAccountToken: false, Save & verify and the startup check report that AIControls can't load the cluster CA bundle. If the API server answers the key request with HTTP 401 or 403, check that the system:service-account-issuer-discovery ClusterRoleBinding still includes the group system:serviceaccounts.

If AIControls runs outside the cluster, it can't reach the in-cluster issuer. Use the cluster's public issuer, or add an oidcProviders entry with a reachable jwksURL for the same issuer.

Step 3 — Configure AIControls to validate SA tokens​

# values.yaml (Helm)
identity:
validationMode: oidc
clusterOIDCIssuer: "https://oidc.eks.us-east-1.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
clusterOIDCAudience: "aicontrols" # must match audience in projected SA tokens; required when clusterOIDCIssuer is set
jwksCacheTTLSeconds: 300
agentCacheTTLSeconds: 60

Step 4 — Project a short-lived token into agent pods​

The agent pod requests a projected ServiceAccount token bound to the aicontrols audience with a 1-hour TTL. Kubernetes rotates it automatically before expiry.

# agents/claude-code-agent-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: claude-code-agent
namespace: agents
spec:
template:
spec:
serviceAccountName: claude-code-agent
volumes:
- name: aicontrols-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600 # 1-hour TTL; auto-rotated
audience: aicontrols
containers:
- name: agent
image: ghcr.io/your-org/claude-code-agent:latest
env:
- name: MCP_SERVER_URL
value: "http://aicontrols.aicontrols.svc.cluster.local:8080"
- name: AICONTROLS_AGENT_TOKEN_PATH
value: "/var/run/aicontrols/token"
volumeMounts:
- name: aicontrols-token
mountPath: /var/run/aicontrols
readOnly: true

Step 5 — Enforce unique SAs with Kyverno​

This policy blocks any agent pod that uses the default ServiceAccount, ensuring every agent has a traceable identity.

# kyverno/require-dedicated-agent-sa.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-dedicated-agent-sa
spec:
validationFailureAction: Enforce
background: false
rules:
- name: deny-default-sa
match:
any:
- resources:
kinds: [Pod]
namespaceSelector:
matchLabels:
aicontrols.nirmata.io/govern-agents: "true"
validate:
message: "Agent pods must use a dedicated ServiceAccount — not 'default'."
deny:
conditions:
any:
- key: "{{request.object.spec.serviceAccountName}}"
operator: AnyIn
value: ["default", ""]

Apply it:

kubectl apply -f kyverno/require-dedicated-agent-sa.yaml

Step 6 — Write per-agent CEL policies​

With identity resolved, you can write policies scoped to a specific agent:

# policy: claude-code-agent-tool-allowlist.yaml
apiVersion: nirmata.io/v1
kind: AIPolicy
metadata:
name: claude-code-read-only
spec:
matchConditions:
- expression: |
object.mcp.tool != "" &&
object.agent.serviceAccount == "claude-code-agent"
validations:
- expression: |
object.mcp.tool in [
"read_file", "list_directory", "search_files",
"get_file_contents", "search_code"
]
message: "claude-code-agent is restricted to read-only file tools."
action: Deny
---
# Separate budget per agent class
apiVersion: nirmata.io/v1
kind: AIPolicy
metadata:
name: ci-agent-daily-budget
spec:
matchConditions:
- expression: |
object.llm.model != "" &&
object.agent.serviceAccount == "ci-agent"
validations:
- expression: object.session.percentUsed < 100.0
message: "CI agent daily token budget exhausted."
action: Deny
Identity in the Admin UI

Every agent SA automatically gets a row in Admin UI → Inventory → Identities showing its deny rate, session count, risk score, and behavioral baseline. No extra configuration needed.

Also issuing this agent a VirtualKey for LLM calls?

If the agent authenticates MCP tool calls with its ServiceAccount token (as above) but authenticates LLM calls with a VirtualKey, set the VirtualKey's optional namespace field to this ServiceAccount's namespace. Otherwise the two call types are tracked as separate rows on the Identities page — one for the ServiceAccount, one for the VirtualKey — instead of converging into a single identity. Alternatively, the agent can present the VirtualKey itself as the bearer token for MCP tool calls instead of a ServiceAccount token — the proxy resolves it to the same identity, so both LLM and MCP traffic converge on one Identities row without needing a ServiceAccount at all.


Scenario 2: Dual identity — agent + delegating user​

When a human user launches an agent that acts on their behalf, you need to know both who ran the agent and for whom it ran. Dual identity captures both in every audit event and makes both available in CEL policies.

The problem with agent-only identity​

Without dual identity:

QuestionAgent-only answerDual identity answer
Who touched production?ci-agentsarah@company.com via ci-agent
Can developers run write agents?Cannot expressobject.user.groups check
Who to notify for HITL?Generic admin channelThe developer who triggered the action
Compliance: "Show all changes by contractor X"Not answerableFilterable by user email

How it works​

AIControls reads two tokens from each request:

HeaderCarriesResolved as
Authorization: Bearer <token>Agent's projected SA tokenobject.agent.*
X-User-Token: <token>User's OIDC token or developer PATobject.user.*

Both identities appear in every audit event. object.user.isPresent is true only when the user token was successfully validated.

Step 1 — Configure user OIDC providers​

Both clusterOIDCIssuer/clusterOIDCAudience (used to validate agent ServiceAccount tokens, Scenario 1 above) and oidcProviders (used to validate the user's token for dual identity) are app settings — see Configuration Model — so values.yaml only seeds them on a fresh install. Manage cluster OIDC going forward from Settings → Identity & Access (or the onboarding flow), and user OIDC providers from Settings → Identity & Access → SSO / OIDC in the Admin UI.

# values.yaml — seeds cluster OIDC and oidcProviders once on first boot only
identity:
validationMode: oidc
clusterOIDCIssuer: "https://oidc.eks.us-east-1.amazonaws.com/id/AAAAAAA"
oidcProviders:
- name: okta
issuer: "https://company.okta.com/oauth2/default"
audience: "aicontrols"
groupsClaim: groups # OIDC claim containing group memberships
- name: google-workspace
issuer: "https://accounts.google.com"
audience: "CLIENT_ID.apps.googleusercontent.com"
hostedDomain: company.com
Already have OIDC providers configured?

If you're adding or changing a provider on a cluster that's been running for a while, edit it in Settings → Identity & Access → SSO / OIDC instead of values.yaml — the database already has a value, so values.yaml is no longer consulted for this field.

Step 2 — Pass the user token from the agent​

Option A — Developer PAT (easiest for Claude Code users)

Developers create a Personal Access Token in Settings → Personal Access Tokens. The PAT carries both the user's identity and is scoped to their agent session. Set it as the agent's bearer token — no extra header needed.

# In Claude Code's MCP config:
# AICONTROLS_AGENT_TOKEN contains the developer's PAT
export MCP_SERVER_URL=http://aicontrols.aicontrols.svc.cluster.local:8080

Option B — Explicit X-User-Token header

For orchestrator agents that launch sub-agents on a user's behalf, forward the user's OIDC token:

POST /mcp HTTP/1.1
Authorization: Bearer eyJ... (agent SA token)
X-User-Token: eyJ... (user OIDC token from Okta/Google/etc.)

The agent obtains the user's token from the session that triggered it (e.g., from an OAuth flow, from Okta device flow, or from an enterprise SSO cookie).

Step 3 — Write dual-identity policies​

# Require platform-engineering group for production write tools
apiVersion: nirmata.io/v1
kind: AIPolicy
metadata:
name: production-writes-require-platform-eng
spec:
matchConditions:
- expression: |
object.mcp.tool in [
"apply_manifest", "delete_resource", "patch_resource",
"scale", "run_command", "write_file"
]
validations:
- expression: |
object.user.isPresent &&
object.user.groups.exists(g, g == "platform-engineering")
message: >
Production write operations require a platform-engineering group member.
Developers outside this group and unauthenticated agents are blocked.
action: Deny
# HITL for any developer-triggered destructive operation
# routes the approval notification to the specific developer
apiVersion: nirmata.io/v1
kind: AIPolicy
metadata:
name: hitl-developer-destructive-ops
spec:
matchConditions:
- expression: |
object.mcp.tool in ["apply_manifest", "delete_resource", "patch_resource"] &&
object.user.isPresent &&
!object.user.email.endsWith("@ci.internal")
action: Approve
approvalConfig:
timeoutMinutes: 30
timeoutAction: deny
channels: ["slack", "email"]
message: >
{{object.user.email}} is requesting {{object.mcp.tool}} via
{{object.agent.serviceAccount}}. Approve or deny within 30 minutes.
# Deny agents acting without user context in production
apiVersion: nirmata.io/v1
kind: AIPolicy
metadata:
name: require-user-context-in-production
spec:
matchConditions:
- expression: |
object.mcp.tool != "" &&
object.agent.namespace == "production-agents"
validations:
- expression: object.user.isPresent
message: "Agents in production-agents namespace must carry a validated user context."
action: Deny

CEL identity fields reference​

FieldTypeDescriptionExample
object.agent.serviceAccountstringK8s ServiceAccount nameclaude-code-agent
object.agent.namespacestringPod namespaceagents
object.agent.denyRate24hfloatFraction of calls denied in last 24h0.12
object.agent.baselineZScorefloatDeviation from behavioral baseline2.1
object.user.isPresentboolUser token was validatedtrue
object.user.emailstringUser email from OIDC claimsarah@company.com
object.user.groupslistGroup memberships from OIDC claim["platform-engineering"]
object.session.toolCallCountintTool calls so far this session47
object.session.deniedInSessionintDenied calls this session2
object.session.percentUsedfloatBudget consumed (0–100)62.4
object.mcp.toolstringTool name; empty for LLM callsread_file
object.llm.modelstringModel ID; empty for MCP callsclaude-sonnet-4-6
Complete CEL reference

See Policy Context Reference for all available fields, their types, and example values.


Scenario 3: Agent identity for network egress calls​

Network egress filtering governs the raw HTTP(S) calls an agent process makes directly — a curl, a kubectl command, a webhook — outside the MCP protocol entirely. Those calls have no MCP envelope to carry a custom header in, so the Authorization/X-Aicontrols-Agent-Token mechanism from Scenarios 1 and 2 doesn't apply: a kubectl or gh process can't be told to set an arbitrary header, and often there's no agent-side code to modify at all.

The problem with header-based identity for egress​

An egress call is just whatever HTTP(S) request the agent process happens to make — there's no client library in the loop that AIControls controls or that the agent developer necessarily wrote. Requiring a custom header would mean patching every tool an agent might shell out to.

How it works​

The egress proxy is reached as an HTTP(S) proxy (HTTP_PROXY/HTTPS_PROXY), so it identifies callers using the credential every proxy-aware HTTP client already knows how to send: Proxy-Authorization (RFC 7235), populated automatically when the proxy URL includes a userinfo component. Embed the agent's token there instead of in a custom header:

env:
- name: HTTP_PROXY
value: "http://$(AGENT_TOKEN)@aicontrols.aicontrols.svc.cluster.local:8443"
- name: HTTPS_PROXY
value: "http://$(AGENT_TOKEN)@aicontrols.aicontrols.svc.cluster.local:8443"

From that point on, every request the agent process sends through the proxy carries the credential without any code change: once per TCP connection via the CONNECT request for HTTPS, and on each individual request for plain HTTP. curl, kubectl (a standard Go net/http client), gh, and effectively any HTTP client that honors the proxy environment variables all pick this up for free.

AIControls resolves the Proxy-Authorization credential exactly the way it resolves X-Aicontrols-Agent-Token on the MCP/LLM planes — the same VirtualKey/PAT/ServiceAccount-token lookup — and populates the same object.agent.* CEL fields (serviceAccount, namespace, podName, podUID, nodeName, and so on), so the per-agent policies from Scenario 1 apply unchanged to egress traffic. If a request carries both an explicit X-Aicontrols-Agent-Token header and a Proxy-Authorization credential, the header takes precedence.

Step — Write a policy scoped by agent and destination​

# Allow the CI agent to reach GitHub and npm, deny everything else,
# without affecting any other agent's egress
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: ci-agent-egress-allowlist
spec:
matchConditions:
- expression: |
object.http.host != "" &&
object.agent.serviceAccount == "ci-agent"
validations:
- expression: |
object.http.host in ["api.github.com", "registry.npmjs.org"]
message: "ci-agent egress is restricted to GitHub and npm."
validationActions: [Deny]

See Enable Network Egress Filtering for the full setup steps, and Policy Context — object.http.* for every egress-call field.


Requiring verified identity​

Both scenarios above produce a verified identity: the ServiceAccount JWT is checked against the cluster's OIDC issuer, and a VirtualKey is resolved via hash lookup. Identities built any other way — anonymous access, or unsigned claims parsed under validationMode: passthrough — are unverified.

validationMode: passthrough does not require the caller's bearer token to be a JWT. A non-JWT opaque credential — a raw API key, or a cloud provider's own OAuth2 access token (for example Google's ya29..., which a standard Vertex AI client sends by default) — is accepted the same way a JWT-shaped token is: with an unverified synthetic identity, never a hard rejection based on token shape. Passthrough mode's built-in validator never checks a JWT's signature either, so gating on shape alone would not add real security by itself.

Google OAuth2 access tokens are a special case, because they are the credential every native Google SDK client sends by default. See Attributing Google access tokens to a real user below — those are resolved to the caller's actual email rather than left as a synthetic identity, though they remain unverified for the purposes of requireVerifiedIdentity.

validationMode: oidc performs that cryptographic verification automatically, in Go, before a request is admitted. passthrough/cel mode does not verify anything itself, but it preserves the caller's raw bearer token as object.agent.rawToken (and object.user.rawToken for a delegated user) so a policy can verify it explicitly:

variables:
- name: decoded
expression: >
jwt.Decode(object.agent.rawToken, jwks.Fetch("https://your-idp.example.com/.well-known/jwks.json"))
validations:
- expression: "variables.decoded.valid"
message: "Agent bearer token is invalid or expired"

In other words, oidc gets you verification automatically; passthrough/cel lets you write it as a policy — including for opaque, non-JWT tokens where jwt.Decode would simply fail closed via the same validations check.

By default, unverified identities are allowed through like any other caller. Set identity.requireVerifiedIdentity: true to hard-block them instead: every request from an unverified identity is denied and recorded in the audit log with Policy: unverified-identity.

# values.yaml
identity:
validationMode: oidc
clusterOIDCIssuer: "https://oidc.eks.us-east-1.amazonaws.com/id/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
clusterOIDCAudience: "aicontrols"
requireVerifiedIdentity: true

This is a startup-time setting — it takes effect on the next pod restart (helm upgrade or a rollout restart), not immediately. There is no interactive toggle for it in the admin UI; About shows a read-only "Verified Identity Required" status reflecting the current config value.

requireVerifiedIdentity is validated against the rest of your identity config at startup:

Combined withResult
validationMode: noneStartup error. none mode never cryptographically verifies anyone, so every request would be blocked — the proxy refuses to start.
allowAnonymous: trueStartup error. allowAnonymous substitutes an unverified synthetic identity for callers with no Authorization header, which requireVerifiedIdentity would then reject — the two settings are contradictory.
validationMode: passthrough (alias cel)Startup warning, not an error. Passthrough never checks JWT signatures, so almost all JWT-based callers will be blocked; only VirtualKey, PAT, and session-token callers verify successfully in this mode.

Before enabling the hard block, enable the audit-mode builtin policy require-agent-identity from Policies → Library to see how many current requests would be affected — it evaluates the same object.identity.verified condition but only flags, never denies.


Trusting AI Gateway routing headers​

Some deployments route both MCP tool-call traffic and LLM traffic through the same AIControls authorization check, fronted by an Envoy-based AI gateway (for example Envoy AI Gateway, sometimes called "Agent Router") that inspects request bodies and adds routing headers before forwarding to AIControls. That gateway can set x-ai-eg-model, x-ai-eg-backend-schema, and x-ai-eg-backend-name on any request it has identified as a model/LLM call.

By default, AIControls ignores these headers and evaluates every request through this authorization path as an ordinary MCP tool call. Set identity.trustAIGatewayHeaders: true to have AIControls trust them instead — once trusted, a request carrying x-ai-eg-model is classified as LLM traffic (populating object.llm.* and leaving object.mcp.* empty) rather than an MCP tool call.

# values.yaml
identity:
trustAIGatewayHeaders: true
Only enable this if your gateway guarantees the headers can't be forged

These are ordinary HTTP headers on the incoming request. AIControls has no way to tell the difference between a value your gateway's own routing logic set and one an agent simply included in its own request, and it treats any non-empty x-ai-eg-model as proof the request is LLM traffic.

That means the required guarantee is not "every route has these headers set or overwritten" — overwriting an MCP route's header with any non-empty value, even a placeholder, still misclassifies it as LLM traffic. Your gateway's trusted routing logic must instead, on every route, before AIControls ever sees the request:

  1. Strip any caller-supplied value for x-ai-eg-model, x-ai-eg-backend-schema, and x-ai-eg-backend-name unconditionally.
  2. Emit x-ai-eg-model only on a request it has positively classified as LLM traffic.

An ordinary MCP tool-call route must reach AIControls with this header entirely absent, not present-but-overwritten. If any route — for example, one your gateway's model-routing logic never touches — can let a caller-supplied x-ai-eg-model through unstripped, a caller with a valid agent token could set it on an ordinary MCP request itself. AIControls would then treat the call as LLM traffic: object.mcp.* fields would read empty, and any MCP-scoped policy that should have applied to that call would be silently skipped.

Leave this at its default (false) unless you have verified this guarantee holds for your specific gateway configuration.

SettingDefaultPurpose
identity.trustAIGatewayHeadersfalseTrust x-ai-eg-model/x-ai-eg-backend-schema/x-ai-eg-backend-name as an authoritative signal that a request is LLM traffic, rather than an MCP tool call.

Attributing Google access tokens to a real user​

Clients that authenticate with Google Application Default Credentials — the Gemini CLI, gcloud, @langchain/google-vertexai, and most native Vertex AI SDKs — send an opaque ya29... OAuth2 access token. Unlike an ID token it carries no readable claims, so there is nothing in the token itself to attribute the request to.

Under validationMode: passthrough, AIControls introspects these tokens against Google's tokeninfo endpoint and attributes the request to the email it returns. Without this, each such caller would appear as a synthetic opaque:<hash> identity derived from the token — and because Google rotates access tokens roughly hourly, one person's traffic would fragment into a new identity on every refresh: spend split across multiple Cost Attribution rows, an active-user count that climbs without anyone new appearing, per-user budgets that never recognise a returning caller, and audit rows naming a hash rather than a person.

This is enabled by default and requires no configuration. It applies only to tokens carrying Google's ya29. prefix — no other credential (a provider API key, an aic_ PAT, another vendor's opaque token) is ever sent to Google.

Egress implications​

This makes an outbound HTTPS call to oauth2.googleapis.com from the proxy pod, so restricted-egress clusters need to account for it:

  • The call fires only for ya29.-prefixed tokens. A deployment that never receives one never makes the call.
  • Results are cached per token (capped by the token's own remaining lifetime), and failures are cached briefly too, so a busy client costs roughly one call per token rotation rather than one per request.
  • It is fail-open: an unreachable endpoint, a timeout, or any error response falls back to the previous opaque:<hash> identity. Resolution failing never turns into a rejected request.

If your environment requires a guarantee that the identity path makes no outbound call at all, disable it:

# values.yaml
identity:
googleTokenInfo:
disabled: true

Settings​

SettingDefaultPurpose
identity.googleTokenInfo.disabledfalseTurn off online resolution entirely. Callers fall back to the opaque:<hash> identity.
identity.googleTokenInfo.endpointGoogle's public endpointRoute introspection through an egress proxy or private mirror.
identity.googleTokenInfo.timeoutSeconds2Bounds a single introspection call. This sits on the request path for the first request of each new token, so keep it well below your LLM timeout.
identity.googleTokenInfo.cacheTTLSeconds300Caps how long a resolved identity is reused. The effective TTL is the lower of this and the token's own remaining lifetime.
identity.googleTokenInfo.negativeCacheTTLSeconds60How long an unresolvable token is remembered as such, so a token Google will never resolve does not re-trigger a call on every request.

Identity and verification​

A resolved caller is attributed to the verified email Google returns. If Google reports the address as unverified, AIControls falls back to a google-sub:<id> identity instead — still stable across token rotation, but never asserting an email address Google itself declines to vouch for.

Resolution does not mark the identity as verified. tokeninfo confirms the token is live and names its owner, but that is not the cryptographic signature verification requireVerifiedIdentity is asking about — so these callers are still subject to the unverified-identity rules described above.


Private identity endpoints​

AIControls checks the resolved destination addresses when it probes a cluster OIDC issuer, discovers MCP or OAuth metadata, fetches a dynamically registered client's JWKS, or exchanges tokens with a Cross-App Access endpoint. Private, loopback, link-local, and other internal destinations are blocked unless you explicitly allow their IP addresses or CIDR ranges in proxy.ssrfAllowlist. The one exception is the cluster's own API server as the cluster OIDC issuer, described in In-cluster API server issuer.

For a private issuer or JWKS host, add the narrowest required addresses to your Helm values.yaml. For example:

proxy:
ssrfAllowlist:
- "10.20.30.40/32" # Private issuer or JWKS service
- "fd00:1234::40/128" # Its IPv6 address, if used

Apply the updated values with your usual Helm upgrade and restart the AIControls pods so they load the configuration. The allowlist accepts IP addresses and CIDRs, not hostnames. If an endpoint resolves to several addresses, every otherwise-blocked address must be permitted. DNS resolution is checked again when a new connection is opened, and the client connects directly to a checked address.

If the first checked address is unreachable, the client reserves time to try the remaining checked addresses within its connection timeout. This also permits fallback from an unreachable IPv6 address to a reachable IPv4 address.

These metadata, probe, JWKS, and token requests do not follow HTTP redirects or use HTTP_PROXY/HTTPS_PROXY environment settings. Configure the final endpoint URL and allow direct connectivity from the AIControls pods. An allowlist entry permits the destination address; HTTPS certificate validation still applies.

If an endpoint fails with outbound host resolves to a blocked address, check its DNS results from the AIControls pod's network and add only the intended service addresses. A redirect response requires correcting the configured endpoint URL; adding more CIDRs does not enable redirects.

Next steps​