Enterprise security for Ansible with HashiCorp Vault

HashiCorp Vault part

Start by logging into your account using Vault’s CLI:

export VAULT_ADDR='https://your-vault.example.com:8200'

vault login -method='your_method' username='your_root_account'
# or
export VAULT_TOKEN='hvs.your-token-here'

Enable KV-v2 secrets engine:

vault secrets enable -path=secret kv-v2

Use your created secrets engine and choose a subpath where you want to put secret data:

vault kv put secret/users/user1 username='name' password='secret'
vault kv get secret/users/user1

If you want to delete the secret, use kv delete:

vault kv delete secret/users/user1

Create a policy granting the token read access to the new secret:

vault policy write user1-read-policy - <<EOF
path "secret/data/users/user1" {
  capabilities = ["read"]
}
EOF

Please note that Vault’s KV v2 engine has a different path structure for read/write operations. The actual data is stored under secret/data/, while the metadata is under secret/metadata/. This means that when you create policies, you need to reference the correct path for the data you want to access.

Create a token with attached policy:

vault token create -policy=user1-read-policy -orphan=true -period=60m -renewable=false
export VAULT_TOKEN='hvs.your-token-here'

Example output:

[user@rocky10-aarch64 ~]# vault token create -policy=user1-read-policy -orphan=true -period=60m -renewable=false
Key                  Value
---                  -----
token                hvs.CAE4615hfcUGxAwdcntfDebxJRC6qKo_Ws1SWEyS9qpf7UpuDGa4KHGh2cy5iT0dWdtN2l4eWFHVlJkQ0lkTzk
token_accessor       oAUEhvDjshUyNmOBoOEFvi
token_duration       60m
token_renewable      false
token_policies       ["default" "user1-read-policy"]
identity_policies    []
policies             ["default" "user1-read-policy"]

Check the token capabilities:

[user@rocky10-aarch64 ~]# vault token capabilities secret/users/user1
deny
[user@rocky10-aarch64 ~]# vault token capabilities secret/data/users/user1
read

Test data retrieval with the new token:

vault kv get secret/users/user1

Note that the path without ‘data’ is used when using vault kv get.

Ansible part

Create requirements.yml for Ansible collections:

---
collections:
  - name: community.hashi_vault

Create a python virtual environment, activate it and install dependencies:

python3 -m venv ~/.venv
source ~/.venv/bin/activate
~/.venv/bin/pip install ansible hvac

Install required collections:

ansible-galaxy collection install -r requirements.yml

Set these two environment variables on the Ansible control node correctly:

echo $VAULT_ADDR # Must point to the vault containing secret data
echo $VAULT_TOKEN # Token you have created using 'vault token create'

Check that VAULT_ADDR contains https://, and that VAULT_TOKEN is set to the token you created in the previous step.

Retrieve and parse the secrets in your playbook:

- name: Install and configure IPA client
  hosts: all
  gather_facts: true
  become: false
  remote_user: root
  vars:
    vault_url: "{{ lookup('env', 'VAULT_ADDR') }}"
    vault_token: "{{ lookup('env', 'VAULT_TOKEN') }}"

    user1: "{{ lookup('community.hashi_vault.vault_kv2_get',
                    'users/user1',
                    url=vault_url,
                    auth_method='token',
                    token=vault_token
                  ) }}"

  tasks:
    - name: Preflight check required env vars
      ansible.builtin.assert:
        that:
          - lookup('env', 'VAULT_ADDR') | length > 0
          - lookup('env', 'VAULT_TOKEN') | length > 0
        fail_msg: >
          Required environment variables or variables are not set.
          Ensure VAULT_ADDR, VAULT_TOKEN are defined.

    - name: Debug user1 variable
      ansible.builtin.debug:
        var: user1

    - name: Extract IPA credentials
      ansible.builtin.set_fact:
        ipa_user: "{{ user1.data.data['username'] }}"
        ipa_pass: "{{ user1.data.data['password'] }}"

Note the path users/user1 used in the lookup command:

user1: "{{ lookup('community.hashi_vault.vault_kv2_get',
                'users/user1',
                url=vault_url,
                auth_method='token',
                token=vault_token
              ) }}"

This is because the vault_kv2_get lookup is specifically designed for KV-v2 engines. It knows that KV-v2 stores data under mount_point/data/path, so it automatically handles the path translation.

If you have named your KV-v2 anything other than ‘secret’, you must define the path using the engine_mount_point variable.

Example of debug output of user1 variable in ansible:

"user1": {
    "data": {
        "data": {
            "password": "secret",
            "username": "name"
        },
        "metadata": {
            "created_time": "2026-05-18T19:59:26.170339039Z",
            "custom_metadata": null,
            "deletion_time": "",
            "destroyed": false,
            "version": 1
        }
    },
    "metadata": {
        "created_time": "2026-05-18T19:59:26.170339039Z",
        "custom_metadata": null,
        "deletion_time": "",
        "destroyed": false,
        "version": 1
    },
    "raw": {
        "auth": null,
        "data": {
            "data": {
                "password": "secret",
                "username": "name"
            },
            "metadata": {
                "created_time": "2026-05-18T19:59:26.170339039Z",
                "custom_metadata": null,
                "deletion_time": "",
                "destroyed": false,
                "version": 1
            }
        },
        "lease_duration": 0,
        "lease_id": "",
        "mount_type": "kv",
        "renewable": false,
        "request_id": "4a87jfc08-1111-fdd3-2766-3bhfv544b6b7d",
        "warnings": null,
        "wrap_info": null
    },
    "secret": {
        "password": "secret",
        "username": "name"
    }
}

Sources