A CLI for managing Ansible Vault secrets — with metadata tracking, expiry monitoring, type detection, and self-updating standalone binaries.
For the full read-it-end-to-end book: docs/manual.pdf (23 pages).
# Download standalone binary (no Python required)
curl -fsSL https://github.com/cdds-ab/vaultctl/releases/latest/download/vaultctl-linux-amd64 -o vaultctl
chmod +x vaultctlAlternative: install from source (requires Python >= 3.13)
uv sync && uv run vaultctl --help
# or: pip install .vaultctl shells out to external binaries that must be installed separately:
ansible-vault— handles the actual encryption/decryption. Install via your distro package manager orpip install ansible-core.cue(optional, for schema validation — see #34) — enables futurevaultctl validateand schema-checked imports. Install: https://cuelang.org/docs/install/. vaultctl gracefully skips schema features whencueis not on$PATH.
vaultctl init
vaultctl set db_password --prompt --expires 2026-12-31
vaultctl listAlready have an Ansible Vault? Point init at it — vaultctl scans the vault locally, generates a metadata skeleton, and auto-detects entry types. No secrets leave the process.
vaultctl init --vault-file inventory/group_vars/all/vault.yml
# → enters vault password
# → scans keys, detects types (usernamePassword, sshKey, certificate, ...)
# → generates vault-keys.yml with all entries
# → asks to apply detected typesAfter import, fill in descriptions and rotation schedules:
vaultctl describe my_api_token # see current metadata
vaultctl check # which keys need attention?| Command | Description |
|---|---|
vaultctl init |
New vault or import existing (auto-detects types) |
vaultctl list |
List all keys (shows [type] tags for structured entries) |
vaultctl get <key> |
Print secret value (--field name for structured entries) |
vaultctl set <key> [value] |
Set a key (--prompt, --file, --expires, --no-backup) |
vaultctl delete <key> |
Remove a key |
vaultctl describe <key> |
Show metadata (rotation, consumers, expiry) |
vaultctl restore <key> |
Swap current value with _previous backup |
vaultctl edit |
Open vault in $EDITOR via ansible-vault edit |
vaultctl check |
Report expired/expiring keys (--json, --quiet, --warn-days N) |
vaultctl detect-types |
Auto-detect entry types (--apply, --ai, --show-redacted) |
vaultctl validate |
Validate config, metadata, and vault content against CUE schemas |
vaultctl schema infer |
Generate a CUE schema baseline from current vault content |
vaultctl schema sync |
Detect drift between baseline and current vault (--apply to update) |
vaultctl self-update |
Update binary to latest release (standalone only) |
All mutating commands support --force to skip confirmation prompts.
vaultctl looks for .vaultctl/config.yml in: $VAULTCTL_CONFIG → current directory upwards to git root → ~/.config/vaultctl/config.yml.
# .vaultctl/config.yml
vault_file: inventory/group_vars/all/vault.yml
keys_file: inventory/group_vars/all/vault-keys.yml
password:
env: VAULT_PASS # tried first
file: ~/.ansible-vault-pass # then file
cmd: pass show project/vault # then commandPaths resolve relative to the project root (the parent of .vaultctl/), so vault and keys files live where Ansible expects them — typically next to your inventory data, not inside .vaultctl/ itself.
Secret metadata lives in vault-keys.yml (unencrypted, separate from secrets):
vault_keys:
my_api_token:
description: "API token for service X"
rotate: "365d"
expires: "2026-12-01"
consumers: ["host01", "host02"]
rotate_cmd: "Web UI → Settings → Regenerate"vaultctl check uses the expires field to flag expired or soon-to-expire credentials — exit code 1 for CI/cron integration.
vaultctl validate checks your project against CUE schemas — typo detection, type constraints, cross-file consistency. Useful as a CI gate or pre-commit hook.
vaultctl validate # full check: config, metadata, vault, cross-consistency
vaultctl validate --skip-content # skip vault content (no decryption needed)What it checks:
.vaultctl/config.ymlagainst the bundled#Configschema (catches typos likepasword:).vault-keys.ymlagainst the bundled#KeysFileschema (rejects unknown fields, invalid types likebogusType, malformedexpiresdates).vault.ymlcontent against the bundled#VaultFileschema (permissive by default — strings or structured objects).- Cross-file consistency: every key in
vault.ymlhas metadata invault-keys.ymland vice versa (_previousbackup keys exempt).
Custom schemas:
Drop your own CUE schema next to the config to override the bundled defaults — keeps strict, project-specific rules close to the data.
.vaultctl/
├── config.yml
├── vault.cue # overrides #VaultFile (e.g. require min password length)
├── vault.constraints.cue # additional constraints; CUE merges with vault.cue
└── keys.cue # overrides #KeysFile (e.g. require descriptions)
Bootstrap from existing data:
vaultctl schema infer # writes .vaultctl/vault.cue covering the current vault
vaultctl schema infer --force # overwrites an existing baselineThe generated baseline is a closed #VaultFile definition — adding a new key without re-running infer (or extending the schema by hand) makes validate fail. Hand-edited rules (regex constraints, value ranges, required descriptions) belong in a sibling vault.constraints.cue so subsequent infer runs don't trample them.
Detect drift between vault and baseline:
vaultctl schema sync # diff-only, exit 1 on drift — CI-friendly
vaultctl schema sync --apply # rewrite the baseline to match the vaultschema sync re-derives the schema from the current vault content and compares it to .vaultctl/vault.cue. Without --apply it prints a unified diff and exits non-zero; with --apply it rewrites the baseline. vault.constraints.cue is never touched — CUE merges it back in at validation time.
Schema-aware set:
When a baseline exists, vaultctl set checks the new vault content against it before encrypting. If a value introduces structure the schema doesn't cover (e.g. a new key), set shows the diff and asks whether to extend the baseline:
vaultctl set new_token "abc123"
# → Schema does not cover this change:
# --- .vaultctl/vault.cue (current)
# +++ .vaultctl/vault.cue (inferred)
# ...
# + new_token: string
# Extend .vaultctl/vault.cue with this change? [y/N]:Flags for non-interactive use:
--extend-schema— extend silently.--no-extend-schema— leave the baseline alone, accept drift (vaultctl validatewill flag it later).--forcewithout either flag — same as--no-extend-schema(safer non-interactive default).
Without a baseline or without the cue binary, the check is skipped entirely.
Without cue installed: schema checks are skipped with a warning; the cross-consistency check still runs.
vaultctl can auto-detect structured entry types (usernamePassword, sshKey, certificate, secretText) from field structure, value patterns, and key names:
vaultctl detect-types # dry-run: show suggestions
vaultctl detect-types --apply # write types to vault-keys.yml
vaultctl detect-types --ai --yes # use AI-assisted detection (GDPR consent required)AI detection sends only redacted metadata (key names, field names) — no secret values leave the process.
Standalone binaries update themselves with SHA256 checksum verification:
vaultctl self-updateDowngrades are prevented. Updates without published checksums are refused.
"Decryption failed (no vault secrets were found that could decrypt)"
vaultctl cannot find or resolve the vault password. Check your .vaultctl/config.yml:
password:
env: VAULT_PASS # set this env var, or
file: ~/.ansible-vault-pass # point to a password file, or
cmd: pass show project/vault # run a command that prints the passwordAt least one source must be configured. The chain is tried top to bottom — first match wins.
"No config file found"
vaultctl searches for .vaultctl/config.yml upwards from the current directory. Run vaultctl init to create one, or set VAULTCTL_CONFIG=/path/to/config.yml.
vaultctl init overwrites my password config
init creates a fresh .vaultctl/config.yml with defaults. If you re-run init in a directory that already has a config, the password section resets. Edit .vaultctl/config.yml manually after init to add your password source.
self-update says "only available for standalone binaries"
You installed via pip or uv tool install, not the standalone binary. Update with:
uv tool install --force --from git+ssh://git@github.com/cdds-ab/vaultctl.git vaultctlvaultctl is designed to handle credential data safely. All operations run locally, secrets never leave the process, and AI-assisted features use triple-layer redaction before any external communication.
For a detailed security analysis — covering data flow, trust boundaries, temporary file handling, and verification steps — see docs/SECURITY.md.
MIT