Skip to content

feat: support structured/complex data types in vault entries #14

Description

@f3rdy

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

  1. If a vault value is a string → treat as secretText (backward compatible)
  2. If a vault value is a dict → read type field (default: secretText if type is absent)
  3. 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

  1. 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
  2. Should field prompts be hardcoded per type or configurable?
    • Proposal: start with hardcoded for common types, make extensible later
  3. Maximum nesting depth? Flat dicts only, or allow arbitrary nesting?
    • Proposal: one level of nesting only (dict of scalars) to keep it simple

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions