Ansible Vault encrypts variables and files with AES256 so you can keep passwords, API tokens and private keys in the same Git repository as your playbooks without exposing them in plain text. Ansible decrypts the data in memory at run time when you supply the vault password. In this tutorial you will set up an Ansible project on Ubuntu 24.04, store secrets in an encrypted variables file, use them in a playbook, separate development and production secrets with vault IDs, rotate a vault password and run vaulted playbooks non-interactively in CI.

Prerequisites

To follow this tutorial, you will need:

  • An Ubuntu 24.04 LTS machine to act as the Ansible control node, such as your workstation or a CubePath VPS.
  • A non-root user with sudo privileges.
  • Basic familiarity with Ansible playbooks and inventories.

The examples run against localhost, so you do not need a second server to follow along.

Step 1 - Installing Ansible and creating a project

Ansible Vault ships with Ansible itself. Install Ansible from the Ubuntu repositories:

sudo apt update
sudo apt install ansible

Check that the ansible-vault command is available:

ansible-vault --version
ansible-vault [core 2.16.3]

Create a project with a local inventory and the standard group_vars layout:

mkdir -p ~/vault-demo/group_vars/all
cd ~/vault-demo

Create the inventory:

nano inventory.ini
[app]
localhost ansible_connection=local

Create a project-level ansible.cfg so you do not have to pass the inventory every time:

nano ansible.cfg
[defaults]
inventory = inventory.ini

Step 2 - Creating an encrypted variables file

A common and readable pattern is to split each group's variables into two files: vars.yml in plain text and vault.yml encrypted. Every secret in vault.yml gets a vault_ prefix, and vars.yml references it. That way grep still finds every variable name, but values stay encrypted.

ansible-vault create opens a new file in your editor and encrypts it when you save. Set the editor first, then create the file:

export EDITOR=nano
ansible-vault create group_vars/all/vault.yml

Enter a new vault password twice, then add your secrets:

vault_db_password: your_strong_db_password
vault_api_token: your_api_token

Save and close the editor. Look at the file on disk:

head -3 group_vars/all/vault.yml
$ANSIBLE_VAULT;1.1;AES256
62313365396662343061393464336163383764373764613633653634306231386433626436623361
6230616435363731346436653230333138326534356535390a383534386135373539336537343331

The header identifies the Vault format and cipher; the rest is ciphertext that is safe to commit.

Now create the plain-text variables file that maps readable names to the vaulted values:

nano group_vars/all/vars.yml
db_user: app
db_password: "{{ vault_db_password }}"
api_token: "{{ vault_api_token }}"

Step 3 - Viewing and editing encrypted files

Use view to read an encrypted file without writing a decrypted copy to disk:

ansible-vault view group_vars/all/vault.yml
Vault password:
vault_db_password: your_strong_db_password
vault_api_token: your_api_token

Use edit to change it. Ansible decrypts it to a temporary file, opens your editor and re-encrypts on save:

ansible-vault edit group_vars/all/vault.yml

To encrypt a file that already exists in plain text, such as a TLS private key at files/server.key, use encrypt. To reverse it, use decrypt:

ansible-vault encrypt files/server.key
ansible-vault decrypt files/server.key

Step 4 - Using vaulted variables in a playbook

Create a playbook that renders an application configuration file containing the secrets. The file is written with mode 0600, and no_log: true keeps the secret values out of Ansible's output if a task fails:

mkdir -p templates
nano templates/app.env.j2
DB_USER={{ db_user }}
DB_PASSWORD={{ db_password }}
API_TOKEN={{ api_token }}
nano site.yml
---
- name: Configure the application
  hosts: app
  gather_facts: false
  tasks:
    - name: Create the configuration directory
      ansible.builtin.file:
        path: "{{ playbook_dir }}/build"
        state: directory
        mode: "0700"

    - name: Render the application env file
      ansible.builtin.template:
        src: app.env.j2
        dest: "{{ playbook_dir }}/build/app.env"
        mode: "0600"
      no_log: true

In a real deployment, dest would be a path on the managed server, such as /etc/myapp/app.env.

Run the playbook. --ask-vault-pass makes Ansible prompt for the password before it loads any encrypted variable:

ansible-playbook site.yml --ask-vault-pass
Vault password:

PLAY [Configure the application] ***********************************************

TASK [Create the configuration directory] **************************************
changed: [localhost]

TASK [Render the application env file] *****************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Confirm the secrets were decrypted into the rendered file:

cat build/app.env
DB_USER=app
DB_PASSWORD=your_strong_db_password
API_TOKEN=your_api_token

Add the build/ directory to .gitignore so generated files with secrets are never committed:

echo "build/" >> .gitignore

If you run the playbook without a vault password, Ansible stops with Attempting to decrypt but no vault secrets found.

Step 5 - Using a password file

Typing the password on every run gets tedious. Store it in a file outside the project, readable only by your user:

mkdir -p ~/.ansible
nano ~/.ansible/vault_pass_demo

Put only the vault password on the first line, save, and restrict the permissions:

chmod 600 ~/.ansible/vault_pass_demo

Point Ansible at the file in ansible.cfg:

nano ansible.cfg
[defaults]
inventory = inventory.ini
vault_password_file = ~/.ansible/vault_pass_demo

Now the playbook runs without a prompt:

ansible-playbook site.yml

The play finishes with ok=2 and changed=0, because the file already has the right content.

Step 6 - Encrypting single values with encrypt_string

Sometimes you only need one secret in an otherwise plain file, for example in host_vars. encrypt_string encrypts one value and prints YAML you can paste directly. The --stdin-name option sets the variable name:

