Vault
Secure AI agent authentication with A2A protocol and Vault
As organizations deploy more AI agents, they encounter the challenge of managing dynamic non-human identities (NHIs). Traditional identity and access management assumes deterministic access, but AI agents have the autonomy to access other agents and systems non-deterministically. Without proper identity management, agents risk unauthorized access, privilege escalation, or the confused deputy problem where an agent coerces another entity with more permissions to perform an action.
In this tutorial, you deploy HashiCorp Vault as an OpenID Connect (OIDC) identity provider on a local Kubernetes cluster. You configure two AI agents that use the Agent2Agent (A2A) protocol to discover and communicate with each other. Vault authenticates end users, issues scoped access tokens, and provides an auditable authorization layer between agents.
By the end of this tutorial, you:
- Deploy Vault on minikube and configure it as an OIDC identity provider
- Configure Vault identity entities, groups, scopes, and an OIDC client for AI agents
- Deploy an A2A server agent with scope-based authorization middleware
- Deploy an A2A client agent that uses the Vault Agent sidecar to inject OIDC credentials
- Test the OIDC authorization code flow and observe how missing scopes result in 403 Forbidden responses
Challenge
AI agents that access other agents need identity and authorization. The A2A protocol standardizes how agents discover each other through Agent Cards and communicate securely. Agent Cards include security schemes that restrict certain skills to authenticated clients. However, agents still need a trusted identity provider to issue credentials, enforce scope-based access control, and audit authorization requests.
Solution
Vault acts as an OIDC identity provider that issues scoped access tokens for agent communication. A client agent authenticates to Vault using its Kubernetes service account, retrieves OIDC client credentials, and redirects an end user to Vault for authorization. Vault authenticates the user, checks the requested scopes, and returns an access token. The client agent presents this token to the server agent, which validates it against Vault's UserInfo endpoint and checks that the token's scopes match the required permissions.
This workflow enables you to:
- Downscope agent access: Reduce agent permissions to only the skills they need
- Audit authorization requests: Track every OIDC flow step in Vault audit logs
- Use temporary credentials: Issue tokens with configurable TTLs that expire automatically
- Separate authentication from authorization: End users log into Vault, but agents hold the credentials
The following diagram describes the OIDC authorization code flow between the components:
┌──────────┐ ┌─────────────┐ ┌───────┐ ┌──────────────────────┐
│ End User │ │ test-client │ │ Vault │ │ helloworld-agent │
│ (Browser)│ │ (A2A Client)│ │(OIDC) │ │ (A2A Server) │
└────┬─────┘ └──────┬──────┘ └───┬───┘ └──────────┬───────────┘
│ 1. Login │ │ │
│────────────────>│ │ │
│ │ 2. Redirect │ │
│<────────────────│ to Vault │ │
│ │ │ │
│ 3. Log in to Vault │ │
│────────────────────────────────>│ │
│ │ │ │
│ 4. Authorization code │ │
│<────────────────────────────────│ │
│ │ │ │
│ 5. Code to │ │ │
│ test-client │ │ │
│────────────────>│ │ │
│ │ 6. Exchange │ │
│ │ code for │ │
│ │ token │ │
│ │──────────────>│ │
│ │ │ │
│ │ 7. Access │ │
│ │ token │ │
│ │<──────────────│ │
│ │ │ │
│ │ 8. A2A request with Bearer token │
│ │──────────────────────────────────>│
│ │ │ │
│ │ │ 9. Validate │
│ │ │ token │
│ │ │<──────────────────│
│ │ │ │
│ │ │ 10. UserInfo │
│ │ │ (scopes) │
│ │ │──────────────────>│
│ │ │ │
│ │ 11. Response (200 or 403) │
│ │<──────────────────────────────────│
│ 12. Display │ │ │
│<────────────────│ │ │
Scenario
HashiCups developed an AI agent that can answer customer questions about their coffee products.
Oliver, the Vault administrator, sets up Vault as an OIDC provider and creates an OIDC client for the agents. Oliver configures an end-user account and assigns it to a group that can request tokens from the OIDC provider.
Danielle from the development team deploys two AI agent services on Kubernetes:
- Test client service: The A2A client agent that retrieves OIDC client credentials from Vault and performs the authorization code flow
- Helloworld agent service: The A2A server agent that validates access tokens and enforces scope-based access to its skills
Personas
You will take on two personas in this tutorial:
- Vault administrator: Oliver configures Vault authentication methods, identity, OIDC provider, and policies.
- Developer: Danielle deploys services and tuthenticates to Vault through the application and authorizes the client agent to act on their behalf.
Prerequisites
- minikube installed
- kubectl installed
- Helm installed
- Docker installed and running
- Vault CLI installed
- Git installed
- A web browser for the OIDC login flow
Set up the lab
Start minikube
Start a minikube cluster with sufficient resources for Vault and the agent workloads. The agents and Vault require at least 4 CPUs and 8 GB of memory.
$ minikube start --cpus=4 --memory=8192Configure your shell to use minikube's Docker daemon.
$ eval $(minikube docker-env)This allows you to build Docker images locally and make them immediately available to Kubernetes without pushing to a registry.
Clone the repository and build agent images
Clone the reference repository that contains the A2A agent source code.
$ git clone https://github.com/hashicorp-education/learn-vault-a2a-oidc.gitChange into the repository directory.
$ cd learn-vault-a2a-oidcStay in this directory for the remainder of the tutorial.
Build the helloworld-agent-server Docker image.
$ docker build -t helloworld-agent-server:latest agents/helloworld/This agent includes an A2A server with a set of extended skills enforced with authentication and authorization.
Build the test-client Docker image.
$ docker build -t test-client:latest agents/test-client/This agent acts as the A2A client that calls the A2A server agent. In order to use the extended skills, the client must perform the OIDC authorization code flow before using the skills from the server agent.
Verify both images are available in minikube's Docker daemon.
$ docker images | grep -E "helloworld-agent-server|test-client" helloworld-agent-server latest <IMAGE_ID> <TIME> <SIZE> test-client latest <IMAGE_ID> <TIME> <SIZE>
Install Vault on Kubernetes
(Persona: Vault administrator)
Add the HashiCorp Helm repository.
$ helm repo add hashicorp https://helm.releases.hashicorp.comUpdate the Helm repository to get the latest chart versions.
$ helm repo updateInstall Vault in dev mode.
$ helm install vault hashicorp/vault \ --namespace vault \ --create-namespace \ --set "server.dev.enabled=true" \ --set "server.dev.devRootToken=root" \ --set "injector.enabled=true"Dev mode runs Vault with an in-memory storage backend and no TLS. The
server.dev.devRootTokenparameter sets the root token value toroot. This simplifies the setup for learning purposes.Wait for the Vault pod to reach a ready state.
$ kubectl wait --for=condition=ready pod/vault-0 \ --namespace vault \ --timeout=120sExample output:
pod/vault-0 condition metIn a new terminal, port-forward Vault to make it accessible from your local machine.
$ kubectl port-forward svc/vault 8200:8200 --namespace vaultReturn to the terminal where you cloned the git repository and set the Vault address and token as environment variables.
$ export VAULT_ADDR="http://127.0.0.1:8200" VAULT_TOKEN="root"Verify you can access Vault.
$ vault status Key Value --- ----- Seal Type shamir Initialized true Sealed false ...The output shows Vault started and in the unsealed state (
Sealedisfalse).
Configure Vault authentication backends
(Persona: Vault administrator)
Configure the authentication methods that the end user and the client agent use to authenticate to Vault.
Configure the userpass auth method
Enable the userpass auth method.
$ vault auth enable userpassThe end user authenticates to Vault with a username and password during the OIDC authorization flow.
Create a userpass account for the end user.
$ vault write auth/userpass/users/end-user \ password="password" \ token_ttl="1h"
Configure the Kubernetes auth method
Enable the Kubernetes auth method.
$ vault auth enable kubernetesThe test-client pod authenticates to Vault using its Kubernetes service account token.
Set the Kubernetes API server address for the Kubernetes auth method.
$ vault write auth/kubernetes/config \ kubernetes_host="https://kubernetes.default.svc.cluster.local:443" \ disable_iss_validation="true"Inside the cluster, Vault uses the Kubernetes service address.
Configure Vault identity
(Persona: Vault administrator)
Configure Vault as an OIDC identity provider to establish the trust chain between the end user, the client agent, and the server agent. Once configured, Vault associates logins from the userpass method with the correct identity entity.
Create an identity entity that represents the end user.
$ vault write identity/entity \ name="end-user" \ metadata="description=End user for A2A agent authorization"Vault uses this entity to track the user across different authentication methods.
Save the entity ID for later use.
$ END_USER_ENTITY_ID=$(vault read -field=id identity/entity/name/end-user)Link the identity entity to the userpass auth method using the userpass auth method accessor.
$ USERPASS_ACCESSOR=$(vault auth list -format=json \ | jq -r '.["userpass/"].accessor')Create the entity alias.
$ vault write identity/entity-alias \ name="end-user" \ canonical_id="$END_USER_ENTITY_ID" \ mount_accessor="$USERPASS_ACCESSOR"Create an internal identity group named
agentwith the end user as a member.$ vault write identity/group \ name="agent" \ type="internal" \ member_entity_ids="$END_USER_ENTITY_ID"The OIDC provider uses this group in the assignment to determine which users can request tokens.
Save the group ID.
$ AGENT_GROUP_ID=$(vault read -field=id identity/group/name/agent)
Configure Vault as an OIDC provider
(Persona: Vault administrator)
Set the global OIDC issuer to the Vault address.
$ vault write identity/oidc/config \ issuer="http://127.0.0.1:8200"This configures the base issuer URL for Vault's identity token endpoints. Since Vault runs in dev mode over HTTP, set the issuer to the HTTP address.
Create a named key that Vault uses to sign OIDC tokens.
$ vault write identity/oidc/key/agent \ algorithm="RS256" \ allowed_client_ids="*" \ verification_ttl="7200" \ rotation_period="3600"The key uses the RS256 algorithm, rotates every hour, and tokens are valid for verification for 2 hours.
Create an OIDC assignment that binds the end-user entity and the agent group to the OIDC client.
$ vault write identity/oidc/assignment/end-user-assignment \ entity_ids="$END_USER_ENTITY_ID" \ group_ids="$AGENT_GROUP_ID"Only entities and groups in this assignment can request tokens.
Create an OIDC client named
agent.$ vault write identity/oidc/client/agent \ redirect_uris="http://localhost:9000/callback,http://test-client:9000/callback" \ assignments="end-user-assignment" \ key="agent" \ id_token_ttl="3600" \ access_token_ttl="7200"The client agent uses this client's
client_idandclient_secretto perform the OAuth 2.0 authorization code flow. Theredirect_urismust include the test-client's callback URL.The
id_token_ttlof 3600 seconds (1 hour) controls how long the client authentication lasts. Theaccess_token_ttlof 7200 seconds (2 hours) controls how long the client agent can access the server agent's protected skills.Read the client to verify the configuration and note the generated
client_id.$ vault read identity/oidc/client/agent Key Value --- ----- access_token_ttl 7200 assignments [end-user-assignment] client_id <GENERATED_CLIENT_ID> client_secret <GENERATED_CLIENT_SECRET> client_type confidential id_token_ttl 3600 key agent redirect_uris [http://localhost:9000/callback http://test-client:9000/callback]Save the client ID for the OIDC provider configuration.
$ OIDC_CLIENT_ID=$(vault read -field=client_id identity/oidc/client/agent)
Create OIDC scopes
(Persona: Vault administrator)
Create OIDC scopes that define what claims Vault includes in the access token. Each scope uses a JSON template that maps to claims the server agent checks.
Create the
helloworld-readscope.$ vault write identity/oidc/scope/helloworld-read \ description="helloworld read scope" \ template='{"hello_world": "read"}'When the client agent requests this scope, Vault includes
{"hello_world": "read"}in the token claims. The server agent checks for this claim to authorize access to its extended skills.Create the
userscope to include the username in token claims.$ vault write identity/oidc/scope/user \ description="User scope with Vault entity metadata" \ template='{"username": {{identity.entity.name}}}'Create the
groupsscope to include group membership in token claims.$ vault write identity/oidc/scope/groups \ description="Groups scope with Vault group membership" \ template='{"groups": {{identity.entity.groups.names}}}'
Create the agent OIDC provider
(Persona: Vault administrator)
Create the OIDC provider named
agent.$ vault write identity/oidc/provider/agent \ https_enabled=false \ allowed_client_ids="$OIDC_CLIENT_ID" \ scopes_supported="helloworld-read,user,groups"This provider ties together the OIDC client, supported scopes, and issuer configuration. The test-client and helloworld-agent-server both reference this provider's well-known configuration endpoint.
Verify the OIDC provider's well-known configuration.
$ vault read identity/oidc/provider/agent/.well-known/openid-configuration Key Value --- ----- authorization_endpoint http://<POD_IP>:8200/ui/vault/identity/oidc/provider/agent/authorize id_token_signing_alg_values_... [RS256] issuer http://<POD_IP>:8200/v1/identity/oidc/provider/agent jwks_uri http://<POD_IP>:8200/v1/identity/oidc/provider/agent/.well-known/keys response_types_supported [code] scopes_supported [openid helloworld-read user groups] subject_types_supported [public] token_endpoint http://<POD_IP>:8200/v1/identity/oidc/provider/agent/token userinfo_endpoint http://<POD_IP>:8200/v1/identity/oidc/provider/agent/userinfoThe output shows the authorization, token, and userinfo endpoints. The URLs contain Vault's internal pod IP address because the Vault Helm chart sets
VAULT_API_ADDRto the pod IP.Configure Vault's CORS settings to allow the OIDC authorization flow from the browser.
$ vault write sys/config/cors \ enabled=true \ allowed_headers="Access-Control-Allow-Origin" \ allowed_origins="http://localhost:9000,http://127.0.0.1:8200"During the OIDC flow, the test-client's JavaScript makes API requests to Vault from a different origin (localhost:9000 to 127.0.0.1:8200). The CORS configuration allows these cross-origin requests for configuration discovery and token exchange.
Create Vault policies
(Persona: Vault administrator)
Create Vault policies that enforce the principle of least privilege for each persona.
Create an end-user policy.
$ vault policy write helloworld-agent-oidc - <<EOF path "identity/oidc/provider/agent/authorize" { capabilities = ["read"] } EOFThe policy grants permission to authorize against the OIDC provider. The end user needs only this permission to complete the OIDC authorization code flow.
Create a client agent policy.
$ vault policy write helloworld-agent-oidc-client - <<EOF path "identity/oidc/client/agent" { capabilities = ["read"] } EOFGrants permission to read the OIDC client credentials (
client_idandclient_secret). The test-client uses its Kubernetes service account to authenticate to Vault and retrieve these credentials.Update the end-user's userpass configuration to attach the OIDC policy.
$ vault write auth/userpass/users/end-user \ password="password" \ token_policies="helloworld-agent-oidc" \ token_ttl="1h"Create a Kubernetes auth role that binds the
test-clientservice account to the client agent policy.$ vault write auth/kubernetes/role/test-client \ bound_service_account_names="test-client" \ bound_service_account_namespaces="default" \ token_ttl="1h" \ token_policies="helloworld-agent-oidc-client"When the test-client pod authenticates to Vault using its service account, it receives a token with the
helloworld-agent-oidc-clientpolicy.
Deploy the helloworld-agent-server
(Persona: Developer)
Deploy the A2A server agent on Kubernetes. The agent exposes a public Agent
Card with a basic hello_world skill and an extended Agent Card (requiring
authentication) with a super_hello_world skill.
Review the server agent code
Before deploying, review the key parts of the server agent that implement OIDC authorization.
The server agent's __main__.py adds an OpenIdConnectSecurityScheme to the
Agent Card. This tells client agents how to authenticate and what scopes they
need.
if OPENID_CONNECT_URL:
security_schemes["oauth"] = SecurityScheme(
root=OpenIdConnectSecurityScheme(
description="OIDC provider",
type="openIdConnect",
open_id_connect_url=OPENID_CONNECT_URL,
)
)
security.append({"oauth": ["hello_world:read"]})
The AuthMiddleware in auth_middleware.py intercepts requests, extracts the
Bearer token, calls Vault's UserInfo endpoint, and checks that the token's
claims include the required scopes.
async def get_userinfo(self, access_token):
try:
userinfo_endpoint = await self._get_userinfo_endpoint()
userinfo = httpx.get(
f"{userinfo_endpoint}",
headers={"Authorization": f"Bearer {access_token}"},
verify=self.verify_ssl
)
return userinfo.json()
except Exception as e:
logger.error(f"Failed to get userinfo with token: {str(e)}")
return None
def check_oidc_scopes(self, userinfo):
missing_scopes = []
if self.a2a_auth["required_scopes"]:
for scope in self.a2a_auth["required_scopes"]:
scope_key, scope_value = scope.split(":")
if (
scope_key not in userinfo.keys()
or scope_value != userinfo.get(scope_key)
):
missing_scopes.append(scope)
return missing_scopes
When the required scopes are missing, the middleware returns a 403 Forbidden response.
Create the Kubernetes resources
The server agent needs to know its own URL (for the Agent Card) and the Vault OIDC provider's well-known configuration URL (for token validation).
Create a ConfigMap with the required values.
$ kubectl apply -f - <<EOF apiVersion: v1 kind: ConfigMap metadata: name: helloworld-agent-server namespace: default data: AGENT_URL: "http://helloworld-agent-server:9999" OPENID_CONNECT_URL: "http://vault.vault.svc.cluster.local:8200/v1/identity/oidc/provider/agent/.well-known/openid-configuration" EOFThe
OPENID_CONNECT_URLpoints to the Vault OIDC provider's well-known configuration endpoint. Inside the cluster, the server agent accesses Vault through the Kubernetes service URL.Create the deployment.
$ kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: helloworld-agent-server namespace: default labels: app: helloworld-agent-server spec: replicas: 1 selector: matchLabels: app: helloworld-agent-server template: metadata: labels: app: helloworld-agent-server spec: containers: - name: helloworld-agent-server image: helloworld-agent-server:latest imagePullPolicy: Never ports: - containerPort: 9999 name: http protocol: TCP env: - name: AGENT_URL valueFrom: configMapKeyRef: name: helloworld-agent-server key: AGENT_URL - name: OPENID_CONNECT_URL valueFrom: configMapKeyRef: name: helloworld-agent-server key: OPENID_CONNECT_URL resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /.well-known/agent-card.json port: 9999 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /.well-known/agent-card.json port: 9999 initialDelaySeconds: 5 periodSeconds: 5 EOFThe
imagePullPolicy: Neversetting tells Kubernetes to use the locally built image instead of trying to pull it from a registry.Create a ClusterIP service.
$ kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: name: helloworld-agent-server namespace: default labels: app: helloworld-agent-server spec: type: ClusterIP ports: - port: 9999 targetPort: 9999 protocol: TCP name: http selector: app: helloworld-agent-server EOFThe server agent stays internal to the cluster. Only the test-client communicates with it directly.
Wait for the helloworld-agent-server pod to become ready.
$ kubectl wait --for=condition=ready pod \ -l app=helloworld-agent-server \ --timeout=120sTemporarily enable port-forwarding to access the server agent's public Agent Card.
$ kubectl port-forward svc/helloworld-agent-server 9999:9999 &Retrieve the Agent Card and verify the
hello_worldskill is present.$ curl -s http://localhost:9999/.well-known/agent-card.json | jq '.name, .skills[].id' "Hello World Agent" "hello_world"The output shows the public Agent Card with the
hello_worldskill.Stop the port-forward.
$ kill %1
Deploy the test-client
(Persona: Developer)
Deploy the A2A client agent. This agent uses the Vault Agent sidecar injector to automatically inject OIDC client credentials into its pod.
Review the client agent code
The test-client reads OIDC configuration from files that the Vault Agent
sidecar injects. The OIDCAuthenticationConfig class loads client_secrets.json
and oidc_provider.json from the /vault/secrets/ directory.
class OIDCAuthenticationConfig:
def __init__(self, client_secrets_path, oidc_provider_config_path,
oidc_scopes=""):
scopes = oidc_scopes.split() if oidc_scopes else []
if "openid" not in scopes:
scopes.append("openid")
self.scope = " ".join(scopes)
with open(client_secrets_path, 'r') as f:
client_secrets = json.load(f)
self.client_id = client_secrets["client_id"]
self.client_secret = client_secrets["client_secret"]
self.redirect_uris = client_secrets["redirect_uris"]
with open(oidc_provider_config_path, 'r') as f:
oidc_provider = json.load(f)
self.authorization_endpoint = \
oidc_provider["authorization_endpoint"]
self.token_endpoint = oidc_provider["token_endpoint"]
When a user clicks Login, the test-client redirects them to Vault's authorization endpoint. After the user authenticates, Vault returns an authorization code. The test-client exchanges this code for an access token, which it stores in the user's session.
Create the Kubernetes resources
Create a ServiceAccount for the test-client.
$ kubectl apply -f - <<EOF apiVersion: v1 kind: ServiceAccount metadata: name: test-client namespace: default labels: app: test-client EOFVault uses this service account to authenticate the pod through the Kubernetes auth method.
Create a ConfigMap with the test-client's base URL.
$ kubectl apply -f - <<EOF apiVersion: v1 kind: ConfigMap metadata: name: test-client namespace: default data: BASE_URL: "http://localhost:9000" EOFThe test-client uses this to construct the OAuth redirect URI.
Create the deployment with Vault Agent sidecar annotations.
$ kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: test-client namespace: default labels: app: test-client spec: replicas: 1 selector: matchLabels: app: test-client template: metadata: labels: app: test-client annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "test-client" vault.hashicorp.com/agent-inject-token: "true" vault.hashicorp.com/agent-run-as-same-user: "true" vault.hashicorp.com/tls-skip-verify: "true" vault.hashicorp.com/agent-inject-secret-client_secrets.json: "identity/oidc/client/agent" vault.hashicorp.com/agent-inject-template-client_secrets.json: | { {{- with secret "identity/oidc/client/agent" }} "client_id": "{{ .Data.client_id }}", "client_secret": "{{ .Data.client_secret }}", "redirect_uris": {{ .Data.redirect_uris | toJSON }} {{- end }} } vault.hashicorp.com/agent-inject-secret-oidc_provider.json: "identity/oidc/provider/agent/.well-known/openid-configuration" vault.hashicorp.com/agent-inject-template-oidc_provider.json: | { "authorization_endpoint": "http://127.0.0.1:8200/ui/vault/identity/oidc/provider/agent/authorize", "issuer": "http://vault.vault.svc.cluster.local:8200/v1/identity/oidc/provider/agent", "token_endpoint": "http://vault.vault.svc.cluster.local:8200/v1/identity/oidc/provider/agent/token", "userinfo_endpoint": "http://vault.vault.svc.cluster.local:8200/v1/identity/oidc/provider/agent/userinfo" } spec: serviceAccountName: test-client containers: - name: test-client image: test-client:latest imagePullPolicy: Never ports: - containerPort: 9000 name: http protocol: TCP securityContext: runAsUser: 1000 runAsGroup: 1000 env: - name: AGENT_URL value: "http://helloworld-agent-server:9999" - name: BASE_URL valueFrom: configMapKeyRef: name: test-client key: BASE_URL - name: OIDC_PROVIDER_CONFIG_PATH value: "/vault/secrets/oidc_provider.json" - name: CLIENT_SECRETS_PATH value: "/vault/secrets/client_secrets.json" - name: VERIFY_TLS value: "false" resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: / port: 9000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: / port: 9000 initialDelaySeconds: 5 periodSeconds: 5 EOFThe annotations tell the Vault Agent injector to:
Authenticate to Vault using the
test-clientKubernetes auth roleRead the OIDC client credentials from
identity/oidc/client/agentInject the OIDC provider endpoints as a static JSON file with
127.0.0.1:8200URLsRender these secrets as JSON files at
/vault/secrets/
The Vault Agent sidecar annotations control how secrets get injected:
Annotation Purpose vault.hashicorp.com/agent-injectEnables the Vault Agent sidecar injector vault.hashicorp.com/roleKubernetes auth role for Vault authentication vault.hashicorp.com/agent-inject-tokenInjects the Vault token into the pod vault.hashicorp.com/agent-run-as-same-userRuns the Vault Agent as the same user as the application vault.hashicorp.com/tls-skip-verifySkips TLS verification (dev mode only) vault.hashicorp.com/agent-inject-secret-<filename>The Vault path to read and render as <filename>vault.hashicorp.com/agent-inject-template-<filename>Go template that formats the secret data into the file The template for
client_secrets.jsonuses Go template syntax to format theOIDC client credentials as JSON:
{ {{- with secret "identity/oidc/client/agent" }} "client_id": "{{ .Data.client_id }}", "client_secret": "{{ .Data.client_secret }}", "redirect_uris": {{ .Data.redirect_uris | toJSON }} {{- end }} }The template for
oidc_provider.jsonuses hardcoded URLs instead of reading them from the well-known endpoint. The Vault Helm chart setsVAULT_API_ADDRto the pod IP, which causes the well-known endpoint to return pod-internal URLs. The URLs use two different addresses because of how the OIDC authorization code flow works:authorization_endpointuses127.0.0.1:8200because the browser redirects to this URL for login. The browser reaches Vault through thekubectl port-forwardsession.token_endpointanduserinfo_endpointusevault.vault.svc.cluster.local:8200because the test-client pod calls these endpoints server-side to exchange the authorization code for tokens. Inside the cluster, the pod reaches Vault through the Kubernetes Service DNS name.issueruses the cluster-internal address to match the issuer in tokens that Vault issues.
Create a Service for the test-client.
$ kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: name: test-client namespace: default labels: app: test-client spec: type: ClusterIP ports: - port: 9000 targetPort: 9000 protocol: TCP name: http selector: app: test-client EOFWait for the test-client pod to become ready. The pod has an init container (Vault Agent) that runs first, so it may take a moment.
$ kubectl wait --for=condition=ready pod \ -l app=test-client \ --timeout=180sVerify the Vault Agent sidecar injected the secrets by checking the pod's logs.
$ kubectl logs -l app=test-client -c vault-agent --tail=5Example output:
2026-03-09T20:42:05.007Z [INFO] agent: (runner) stopping 2026-03-09T20:42:05.007Z [INFO] agent: (runner) creating new runner (dry: false, once: false) 2026-03-09T20:42:05.007Z [INFO] agent: (runner) creating watcher 2026-03-09T20:42:05.007Z [INFO] agent: (runner) starting 2026-03-09T20:42:05.008Z [INFO] agent.auth.handler: renewed auth tokenIn a separate terminal, port-forward the test-client to make it accessible from your browser. Keep this terminal running.
$ kubectl port-forward svc/test-client 9000:9000
Test the OIDC authorization flow
(Persona: Developer)
You can test the OIDC authorization flow by logging in through the test-client UI and sending requests to the server agent. First, test without requesting any scopes to see that you cannot access the server agent's extended skills. Then, log in again with the correct scope to see a successful response.
Test without scopes (expect 403 Forbidden)
Open a web browser using incognito mode and navigate to http://localhost:9000.
Leave the OIDC Scopes field empty.
Click Login. The browser redirects to the Vault login page.
Click the Method pulldown menu and select Userpass.
Log in with the username
end-userand passwordpassword.After successful authentication, Vault redirects back to the test-client.
Type a message such as
Give me a hello world.and click Send Request.Example output:
Error sending message: HTTP Error 403: Client error '403 Forbidden' for url 'http://helloworld-agent-server:9999' For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403The server agent returns a 403 Forbidden error because the access token does not contain the required
hello_world:readscope.Close the incognito browser window/tab.
Log out and test with the correct scope
Open a web browser using incognito mode and navigate to http://localhost:9000.
Enter
helloworld-readin the OIDC Scopes field.Click Login. The browser redirects to the Vault login page.
Click the Method pulldown menu and select Userpass.
Log in with the username
end-userand passwordpassword.After authentication, Vault redirects back to the test-client. The Authentication Status field shows that you are logged in with the
helloworld-readscope.Type
Give me a hello world.in the message field and click Send Request.Example output:
SUCCESS Hello World
Understand the authorization flow
The successful request involved the following steps:
User initiates login: The test-client redirects the user to Vault's authorization endpoint with the
helloworld-readscope.User authenticates: The end user logs into Vault with their userpass credentials.
Vault issues authorization code: Vault verifies the user is part of the OIDC assignment and that the requested scope is supported by the provider.
Code exchange: The test-client exchanges the authorization code for an access token at Vault's token endpoint.
A2A request: The test-client sends an A2A message to the helloworld-agent-server with the access token as a Bearer token.
Token validation: The helloworld-agent-server calls Vault's UserInfo endpoint with the access token to retrieve the token's claims.
Scope check: The middleware checks that the claims include
"hello_world": "read", which matches the requiredhello_world:readscope.Response: The scope check passes, so the server agent processes the request and returns "Hello World".
Examine Vault audit logs (optional)
(Persona: Vault administrator)
Enable Vault audit logging to observe the OIDC authorization code flow in detail.
Create the audit log directory inside the Vault pod.
$ kubectl exec vault-0 --namespace vault -- mkdir -p /vault/auditEnable the file audit device.
$ vault audit enable file file_path=/vault/audit/audit.logRepeat the OIDC login flow from the test-client UI.
Examine the audit log entries.
$ kubectl exec vault-0 --namespace vault -- \ cat /vault/audit/audit.log | jq -r '.request.path' 2>/dev/null | sort | uniq -c | sort -rnThe audit logs show four distinct steps in the OIDC authorization code flow:
OIDC configuration request: The client agent reads the OIDC provider's well-known configuration from
identity/oidc/provider/agent/.well-known/openid-configuration.Client credential retrieval: The client agent reads the
client_idandclient_secretfromidentity/oidc/client/agent.User authorization redirect: The end user authorizes the request at
identity/oidc/provider/agent/authorize.UserInfo endpoint verification: The server agent validates the access token at
identity/oidc/provider/agent/userinfo.
View the raw audit logs to see the full request and response details, including the user agent header that distinguishes browser requests from server agent requests.
$ kubectl exec vault-0 --namespace vault -- \ cat /vault/audit/audit.log | jq 'select(.request.path == "identity/oidc/provider/agent/userinfo") | .request.headers'
Clean up
Remove the resources created during this tutorial.
Switch to the terminal running the kubectl proxy forwarding traffic on port 9000 and type
ctrl-con your keyboard to stop the proxy.Switch to the terminal running the kubectl proxy forwarding traffic on port 8200 and type
ctrl-con your keyboard to stop the proxy.Stop minikube.
$ minikube stopDelete the minikube cluster entirely.
$ minikube deleteRemove the cloned repository.
$ cd .. && rm -rf learn-vault-a2a-oidc
Next steps
In this tutorial, you deployed Vault on Kubernetes as an OIDC identity provider for two A2A protocol agents with scope-based authorization.
The helloworld-agent-server validates access tokens against Vault's UserInfo endpoint and only grants access to extended skills when the client agent presents a token with the correct scopes. This approach gives you centralized identity management, auditable authorization flows, and temporary credentials for AI agent communication.
To extend what you have learned:
- Review the Vault OIDC identity provider documentation for detailed configuration options
- Explore the A2A protocol specification for additional security schemes and agent discovery patterns
- Complete the OIDC identity provider tutorial for a deeper dive into Vault's OIDC capabilities
- Read the validated patterns for AI agent identity for production deployment guidance
- Review the reference implementation blog post for the full agent source code and Terraform-based deployment on AWS EKS
- Use the A2A protocol for AI agent communication
For production deployments, consider the following enhancements:
- Enable TLS on Vault and use proper certificate management
- Use SPIFFE or JWT/OIDC auth methods for agent authentication instead of userpass
- Configure appropriate token TTLs based on your security requirements
- Add additional agents and scopes to model complex multi-agent authorization patterns
- Integrate Vault audit logs with a centralized logging platform for monitoring and alerting