---
source_url: "https://developer.hashicorp.com/vault/tutorials/auth-methods/secure-ai-agent-communication-a2a-vault-kubernetes?utm_source=openai"
title: "Secure AI agent authentication with A2A protocol and Vault | Vault | HashiCorp Developer"
mirrored_at: 2026-08-05T01:00:59.075Z
host: developer.hashicorp.com
cited_in_42a: true
mirror_canonical: "https://index.42a.ai/developer.hashicorp.com/vault/tutorials/auth-methods/secure-ai-agent-communication-a2a-vault-kubernetes__q__utm_source_openai"
---

> **Original source:** https://developer.hashicorp.com/vault/tutorials/auth-methods/secure-ai-agent-communication-a2a-vault-kubernetes?utm_source=openai

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](https://www.hashicorp.com/en/blog/before-you-build-agentic-ai-understand-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](https://developer.hashicorp.com/vault/docs/concepts/oidc-provider) on a local Kubernetes cluster. You configure two AI agents that use the [Agent2Agent (A2A) protocol](https://a2a-protocol.org/latest/) 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

AI agents that access other agents need identity and authorization. The [A2A protocol](https://a2a-protocol.org/latest/) standardizes how agents discover each other through [Agent Cards](https://a2a-protocol.org/latest/topics/agent-discovery/#the-role-of-the-agent-card) and communicate securely. Agent Cards include [security schemes](https://github.com/a2aproject/A2A/blob/main/specification/a2a.proto#L549) 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.

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     │               │                   │
     │<────────────────│               │                   │
```

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

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.

-   [minikube](https://minikube.sigs.k8s.io/docs/start/) installed
-   [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) installed
-   [Helm](https://helm.sh/docs/intro/install/) installed
-   [Docker](https://docs.docker.com/get-docker/) installed and running
-   [Vault CLI](https://developer.hashicorp.com/vault/install) installed
-   [Git](https://git-scm.com/downloads) installed
-   A web browser for the OIDC login flow

Warning

This tutorial runs Vault in dev mode over HTTP. Dev mode is not secure and you should not use it in production. For production deployments, enable TLS and connect using secure protocols.

### Start minikube

1.  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=8192
    ```
    
2.  Configure 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

1.  Clone the reference repository that contains the A2A agent source code.
    
    ```
    $ git clone https://github.com/hashicorp-education/learn-vault-a2a-oidc.git
    ```
    
2.  Change into the repository directory.
    
    ```
    $ cd learn-vault-a2a-oidc
    ```
    
    Stay in this directory for the remainder of the tutorial.
    
3.  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.
    
4.  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.
    
5.  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**)

1.  Add the HashiCorp Helm repository.
    
    ```
    $ helm repo add hashicorp https://helm.releases.hashicorp.com
    ```
    
2.  Update the Helm repository to get the latest chart versions.
    
    ```
    $ helm repo update
    ```
    
3.  Install 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.devRootToken` parameter sets the root token value to `root`. This simplifies the setup for learning purposes.
    
4.  Wait for the Vault pod to reach a ready state.
    
    ```
    $ kubectl wait --for=condition=ready pod/vault-0 \
        --namespace vault \
        --timeout=120s
    ```
    
    **Example output:**
    
    ```
    pod/vault-0 condition met
    ```
    
5.  In a new terminal, port-forward Vault to make it accessible from your local machine.
    
    ```
    $ kubectl port-forward svc/vault 8200:8200 --namespace vault
    ```
    
    Tip
    
    Keep this terminal running throughout the tutorial.
    
6.  Return 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"
    ```
    
7.  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 (`Sealed` is `false`).
    

(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

1.  Enable the [userpass auth method](https://developer.hashicorp.com/vault/docs/auth/userpass).
    
    ```
    $ vault auth enable userpass
    ```
    
    The end user authenticates to Vault with a username and password during the OIDC authorization flow.
    
2.  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

1.  Enable the [Kubernetes auth method](https://developer.hashicorp.com/vault/docs/auth/kubernetes).
    
    ```
    $ vault auth enable kubernetes
    ```
    
    The test-client pod authenticates to Vault using its Kubernetes service account token.
    
2.  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.
    
    Note
    
    When Vault runs inside the Kubernetes cluster, it can automatically discover the cluster CA certificate and the token reviewer JWT from its pod's service account. Setting `disable_iss_validation` to `true` allows tokens from any issuer, which is necessary for some Kubernetes distributions.
    

(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.

1.  Create an [identity entity](https://developer.hashicorp.com/vault/docs/concepts/identity) 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.
    
2.  Save the entity ID for later use.
    
    ```
    $ END_USER_ENTITY_ID=$(vault read -field=id identity/entity/name/end-user)
    ```
    
3.  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')
    ```
    
4.  Create the entity alias.
    
    ```
    $ vault write identity/entity-alias \
        name="end-user" \
        canonical_id="$END_USER_ENTITY_ID" \
        mount_accessor="$USERPASS_ACCESSOR"
    ```
    
5.  Create an internal [identity group](https://developer.hashicorp.com/vault/docs/concepts/identity#identity-groups) named `agent` with 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.
    
6.  Save the group ID.
    
    ```
    $ AGENT_GROUP_ID=$(vault read -field=id identity/group/name/agent)
    ```
    

(Persona: **Vault administrator**)

1.  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.
    
    Warning
    
    Setting `issuer` to `http://127.0.0.1:8200` works for local development. In production, configure TLS and set the `issuer` to the Vault cluster's secure HTTPS URL.
    
2.  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.
    
    Allowed client IDs
    
    As agents tend to be non-deterministic, set the `allowed_client_ids` parameter to `*` to allow any client agent to authenticate with Vault as an OIDC provider.
    
3.  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.
    
4.  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_id` and `client_secret` to perform the OAuth 2.0 authorization code flow. The `redirect_uris` must include the test-client's callback URL.
    
    The `id_token_ttl` of 3600 seconds (1 hour) controls how long the client authentication lasts. The `access_token_ttl` of 7200 seconds (2 hours) controls how long the client agent can access the server agent's protected skills.
    
5.  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]
    ```
    
6.  Save the client ID for the OIDC provider configuration.
    
    ```
    $ OIDC_CLIENT_ID=$(vault read -field=client_id identity/oidc/client/agent)
    ```
    

(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.

1.  Create the `helloworld-read` scope.
    
    ```
    $ 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.
    
2.  Create the `user` scope 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}}}'
    ```
    
3.  Create the `groups` scope 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}}}'
    ```
    

(Persona: **Vault administrator**)

1.  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.
    
    Note
    
    Setting `https_enabled=false` is required because Vault runs in dev mode over HTTP.
    
2.  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/userinfo
    ```
    
    The 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_ADDR` to the pod IP.
    
    Note
    
    The pod IP addresses in the OIDC URLs are not reachable from your browser. The helloworld-agent-server uses its own internal `OPENID_CONNECT_URL` to reach Vault for token validation, so pod IP URLs work for server-side operations. The test-client Deployment in the next section uses hardcoded `127.0.0.1:8200` URLs in its Vault Agent template to ensure the browser redirects to the port-forwarded address.
    
3.  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.
    

(Persona: **Vault administrator**)

Create Vault policies that enforce the [principle of least privilege](https://developer.hashicorp.com/well-architected-framework/secure-systems/identity-access-management/grant-least-privilege) for each persona.

1.  Create an end-user policy.
    
    ```
    $ vault policy write helloworld-agent-oidc - <<EOF
    path "identity/oidc/provider/agent/authorize" {
      capabilities = ["read"]
    }
    EOF
    ```
    
    The policy grants permission to authorize against the OIDC provider. The end user needs only this permission to complete the OIDC authorization code flow.
    
2.  Create a client agent policy.
    
    ```
    $ vault policy write helloworld-agent-oidc-client - <<EOF
    path "identity/oidc/client/agent" {
      capabilities = ["read"]
    }
    EOF
    ```
    
    Grants permission to read the OIDC client credentials (`client_id` and `client_secret`). The test-client uses its Kubernetes service account to authenticate to Vault and retrieve these credentials.
    
3.  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"
    ```
    
4.  Create a Kubernetes auth role that binds the `test-client` service 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-client` policy.
    

(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).

1.  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"
    EOF
    ```
    
    The `OPENID_CONNECT_URL` points to the Vault OIDC provider's well-known configuration endpoint. Inside the cluster, the server agent accesses Vault through the Kubernetes service URL.
    
2.  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
    EOF
    ```
    
    The `imagePullPolicy: Never` setting tells Kubernetes to use the locally built image instead of trying to pull it from a registry.
    
3.  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
    EOF
    ```
    
    The server agent stays internal to the cluster. Only the test-client communicates with it directly.
    
4.  Wait for the helloworld-agent-server pod to become ready.
    
    ```
    $ kubectl wait --for=condition=ready pod \
        -l app=helloworld-agent-server \
        --timeout=120s
    ```
    
5.  Temporarily enable port-forwarding to access the server agent's public Agent Card.
    
    ```
    $ kubectl port-forward svc/helloworld-agent-server 9999:9999 &
    ```
    
6.  Retrieve the Agent Card and verify the `hello_world` skill 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_world` skill.
    
7.  Stop the port-forward.
    
    ```
    $ kill %1
    ```
    

(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

1.  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
    EOF
    ```
    
    Vault uses this service account to authenticate the pod through the Kubernetes auth method.
    
2.  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"
    EOF
    ```
    
    The test-client uses this to construct the OAuth redirect URI.
    
3.  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
    EOF
    ```
    
    The annotations tell the Vault Agent injector to:
    
    1.  Authenticate to Vault using the `test-client` Kubernetes auth role
        
    2.  Read the OIDC client credentials from `identity/oidc/client/agent`
        
    3.  Inject the OIDC provider endpoints as a static JSON file with `127.0.0.1:8200` URLs
        
    4.  Render these secrets as JSON files at `/vault/secrets/`
        
    
    The Vault Agent sidecar annotations control how secrets get injected:
    
    Annotation
    
    Purpose
    
    `vault.hashicorp.com/agent-inject`
    
    Enables the Vault Agent sidecar injector
    
    `vault.hashicorp.com/role`
    
    Kubernetes auth role for Vault authentication
    
    `vault.hashicorp.com/agent-inject-token`
    
    Injects the Vault token into the pod
    
    `vault.hashicorp.com/agent-run-as-same-user`
    
    Runs the Vault Agent as the same user as the application
    
    `vault.hashicorp.com/tls-skip-verify`
    
    Skips 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.json` uses Go template syntax to format the
    
    OIDC 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.json` uses hardcoded URLs instead of reading them from the well-known endpoint. The Vault Helm chart sets `VAULT_API_ADDR` to 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_endpoint`** uses `127.0.0.1:8200` because the browser redirects to this URL for login. The browser reaches Vault through the `kubectl port-forward` session.
    -   **`token_endpoint`** and **`userinfo_endpoint`** use `vault.vault.svc.cluster.local:8200` because 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.
    -   **`issuer`** uses the cluster-internal address to match the issuer in tokens that Vault issues.
4.  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
    EOF
    ```
    
5.  Wait 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=180s
    ```
    
6.  Verify the Vault Agent sidecar injected the secrets by checking the pod's logs.
    
    ```
    $ kubectl logs -l app=test-client -c vault-agent --tail=5
    ```
    
    **Example 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 token
    ```
    
7.  In 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
    ```
    

(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)

1.  Open a web browser using incognito mode and navigate to [http://localhost:9000](http://localhost:9000/).
    
2.  Leave the **OIDC Scopes** field empty.
    
3.  Click **Login**. The browser redirects to the Vault login page.
    
4.  Click the **Method** pulldown menu and select **Userpass**.
    
5.  Log in with the username `end-user` and password `password`.
    
6.  After successful authentication, Vault redirects back to the test-client.
    
7.  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/403
    ```
    
    The server agent returns a **403 Forbidden** error because the access token does not contain the required `hello_world:read` scope.
    
8.  Close the incognito browser window/tab.
    

### Log out and test with the correct scope

1.  Open a web browser using incognito mode and navigate to [http://localhost:9000](http://localhost:9000/).
    
2.  Enter `helloworld-read` in the **OIDC Scopes** field.
    
3.  Click **Login**. The browser redirects to the Vault login page.
    
4.  Click the **Method** pulldown menu and select **Userpass**.
    
5.  Log in with the username `end-user` and password `password`.
    
6.  After authentication, Vault redirects back to the test-client. The **Authentication Status** field shows that you are logged in with the `helloworld-read` scope.
    
7.  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:

1.  **User initiates login**: The test-client redirects the user to Vault's authorization endpoint with the `helloworld-read` scope.
    
2.  **User authenticates**: The end user logs into Vault with their userpass credentials.
    
3.  **Vault issues authorization code**: Vault verifies the user is part of the OIDC assignment and that the requested scope is supported by the provider.
    
4.  **Code exchange**: The test-client exchanges the authorization code for an access token at Vault's token endpoint.
    
5.  **A2A request**: The test-client sends an A2A message to the helloworld-agent-server with the access token as a Bearer token.
    
6.  **Token validation**: The helloworld-agent-server calls Vault's UserInfo endpoint with the access token to retrieve the token's claims.
    
7.  **Scope check**: The middleware checks that the claims include `"hello_world": "read"`, which matches the required `hello_world:read` scope.
    
8.  **Response**: The scope check passes, so the server agent processes the request and returns "Hello World".
    

(Persona: **Vault administrator**)

Enable Vault audit logging to observe the OIDC authorization code flow in detail.

1.  Create the audit log directory inside the Vault pod.
    
    ```
    $ kubectl exec vault-0 --namespace vault -- mkdir -p /vault/audit
    ```
    
2.  Enable the file audit device.
    
    ```
    $ vault audit enable file file_path=/vault/audit/audit.log
    ```
    
3.  Repeat the OIDC login flow from the test-client UI.
    
4.  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 -rn
    ```
    
    The audit logs show four distinct steps in the OIDC authorization code flow:
    
    1.  **OIDC configuration request**: The client agent reads the OIDC provider's well-known configuration from `identity/oidc/provider/agent/.well-known/openid-configuration`.
        
    2.  **Client credential retrieval**: The client agent reads the `client_id` and `client_secret` from `identity/oidc/client/agent`.
        
    3.  **User authorization redirect**: The end user authorizes the request at `identity/oidc/provider/agent/authorize`.
        
    4.  **UserInfo endpoint verification**: The server agent validates the access token at `identity/oidc/provider/agent/userinfo`.
        
5.  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'
    ```
    

Remove the resources created during this tutorial.

1.  Switch to the terminal running the kubectl proxy forwarding traffic on port 9000 and type `ctrl-c` on your keyboard to stop the proxy.
    
2.  Switch to the terminal running the kubectl proxy forwarding traffic on port 8200 and type `ctrl-c` on your keyboard to stop the proxy.
    
3.  Stop minikube.
    
    ```
    $ minikube stop
    ```
    
4.  Delete the minikube cluster entirely.
    
    ```
    $ minikube delete
    ```
    
5.  Remove the cloned repository.
    
    ```
    $ cd .. && rm -rf learn-vault-a2a-oidc
    ```
    

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](https://developer.hashicorp.com/vault/docs/concepts/oidc-provider) for detailed configuration options
-   Explore the [A2A protocol specification](https://a2a-protocol.org/latest/specification/) for additional security schemes and agent discovery patterns
-   Complete the [OIDC identity provider tutorial](https://developer.hashicorp.com/vault/tutorials/auth-methods/oidc-identity-provider) for a deeper dive into Vault's OIDC capabilities
-   Read the [validated patterns for AI agent identity](https://developer.hashicorp.com/validated-patterns/vault/ai-agent-identity-with-hashicorp-vault) for production deployment guidance
-   Review the [reference implementation blog post](https://medium.com/hashicorp-engineering/ai-agent-authorization-with-a2a-protocol-and-hashicorp-vault-2e0c36fc2efc) for the full agent source code and Terraform-based deployment on AWS EKS
-   [Use the A2A protocol for AI agent communication](https://www.ibm.com/think/tutorials/use-a2a-protocol-for-ai-agent-communication)

For production deployments, consider the following enhancements:

-   Enable TLS on Vault and use proper certificate management
-   Use [SPIFFE](https://developer.hashicorp.com/vault/docs/auth/spiffe) or [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt) 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