AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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 expire

The 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:

ScenarioJWT expiration
Job has a configured timeoutEquals the job timeout
Job has no configured timeoutPlatform default: 5 minutes + 1 minute clock skew (6 min total)
The JWT is only used during the control plane phase — before automation starts executing — to retrieve secrets from Vault. Once the secrets are retrieved, the JWT is no longer needed. The default 5-minute window is sufficient for most deployments, but it is configurable if jobs consistently fail due to JWT expiry during secret retrieval.

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 jwt

Point 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/o

If AAP uses a CA-trusted certificate:

vault write auth/jwt/config \
  oidc_discovery_url=https://aap.example.com/o

If 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.pem

aap-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-policy

See 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:

FieldValue
Credential TypeHashiCorp Vault Secret Lookup (OIDC)
Vault Addresshttps://vault.example.com:8200
JWT RoleName of the JWT role created in Vault (e.g., aap-example-role)
Path to KV Secrete.g., secret/data/production/db-password
KV Secret Keye.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.

FieldValue
Credential TypeHashiCorp Vault Signed SSH (OIDC)
Vault Addresshttps://vault.example.com:8200
JWT RoleName of the JWT role in Vault
SSH Secret PathVault SSH secret engine mount path (e.g., ssh)
SSH RoleVault SSH role to sign with (e.g., ansible-automation)
Both credential types are used the same way as any other credential in AAP: attach them to a job template, and AAP handles the JWT issuance and Vault authentication transparently at job launch.

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"]
}
EOF

How 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"]
}
EOF

How 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:

ClaimDescriptionExample value
subUnique job identifierjob:42
aap_controller_organization_nameName of the AAP organization running the jobProduction
aap_controller_job_template_nameName of the job templateconfigure_firewall
aap_controller_user_nameUsername that launched the jobjsmith
aap_controller_playbookPlaybook file being executedsite.yml
aap_controller_launch_typeHow the job was launchedmanual, scheduled, workflow
aap_controller_instance_group_nameInstance group executing the jobproduction-workers
All claims are populated authoritatively by AAP. Playbooks and users cannot modify the values in the JWT — Vault can trust them as verified automation context.

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_password

Store 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):

FieldValue
Credential TypeHashiCorp Vault Secret Lookup (OIDC)
Vault Addresshttps://vault.example.com:8200
JWT Roleaap-dynamic-role
Path to KV Secretsecret/data/aap/{{ aap_controller_organization_name }}/{{ aap_controller_job_template_name }}
KV Secret Keydb_password
The path template resolves to the correct secret at runtime based on which organization and job template launches the job. A 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.tool

Vault 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.tool

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

Category: troubleshooting

Browse all Ansible tutorials · AnsiblePilot Home