Introduction
Managing credentials across hundreds of playbooks and thousands of hosts is one of the hardest problems in enterprise automation. Hardcoded passwords, shared service accounts, and static API keys create security risks that grow with every new automation workflow. HashiCorp Vault solves this by providing centralized, audited, and dynamic secret management — and Ansible Automation Platform (AAP) has native integration to pull credentials from Vault at runtime.
This guide covers the full integration: configuring Vault for Ansible, setting up AAP external credential lookups, using dynamic database credentials, and implementing enterprise patterns for credential rotation at scale.
Why Vault + AAP
| Challenge | Without Vault | With Vault |
|---|---|---|
| Secret storage | Ansible Vault files in Git | Centralized, encrypted, API-driven |
| Credential rotation | Manual, error-prone | Automatic, policy-driven |
| Audit trail | Git blame (who committed) | Full API access logs |
| Dynamic secrets | Not possible | Database, cloud, SSH creds on demand |
| Access control | File permissions | Fine-grained policies per team/project |
| Secret sprawl | Copies in multiple repos | Single source of truth |
Architecture Overview
┌─────────────────────────┐
│ Ansible Automation │
│ Platform (Controller) │
│ │
│ ┌───────────────────┐ │ ┌──────────────────┐
│ │ Credential Type: │──┼────▶│ HashiCorp Vault │
│ │ HashiCorp Vault │ │ │ │
│ └───────────────────┘ │ │ ┌─────────────┐ │
│ │ │ │ KV v2 Engine│ │
│ ┌───────────────────┐ │ │ │ secret/ │ │
│ │ Job Template │ │ │ ├─────────────┤ │
│ │ uses credential │ │ │ │ DB Engine │ │
│ │ lookup at runtime │ │ │ │ database/ │ │
│ └───────────────────┘ │ │ ├─────────────┤ │
│ │ │ │ SSH Engine │ │
│ │ │ │ ssh-client/ │ │
│ │ │ └─────────────┘ │
└─────────────────────────┘ └──────────────────┘
Prerequisites
| Component | Version | Purpose |
|---|---|---|
| AAP Controller | 2.4+ | External credential plugin support |
| HashiCorp Vault | 1.12+ | Secrets engine |
| Vault CLI | 1.12+ | Initial configuration |
| Network | HTTPS between AAP and Vault | API communication |
Step 1: Configure Vault
Enable KV v2 Secrets Engine
# Enable the KV v2 secrets engine
vault secrets enable -version=2 -path=ansible kv
# Store a credential
vault kv put ansible/network/switches \
username="ansible_svc" \
password="S3cur3P@ssw0rd!" \
enable_password="En@bl3!"
# Store database credentials
vault kv put ansible/databases/production \
db_host="db-prod.internal" \
db_user="app_deploy" \
db_password="Pr0dD8P@ss!"
# Verify
vault kv get ansible/network/switches
Create a Vault Policy for AAP
# aap-policy.hcl
# Allow AAP to read secrets under ansible/*
path "ansible/data/*" {
capabilities = ["read"]
}
# Allow AAP to list available secrets
path "ansible/metadata/*" {
capabilities = ["read", "list"]
}
# Allow dynamic database credential generation
path "database/creds/ansible-*" {
capabilities = ["read"]
}
# Allow SSH certificate signing
path "ssh-client/sign/ansible-role" {
capabilities = ["create", "update"]
}
# Write the policy
vault policy write aap-automation aap-policy.hcl
# Create an AppRole for AAP
vault auth enable approle
vault write auth/approle/role/aap-controller \
secret_id_ttl=0 \
token_num_uses=0 \
token_ttl=1h \
token_max_ttl=4h \
token_policies="aap-automation"
# Get RoleID and SecretID
vault read auth/approle/role/aap-controller/role-id
vault write -f auth/approle/role/aap-controller/secret-id
Step 2: Configure AAP Credential Type
Create HashiCorp Vault Credential in AAP
In the AAP Controller UI:
- Navigate to Resources → Credentials
- Click Add
- Select Credential Type: HashiCorp Vault Secret Lookup
- Fill in:
| Field | Value |
|---|---|
| Name | Vault Production |
| Server URL | https://vault.internal:8200 |
| AppRole Role ID | (from Step 1) |
| AppRole Secret ID | (from Step 1) |
| API Version | v2 |
| Default Secrets Path | ansible |
Create a Machine Credential Using Vault Lookup
- Create a new Machine credential
- For Username, click the key icon → External → select
Vault Production - Set Secret Path:
ansible/data/network/switches - Set Secret Key:
username - Repeat for Password with key
password
Now every job using this credential fetches the latest value from Vault at runtime.
Step 3: Dynamic Database Credentials
Dynamic secrets are Vault's killer feature — credentials that are created on demand and automatically revoked after a TTL.
Configure Vault Database Engine
# Enable database secrets engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/prod-postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="ansible-readonly,ansible-deploy" \
connection_url="postgresql://{{username}}:{{password}}@db-prod.internal:5432/appdb?sslmode=require" \
username="vault_admin" \
password="VaultAdm1n!"
# Create a role for read-only access (1h TTL)
vault write database/roles/ansible-readonly \
db_name=prod-postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="1h" \
max_ttl="4h"
# Create a role for deploy access (15min TTL)
vault write database/roles/ansible-deploy \
db_name=prod-postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="15m" \
max_ttl="1h"
Use Dynamic Credentials in Playbook
---
- name: Deploy database migration with dynamic credentials
hosts: db_servers
vars:
# These come from Vault via AAP credential lookup
db_user: "{{ lookup('hashi_vault', 'database/creds/ansible-deploy', url='https://vault.internal:8200') }}"
tasks:
- name: Run database migration
community.postgresql.postgresql_query:
login_host: "{{ db_host }}"
login_user: "{{ db_user.username }}"
login_password: "{{ db_user.password }}"
db: appdb
query: "{{ lookup('file', 'migrations/' + migration_version + '.sql') }}"
Step 4: SSH Certificate Signing
Instead of distributing SSH keys, Vault can sign short-lived SSH certificates.
# Enable SSH secrets engine
vault secrets enable -path=ssh-client ssh
# Configure CA
vault write ssh-client/config/ca generate_signing_key=true
# Create a role
vault write ssh-client/roles/ansible-role \
key_type=ca \
default_user=ansible \
allowed_users="ansible,deploy,root" \
ttl=30m \
max_ttl=2h \
allow_user_certificates=true
---
- name: Sign SSH key via Vault and connect
hosts: localhost
tasks:
- name: Sign the public key
ansible.builtin.uri:
url: "https://vault.internal:8200/v1/ssh-client/sign/ansible-role"
method: POST
headers:
X-Vault-Token: "{{ vault_token }}"
body_format: json
body:
public_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
valid_principals: "ansible"
ttl: "30m"
register: signed_cert
- name: Write signed certificate
ansible.builtin.copy:
content: "{{ signed_cert.json.data.signed_key }}"
dest: "~/.ssh/id_ed25519-cert.pub"
mode: "0600"
Enterprise Patterns
Multi-Environment Secret Organization
ansible/
├── data/
│ ├── production/
│ │ ├── network/switches
│ │ ├── databases/postgres
│ │ └── cloud/aws-keys
│ ├── staging/
│ │ ├── network/switches
│ │ └── databases/postgres
│ └── shared/
│ ├── certificates/wildcard
│ └── api-keys/monitoring
Credential Rotation Workflow
---
- name: Rotate service account passwords
hosts: localhost
tasks:
- name: Generate new password
ansible.builtin.set_fact:
new_password: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters,digits,punctuation') }}"
- name: Update password in Vault
ansible.builtin.uri:
url: "https://vault.internal:8200/v1/ansible/data/{{ service_path }}"
method: POST
headers:
X-Vault-Token: "{{ vault_token }}"
body_format: json
body:
data:
username: "{{ service_user }}"
password: "{{ new_password }}"
- name: Update password on target system
ansible.builtin.user:
name: "{{ service_user }}"
password: "{{ new_password | password_hash('sha512') }}"
delegate_to: "{{ target_host }}"
Using the hashi_vault Lookup Plugin
# Install the collection
ansible-galaxy collection install community.hashi_vault
---
- name: Direct Vault lookup in playbook
hosts: webservers
vars:
vault_addr: "https://vault.internal:8200"
tasks:
- name: Get database credentials from Vault
ansible.builtin.set_fact:
db_creds: "{{ lookup('community.hashi_vault.hashi_vault',
'ansible/data/databases/production',
url=vault_addr,
auth_method='approle',
role_id=lookup('env', 'VAULT_ROLE_ID'),
secret_id=lookup('env', 'VAULT_SECRET_ID')
) }}"
- name: Deploy application config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
mode: "0640"
vars:
db_host: "{{ db_creds.db_host }}"
db_user: "{{ db_creds.db_user }}"
db_password: "{{ db_creds.db_password }}"
Troubleshooting
Permission Denied
# Check token capabilities
vault token capabilities ansible/data/network/switches
# Expected: read
# If missing, update the policy
vault policy write aap-automation aap-policy.hcl
Vault Seal Status
vault status
# Ensure Sealed = false
AAP Cannot Reach Vault
# Test from AAP controller node
curl -k https://vault.internal:8200/v1/sys/health
# {"initialized":true,"sealed":false,"standby":false,...}
Related Articles
- Ansible Vault Encryption Guide
- Ansible Automation Platform Guide
- Ansible Best Practices Guide
- Ansible Roles and Collections
- Privilege Escalation in Ansible
Conclusion
HashiCorp Vault + AAP eliminates static credentials from your automation pipeline. Use the KV v2 engine for stored secrets, the database engine for dynamic short-lived credentials, and SSH certificate signing to replace key distribution. In AAP, the external credential lookup plugin fetches secrets at job runtime — no credentials stored in Git, no manual rotation, and full audit trails. For enterprise deployments, organize secrets by environment, implement AppRole auth with scoped policies, and use dynamic secrets wherever possible.