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

ChallengeWithout VaultWith Vault
Secret storageAnsible Vault files in GitCentralized, encrypted, API-driven
Credential rotationManual, error-proneAutomatic, policy-driven
Audit trailGit blame (who committed)Full API access logs
Dynamic secretsNot possibleDatabase, cloud, SSH creds on demand
Access controlFile permissionsFine-grained policies per team/project
Secret sprawlCopies in multiple reposSingle 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

ComponentVersionPurpose
AAP Controller2.4+External credential plugin support
HashiCorp Vault1.12+Secrets engine
Vault CLI1.12+Initial configuration
NetworkHTTPS between AAP and VaultAPI 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:

  1. Navigate to Resources → Credentials
  2. Click Add
  3. Select Credential Type: HashiCorp Vault Secret Lookup
  4. Fill in:
FieldValue
NameVault Production
Server URLhttps://vault.internal:8200
AppRole Role ID(from Step 1)
AppRole Secret ID(from Step 1)
API Versionv2
Default Secrets Pathansible

Create a Machine Credential Using Vault Lookup

  1. Create a new Machine credential
  2. For Username, click the key icon → External → select Vault Production
  3. Set Secret Path: ansible/data/network/switches
  4. Set Secret Key: username
  5. 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,...}

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.