AAP 2.7 Workload Identity: Configure OIDC Credential Types for HashiCorp Vault (Step-by-Step)
By Luca Berton · Published 2026-06-30 · Category: troubleshooting
Step-by-step guide to configuring AAP 2.7 OIDC workload identity for HashiCorp Vault: Vault server setup, JWT credential types, static and templated Vault policies.
Ansible Automation Platform 2.7 introduces workload identity for HashiCorp Vault as a Technology Preview. Instead of storing static Vault tokens or credentials in AAP, the platform issues a short-lived JSON Web Token (JWT) per automation job. Vault validates that token via OIDC and returns scoped access — no static secrets to rotate, no credentials to store, no credential leakage after job completion.
This article walks through the complete configuration: Vault server setup, the two new OIDC credential types, JWT expiration behavior, and both static and templated Vault policy approaches.
How It Works
AAP Job Launch
│
├─ AAP issues short-lived JWT (signed, contains job claims)
│
├─ JWT sent to Vault via OIDC JWT auth
│
├─ Vault validates JWT against AAP's OIDC discovery URL
│ └─ Checks bound_audiences, bound_claims, expiration
│
├─ Vault applies policy → returns scoped Vault token
│
├─ AAP uses Vault token to fetch secrets
│ (passwords, API keys, SSH certificates)
│
└─ Automation runs with secrets → JWT and Vault token expireThe JWT is issued with claims that describe the automation context: organization, job template, user, playbook, launch type, instance group. Vault policies can reference these claims to enforce granular, principle-of-least-privilege access.
See also: Ansible AAP as OIDC Authentication Provider for HashiCorp Vault: Zero Trust Workflow
JWT Expiration and Timeout Behavior
AAP sets the JWT expiration to match the job's configured timeout. This aligns the token lifetime with the expected duration of the job:
| Scenario | JWT expiration |
|---|---|
| Job has a configured timeout | Equals the job timeout |
| Job has no configured timeout | Platform default: 5 minutes + 1 minute clock skew (6 min total) |
Step 1: Configure the HashiCorp Vault Server
This is a one-time setup on the Vault side. You need:
- The AAP base URL (e.g.,
https://aap.example.com) - A CA certificate if AAP uses a custom CA
Enable JWT Authentication
vault auth enable jwtPoint Vault's JWT Backend at AAP's OIDC Discovery URL
AAP's OIDC discovery URL is your instance base URL followed by /o:
https://aap.example.com/oIf AAP uses a CA-trusted certificate:
vault write auth/jwt/config \
oidc_discovery_url=https://aap.example.com/oIf AAP uses a custom CA not trusted by Vault:
vault write auth/jwt/config \
oidc_discovery_url=https://aap.example.com/o \
oidc_discovery_ca_pem=@aap-ca.pemaap-ca.pem is the PEM-encoded CA certificate that signed your AAP instance's TLS certificate.
Create a Basic JWT Role
Create at least one JWT role to complete the setup. The role binds AAP-issued JWTs to a Vault policy. bound_audiences must match the Vault server URL:
vault write auth/jwt/role/aap-example-role \
role_type=jwt \
bound_audiences="https://vault.example.com:8200" \
user_claim=sub \
policies=aap-example-policySee Step 3 for complete policy and role examples.
See also: Ansible OIDC Integration with HashiCorp Vault: Zero Trust Credential Management (Complete Guide)
Step 2: Configure AAP OIDC Credential Types
AAP 2.7 ships two new credential types for Vault OIDC authentication.
HashiCorp Vault Secret Lookup (OIDC)
Fetches Key/Value (KV) secrets from Vault — passwords, API keys, private keys, any arbitrary secret. Use this when your playbooks need to pull secrets from Vault at job time without storing them in AAP.
Configure in the AAP UI under Credentials → New Credential:
| Field | Value |
|---|---|
| Credential Type | HashiCorp Vault Secret Lookup (OIDC) |
| Vault Address | https://vault.example.com:8200 |
| JWT Role | Name of the JWT role created in Vault (e.g., aap-example-role) |
| Path to KV Secret | e.g., secret/data/production/db-password |
| KV Secret Key | e.g., password |
HashiCorp Vault Signed SSH (OIDC)
Vault digitally signs an SSH certificate that AAP uses to authenticate to remote hosts. Use this when your automation targets hosts that accept Vault-signed SSH certificates instead of pre-shared SSH keys.
| Field | Value |
|---|---|
| Credential Type | HashiCorp Vault Signed SSH (OIDC) |
| Vault Address | https://vault.example.com:8200 |
| JWT Role | Name of the JWT role in Vault |
| SSH Secret Path | Vault SSH secret engine mount path (e.g., ssh) |
| SSH Role | Vault SSH role to sign with (e.g., ansible-automation) |
Step 3: Write Vault Policies and JWT Roles
Vault is default-deny — you must explicitly grant access to every secret path. The two approaches differ in flexibility vs. simplicity.
Option A: Static Policies (Simple)
Map AAP organizations to Vault secret paths using bound_claims. Each organization gets a dedicated policy and JWT role.
# Policy for Development org: read-only access to dev secrets
vault policy write aap-development-policy - <<EOF
path "secret/data/development/*" {
capabilities = ["read"]
}
EOF
# Policy for Production org: read-only access to prod secrets
vault policy write aap-production-policy - <<EOF
path "secret/data/production/*" {
capabilities = ["read"]
}
EOF
# JWT Role for Development — bound to the Development AAP organization
vault write auth/jwt/role/aap-development-role - <<EOF
{
"role_type": "jwt",
"bound_audiences": ["https://vault.example.com:8200"],
"user_claim": "sub",
"bound_claims": {
"aap_controller_organization_name": ["Development"]
},
"policies": ["aap-development-policy"]
}
EOF
# JWT Role for Production — bound to the Production AAP organization
vault write auth/jwt/role/aap-production-role - <<EOF
{
"role_type": "jwt",
"bound_audiences": ["https://vault.example.com:8200"],
"user_claim": "sub",
"bound_claims": {
"aap_controller_organization_name": ["Production"]
},
"policies": ["aap-production-policy"]
}
EOFHow it works: When a job launches in the Development organization, AAP issues a JWT with the claim aap_controller_organization_name: Development. Vault only accepts aap-development-role for JWTs where that claim equals "Development". Even if a credential in the Development organization is mistakenly configured with aap-production-role, Vault will reject the JWT because the claim doesn't match.
The aap_controller_organization_name claim is authoritatively populated by AAP at launch time — it cannot be spoofed by the user or playbook.
Configure credentials:
- Development org jobs → use
aap-development-role - Production org jobs → use
aap-production-role
Option B: Templated Policies (Advanced)
A single policy and single JWT role that use JWT claims as variables in the Vault path. Scales to any number of organizations and job templates without creating new policies per team.
# Read the JWT auth method accessor — needed for claim-mapped path templates
JWT_ACCESSOR=$(vault auth list --format json | jq -r '."jwt/".accessor')
# Write a templated policy — org and job_template are resolved at access time
# from the JWT claim mappings defined in the role below
vault policy write aap-template-policy - <<EOF
path "secret/data/aap/{{identity.entity.aliases.${JWT_ACCESSOR}.metadata.org}}/{{identity.entity.aliases.${JWT_ACCESSOR}.metadata.job_template}}/*" {
capabilities = ["read"]
}
EOF
# Single JWT role for all allowed organizations
# claim_mappings binds JWT claims to the metadata variables used in the policy template
vault write auth/jwt/role/aap-dynamic-role - <<EOF
{
"role_type": "jwt",
"bound_audiences": ["https://vault.example.com:8200"],
"user_claim": "sub",
"bound_claims": {
"aap_controller_organization_name": ["Development", "Production", "Staging"]
},
"claim_mappings": {
"aap_controller_organization_name": "org",
"aap_controller_job_template_name": "job_template"
},
"policies": ["aap-template-policy"]
}
EOFHow it works: The claim_mappings section maps JWT claims to entity metadata aliases (org and job_template). The policy template uses those aliases to construct the secret path at access time.
For example, a job running in Production using the configure_firewall job template can only access secrets under:
secret/data/aap/Production/configure_firewall/*A job in Development using deploy_web_app can only access:
secret/data/aap/Development/deploy_web_app/*This pattern scales horizontally — adding a new organization or job template requires only organizing secrets under the correct path, not creating new Vault policies or roles.
See also: Ansible Automation Platform and HashiCorp Vault: End-to-End Trusted Automation (Integration Guide)
JWT Claims Reference
AAP populates the following claims in every workload identity JWT:
| Claim | Description | Example value |
|---|---|---|
sub | Unique job identifier | job:42 |
aap_controller_organization_name | Name of the AAP organization running the job | Production |
aap_controller_job_template_name | Name of the job template | configure_firewall |
aap_controller_user_name | Username that launched the job | jsmith |
aap_controller_playbook | Playbook file being executed | site.yml |
aap_controller_launch_type | How the job was launched | manual, scheduled, workflow |
aap_controller_instance_group_name | Instance group executing the job | production-workers |
Worked Example: Database Credentials per Job Template
Suppose you want each job template to access only its own database credentials in Vault. Using the templated policy:
Vault secret structure:
secret/data/aap/Production/deploy_web_app/db_password
secret/data/aap/Production/run_migration/db_password
secret/data/aap/Staging/deploy_web_app/db_passwordStore the secret:
vault kv put secret/aap/Production/deploy_web_app db_password="s3cur3-pr0d-p@ssw0rd"
vault kv put secret/aap/Staging/deploy_web_app db_password="st4g1ng-p@ssw0rd"AAP credential configuration (both environments use the same aap-dynamic-role):
| Field | Value |
|---|---|
| Credential Type | HashiCorp Vault Secret Lookup (OIDC) |
| Vault Address | https://vault.example.com:8200 |
| JWT Role | aap-dynamic-role |
| Path to KV Secret | secret/data/aap/{{ aap_controller_organization_name }}/{{ aap_controller_job_template_name }} |
| KV Secret Key | db_password |
deploy_web_app job in Production gets the production database password; the same job template in Staging gets the staging password — with a single credential configured in AAP.
Troubleshooting
JWT expired before secret retrieval
If jobs fail with JWT expired or permission denied during the Vault lookup phase, the control-plane secret retrieval is taking longer than the JWT lifetime.
Solution: increase the configured job timeout on the job template, which increases the JWT lifetime to match.
If no timeout is set and 6 minutes is insufficient, configure the platform default JWT lifetime via the AAP settings API (available in a future GA release of this feature).
bound_audiences mismatch
Vault rejects JWTs with audience claim does not match any bound audience. Ensure bound_audiences in the JWT role exactly matches the Vault server URL including port:
# Correct — must match https://vault.example.com:8200 exactly
vault write auth/jwt/role/aap-example-role \
bound_audiences="https://vault.example.com:8200"bound_claims rejection for correct organization
Verify the claim name and value. AAP uses aap_controller_organization_name (underscore-separated, snake_case). Check the actual claims in the JWT:
# Decode the JWT issued by AAP (base64-decode the payload)
echo "<jwt-payload>" | base64 -d | python3 -m json.toolVault not reaching AAP OIDC discovery URL
Vault must be able to reach https://aap.example.com/o/.well-known/openid-configuration. Test from the Vault host:
curl -sk https://aap.example.com/o/.well-known/openid-configuration | python3 -m json.toolIf Vault uses a custom CA, ensure oidc_discovery_ca_pem is set with the correct CA cert.
Technology Preview Status
OIDC workload identity for HashiCorp Vault is Technology Preview in AAP 2.7. Tech Preview features are functional and supported for testing and evaluation, but Red Hat does not yet recommend them for production workloads. The credential types and JWT claims structure may change before GA.
Related Articles
Category: troubleshooting