Skip to content
Migration7 min read

Moving team secrets out of Ansible Vault

Why teams move secrets out of Ansible Vault, how to export them with ansible-vault, bring them into per-environment storage, and keep playbooks working.

Crunu teamPublished 7 min read

Example. Kiteline is a fictional company.
On this page

To move secrets out of Ansible Vault, decrypt each vault file with ansible-vault view or ansible-vault decrypt, bring the values into a store that gives each person and machine its own access, and change how hosts receive them: either the playbook reads them from the environment at run time, or the application on each host fetches them itself. Do it one inventory group or environment at a time, and reissue the values that were shared under a password everyone knew. Crunu can't import arbitrary Ansible Vault files, so the move goes through decrypted files you drag in.

This post explains the usual reasons for leaving, a step-by-step move, and how to keep a playbook working during and after the switch. It doesn't argue that Ansible Vault is wrong for everyone; for a small team that already runs everything through Ansible, it is often enough.

Why teams move off it

Ansible Vault does what it says: it "encrypts variables and files so you can protect sensitive content such as passwords or keys rather than leaving it visible as plaintext in playbooks or roles" (Ansible Vault). The friction appears as a team grows.

Access is a password, not a person. Anyone who has the vault password can decrypt everything encrypted with it. The Ansible documentation suggests a single password for "a small team or few sensitive values" and multiple passwords for larger teams, for example "different passwords for different users or different levels of access", managed with vault IDs (Ansible: managing vault passwords). That works, but every password is still shared by everyone at its level, and nothing records who decrypted what.

Leaving is expensive. When someone with the production vault password leaves, every file under that password needs a rekey, every inline encrypted variable has to be re-created because "you cannot rekey encrypted variables" (Ansible: encrypting content), and the old password still opens every earlier commit. The post on removing someone from git-crypt, SOPS or Ansible Vault goes through that in detail.

Reviews and merges are awkward. With file-level encryption, the documentation notes, "the contents of files are no longer easy to access and read." To Git, an encrypted file is an opaque block: a diff shows changed ciphertext, and two branches that touch the same vault file conflict in text nobody can resolve by reading it. One workaround is a diff driver that runs ansible-vault view, with vault files marked merge=binary so Git doesn't try to merge ciphertext (Ansible: human readable git diff for vault files). It helps, but each developer has to set it up and hold the password.

It only covers data at rest. The documentation warns: "Encryption with Ansible Vault ONLY protects 'data at rest'. Once the content is decrypted ('data in use'), play and plugin authors are responsible for avoiding any secret disclosure." Values decrypted on the control node and templated onto hosts end up in files on those hosts.

Based on Ansible Vault’s public documentation, checked Oct 30, 2026.

Before you start: an inventory

List what you have before decrypting anything.

  • Vault files. Usually group_vars/<group>/vault.yml and host_vars/<host>/vault.yml, plus any encrypted task or template files.
  • Inline encrypted variables. Search for !vault | in your YAML.
  • Vault IDs and password sources. Which label belongs to which environment, and where each password lives: a file, a script, a secret manager.
  • Consumers. Which roles and templates use which variables, and where the values land on the hosts: a systemd environment file, an application config, a .env.

The last list matters most. It tells you how values should reach each host after the move.

Step by step

  1. Pick one environment to move first, typically staging. Create a matching environment in the new store, such as payments-api / staging.
  2. Print each vault file to the terminal with ansible-vault view --vault-id staging@prompt group_vars/staging/vault.yml, or write a temporary decrypted copy with ansible-vault decrypt --output to a path outside the repository.
  3. Turn the values into the files your applications actually read: a .env for a service, an INI file such as ~/.aws/credentials, or a flat YAML file. Drag them onto the environment and check the variable names and paths it shows.
  4. Delete the temporary decrypted copies.
  5. Change how the hosts get the values, using one of the two patterns below, and deploy to staging.
  6. When staging runs on the new path, remove the old vault file from the playbook and repeat for the next environment.
  7. After production moves, reissue the values that were protected only by a shared password, starting with the most sensitive.

Step 7 is the easiest one to skip. Everyone who ever had the production vault password could read every production value. Moving them to a new store doesn't change that; new values at the providers do.

Keeping playbooks working: two patterns

Pattern 1: the playbook reads values from its environment. Run ansible-playbook under crunu run on the control node, and replace vault variables with the env lookup, which "allows you to query the environment variables available on the controller when you invoked Ansible" (Ansible: env lookup):

YAML
# group_vars/staging/main.yml
db_password: "{{ lookup('ansible.builtin.env', 'DB_PASSWORD') }}"
stripe_secret_key: "{{ lookup('ansible.builtin.env', 'STRIPE_SECRET_KEY') }}"
Shell
crunu run --env payments-api/staging -- ansible-playbook -i inventory/staging site.yml

This is the smallest change: templates and roles stay as they are, and no vault password is needed. During the switch, the lookup's default parameter, added in ansible-core 2.13, lets the old vault variable stand in when the environment variable is missing, for example lookup('ansible.builtin.env', 'DB_PASSWORD', default=vault_db_password). Remove the fallback and the vault file once every run goes through crunu run.

If playbooks run from CI rather than a laptop, the same command works with a service token in CRUNU_TOKEN. Give that token only the environment the pipeline deploys and bind it to the repository and branch with OIDC, so a copied token is useless outside the pipeline. Values still pass through the control node and still land in whatever files the templates write on the hosts.

Pattern 2: hosts fetch their own values. Enroll each host as a machine with the Crunu agent, and have Ansible deploy only the service unit that starts the application through crunu run. The playbook never handles a secret.

INI
# templates/payments-api.service.j2
[Service]
User=payments
ExecStart=/usr/local/bin/crunu run --env payments-api/{{ env_name }} --restart on-change -- /opt/payments/bin/server

The payments user must belong to the crunu group to reach the agent's socket. If an application insists on an environment file, crunu pull --env-out writes one for systemd's EnvironmentFile= with permissions 0600. The systemd integration has the full setup. This pattern takes longer to adopt, but it removes secrets from the control node and the playbook repository entirely.

What changes for people

Example. Kiteline is a fictional company.

In Crunu, each environment has its own key, sealed to each person and machine with a role on it. A developer with Editor on staging doesn't hold the production key at all. When someone leaves, removing them rotates the keys of the environments they had and shows which versions they received, instead of a rekey of every file under a shared password. Each saved change is a new version with its author, and History lists what changed by variable name.

Going back the other way

Some playbooks or teammates will still expect Ansible Vault for a while. Crunu can export an environment to a keys.vault.yml file in the Ansible Vault 1.1 format with AES256, which ansible-vault view opens with a separate password generated in the Export… dialog. Inside is a YAML document that lists each file by its path with its contents, so a playbook reads it as data rather than as ready-made variables. The export is a snapshot; a new version needs a new export. The Ansible Vault integration describes it.

Honest limits

  • No importer for Ansible Vault files. Crunu reads only the keys.vault.yml files made by its own earlier versions. Everything else comes in as decrypted files.
  • Nested YAML stays text. Flat YAML files show as fields; nested structures are stored and edited as the whole file.
  • Not an Ansible plugin. There is no lookup plugin or inventory source; the integration is the process environment and files on the host.

If you're still weighing options rather than moving, see the Crunu vs Ansible Vault page and the SOPS vs Ansible Vault comparison. The wider offboarding picture is in the guide on what to rotate when a developer leaves.

Sources