Set up OBO delegation
Enterprise
Appropriate Vault Enterprise license or HCP Vault Dedicated cluster required.
Use Vault Enterprise and your ID provider to implement zero trust principles with delegated ephemeral and narrowly scoped agent credentials. Accept JWTs with RAR authorization details, and OBO semantics as means to perform authorized operations on Vault.
Before you start
You must have a Vault Enterprise or HCP Vault Dedicated server deployed.
You must configure an identity or OAuth provider that supports OBO:
You must have admin permissions on Vault. You need a Vault token with sufficient ACL policies attached to configure and modify your Vault deployment.
Step 1: Create an OAuth resource server profile
Create an OAuth resource server configuration file called
verify-oauth.jsonusing the environment variables you saved during provider setup:$ cat > verify-oauth.json << EOF { "issuer_id": "${ISSUER}", "use_jwks": true, "user_claim": "sub", "supported_algorithms": ["RS256"], "jwks_uri": "${JWKS_URI}" } EOFCreate an OAuth resource server profile using the JSON file:
$ vault write sys/config/oauth-resource-server/verify @verify-oauth.json WARNING! The following warnings were returned from Vault: * algorithm "RS256" is functional but consider using PS256 (RSA-PSS) for better security * profile supports only one algorithm family - consider supporting multiple families for flexibilityRead the profile to check for correctness.
$ vault read sys/config/oauth-resource-server/verify Key Value --- ----- audiences [https://localhost:8200/v1] clock_skew_leeway 0 config_id de81dc66-e00d-23c6-7dfc-9cdbc5aaf97e enabled true issuer_id https://mydomain.verify.ibm.com/oauth2 jwks_uri https://mydomain.verify.ibm.com/oauth2/jwks jwt_type access_token no_default_policy false profile_name verify supported_algorithms [RS256] use_jwks true
Step 2: Create Vault entities for the external users
Map your external agent and human users to Vault entities.
Create a new subject entity in for the human user.
$ vault write identity/entity name="obo-human" external_id="${SUBJECT_EXT_ID}" Key Value --- ----- aliases <nil> id 0427b0f5-ac11-4f25-75f6-5764b85176aa name obo-humanCreate a new actor entity for the AI agent user.
$ vault write identity/entity name="obo-agent" external_id="${ACTOR_EXT_ID}" Key Value --- ----- aliases <nil> id ef897bd9-0060-1fa4-68ba-65ae5b0747ec name obo-agentSave the IDs created for the human and agent entities to environment variables for later use:
$ export OBO_HUMAN_CANONICAL_ID="0427b0f5-ac11-4f25-75f6-5764b85176aa" \ OBO_AGENT_CANONICAL_ID="ef897bd9-0060-1fa4-68ba-65ae5b0747ec"
Step 3: Create entity aliases
Create aliases for the newly created Vault entities.
Normally, creating an entity alias requires a mount_accessor value. If you
provide both an external ID and an issuer that matches your OAuth resource
server profile, Vault synthesizes the correct mount accessor for you when you
create aliases for agentic IAM.
Instead of associating the generated alias with an auth mount, the alias points to the issuer that grants access for the alias. Vault uses the following format for the mount accessor synthesis:
oauth-resource-server + _ + <namespace ID> + _ + <server profile config ID>
For example, a mount accessor in the root namespace would be:
oauth-resource-server_root_de81dc66-e00d-23c6-7dfc-9cdbc5aaf97e
Create an entity alias for the human user entity:
$ vault write identity/entity-alias \ name="jwt-binding-subject" \ canonical_id="${OBO_HUMAN_CANONICAL_ID}" \ issuer="${ISSUER}" \ external_id="${SUBJECT_EXT_ID}" Key Value --- ----- canonical_id 0427b0f5-ac11-4f25-75f6-5764b85176aa id e3b9d5b0-c623-1f02-b1ef-95503ad7be03Create an entity alias for the agent entity:
$ vault write identity/entity-alias \ name="jwt-binding-actor" \ canonical_id="${OBO_AGENT_CANONICAL_ID}" \ issuer="${ISSUER}" \ external_id="${ACTOR_EXT_ID}" Key Value --- ----- canonical_id ef897bd9-0060-1fa4-68ba-65ae5b0747ec id e929f437-f655-45c0-03e7-eb6212dba58d
Step 4: Create a ceiling policy
Create a ceiling policy to encompass all the possible operations the agent might perform.
Create a policy configuration file called
ceiling-policy.hcl. For example, to restrict access on thekv,pki, andtransitplugins:$ cat > ceiling-policy.hcl << EOF # Allow KV2 read and write path "secret-v2/data/shared-secret/*" { capabilities = ["read", "list", "update"] } path "secret-v2/metadata/shared-secret/*" { capabilities = ["list"] } # Allow PKI operations path "pki/issue/example-dot-com" { capabilities = ["create", "update"] } path "pki/cert/*" { capabilities = ["read"] } path "pki/certs" { capabilities = ["list"] } # Allow transit engine operations path "transit/encrypt/app-key" { capabilities = ["update", "create", "read"] } # Allow decrypt path "transit/decrypt/app-key" { capabilities = ["update", "create", "read"] } # Optional: allow key metadata read (safe) path "transit/keys/app-key" { capabilities = ["read"] } EOFWrite the ceiling policy to Vault:
$ vault policy write ceiling-all ceiling-policy.hcl Success! Uploaded policy: ceiling-all
Step 5: Register the agent with Vault
Create an entry in the Vault agent registry using the canonical ID of the agent entity and your ceiling policy:
$ vault write agent-registry/register \
display_name="<descriptive_name>" \
description="<meaningful_description>" \
entity_id="${OBO_AGENT_CANONICAL_ID}" \
ceiling_policies='["ceiling-all"]'
For example:
$ vault write agent-registry/register \
display_name="obo-danielle-agent" \
description="Personal code review agent" \
entity_id="${OBO_AGENT_CANONICAL_ID}" \
ceiling_policies='["ceiling-all"]'
Key Value
--- -----
display_name obo-danielle-agent
id ef897bd9-0060-1fa4-68ba-65ae5b0747ec
Step 6: Create temporary resources for testing
You can use KV version 2 operations with delegated RAR tokens to validate the ACL and ceiling policies. Vault does not consider actor (agent) policies when performing OBO operations. Vault only uses the intersection of the ceiling policy, policies assigned to the human user, and RAR constraints to evaluate agent access.
Enable a KV version 2 secrets engine at the path
secret-v2.$ vault secrets enable -path=secret-v2 kv-v2 Success! Enabled the kv-v2 secrets engine at: secret-v2/ Create a policy to perform KV operations and link it to the subject entity.Create a policy file called
kv2-policy.hclwith full access for the human (subject) entity:$ cat > kv2-policy.hcl <<'EOF' path "secret-v2/data/shared-secret/*" { capabilities = ["create", "read", "update", "delete", "list"] } path "secret-v2/metadata/shared-secret/*" { capabilities = ["list"] } EOFWrite the new policy to Vault.
$ vault policy write kv2-policy kv2-policy.hcl Success! Uploaded policy: kv2-policyCheck existing policies for the human user entity (
obo-human). Vault should return an empty list for the policy assignment:$ vault read identity/entity/name/obo-human Key Value --- ----- aliases [map[canonical_id:dfee09b9-3b37-0ff3-0324-49b4909c566c creation_time:2026-06-16T12:03:12.361169Z custom_metadata:<nil> id:124d59ef-2efe-af30-bdda-c4cc9be4e184 last_update_time:2026-06-16T12:03:12.361169Z local:false merged_from_canonical_ids:<nil> metadata:<nil> mount_accessor:oauth-resource-server_root_774005a7-6e38-d8cf-37ce-9b91b2910322 name:jwt-binding-subject]] creation_time 2026-06-16T12:03:12.085015Z direct_group_ids [] disabled false group_ids [] id dfee09b9-3b37-0ff3-0324-49b4909c566c inherited_group_ids [] last_update_time 2026-06-16T12:23:59.527254Z merged_entity_ids <nil> metadata <nil> mfa_secrets map[] name obo-human namespace_id root policies []Link the
kv-policypolicy to theobo-humanentity:$ vault write identity/entity/name/obo-human policies='kv2-policy'Verify the policy linkage:
$ vault read identity/entity/name/obo-human Key Value --- ----- aliases [map[canonical_id:dfee09b9-3b37-0ff3-0324-49b4909c566c creation_time:2026-06-16T12:03:12.361169Z custom_metadata:<nil> id:124d59ef-2efe-af30-bdda-c4cc9be4e184 last_update_time:2026-06-16T12:03:12.361169Z local:false merged_from_canonical_ids:<nil> metadata:<nil> mount_accessor:oauth-resource-server_root_774005a7-6e38-d8cf-37ce-9b91b2910322 name:jwt-binding-subject]] creation_time 2026-06-16T12:03:12.085015Z direct_group_ids [] disabled false group_ids [] id dfee09b9-3b37-0ff3-0324-49b4909c566c inherited_group_ids [] last_update_time 2026-06-16T12:23:59.527254Z merged_entity_ids <nil> metadata <nil> mfa_secrets map[] name obo-human namespace_id root policies [kv2-policy]
Step 7: Validate the OBO flow
Create an authorization details file,
rarRequest.json, that only allowscreateandreadaccess tokvsecrets:{ "type": "vault:path_access", "path": "secret-v2/data/shared-secret/foo", "capabilities": ["create", "read"] }Use
curlto request a delegation token from Verify and save it to an environment variable:$ export AGENT_TOKEN=$(curl --request POST \ --variable authz_request=[@rarRequest.json] \ --expand-data \ --data 'authorization_details=[{{authz_request}}]' \ ${ISSUER} | jq -r '.access_token)Use the delegation token from the token exchange to write a new secret:
$ echo '{"data": {"foo": "bar"}}' | \ VAULT_TOKEN=${DELEGATION_TOKEN} vault write secret-v2/data/shared-secret/foo - Key Value --- ----- created_time 2026-06-11T07:41:13.074Z custom_metadata <nil> deletion_time n/a destroyed false version 1Try to perform
vault write secretwrite on the same path to update the secret:$ echo '{"data": {"foo": "bar_new"}}' | VAULT_TOKEN=${DELEGATION_TOKEN} \ vault write secret-v2/data/shared-secret/foo - Error writing data to secret-v2/data/shared-secret/foo: Error making API request. URL: PUT http://127.0.0.1:8200/v1/secret-v2/data/shared-secret/foo Code: 403. Errors: * 2 errors occurred: * RAR_NO_MATCH: No valid authorization_details claim found matching this request. * permission deniedVault prohibits the update because the current RAR policy only allows
createandread.Update
rarRequest.jsonto includeupdateanddeletepermission:{ "type": "vault:path_access", "path": "secret-v2/data/shared-secret/foo", "capabilities": ["create", "read", "update", "delete"] }Request an updated RAR token:
$ export AGENT_TOKEN=$(curl --request POST \ --variable authz_request=[@rarRequest.json] \ --expand-data \ --data 'authorization_details=[{{authz_request}}]' \ ${ISSUER} | jq -r '.access_token)Update the existing secret:
$ echo '{"data": {"foo": "bar_new"}}' | VAULT_TOKEN=${DELEGATION_TOKEN} \ vault write secret-v2/data/shared-secret/foo - Key Value --- ----- created_time 2026-06-11T08:02:39.161111Z custom_metadata <nil> deletion_time n/a destroyed false version 2Read the new secret value with the updated token:
$ VAULT_TOKEN=${DELEGATION_TOKEN} vault read secret-v2/data/shared-secret/foo Key Value --- ----- data map[foo:bar_new] metadata map[created_time:2026-06-11T08:04:13.817972Z custom_metadata:<nil> deletion_time: destroyed:false version:2]Try to delete the secret value with the updated token:
$ VAULT_TOKEN=${DELEGATION_TOKEN} vault delete secret-v2/data/shared-secret/foo Error deleting data from secret-v2/data/shared-secret/foo: Error making API request. URL: PUT http://127.0.0.1:8200/v1/secret-v2/data/shared-secret/foo Code: 403. Errors: * 2 errors occurred: * RAR_NO_MATCH: No valid authorization_details claim found matching this request. * permission deniedVault prohibits the delete even though the current RAR policy allows
deletebecause the ceiling policy on the agent does not.