Releases: cdds-ab/vaultctl
v0.12.0
v0.12.0 (2026-03-14)
Features
Summary
SSH private keys and certificates stored in Ansible Vault suffer from whitespace corruption during YAML multiline formatting. This PR adds clean import/export modes to prevent these issues.
get --raw: Outputs value without Type: headers or field labels, strips trailing whitespace per line, ensures single trailing newline. Ideal forvaultctl get key --field privateKey --raw > key.pem-get --base64: Outputs value as a single base64-encoded line, suitable for environments that cannot handle multiline values -set --base64: Accepts an inline base64-encoded value, decodes before storing -set --base64-file: Reads base64 from a file or stdin (-), decodes before storing -set --file: Now applies whitespace cleanup (trailing space removal) on import -clean_multiline_value(): New helper that strips trailing whitespace per line and ensures exactly one trailing newline
Problem
When SSH keys are stored in YAML via ansible-vault, the multiline formatting introduces trailing spaces on lines. Extracting these keys with vaultctl get ... --json | jq -r produces keys that SSH rejects. There was no way to get a clean, whitespace-safe export or to import base64-encoded values.
Changed Files
| File | Change | |------|--------| | src/vaultctl/cli.py | Added --raw and --base64 flags to get, --base64 and --base64-file options to set, mutual exclusivity validation, _output_raw() and _output_base64_encoded() helpers | | src/vaultctl/yaml_util.py | Added clean_multiline_value() helper | | tests/test_cli.py | 17 new integration tests covering all new flags and edge cases | | tests/test_yaml_util.py | 7 unit tests for clean_multiline_value() | | tests/conftest.py | Added ssh_key fixture entry with trailing whitespace for testing |
Design Decisions
clean_multiline_valueinyaml_util.py— It is a value formatting utility closely related to YAML handling, keeping it here avoids a new module for one function 2. Mutual exclusivity of--json,--raw,--base64— Validated at runtime with a clear error message rather than Click'scls=MutuallyExclusiveOptionto keep it simple 3.--filenow cleans whitespace on import — Prevents storing corrupted values at the source. This is a minor behavioral change but strictly an improvement 4.--base64-file -for stdin — Follows Unix convention, enables piping:cat key.pem | base64 | vaultctl set key --base64-file -
Test Plan
-
get --rawon plain strings outputs clean value - [x]get --rawon multiline values strips trailing whitespace - [x]get --raw --fieldextracts single field without headers - [x]get --rawon structured entries outputs YAML without Type: header - [x]get --base64produces valid single-line base64 - [x]get --base64on multiline values cleans before encoding - [x]get --base64 --fieldworks on individual fields - [x]--json,--raw,--base64are mutually exclusive - [x]set --base64decodes and stores correctly - [x]set --base64rejects invalid input - [x]set --base64-filereads from file - [x]set --base64-file -reads from stdin - [x]set --filecleans trailing whitespace - [x] Multiple input sources rejected - [x]clean_multiline_valueunit tests (7 cases) - [x] All 319 tests pass, coverage 88%
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.11.1...v0.12.0
v0.11.1
v0.11.1 (2026-03-14)
Bug Fixes
Problem
vaultctl get on structured entries (dicts, lists) outputs Python repr() format — single quotes, no indentation, not parseable by jq or other tools. Example:
domains: [{'name': 'docker build hosts', 'credentials': [{'type': 'x509ClientCert', ...
This makes credentialStore entries with 50+ nested credentials completely unreadable.
Solution
1. Human-readable output: YAML formatting
Added _format_value() helper (cli.py:35-46) that formats nested values: - Strings: returned as-is (no change to existing behavior) - Dicts/Lists: formatted as YAML via yaml.dump(default_flow_style=False) - Other types: converted via str()
The get command now calls _format_value(value[f]) instead of directly printing value[f], so nested structures render as readable YAML with proper indentation.
2. Machine-readable output: --json flag
New --json flag on the get command outputs the value as JSON:
bash vaultctl get vault_jenkins_credentials --json | jq '.global.credentials | length'
Works with --field too:
bash vaultctl get db_creds --field username --json
Uses json.dumps(indent=2, ensure_ascii=False) for readable JSON that pipes cleanly to jq.
Files changed
src/vaultctl/cli.py: - Added_format_value()helper (lines 35-46) - Added--json/output_jsonoption togetcommand - Changed dict field output fromvalue[f]to_format_value(value[f])- JSON output path for both full value and--fieldaccess
Test plan - [ ] vaultctl get <dict-key> shows readable YAML (not Python repr) - [ ] vaultctl get <dict-key> --json | jq . parses correctly - [ ] vaultctl get <string-key> unchanged (plain string output) - [ ] vaultctl get <dict-key> --field username unchanged - [ ] All 298 existing tests pass
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.11.0...v0.11.1
v0.11.0
v0.11.0 (2026-03-14)
Features
Summary
- New
vaultctl completion <shell>command (bash, zsh, fish) - Uses Click's
shell_completionAPI - Works without
.vaultctl.ymlconfig
Install
bash eval "$(vaultctl completion bash)" # bash eval "$(vaultctl completion zsh)" # zsh vaultctl completion fish > ~/.config/fish/completions/vaultctl.fish # fish
Test plan
-
vaultctl completion bashoutputs valid bash completion -
vaultctl completion zshoutputs valid zsh completion -
vaultctl completion fishoutputs valid fish completion - Tab completion works after eval
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.10.0...v0.11.0
v0.10.0
v0.10.0 (2026-03-14)
Features
Summary
- Add
--context / -cflag tovaultctl searchthat shows the parent dict of each matched field - Sibling fields are redacted by default (
****), matched field shows first 4 chars +... - Combine with
--show-matchto display all field values in cleartext - Multiple matches in the same parent object are grouped into a single block
Closes #20
Test plan
- Unit tests for
search_values(include_context=True)covering nested dicts, lists, top-level strings, multiple matches - CLI integration tests for
--context,--context --show-match, and top-level string fallback - All 298 tests pass, 88% coverage
- mypy strict, ruff, bandit clean
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.9.0...v0.10.0
v0.9.0
v0.9.0 (2026-03-14)
Features
Summary
vaultctl list --filter/-f PATTERN: Regex filter on key names, descriptions, and consumers from vault-keys.yml metadata. No additional decryption beyond whatlistalready does. -vaultctl search PATTERN: New subcommand that decrypts the vault and recursively searches all values (strings in nested dicts/lists). Output shows only key names and dot-path locations — never values unless--show-matchis explicitly used. ---keys-only / -k: Search only key names and metadata (no vault decryption needed) ---show-match: Display matched values (with security warning) - Exit code 0 if matches found, 1 if not (scripting-friendly)
Security considerations - Search pattern is never logged or included in error output - Values are never shown without explicit --show-match flag - --show-match displays a yellow WARNING to stderr - Recursive search is depth-limited (max 20 levels) - All search logic is in a pure-function module (search.py) — no side effects
Architecture - New module src/vaultctl/search.py with search_values() and filter_keys() — pure functions, fully unit-testable - CLI wiring in cli.py follows existing command patterns - 100% test coverage on search.py, 87% overall
Closes #TBD
Test plan
- Unit tests for
search_values()— flat values, nested dicts, nested lists, depth limit, include_values toggle - [x] Unit tests forfilter_keys()— key name, description, consumer matching, regex, case insensitivity - [x] Integration tests forvaultctl list --filter— name match, description match, regex, no match, invalid regex - [x] Integration tests forvaultctl search— value found, not found, nested, show-match, keys-only, invalid regex - [x] All 272 tests pass, 87.45% coverage - [x] ruff, mypy --strict, bandit all clean
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.8.2...v0.9.0
v0.8.2
v0.8.2 (2026-03-14)
Bug Fixes
Summary
_collect_nested_credential_types()now handles list values directly (not only lists inside dicts)detect_type_heuristic()checksisinstance(value, (dict, list))for credential store detection- Fixes detection for vault entries that are credential lists at the top level
- 5 new tests for list-based credential structures
Test plan
-
uv run pytest— 237 tests green -
vaultctl detect-typeson vaults with list-based credential entries
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Documentation
Summary
Adds troubleshooting section covering the most common issues:
- Decryption failures from missing/misconfigured password source
- Config file not found
initoverwriting password config on re-runself-updateon pip/uv installs
Test plan
- README renders correctly on GitHub
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.8.1...v0.8.2
v0.8.1
v0.8.1 (2026-03-14)
Bug Fixes
Summary
- F-04: Recursion depth limit (max 50) in
_collect_nested_credential_types()andredact_value()- A-01: Runtime redaction guard inbuild_payload()usingcontains_unredacted()— aborts AI detection if redaction fails - F-05: Trust-boundary comments onshell=Trueinpassword.pyandai_detect.py- docs/SECURITY.md: Comprehensive security architecture documentation covering data flow, triple-layer AI protection, trust boundaries, and verification steps - 7 new tests for recursion limits and redaction guard
Based on findings from cybersecurity audit of #24.
Test plan - [ ] uv run pytest — all 233+ tests green - [ ] Review docs/SECURITY.md for completeness - [ ] vaultctl detect-types --show-payload still works correctly
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.8.0...v0.8.1
v0.8.0
v0.8.0 (2026-03-14)
Features
Summary
- Adds recursive scanning of nested dict/list structures for credential type fields
- Detects Jenkins JCasC-style credential stores
- New credentialStore type with sub-type summary
- 10 new tests
Closes #24
Test plan
- uv run pytest — all tests green
- vaultctl detect-types on real Jenkins JCasC vault shows nested types
- Existing detection behavior unchanged
Co-authored-by: Fred Thiele 8555720+f3rdy@users.noreply.github.com
Detailed Changes: v0.7.2...v0.8.0