Introduction

HashiCorp Vault manages secrets, encryption, and dynamic credentials. Ansible integrates through lookup plugins and modules — read static secrets, generate dynamic database credentials, issue PKI certificates, and manage Vault policies. This keeps secrets out of playbooks and inventory while providing audit trails for every access.

Note: HashiCorp Vault is different from Ansible Vault. Ansible Vault encrypts files at rest; HashiCorp Vault is a centralized secrets management platform.

Prerequisites

ansible-galaxy collection install community.hashi_vault
pip install hvac

Read Secrets with Lookup Plugin

---
- name: Use HashiCorp Vault secrets
  hosts: webservers
  become: true
  vars:
    vault_addr: https://vault.example.com:8200
    db_password: "{{ lookup('community.hashi_vault.hashi_vault',
                     'secret/data/myapp:db_password',
                     url=vault_addr,
                     token=vault_token) }}"
  tasks:
    - name: Deploy app config with secrets
      ansible.builtin.template:
        src: app.conf.j2
        dest: /etc/myapp/config.yml
        mode: '0600'
      no_log: true

Authentication Methods

Token Auth

# Set via environment
# export VAULT_TOKEN=hvs.xxx
# export VAULT_ADDR=https://vault.example.com:8200

- name: Read secret with token
  ansible.builtin.set_fact:
    api_key: "{{ lookup('community.hashi_vault.hashi_vault',
                 'secret/data/api:key',
                 token=lookup('env', 'VAULT_TOKEN')) }}"

AppRole Auth

- name: Read secret with AppRole
  ansible.builtin.set_fact:
    db_creds: "{{ lookup('community.hashi_vault.hashi_vault',
                  'secret/data/database',
                  auth_method='approle',
                  role_id=vault_role_id,
                  secret_id=vault_secret_id) }}"

Kubernetes Auth

# For pods running in Kubernetes
- name: Read secret with K8s auth
  ansible.builtin.set_fact:
    app_secret: "{{ lookup('community.hashi_vault.hashi_vault',
                    'secret/data/myapp',
                    auth_method='kubernetes',
                    role='ansible-role',
                    jwt=lookup('file', '/var/run/secrets/kubernetes.io/serviceaccount/token')) }}"

Deploy HashiCorp Vault

---
- name: Deploy HashiCorp Vault
  hosts: vault_servers
  become: true
  vars:
    vault_version: "1.17.0"
  tasks:
    - name: Add HashiCorp repository
      ansible.builtin.apt_repository:
        repo: "deb https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main"
        state: present

    - name: Install Vault
      ansible.builtin.package:
        name: vault
        state: present

    - name: Deploy Vault config
      ansible.builtin.template:
        src: vault.hcl.j2
        dest: /etc/vault.d/vault.hcl
        mode: '0640'
        owner: vault
        group: vault
      notify: restart vault

    - name: Start Vault
      ansible.builtin.service:
        name: vault
        state: started
        enabled: true

  handlers:
    - name: restart vault
      ansible.builtin.service:
        name: vault
        state: restarted
# templates/vault.hcl.j2
ui = true

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_cert_file = "/etc/vault.d/tls/vault.crt"
  tls_key_file  = "/etc/vault.d/tls/vault.key"
}

storage "raft" {
  path    = "/opt/vault/data"
  node_id = "{{ inventory_hostname }}"

{% for host in groups['vault_servers'] if host != inventory_hostname %}
  retry_join {
    leader_api_addr = "https://{{ hostvars[host].ansible_default_ipv4.address }}:8200"
  }
{% endfor %}
}

api_addr     = "https://{{ ansible_default_ipv4.address }}:8200"
cluster_addr = "https://{{ ansible_default_ipv4.address }}:8201"

Dynamic Database Credentials

# Configure database secrets engine (one-time setup)
- name: Enable database secrets engine
  community.hashi_vault.vault_write:
    url: "{{ vault_addr }}"
    token: "{{ vault_root_token }}"
    path: sys/mounts/database
    data:
      type: database

- name: Configure PostgreSQL connection
  community.hashi_vault.vault_write:
    url: "{{ vault_addr }}"
    token: "{{ vault_root_token }}"
    path: database/config/myapp-db
    data:
      plugin_name: postgresql-database-plugin
      connection_url: "postgresql://{{'{{'}}username{{'}}'}}:{{'{{'}}password{{'}}'}}@db.example.com:5432/myapp"
      allowed_roles: "myapp-role"
      username: vault_admin
      password: "{{ vault_db_admin_password }}"

