core.vault
Explicit reading and generation of Vault KV secrets on the Keeper side.
core.vault works with Vault KV on the Keeper itself (both states require on: keeper), not on the host. kv-read reads a secret explicitly and leaves a vault.kv-read audit event, which implicit ${ vault(...) } does not; kv-present idempotently generates missing fields with a crypto-random value per the password policy and does not overwrite existing ones. The generated value is never handed out — not to the output, the audit, or the logs.
Requirements
- Rootnot required
- Side
keeper-side - Collection
soulstack.secrets - Category
secrets
The Vault KV engine version (v1/v2) is determined on the Keeper side automatically (probe) or set by the vault.kv_version override. The module receives an already-flat payload and works the same on both versions; the path is given mount-relative and without the data/ segment.
States
core.vault.kv-read — Explicit read of a secret from Vault KV, recording the vault.kv-read audit event.
Never: it is a read operation, the state is not mutated.
Always — kv-read only reads.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
path | string | required | Vault KV path (mount-relative). |
fields | list<string> | optional | Which keys to extract; empty → the whole secret. |
Example — Read DB credentials for the audit event
- name: Read DB credentials from Vault (audit-tracked) on: keeper module: core.vault.kv-read register: db_creds params: path: secret/redis/admin fields: [username, password]Output
| Field | Type | Description |
|---|---|---|
path | string | echo of the requested path |
data | object | the extracted key→value pairs (after the fields filter) |
fields | array<string> | the key names in data (sorted) |
core.vault.kv-present — Generate-if-absent: ensure the specified secret fields exist, generating the missing ones crypto-randomly per the password policy.
Only when something was actually generated.
All fields are already non-empty — values are not overwritten.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
policy | map | optional | Step-level default policy for all targets: length (8..1024, default 32), charset (alphanumeric|hex|base64url|ascii-printable-safe) OR allowed_chars (mutually exclusive). |
targets | list<map> | required | Non-empty list of targets {path: <Vault KV path without #field>, field?: <field name, default password>, policy?: <per-target override>}. |
Example — Generate Redis passwords at create (generate-if-absent)
- name: Ensure redis passwords exist in Vault (generate if absent) on: keeper module: core.vault.kv-present params: policy: length: 32 charset: alphanumeric targets: - { path: secret/redis/main, field: password } - { path: secret/redis/main/users/replica, field: password }Output
| Field | Type | Description |
|---|---|---|
generated | object | map <vault-path> → sorted list of generated field names (without values); empty if nothing was generated |
Reference
Alphabet (charset) presets for generation
kv-present generates a value from a named charset preset or from an explicit allowed_chars (mutually exclusive). The default is ascii-printable-safe: the password must not break the target config.
| Preset | Alphabet |
|---|---|
| alphanumeric | Latin letters of both cases + digits (safe everywhere) |
| hex | lowercase hex digits 0-9a-f |
| base64url | url-safe base64 (-/_ instead of +//, no =) |
| ascii-printable-safe (default) | printable ASCII (0x21–0x7E) without the characters that break redis.conf / users.acl / shell substitution: space, double and single quotes, #, backslash, backtick, and $ |
Notes
- kv-read exists for the audit trail: implicit ${ vault(...) } is cheap to render but only reads and leaves no separate audit record. kv-read is the explicit form for compliance (PCI-DSS, SOC2); implicit reads remain available for the render phase.
- kv-present is a write form that CEL resolution does not have at all: the service generates the missing passwords itself at create, and the operator does not need to pre-seed secrets with a manual vault kv put.
- Multiple targets on the same path (different fields) are merged into a single WriteKV over the existing fields (read-merge-write) — neighboring fields are not lost and no extra KV versions are spawned.
- destroy does not clean up secrets: a re-create reuses the same passwords. Rotating or deleting a secret is a separate scenario — this state does not do it.
- kv-present must run before the render phase of tasks that read the same secrets through ${ vault(...) } (staged-render, ADR-056): first generation in Vault, then the read.
Security & defaults
- The generated value never leaves the system (ADR-010): not in the register output, the audit payload, the logs/OTel/UI, or the error text — only the fact, the path, and the names of the generated fields. The invariant is locked in by a guard test.
- kv-read: the secret values themselves do not go into the audit payload — only the path and the list of fields are recorded. In the register output the values are present (data.*), but on the write path they are masked (logs / OTel / UI / reports) via MaskSecrets.
- Generation uses crypto/rand (not math/rand), and each character is chosen uniformly (rejection sampling, no modulo bias).
- The default charset ascii-printable-safe excludes the characters that break redis.conf / users.acl / shell substitution — a generated password will not break the target config.
- Requires the Keeper's Vault auth: reads and writes run under the Keeper cluster's account, the module does not accept a token/credentials in params and does not elevate access — it works exactly where the Keeper's policy in Vault allows.
- A Keeper-side operation (on: keeper): root/capability semantics do not apply, and running a scenario with this step is governed by the operator's RBAC.