Open-Source Wikis

/

Ansible

/

Systems

/

Vault encryption

ansible/ansible

Vault encryption

Ansible Vault encrypts variables, files, or arbitrary strings with AES-256-CTR (or older 1.1-format with the same algorithm). Encrypted material can sit in a Git repository safely; the controller decrypts at parse time when it has the right secret. Implementation is at lib/ansible/parsing/vault/__init__.py (1,525 lines).

Header format

A vault-encrypted file (or inline string) begins with a header line:

$ANSIBLE_VAULT;1.2;AES256;<vault_id_label>
Component Meaning
$ANSIBLE_VAULT Magic prefix; identifies the file as vault-encrypted
1.2 Format version. 1.0 (no vault id), 1.1 (no label), 1.2 (current — adds optional label)
AES256 Cipher. The only currently-implemented cipher
<vault_id_label> Optional label that lets multiple vaults coexist in one playbook

After the header, a hex-encoded payload contains: salt, HMAC, ciphertext.

Files and classes

File Role
lib/ansible/parsing/vault/__init__.py Everything: VaultLib, VaultEditor, VaultSecret, PromptVaultSecret, file-format helpers
lib/ansible/cli/vault.py The ansible-vault CLI
lib/ansible/parsing/dataloader.py Calls the vault on every file load
lib/ansible/_internal/_datatag/_tags.py:VaultedValue The tag attached to decrypted-but-still-secret values
lib/ansible/parsing/yaml/loader.py YAML loader that handles inline !vault tags

Encryption flow

graph TD
    PLAINTEXT[Plaintext] --> SALT[Generate 32-byte salt]
    PASSWORD[Vault password] --> KDF[PBKDF2-HMAC-SHA256<br/>10000 rounds]
    SALT --> KDF
    KDF --> KEYS[Derive: AES key, HMAC key, IV]
    PLAINTEXT --> PAD[PKCS7 pad]
    PAD --> AES[AES-256-CTR encrypt]
    KEYS --> AES
    AES --> CIPHERTEXT[Ciphertext]
    CIPHERTEXT --> HMAC[Compute HMAC-SHA256]
    KEYS --> HMAC
    HMAC --> ENVELOPE[Envelope: salt || HMAC || ciphertext]
    ENVELOPE --> HEX[Hex-encode]
    HEX --> HEADER[Prepend $ANSIBLE_VAULT;1.2;AES256;label]
    HEADER --> OUT[Encrypted output]

The KDF is PBKDF2 with HMAC-SHA256, 10,000 rounds. That's modest by modern standards but is the documented format; changing it requires a new format version. Decryption verifies the HMAC before attempting AES decryption (so wrong-password attempts fail at HMAC verification, not on garbled output).

Vault ids

Format 1.2 introduced labeled vault ids so playbooks can mix multiple secret sources:

ansible-playbook --vault-id dev@~/.vault-pass-dev --vault-id prod@prompt site.yml

This sets up two secrets — dev (read from a file) and prod (prompted at runtime). When decrypting, the loader matches on the label embedded in each vault header. If a header's label is empty (1.1 format) or doesn't match, VaultLib tries every loaded secret in order.

The @source part is parsed by lib/ansible/parsing/vault/__init__.py:get_file_vault_secret, which dispatches:

  • @<path> — read the password from the file. If executable, run it as a script and read stdout.
  • @prompt — prompt the user interactively (PromptVaultSecret).
  • @<custom> — anything else is passed to lookup_vault_secrets() for plugin-style secret backends.

Where decryption happens

DataLoader (lib/ansible/parsing/dataloader.py) is the central read-from-disk surface. Every .load_from_file() runs through DataLoader._is_vaulted() and decrypts before parsing YAML. The flow is:

  1. Read raw bytes.
  2. If the first line is $ANSIBLE_VAULT;..., hand to VaultLib.decrypt().
  3. Match the vault id label against loaded secrets; iterate until one decrypts successfully or all fail.
  4. Return the plaintext to the YAML loader.

For inline !vault scalars in a YAML file, the YAML loader decodes them on read and tags the result as a VaultedValue so the rest of the pipeline knows it came from an encrypted source.

api_password: !vault |
  $ANSIBLE_VAULT;1.2;AES256;prod
  31613966...
  ...

VaultedValue tag

After decryption, the value carries the VaultedValue datatag from lib/ansible/_internal/_datatag/_tags.py. This tag flows through the variable manager and the templating engine. Implementations can choose to handle vaulted values specially:

  • The no_log=True parameter system uses it to scrub secrets from displayed task results.
  • The display.vvv paths skip vaulted values when emitting verbose output.
  • Encoding to JSON for an AnsiBallZ archive marks the value as still-secret so the remote side can avoid logging it.

VaultEditor

VaultEditor is the high-level, file-oriented surface used by the ansible-vault CLI:

  • create() — encrypt a new file in $EDITOR.
  • edit() — decrypt to a temporary file, edit, re-encrypt.
  • view() — decrypt to stdout.
  • encrypt(), decrypt() — file-level transforms.
  • rekey() — change the password of an already-encrypted file.

encrypt_string (the inline-scalar form invoked by ansible-vault encrypt_string) wraps the result in YAML's tagged-scalar format so it can be pasted directly into a vars file.

VaultLib (the workhorse)

VaultLib is what DataLoader uses internally:

  • is_encrypted(data) — bytes/text predicate.
  • is_encrypted_file(path) — same, but for a path.
  • encrypt(plaintext, secret, vault_id=None) — encrypt with a specific secret.
  • decrypt(ciphertext, filename=None) — decrypt; tries each loaded secret.
  • decrypt_and_get_vault_id() — same as decrypt, but returns which secret matched.

The cryptography package is the underlying crypto provider (from cryptography.hazmat.primitives.ciphers import Cipher). There's no PyCrypto fallback in the modern code.

Vault secret resolution

Secrets are loaded into a VaultSecretsContext ordered from most-specific to least-specific:

  1. CLI: --vault-id, --vault-password-file, --ask-vault-pass.
  2. ansible.cfg: DEFAULT_VAULT_ID_MATCH, DEFAULT_VAULT_PASSWORD_FILE.
  3. Environment: ANSIBLE_VAULT_PASSWORD_FILE, ANSIBLE_VAULT_IDENTITY_LIST.

Match order during decryption is configurable: with DEFAULT_VAULT_ID_MATCH=true, only the secret whose label matches gets tried; with the default false, all loaded secrets are tried in order.

Integration points

  • Imported by: lib/ansible/parsing/dataloader.py (every YAML load), lib/ansible/cli/__init__.py (CLI startup), lib/ansible/cli/vault.py (the dedicated CLI).
  • Imports: cryptography (crypto), lib/ansible/_internal/_templating/_jinja_common.py (for value handling), lib/ansible/_internal/_datatag/_tags.py (VaultedValue).

Entry points for modification

  • Adding a new format version — extend the parser/writer in parsing/vault/__init__.py. Update VAULT_VERSION_MIN/VAULT_VERSION_MAX constants. Sanity should be updated to test round-trips.
  • A new secret source backend — add to the get_file_vault_secret dispatch, or write a new VaultSecret subclass.
  • Different cipher — would require a new cipher_name (currently only AES256) and a new Cipher* class.
  • Debugging a "decryption failed" error--vault-id label@prompt -vvv reveals which labels were tried.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Vault encryption – Ansible wiki | Factory