Problem
vaultctl currently only supports flat key-value pairs where values are strings. Real-world credential management often requires structured data — for example, Jenkins credential objects that combine multiple fields like username, password, type, and description into a single logical entry.
Current Behavior
# vault.yml (encrypted)
my_credential: "some-password"
# vault-keys.yml (metadata)
vault_keys:
my_credential:
description: "Some password"
expires: "2026-06-01"
Desired Behavior
# vault.yml (encrypted)
my_credential: "some-password" # flat still works (backward compat)
jenkins_xray: # structured entry
type: usernamePassword
username: "<clientId>"
password: "<clientSecret>"
# vault-keys.yml (metadata)
vault_keys:
my_credential:
description: "Simple secret"
expires: "2026-06-01"
jenkins_xray:
description: "Xray Cloud API credentials for Jenkins"
type: usernamePassword
expires: "2026-12-01"
rotate: quarterly
consumers:
- jenkins-prod
Proposed Data Model
Vault Entry Types
| Type |
Fields |
Use Case |
secretText |
value |
Simple secret (default, backward compat) |
usernamePassword |
username, password |
Service accounts, API credentials |
sshKey |
private_key, passphrase (optional) |
SSH deploy keys |
certificate |
cert, key, ca (optional) |
TLS certificates |
| (custom) |
user-defined fields |
Extensible for project-specific types |
Type Resolution
- If a vault value is a string → treat as
secretText (backward compatible)
- If a vault value is a dict → read
type field (default: secretText if type is absent)
- Unknown types are allowed (no closed enum) — validation is optional
Proposed CLI Extensions
vaultctl set — structured input
# Flat value (unchanged)
vaultctl set my_secret "password123"
# Structured value from YAML file
vaultctl set jenkins_xray --file cred.yml --type usernamePassword
# Interactive structured input
vaultctl set jenkins_xray --type usernamePassword
# prompts for: username, password
# Set individual field within structured entry
vaultctl set jenkins_xray.username "new-client-id"
vaultctl get — field access
# Get entire entry (YAML output for structured, plain for flat)
vaultctl get jenkins_xray
# type: usernamePassword
# username: <clientId>
# password: <clientSecret>
# Get specific field (dot notation)
vaultctl get jenkins_xray.password
# <clientSecret>
# Machine-readable output
vaultctl get jenkins_xray --json
# {"type": "usernamePassword", "username": "...", "password": "..."}
vaultctl list — type display
vaultctl list
# jenkins_xray [usernamePassword] Xray Cloud API credentials
# my_credential [secretText] Simple secret
vaultctl describe — extended metadata
vaultctl describe jenkins_xray
# Key: jenkins_xray
# Type: usernamePassword
# Fields: username, password
# Description: Xray Cloud API credentials
# Expires: 2026-12-01
# Rotation: quarterly
# Consumers: jenkins-prod
Implementation Plan
Phase 1: Foundation (non-breaking)
Step 1: Type detection utility (types.py)
- Function to detect entry type from vault value (string →
secretText, dict → read type)
- Dataclass or TypedDict for structured entries
- No CLI changes yet — internal only
Step 2: Extend KeyInfo dataclass (keys.py)
- Add optional
type: str field to KeyInfo
- Backward compatible — missing
type defaults to secretText
Phase 2: Read path
Step 3: Update get command (cli.py)
- If value is a dict, render as YAML (not
repr())
- Support dot notation for field access:
vaultctl get key.field
- Add
--json flag for machine-readable output
Step 4: Update list command (cli.py)
- Show
[type] column when entry has a known type
- Handle both flat and structured entries
Step 5: Update describe command (cli.py)
- Show
Type and Fields for structured entries
Phase 3: Write path
Step 6: Update set command (cli.py)
- Accept
--type option to create structured entries
- Support
--file with YAML content for structured values
- Support dot notation for setting individual fields
- Interactive prompts for known types (username + password, etc.)
Step 7: Update backup/restore logic (cli.py)
- Ensure
_previous backup works with dict values (already dict[str, Any] — likely works)
- Test edge cases: restoring flat ↔ structured
Phase 4: Validation & Polish
Step 8: Optional type validation
- Config option:
strict_types: true to enforce known field schemas
- Default: permissive (accept any dict structure)
Step 9: Update check command
- Structured entries may have per-field expiry (e.g., cert vs key)
- Consider
expires on both vault-keys.yml level and within vault entry
Architecture Considerations
- vault.py requires NO changes — already handles
dict[str, Any]
- yaml_util.py requires NO changes — already generic
- New module
types.py keeps type logic isolated from CLI
- Backward compatibility is guaranteed: all existing flat entries continue working
_previous backup already stores Any — structured backup should work
Security Considerations
- Structured values remain fully encrypted inside
vault.yml — no change in security posture
- The
type field in vault-keys.yml (unencrypted) reveals credential kind — this is intentional metadata, similar to description, but should be documented as a conscious choice
- Input validation needed for
--file to prevent YAML deserialization issues (already uses safe_load)
- Dot notation access must be sanitized to prevent path traversal in nested dicts
Open Questions
- Should
type live in vault.yml (encrypted, alongside data) or vault-keys.yml (metadata, unencrypted), or both?
- Proposal: both —
vault.yml is authoritative, vault-keys.yml mirrors it for unencrypted queries
- Should field prompts be hardcoded per type or configurable?
- Proposal: start with hardcoded for common types, make extensible later
- Maximum nesting depth? Flat dicts only, or allow arbitrary nesting?
- Proposal: one level of nesting only (dict of scalars) to keep it simple
Problem
vaultctl currently only supports flat key-value pairs where values are strings. Real-world credential management often requires structured data — for example, Jenkins credential objects that combine multiple fields like
username,password,type, anddescriptioninto a single logical entry.Current Behavior
Desired Behavior
Proposed Data Model
Vault Entry Types
secretTextvalueusernamePasswordusername,passwordsshKeyprivate_key,passphrase(optional)certificatecert,key,ca(optional)Type Resolution
secretText(backward compatible)typefield (default:secretTextiftypeis absent)Proposed CLI Extensions
vaultctl set— structured inputvaultctl get— field accessvaultctl list— type displayvaultctl describe— extended metadataImplementation Plan
Phase 1: Foundation (non-breaking)
Step 1: Type detection utility (
types.py)secretText, dict → readtype)Step 2: Extend
KeyInfodataclass (keys.py)type: strfield toKeyInfotypedefaults tosecretTextPhase 2: Read path
Step 3: Update
getcommand (cli.py)repr())vaultctl get key.field--jsonflag for machine-readable outputStep 4: Update
listcommand (cli.py)[type]column when entry has a known typeStep 5: Update
describecommand (cli.py)TypeandFieldsfor structured entriesPhase 3: Write path
Step 6: Update
setcommand (cli.py)--typeoption to create structured entries--filewith YAML content for structured valuesStep 7: Update
backup/restorelogic (cli.py)_previousbackup works with dict values (alreadydict[str, Any]— likely works)Phase 4: Validation & Polish
Step 8: Optional type validation
strict_types: trueto enforce known field schemasStep 9: Update
checkcommandexpireson both vault-keys.yml level and within vault entryArchitecture Considerations
dict[str, Any]types.pykeeps type logic isolated from CLI_previousbackup already storesAny— structured backup should workSecurity Considerations
vault.yml— no change in security posturetypefield invault-keys.yml(unencrypted) reveals credential kind — this is intentional metadata, similar todescription, but should be documented as a conscious choice--fileto prevent YAML deserialization issues (already usessafe_load)Open Questions
typelive invault.yml(encrypted, alongside data) orvault-keys.yml(metadata, unencrypted), or both?vault.ymlis authoritative,vault-keys.ymlmirrors it for unencrypted queries