Secure agent permissions with Vault
Challenge
AI agents have become a standard part of the software lifecycle. Common challenges include:
Agents clone branches, run linters, summarize diffs, and post review comments.
Agent work requires credentials to access repositories and services.
Long-lived access tokens are written into environment files and passed to agents.
Tokens often grant more access than required for a single session.
Tokens may never expire on their own.
Token usage is difficult or impractical to audit.
Persistent access tokens pose privilege escalation risks through token capture and misuse.
Solution
Vault Enterprise enables an agent to authenticate with an OAuth access token that carries a Rich Authorization Request (RAR) claim. The tutorial uses a coding agent example, but the solution works for any AI agent.
This least-privilege approach implements zero trust principles by ensuring agent credentials are ephemeral and narrowly scoped through delegation. The agent acts on behalf of a human developer and receives an effective permission set that is the intersection of three independent controls:
Human baseline ACL policies define what the person can do
Agent ceiling policies from the Vault Agent Registry cap the maximum the agent can ever do
Per-session RAR claims carried in the token specify what one session may access
Benefits of this approach include:
Agents can do some of what the human can do, but never all or more
Agent capabilities are ephemeral and narrow-scoped for each request
Every agent action is auditable and attributable to both the human and agent
Token lifetime is reduced from days or weeks to minutes
Eliminates persistent credential exposure risks
Why this matters
For decision-makers: This pattern reduces security risk and operational overhead:
Reduces token lifetime from days/weeks to minutes, minimizing exposure window
Eliminates persistent credentials that can be captured and misused
Provides per-session audit trails for compliance and incident response
Enforces least privilege automatically through policy intersection
Scales to any agent type (CI/CD, monitoring, automation tools)
For implementers: This workflow provides:
Fine-grained control over agent permissions without managing multiple credential sets
Flexible delegation that adapts to different use cases and security requirements
Clear attribution of every agent action to both human and agent identities
Production-ready OAuth 2.0 integration with enterprise identity providers
Personas
The tutorial follows two personas with distinct responsibilities. You can assume their roles to complete the lab portion of the tutorial.
Oliver the Vault operator: Enables Vault features, configures Auth0 trust through an OAuth resource server profile, creates the identity entities and aliases, registers the agent with ceiling policies, and enables auditing.
Danielle the developer and agent owner: Authenticates through Auth0, mints the delegated RAR token, runs the agent, and inspects the audit trail.
Scenario
The HashiCups team maintains a cup-printer service. Pull requests against the cup-printer service codebase pile up faster than the team can review them, and most of the backlog are lightweight tasks with few heavy lifts. Rather than have the team use their time on linting violations, missing tests, and obvious policy issues, the HashiCups team is experimenting with using agents to handle some of these tasks.
Danielle is a developer on the team and wants a coding agent to do the boring first pass on every PR so that the team can focus on the hard problems in a review along with human to human interaction.
The agent needs a repository access token to clone the PR branch and post comments, and that token lives in Vault.
Danielle has broad access to the HashiCups secret tree, both the repo/* build
tokens and the prod/* production secrets. The agent must not inherit all of
this access.
The team imposes the following guardrails for the agent:
The agent may only read the repository token under
repo/*.The agent may never read anything under
prod/*, even though Danielle can.The agent may never write, even though Danielle can.
For any single review session, the agent receives a token scoped to precisely the one secret it needs and nothing else.
Oliver is the HashiCups team Vault operator, and it is their job to configure the trust between Vault and the team's identity provider. In this scenario, the team uses Auth0 as an authorization server and Oliver's jobs to be done include preparing the Auth0 account for this role.
To do so, Oliver must configure the trust between Vault and Auth0, register the agent with Vault Agent Registry, configure ceiling policies that enforce the guardrails, and configure the authorizaiton server.
This enables the agent acting on behalf of Danielle to acquire access tokens that capture the delegation and narrowly scoped permission that the agent can request from Vault.
Prerequisites
You need the following to complete the lab scenario in this tutorial:
A Vault version 2.0.2 or later binary installed and in your
PATH. This tutorial requires a beta build that includes the Agent Registry and OAuth resource server features. Contact your HashiCorp account team to obtain the required beta binary.A Vault Enterprise license.
Enable the Vault Enterprise oauth-resource-server activation flag.
Docker Desktop or other Docker API-compatible containerization solution installed with at least 4GB RAM allocated and 2GB free disk space.
An Auth0 Enterprise plan tenant with permissions to create an Application, an API, a user, and enable/remove Rich Authorization Request (RAR) and Pushed Authorization Requests (PAR). Auth0 Enterprise plan or higher is required for RAR and PAR support. Refer to Auth0 pricing for feature availability by plan tier.
The
oauth2ccommand line tool (v1.15.0 or later) installed and in yourPATHfor the browser-based authorization code workflow.curl,openssl, andxxdfor the local Pushed Authorization Request (PAR) flow that attaches the RAR claim. These ship with most systems and need no extra setup.jq(1.6 or later) for parsing API responses.A coding agent such as OpenCode. The agent is only required for the final integration step. All Vault features in this tutorial work with
vaultandcurlalone.
Set up the lab
Clone the learn-vault-agentic-iam repository containing Docker Compose configuration, initialization scripts, ACL policies, and helper scripts for the lab environment.
Clone the repository.
$ git clone \ https://github.com/hashicorp-education/learn-vault-agentic-iamChange into the project directory.
$ cd learn-vault-agentic-iamExport your Vault Enterprise license to enable enterprise features in the container.
$ export VAULT_LICENSE=3894JFNCOFFEE...Start the Vault server and supporting containers in detached mode.
$ docker compose up -dWait for the initialization to complete by checking the server logs.
$ docker compose logs vault-init | grep 'Vault lab is preconfigured and ready' vault-init-1 | [bootstrap] Vault lab is preconfigured and ready.If you do not find this message, wait a moment and run the command again.
Configure your shell environment by sourcing the environment file.
$ source ./env.sh VAULT_ADDR = https://127.0.0.1:8200 VAULT_CACERT = /path/to/project/tls/vault-ca.pemVerification: Confirm
VAULT_ADDRis set tohttps://127.0.0.1:8200andVAULT_CACERTpoints to the CA certificate file.Verify Vault server is running and unsealed.
$ vault statusExpected result: Output shows
Sealed: falseandVersion: 2.0.2or later.Authenticate to Vault as the operator persona.
$ vault login -method=userpass \ username=oliverWhen prompted for a password, enter:
oliver-example-YgGk0ycnbrUOOEuxVerification: Successful login displays
Success! You are now authenticated.Verify ACL policies are loaded.
$ vault policy listExpected result: Output includes
danielle-developerandagent-repo-reviewer-ceilingpolicies.
Your lab environment is ready for you to complete the scenario steps.
How the request flow works
Before you build anything, it helps to know where each control lives.
Vault acts as an OAuth 2.0 resource server: it accepts the identity
provider's JWT directly in the X-Vault-Token header on every request,
with no separate vault login step.
The JWT acts as the Vault token in this workflow.
Related OAuth 2.0 specifications:
- RFC 9068 - JWT Profile for OAuth 2.0 Access Tokens defines the JWT format Vault validates
- RFC 9126 - OAuth 2.0 Pushed Authorization Requests (PAR) enables attaching RAR claims to authorization requests
- RFC 9396 - OAuth 2.0 Rich Authorization Requests (RAR) provides fine-grained authorization through detailed permission claims
- RFC 8693 - OAuth 2.0 Token Exchange defines the on-behalf-of delegation pattern with
actclaims


When Vault receives an on-behalf-of token (a JWT that contains an act claim),
it authorizes the request as follows.
Vault resolves the subject from the
subclaim to a Vault entity, which contributes that person's baseline policies.Vault resolves the actor (the agent) from the
act.subclaim to a Vault entity. That entity must have an Agent Registry record, which contributes its ceiling policies. Vault ignores the actor's own baseline policies, and only the ceiling policies apply for an agent.Vault allows the request only if both the subject baseline and the ceiling permit it.
When the token carries an
authorization_details(RAR) claim, the request must match one of the entries listed there. If the claim isn't present, Vault doesn't apply this narrowing and instead grants those permissions that the subject and the ceiling policy have in common.
You can demonstrate all four of these behaviors in the lab scenario.
Configure Auth0
(Operator persona)
Oliver now configures Auth0 so it can issue RFC 9068 access tokens for Vault, and so users can mint RAR tokens for their agents.
Enable Pushed Authorization Requests on the tenant
Enable Pushed Authorization Requests (PAR) tenant-wide. PAR (defined in RFC 9126) is required to attach RAR claims because standard authorization requests cannot carry complex JSON structures.
Log in to your Auth0 Dashboard.
Navigate to Settings > Advanced.

Locate Allow Pushed Authorization Requests (PAR) and switch the toggle ON. Auth0 saves the setting automatically.

Create the API
Navigate to Applications > APIs.
Click Create API.
For Name, enter
vault-api.For Identifier, enter the Vault API address:
https://localhost:8200/v1. The identifier value becomes the JWTaudclaim.For JSON Web Token (JWT) Profile, select RFC 9068.

Scroll down and click Create.
Click the Settings.
Scroll down to RBAC Settings.
Click the toggle to switch on Enable RBAC.
Click the toggle to switch on Add Permissions in the Access Token.

Click Save.
Create the application
Navigate to Applications > Applications, then click Create Application.

For Application name, enter
HashiCups Vault.For application type, choose Regular Web Application.
Click Create.

Click Settings.
Click the copy icon next to the value in the Client ID field. Paste and save this value somewhere safe for use later.
Click the copy icon next to the value in the Client Secret field. Paste and save this value somewhere safe for use later.

Scroll to Application URIs.
In Allowed Callback URLs, enter the
oauth2ccallback URL:http://localhost:9876/callback.
Scroll to the Authorization Requests section and turn ON Require Pushed Authorization Requests (PAR) so PAR is mandatory for this application.

Click Advanced Settings.
Click OAuth.
In Allowed APPs/APIs, enter the Vault API address:
https://localhost:8200/v1.
Open the Grant Types tab and ensure Authorization Code is checked.

Open the Endpoints tab and record both the OAuth Authorization URL and the JSON Web Key Set URL values for use later.

Click Save.
Register the RAR authorization details type
Rich Authorization Requests (RAR) require Auth0 to recognize the custom authorization details type that Vault expects (vault:path_access). With PAR enabled, register an authorization details type named vault:path_access for your tenant.
Navigate to Auth0 Dashboard > Applications > APIs.
Click vault-api.
Click Permissions.
Under Add an Authorization Details type in the Type field, enter
vault:path_access.Click +Add and the authorization details type setting gets automatically saved.
Grant application access
With the API and application both created, you need to grant client access from the API to the application.
Navigate to Applications > APIs.
Click vault-api.
Click Application Access.
Next to the HashiCups Vault application, click Edit.
Click Grant Access.
Click the Always grant all permissions checkbox.

Click Save.
Allow the application to request the RAR type
The Always grant all permissions checkbox only grants OAuth scopes.
It does not allow the application to request the vault:path_access
authorization details (RAR) type.
Whether an application may request a registered authorization_details type is
controlled separately by the API's User-Delegated Access policy:
All apps allowed (
allow_all): any application may request everyauthorization_detailstype registered on the API. No per-application configuration is required, and you can set this entirely in the Dashboard.Per-app authorization (
require_client_grant): eachauthorization_detailstype must be explicitly listed in the application's client grant underauthorization_details_types. This field is not exposed in the Dashboard and can only be set through the Auth0 Management API.
For this tutorial, set the policy to All apps allowed so you can stay in the Dashboard:
Navigate to Applications > APIs and click vault-api.
Click the Settings tab and scroll to Application Access Policy.
Set User-Delegated Access to All apps allowed.
Click Save.
Create the agent user
Navigate to User Management > Users.

Click Create User and select Create via UI.
For Email, enter an email address; you can use an
@example.comaddress for the purposes of this tutorial.For Password, enter a strong password and record it somewhere safe to use in later steps.

Click Create.
On the user page, click Edit next to Email.

Set email as verified.

Capture a baseline token
Export the Auth0 values you recorded, then run oauth2c to confirm the trust
and to capture the iss and sub claims you need for the Vault profile and entity aliases.
Export the Auth0 configuration values, replacing placeholders with your actual values from the Auth0 application settings.
$ export APP_AUTH_URL="https://YOUR_TENANT.us.auth0.com/authorize" \ APP_CLIENT_ID="<application client id>" \ APP_CLIENT_SECRET="<application client secret>" \ APP_JWKS_URI="https://YOUR_TENANT.us.auth0.com/.well-known/jwks.json"The export command does not produce any output.
Generate an access token using the OAuth 2.0 authorization code flow with PKCE and PAR.
$ oauth2c "${APP_AUTH_URL}" \ --pkce \ --par \ --client-id "${APP_CLIENT_ID}" \ --client-secret "${APP_CLIENT_SECRET}" \ --response-types code \ --response-mode query \ --grant-type authorization_code \ --audience https://localhost:8200/v1 \ --auth-method client_secret_basicoauth2copens a browser. Sign in with the email address and password you set in the create an agent user section. After successful sign-in, the terminal prints a success message along with the decoded access token:SUCCESS Exchanged authorization code for access tokenExample token values:
{ "aud": "https://localhost:8200/v1", "client_id": "EBYeLHzuLqSeyUYLY7EC3hqRgvaiS9Dv", "exp": 1782402664, "iat": 1782316264, "iss": "https://hashicups-dev.us.auth0.com/", "jti": "baFC3mowpruhSr1JdDRhbJ", "permissions": [], "sub": "auth0|c0ff33a2dc1f8e9de0dd95d7" }Record the
issandsubvalues and export them. Thesubvalue contains a pipe character, so wrap it in single quotes so the shell doesn't use it.$ export APP_ISS_URL="https://hashicups-dev.us.auth0.com/" \ APP_JWT_SUB='auth0|c0ff33a2dc1f8e9de0dd95d7'This
subis the agent's external identity. In the create the subject and actor entities and aliases step you will create a separate external identity for Danielle the human subject.
Create the OAuth resource server profile
(Operator persona)
The profile is the trust anchor: it tells Vault which issuer to trust, where to fetch signing keys, which audience to require, and which algorithms to accept.
Create the profile definition using JWKS mode to fetch signing keys from Auth0. The profile accepts both
ES256andPS256algorithms as Auth0 recommends.$ cat > danielle-agent-profile.json <<EOF { "issuer_id": "${APP_ISS_URL}", "use_jwks": true, "audiences": ["https://localhost:8200/v1"], "user_claim": "sub", "supported_algorithms": ["ES256", "PS256"], "jwks_uri": "${APP_JWKS_URI}" } EOFWrite the profile.
$ vault write sys/config/oauth-resource-server/danielle-agent-profile \ @danielle-agent-profile.jsonExample output:
Success! Data written to: sys/config/oauth-resource-server/danielle-agent-profileVerify the profile was created successfully and note the generated
config_id.$ vault read sys/config/oauth-resource-server/danielle-agent-profileKey Value --- ----- audiences [https://localhost:8200/v1] clock_skew_leeway 0 config_id d2838e74-86b6-ef9f-5a8c-c0ff33e095fc enabled true issuer_id https://hashicups-dev.us.auth0.com jwks_uri https://hashicups-dev.us.auth0.com/.well-known/jwks.json jwt_type access_token no_default_policy false profile_name danielle-agent-profile supported_algorithms [ES256 PS256] use_jwks true user_claim subVerification: Confirm
enabledistrueandconfig_idis a valid UUID. Ifenabledisfalse, verify the oauth-resource-server activation flag is set correctly.Export the
config_idfor use in creating entity aliases.$ export APP_CONFIG_ID="d2838e74-86b6-ef9f-5a8c-c0ff33e095fc"Export the full mount accessor as the value of
MOUNT_ACCESSOR.$ export MOUNT_ACCESSOR="oauth-resource-server_root_${APP_CONFIG_ID}"
Create entities and aliases
(Operator persona)
An on-behalf-of flow involves two identities:
- The subject: Danielle, with a broad baseline policy.
- The actor: in on-behalf-of (OBO) scenarios, agent permissions are granted based on the intersection of the user policy and the agent ceiling policy. RAR enables further constraint of these policies.
Create a Vault entity to represent Danielle's human identity.
$ vault write identity/entity \ name="danielle-human" \ policies="danielle-developer"Example output:
Key Value --- ----- aliases <nil> id 1b75bc66-28e3-9126-5c18-c0ff33d83c5c name danielle-humanCreate a Vault entity to represent the agent identity, with no additional ACL policies.
$ vault write identity/entity name="pr-reviewer-agent"Example output:
Key Value --- ----- aliases <nil> id 0ca161e7-a74e-2827-f3a1-c0ff331f1599 name pr-reviewer-agentExport both entity IDs for the subject (Danielle) and the actor (Danielle's agent) from the command output.
$ export SUBJ_ID="1b75bc66-28e3-9126-5c18-c0ff33d83c5c" \ ACTOR_ID="0ca161e7-a74e-2827-f3a1-c0ff331f1599"Set the external IDs. These are the
subvalues your identity provider issues for the human and the agent. For the Auth0 path, the agent external ID is thesubyou captured in the Capture a baseline token step. Choose a distinct subject identity for Danielle.$ export SUBJECT_EXTERNAL_ID='auth0|danielle-human-0001' \ ACTOR_EXTERNAL_ID="${APP_JWT_SUB}"Create the entity aliases that bind each external subject to its entity through the OAuth resource server mount.
Create the subject entity alias.
$ vault write identity/entity-alias \ name="${SUBJECT_EXTERNAL_ID}" \ canonical_id="${SUBJ_ID}" \ mount_accessor="${MOUNT_ACCESSOR}" \ issuer="${APP_ISS_URL}" \ external_id="${SUBJECT_EXTERNAL_ID}"Example output:
Key Value --- ----- canonical_id 1b75bc66-28e3-9126-5c18-c0ff33d83c5c id 5d463c84-d185-90df-0caf-c0ff33d0e165Create the actor entity alias.
$ vault write identity/entity-alias \ name="${ACTOR_EXTERNAL_ID}" \ canonical_id="${ACTOR_ID}" \ mount_accessor="${MOUNT_ACCESSOR}" \ issuer="${APP_ISS_URL}" \ external_id="${ACTOR_EXTERNAL_ID}"Example output:
Key Value --- ----- canonical_id 0ca161e7-a74e-2827-f3a1-c0ff331f1599 id a89e280e-f4b8-fe5d-3e79-c0ff33c7366c
Register the agent in the Agent Registry
(Operator persona)
Only the actor entity needs a registration record. Registering it both marks the entity as an approved agent (a requirement for OAuth credential use) and attaches the ceiling policies that cap what the agent can ever do.
Create the registration payload defining the agent's display name, entity ID, and ceiling policies.
$ cat > registration_payload.json <<EOF { "display_name": "pr-reviewer-agent", "entity_id": "${ACTOR_ID}", "description": "HashiCups PR review agent (on behalf of Danielle)", "ceiling_policies": ["agent-repo-reviewer-ceiling"] } EOFRegister the agent.
$ vault write agent-registry/register @registration_payload.jsonKey Value --- ----- display_name pr-reviewer-agent id 3487bf8a-62b9-db0a-2117-c0ff3370db84Read the registration back to confirm the ceiling policies. Vault automatically adds the
defaultanddefault-ceilingpolicies unless you setno_default_ceiling_policy.$ vault read agent-registry/registration/display-name/pr-reviewer-agentKey Value --- ----- ceiling_policies [agent-repo-reviewer-ceiling default default-ceiling] creation_time 2026-06-24T16:07:36Z description HashiCups PR review agent (on behalf of Danielle) display_name pr-reviewer-agent entity_id 0ca161e7-a74e-2827-f3a1-c0ff331f1599 id 3487bf8a-62b9-db0a-2117-c0ff3370db84 last_updated_time 2026-06-24T16:07:51Z no_default_ceiling_policy false
The operator setup is complete. Vault now trusts Auth0 tokens, knows which entity is the human and which is the agent, and enforces the agent's ceiling on every delegated request.
Understand the delegated RAR token
(Developer persona)
Rich Authorization Requests (RAR, RFC 9396) enable fine-grained authorization by allowing clients to specify detailed permissions in the authorization request. Pushed Authorization Requests (PAR, RFC 9126) attach the authorization_details (RAR) claim to an authorization request. You exercise the same flow the oauth2c baseline used earlier, but you push the parameters to Auth0 first and add the RAR claim.
{
"iss": "https://hashicups-dev.us.auth0.com/",
"aud": "https://localhost:8200/v1",
"sub": "auth0|danielle-human-0001",
"act": { "sub": "auth0|c0ff33a2dc1f8e9de0dd95d7" },
"authorization_details": [
{
"type": "vault:path_access",
"path": "hashicups-kv/data/repo/github-token",
"action": "read"
}
],
"iat": 0, "nbf": 0, "exp": 0, "jti": "unique"
}
subidentifies the subject (Danielle). Vault maps it to their entity and baseline policies.act.subidentifies the actor (LLM agent). Its presence is what makes this an on-behalf-of token. Vault maps it to the registered agent entity and its ceiling policies.authorization_detailsis the RAR claim for the per-session scope.
The RAR claim is an array of objects, and each object uses exactly these fields:
| Field | Required value | Notes |
|---|---|---|
type | vault:path_access | Must match exactly. Other values grant nothing, so the request is denied. |
path | the exact Vault API path | For KV v2, include the data/ segment, for example hashicups-kv/data/repo/github-token. |
action | a single capability string | One of read, create, update, delete, list, sudo, or deny (lowercase). |
Mint a delegated RAR token
(Developer persona)
Pushed Authorization Requests attach the authorization_details (RAR) claim to
an authorization request. You exercise the same flow the oauth2c baseline used
earlier, but you push the parameters to Auth0 first and add the RAR claim.
The flow has three steps: push the request, authorize it in a browser, then exchange the returned code for an access token. Use the helper script in this section to generate the PKCE pair, push the request, and print only the browser URL you need to open.
Use the
./scripts/rar-authorize-url.shhelper script to push the authorization request to Auth0's PAR endpoint with the RAR claim inauthorization_details.You must then immediately open the output URL.
$ bash ./scripts/rar-authorize-url.sh https://hashicups-dev.us.auth0.com/authorize?client_id=...&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Ac0ff33...Open the printed authorize URL in a browser to approve the pushed request. Only the
client_idandrequest_uriappear in the URL; the RAR claim travels inside the pushed request.Sign in as the agent user if necessary and click Accept. Auth0 then redirects to
http://localhost:9876/callback?code=.... Because no server listens on that port, the browser shows a connection error, but the address bar still holds thecodevalue.Copy it from the address bar and export it as the value to the
AUTH_CODEenvironment variable.$ export AUTH_CODE="c0ff33..."Source the
.rar-pkce.envfile to populate necessary Application, challenge, and request URI values in the shell environment.$ source .rar-pkce.envThis command produces no output.
Exchange the authorization code for an access token, presenting the PKCE
code_verifiergenerated by the helper script.$ export DELEG="$(curl -s -X POST "https://${APP_DOMAIN}/oauth/token" \ -d "grant_type=authorization_code" \ -d "client_id=${APP_CLIENT_ID}" \ -d "client_secret=${APP_CLIENT_SECRET}" \ -d "code=${AUTH_CODE}" \ -d "redirect_uri=http://localhost:9876/callback" \ -d "code_verifier=${CODE_VERIFIER}" | jq -r '.access_token')"The
$DELEGvariable now holds an access token that carries the RAR claim.
To carry the act (on-behalf-of) claim in addition to the RAR claim, Auth0
must perform an RFC 8693 token exchange where the agent client exchanges
Danielle's subject token for a delegated token whose sub is Danielle and whose
act.sub is the agent. Configure an Auth0 Custom Token Exchange profile (or
an Action that injects act during the exchange), then use the exchanged token
as $DELEG.
Demonstrate the enforcement
(Developer persona)
With a delegated token in $DELEG, prove that the subject baseline, agent ceiling, and per-session RAR work together. Mint a fresh token for each scenario and observe the result.
Row 1: ALLOW. Read the repository token with a RAR claim scoped to exactly that path. The subject allows it, the ceiling allows it, and the RAR matches.
$ VAULT_TOKEN="$DELEG" vault read hashicups-kv/data/repo/github-token
Example output:
Key Value
--- -----
data map[token:ghp_demoRepoReadToken123]
metadata map[created_time:2026-06-16T18:36:33.963974Z ... version:1]
Row 2: Denied by the ceiling. Mint a token that tries to write the repository token, then attempt the write. Danielle can write, but the agent's ceiling is read-only.
$ DELEG_WRITE="$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"hashicups-kv/data/repo/github-token","action":"update"}]')"
$ echo '{"token":"x"}' | VAULT_TOKEN="$DELEG_WRITE" vault write hashicups-kv/data/repo/github-token -
Example output:
Code: 403. Errors:
* 1 error occurred:
* permission denied
Row 3: Denied by the ceiling. Attempt to read a production secret. Danielle can read it, but the agent's ceiling is repo-only.
$ DELEG_PROD="$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"hashicups-kv/data/prod/db-password","action":"read"}]')"
$ VAULT_TOKEN="$DELEG_PROD" vault read hashicups-kv/data/prod/db-password
Example output:
Code: 403. Errors:
* 1 error occurred:
* permission denied
Row 4: Denied by RAR. Use a token whose durable policies would allow the read, but whose RAR claim is scoped to the production path instead of the repo path. Even though subject ā© ceiling would permit the repo read, the RAR claim does not match and Vault denies it.
$ DELEG_RAR_PROD="$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"hashicups-kv/data/prod/db-password","action":"read"}]')"
$ VAULT_TOKEN="$DELEG_RAR_PROD" vault read hashicups-kv/data/repo/github-token
Example output:
Code: 403. Errors:
* 1 error occurred:
* permission denied
These four results map directly to the three controls:
| Request | Subj | Ceiling | RAR | Result |
|---|---|---|---|---|
read repo/github-token | allow | allow | allow | ALLOW |
write repo/github-token | allow | deny | n/a | deny |
read prod/db-password | allow | deny | n/a | deny |
read repo/github-token | allow | allow | deny | deny |
Inspect the audit log
(Developer persona)
The audit device records the delegation and RAR metadata, which is the proof that the controls combined as designed. Find the response entry for the successful repo read.
$ grep '"type":"response"' ./vault/logs/vault-server-audit-log.json \
| grep 'repo/github-token' \
| jq 'select(.auth.policy_results.allowed==true)
| {granting_policies: .auth.policy_results.granting_policies,
metadata: .auth.metadata}' \
| tail -n 20
{
"granting_policies": [
{ "type": "" },
{ "type": "" },
{ "name": "danielle-developer", "namespace_id": "root", "type": "acl" },
{ "name": "agent-repo-reviewer-ceiling", "namespace_id": "root", "type": "acl" }
],
"metadata": {
"actor_entity_id": "0ca161e7-a74e-2827-f3a1-c0ff331f1599",
"actor_entity_name": "pr-reviewer-agent",
"enterprise_token_audience": "[\"https://localhost:8200/v1\"]",
"enterprise_token_authorization_details": "[{\"action\":\"read\",\"path\":\"hashicups-kv/data/repo/github-token\",\"type\":\"vault:path_access\"}]",
"enterprise_token_issuer": "https://hashicups-dev.us.auth0.com/",
"enterprise_token_metadata": "t1781792504"
}
}
Three fields in this entry make the design auditable:
actor_entity_idandactor_entity_nameappear just for on-behalf-of tokens. The delegation is visible and attributable to a specific agent.granting_policieslists both the subject policy (danielle-developer) and the ceiling policy (agent-repo-reviewer-ceiling), making the intersection tangible.enterprise_token_authorization_detailsrecords the exact RAR scope for the call, which is invaluable for incident review: you can see precisely what the agent was permitted to touch on each request.
Refer to the audit log entry schema for the full list of OAuth JWT metadata fields.
Clean up
Follow these steps to remove the artifacts and resources created in the tutorial lab scenario.
Stop containers and remove all data.
$ docker compose down --remove-orphansRemove the Vault data, logs, and initialization file.
$ rm -rf tls/* logs/* vault/data vault/init/vault-init.jsonRemove the
.rar-pkce.envfile.$ rm -f .rar-pkce.envUnset the environment variables in your terminal session.
$ unset VAULT_LICENSE VAULT_ADDR VAULT_CACERT ACTOR_ID ACTOR_EXTERNAL_ID \ APP_CLIENT_ID APP_CLIENT_SECRET APP_JWKS_URI APP_AUTH_URL APP_ISS_URL \ APP_JWT_SUB APP_CONFIG_ID MOUNT_ACCESSOR SUBJ_ID SUBJECT_EXTERNAL_ID \ AUTH_CODE DELEGRemove the Auth0 application, API, and user if you no longer need them.
Troubleshoot
Use this table of common symptoms, root causes, and potential fixes to help troubleshoot your lab setup.
| Symptom | Cause | Fix |
|---|---|---|
no alias found / error looking up entity on JWT auth | Entity alias created without issuer= | Recreate the alias with issuer="${APP_ISS_URL}". |
Auth0 shows Oops!, something went wrong after opening the PAR authorize URL | The request_uri expired or was already used | Run the PAR command again and open the new authorize URL immediately. Do not test or refresh the authorize URL before signing in; the request_uri is short-lived and single use. |
permission denied whenever a RAR claim is present | wrong RAR shape (path/secret_path/capabilities[], or type is not vault:path_access) | Use type=vault:path_access, path=<exact path>, action=<single capability>. |
| Auth0 returns an opaque token, not a JWT | Audience not requested | Pass --audience https://localhost:8200/v1. |
| "PAR feature is not enabled for this tenant" | Tenant lacks PAR | Enable PAR for the Auth0 tenant. |
vault kv get fails with an external token but vault read works | the kv helper runs an unauthenticated preflight | Use vault read/vault write against the explicit data/ path. |
| Ceiling change appears ignored | Ceiling policies only restrict on-behalf-of (act) requests | Confirm the token actually carries act.sub. |
| Issuer mismatch | Profile issuer_id versus JWT iss trailing slash or case | Vault normalizes by trimming the trailing slash and lowercasing; align the rest. |
Summary
In this tutorial, you configured Vault Enterprise as an OAuth resource server, registered an LLM coding agent in the Agent Registry with ceiling policies.
You also minted delegated Rich Authorization Request tokens that scope the agent access to the intersection of a human user's baseline ACL policies, the agent's ceiling policies, and a per-session constraint.
You then confirmed enforcement and inspected the audit trail that makes every delegated action attributable.
Additional resources
- Read the concepts documentation:
- Read the API documentation for complete request and response schemas:
Next steps
Foundation skills
- Learn to build ACL policies for baseline and ceiling permissions that define what humans and agents can access.
- Configure OIDC authentication to connect additional identity providers beyond Auth0.
Advanced patterns
- Explore Agent Registry concepts for designing ceiling policies for your own non-human identities.
- Implement cross-namespace agent delegation patterns for multi-tenant environments.
- Design custom ceiling policy frameworks that adapt to different agent types and use cases.
Production deployment
- Review OAuth resource server production deployment considerations including key rotation and token refresh strategies.
- Set up monitoring and alerting for agent activity using audit logs and metrics.
- Integrate with popular CI/CD platforms to secure pipeline agents.
Related use cases
This pattern applies to any agent requiring credentials:
- CI/CD pipeline agents accessing deployment secrets
- Monitoring agents reading health check credentials
- Automation tools performing scheduled maintenance
- Infrastructure agents managing cloud resources
Appendix A: Local signing key example
This appendix sets up a deterministic harness that exercises the complete on-behalf-of, ceiling, and RAR flow without depending on Auth0 RAR support or token exchange. It works by configuring a static-key OAuth resource server profile that trusts a local ECDSA signing key, then minting tokens with that key.
Generate a signing key and static-key profile
Generate an ECDSA private key for local token signing.
$ openssl ecparam -name prime256v1 -genkey -noout -out ec-priv.pem
$ openssl ec -in ec-priv.pem -pubout -out ec-pub.pem
$ cat > local-profile.json <<EOF
{
"issuer_id": "https://hashicups-dev.us.auth0.com/local-test",
"use_jwks": false,
"audiences": ["https://localhost:8200/v1"],
"user_claim": "sub",
"supported_algorithms": ["ES256"],
"public_keys": [ { "key_id": "local-test-key-1", "pem": $(jq -Rs . < ec-pub.pem) } ]
}
EOF
$ vault write sys/config/oauth-resource-server/local-obo-test \
@local-profile.json
Create the subject and actor entities and aliases against this profile's mount
accessor (oauth-resource-server_root_<config_id> of local-obo-test), and
register the actor, exactly as prior steps, but use the external IDs
auth0|danielle-human-0001 (subject) and auth0|pr-reviewer-agent-0001
(actor), and remember to set issuer="https://hashicups-dev.us.auth0.com/local-test" on both aliases.
Mint delegated RAR tokens
Export the authorization details JSON array as the value to the
APP_AUTH_DETAILS environment variable.
$ export APP_AUTH_DETAILS=<authorization_details_json_array>
Use the convenience script, ./scripts/mint-token.sh to mint a delegated
RAR token.
$ ./scripts/mint-token.sh "${APP_AUTH_DETAILS}"
Run the enforcement matrix
You can test enforcement of the ceiling policies in the OBO workflow examples using these steps.
Export the GitHub token secret path as the value of the
APP_REPO_TOKENenvironment variable.$ export APP_REPO_TOKEN="hashicups-kv/data/repo/github-token"Export the database password secret path as the value of the
APP_DB_CREDSenvironment variable.$ export APP_DB_CREDS="hashicups-kv/data/prod/db-password"
Row 1: ALLOW
$ RAR_TOKEN=$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"'$APP_REPO_TOKEN'","action":"read"}]')
$ VAULT_TOKEN="$RAR_TOKEN" vault read "$APP_REPO_TOKEN"
Row 2: deny (ceiling is read-only)
$ RAR_TOKEN=$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"'$APP_REPO_TOKEN'","action":"update"}]')
$ echo '{"token":"x"}' | VAULT_TOKEN="$RAR_TOKEN" vault write "$APP_REPO_TOKEN" -
Row 3: deny (ceiling is repo-only)
$ RAR_TOKEN=$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"'$APP_DB_CREDS'","action":"read"}]')
$ VAULT_TOKEN="$RAR_TOKEN" vault read "$APP_DB_CREDS"
Row 4: deny (RAR scoped elsewhere)
$ RAR_TOKEN=$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"'$APP_DB_CREDS'","action":"read"}]')
$ VAULT_TOKEN="$RAR_TOKEN" vault read "$APP_REPO_TOKEN"
The expected results are ALLOW, deny, deny, and deny.
Wire the token into OpenCode
The final step replaces the unsafe environment-file pattern with a delegated
token. The integration is intentionally thin: a wrapper script obtains a
short-lived delegated RAR token, exports it as VAULT_TOKEN, and launches the
agent.
The agent reads the repository token from Vault at session start instead of from disk.
Create a wrapper script
run-agent.shthat mints a delegated token scoped to the single secret the review session needs.cat > run-agent.sh << 'EOF' #!/usr/bin/env bash set -euo pipefail export VAULT_ADDR="https://127.0.0.1:8200" export VAULT_CACERT = ./tls/vault-ca.pem # Mint a per-session delegated RAR token with the local signing key (no Auth0 needed). DELEG="$(bash ./scripts/mint-token.sh '[{"type":"vault:path_access","path":"hashicups-kv/data/repo/github-token","action":"read"}]')" export VAULT_TOKEN="$DELEG" exec opencode run "$@" EOFMake the script executable.
$ chmod +x run-agent.shConfigure the agent's skill or tool to fetch the repository token from Vault at the start of the session:
$ vault read hashicups-kv/data/repo/github-tokenLaunch a review session:
$ ./run-agent.sh "Confirm that you can access the github-token for PR review by running vault read hashicups-kv/data/repo/github-token. Just answer yes or no as to whether you have access."
The agent now receives a token that is read-only, repo-only, scoped to a single secret, and short-lived. Every operation gets attributed to both Danielle and the agent in the audit log.