- name: Create database role
  community.hashi_vault.vault_write:
    url: "{{ vault_addr }}"
    token: "{{ vault_root_token }}"
    path: database/roles/myapp-role
    data:
      db_name: myapp-db
      creation_statements: |
        CREATE ROLE "{{'{{'}}name{{'}}'}}" WITH LOGIN PASSWORD '{{'{{'}}password{{'}}'}}' VALID UNTIL '{{'{{'}}expiration{{'}}'}}';
        GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "{{'{{'}}name{{'}}'}}";
      default_ttl: "1h"
      max_ttl: "24h"
# Use dynamic credentials in playbooks
- name: Get dynamic database credentials
  ansible.builtin.set_fact:
    db_creds: "{{ lookup('community.hashi_vault.vault_read',
                  'database/creds/myapp-role',
                  url=vault_addr,
                  auth_method='approle',
                  role_id=vault_role_id,
                  secret_id=vault_secret_id) }}"

- name: Deploy app with dynamic credentials
  ansible.builtin.template:
    src: database.yml.j2
    dest: /etc/myapp/database.yml
    mode: '0600'
  vars:
    db_user: "{{ db_creds.data.username }}"
    db_pass: "{{ db_creds.data.password }}"
  no_log: true

PKI Certificates

- name: Issue TLS certificate from Vault PKI
  community.hashi_vault.vault_write:
    url: "{{ vault_addr }}"
    token: "{{ vault_token }}"
    path: pki/issue/webserver
    data:
      common_name: "{{ inventory_hostname }}.example.com"
      alt_names: "{{ inventory_hostname }}"
      ttl: "720h"
  register: cert_result

- name: Deploy certificate
  ansible.builtin.copy:
    content: "{{ cert_result.data.data.certificate }}"
    dest: /etc/ssl/certs/server.crt
    mode: '0644'

- name: Deploy private key
  ansible.builtin.copy:
    content: "{{ cert_result.data.data.private_key }}"
    dest: /etc/ssl/private/server.key
    mode: '0600'
  no_log: true

Manage Vault Policies

- name: Create application policy
  community.hashi_vault.vault_write:
    url: "{{ vault_addr }}"
    token: "{{ vault_root_token }}"
    path: sys/policies/acl/myapp-policy
    data:
      policy: |
        # Read application secrets
        path "secret/data/myapp/*" {
          capabilities = ["read", "list"]
        }
        # Generate dynamic DB credentials
        path "database/creds/myapp-role" {
          capabilities = ["read"]
        }
        # Issue TLS certificates
        path "pki/issue/webserver" {
          capabilities = ["create", "update"]
        }

KV Secrets Engine

# Write secrets
- name: Store secret in Vault
  community.hashi_vault.vault_write:
    url: "{{ vault_addr }}"
    token: "{{ vault_token }}"
    path: secret/data/myapp/config
    data:
      data:
        api_key: "{{ api_key }}"
        db_password: "{{ db_password }}"
        encryption_key: "{{ encryption_key }}"

# Read multiple secrets
- name: Read all app secrets
  ansible.builtin.set_fact:
    app_secrets: "{{ lookup('community.hashi_vault.vault_read',
                    'secret/data/myapp/config',
                    url=vault_addr,
                    token=vault_token) }}"

- name: Use specific secret
  ansible.builtin.debug:
    msg: "API key starts with {{ app_secrets.data.data.api_key[:4] }}..."

Transit Encryption

# Encrypt data with Vault Transit engine
- name: Encrypt sensitive data
  community.hashi_vault.vault_write:
    url: "{{ vault_addr }}"
    token: "{{ vault_token }}"
    path: transit/encrypt/myapp-key
    data:
      plaintext: "{{ sensitive_data | b64encode }}"
  register: encrypted

- name: Store encrypted value
  ansible.builtin.set_fact:
    encrypted_data: "{{ encrypted.data.data.ciphertext }}"

Troubleshooting

"Permission denied" Errors

Check the Vault policy allows the path you're accessing:

vault token capabilities secret/data/myapp

Token Expired

- name: Check token validity
  ansible.builtin.uri:
    url: "{{ vault_addr }}/v1/auth/token/lookup-self"
    headers:
      X-Vault-Token: "{{ vault_token }}"
  register: token_info
  ignore_errors: true

Conclusion

HashiCorp Vault + Ansible provides centralized secrets management with full audit trails. Use the hashi_vault lookup plugin to read secrets in playbooks, dynamic database credentials for short-lived access, PKI certificates for automatic TLS, and Transit encryption for data protection. Authenticate via token, AppRole, or Kubernetes auth depending on your environment. Secrets never touch disk or version control.