echo -n 'smtp_secret_value' | ansible-vault encrypt_string --stdin-name 'smtp_password'
smtp_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          38356130393835306439353136623833643738373865643934643664346235336134623566303838
          6561393262313262623230366330363030626566393064650a653233383837353764613162393931
          ...
Encryption successful

Piping the value avoids leaving the secret in your shell history as a command argument, but echo itself is still recorded. To avoid that entirely, run ansible-vault encrypt_string --stdin-name 'smtp_password' with no pipe, type the value and press Ctrl+D twice.

Add the printed block to any variables file. Ansible decrypts it transparently. To check the value, use the debug module:

ansible localhost -m ansible.builtin.debug -a var=smtp_password

This assumes you saved the block in a file under group_vars/all/, such as group_vars/all/smtp.yml, which Ansible loads automatically. Encrypted strings cannot be viewed with ansible-vault view, and you must re-encrypt them to change the value, so prefer whole encrypted files when you have many secrets.

Step 7 - Separating environments with vault IDs

Development and production secrets should not share a password: developers need the first, only the deployment pipeline should have the second. Vault IDs label each encrypted item with the password it needs.

Create one password file per environment, with a different password in each:

nano ~/.ansible/vault_pass_dev
nano ~/.ansible/vault_pass_prod
chmod 600 ~/.ansible/vault_pass_dev ~/.ansible/vault_pass_prod

Encrypt a file with a specific ID using --vault-id label@source:

mkdir -p group_vars/prod
ansible-vault create --vault-id prod@~/.ansible/vault_pass_prod --encrypt-vault-id prod group_vars/prod/vault.yml

--encrypt-vault-id is required here because ansible.cfg already provides a second password (the default ID from Step 5), and Ansible refuses to guess which one to encrypt with.

The label is stored in the file header:

head -1 group_vars/prod/vault.yml
$ANSIBLE_VAULT;1.2;AES256;prod

At run time, pass every ID the playbook might need. Ansible uses the label as a hint and tries the matching password first:

ansible-playbook site.yml \
  --vault-id dev@~/.ansible/vault_pass_dev \
  --vault-id prod@~/.ansible/vault_pass_prod

To avoid typing them, list the IDs in ansible.cfg instead of vault_password_file. Keep the default entry, since the files from the previous steps were encrypted without a label:

[defaults]
inventory = inventory.ini
vault_identity_list = default@~/.ansible/vault_pass_demo, dev@~/.ansible/vault_pass_dev, prod@~/.ansible/vault_pass_prod

With several IDs configured, always tell create, encrypt and encrypt_string which one to use with --encrypt-vault-id. Use prompt as the source (--vault-id prod@prompt) when you want Ansible to ask for that password interactively.

Step 8 - Rotating a vault password

Rotate the password when someone who knew it leaves the team or when a password file may have leaked. rekey decrypts files with the old password and re-encrypts them with a new one:

ansible-vault rekey group_vars/all/vault.yml
Vault password:
New Vault password:
Confirm New Vault password:
Rekey successful

For a file encrypted with a vault ID, give the old and new sources explicitly:

ansible-vault rekey \
  --vault-id prod@~/.ansible/vault_pass_prod \
  --new-vault-id prod@prompt \
  group_vars/prod/vault.yml

Then update the password file. Rekeying does not change the secrets themselves: if they may have been exposed, rotate the real database passwords and API tokens as well.

Step 9 - Running vaulted playbooks in CI

In a CI/CD pipeline there is nobody to type a password. Store the vault password as a masked secret in your CI system and expose it to the job as an environment variable, for example ANSIBLE_VAULT_PASSWORD.

If the vault password file is executable, Ansible runs it and reads the password from its standard output. Create a small script in the repository that prints the variable, so the password never touches the disk:

nano vault-pass-from-env.sh
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "${ANSIBLE_VAULT_PASSWORD:?ANSIBLE_VAULT_PASSWORD is not set}"

Make it executable:

chmod +x vault-pass-from-env.sh

In the pipeline job, run the playbook with the script as the password source:

ansible-playbook site.yml --vault-password-file ./vault-pass-from-env.sh

Test it locally the same way the CI job will run it:

ANSIBLE_VAULT_PASSWORD='your_vault_password' ansible-playbook site.yml --vault-password-file ./vault-pass-from-env.sh

If the variable is missing, the script exits with a clear error instead of passing an empty password. Give the production CI job only the production vault password, and development jobs only the development one.

Troubleshooting

  • Decryption failed (no vault secrets were found that could decrypt): the password is wrong, or the file was encrypted with a different vault ID. Check the ID in the file header with head -1 and pass the matching --vault-id.
  • Attempting to decrypt but no vault secrets found: Ansible needs a vaulted variable but you did not supply any password. Add --ask-vault-pass, --vault-password-file or configure vault_password_file in ansible.cfg.
  • Secrets appear in the output of a failed task: add no_log: true to every task that handles secret values, including debug tasks left over from testing.
  • Merge conflicts in encrypted files: vault files cannot be merged line by line. Resolve them by running ansible-vault view on both versions and re-creating the file with the combined content.

Conclusion

You encrypted secrets with Ansible Vault, referenced them through plain variable names in a playbook, used password files and vault IDs to separate environments, rotated a password and ran vaulted playbooks non-interactively in CI. As next steps, you can move the vault password itself into a password manager and read it with an executable password script, add a pre-commit hook that rejects unencrypted vault.yml files, or look up secrets at run time from an external store such as HashiCorp Vault with the community.hashi_vault collection.