← All engines · Security
CredentialVault
stable
v1.0.0 · last updated 2026-06-06 · source: cybercli.vault.backend.CredentialVault
§1
What it does
Per-secret AES-GCM encryption under a master key kept off the engine box — every API key, webhook secret, and connector credential is at rest encrypted and bound to a fingerprintable Associated Data (AAD) so a leaked vault row is useless without the master.
§2
Why you care
The CredentialVault is the engine that turns 'we hold your secrets' from a liability into a defensible posture. Every secret (Wazuh API key, Slack webhook URL, Anthropic API key, SMTP password) sits in the vault encrypted with a key that doesn't live on disk next to it. Steal the SQLite file and you get random bytes; steal the master key and you still need the AAD to bind each row to its purpose.
Other AI security products either ship secrets in plaintext config files (Wazuh historically), let you wire your own vault and pretend that's a feature (Sentinel), or keep your secrets in their cloud (Charlotte AI, Dropzone) — which means a breach of their infrastructure breaches yours. CyberCLI's vault sits on your engine box; the master key lives wherever you put it (KMS, HSM, a file with restrictive perms, a hardware token). We never see your secrets even in principle.
The vault is also the engine that makes the credential lifecycle audit-able. Every put / get / delete writes an audit row keyed on the secret name (not the value). A forensic replay can answer 'when did we add the Slack webhook, who put it in, when did it last get read' without ever seeing the actual webhook URL.
§3
How it works
On `put(name, value, aad)` the vault first asks the configured KeyProvider for the master key (file backend, env backend, or KMS/HSM backend in v1.x). It then derives a per-entry nonce, encrypts the value with AES-GCM-256 using the master + the nonce + the AAD as additional authenticated data, and stores `(name, nonce, ciphertext, aad_fingerprint, created_at, last_accessed_at)` in the vault SQLite.
On `get(name, aad)` the vault loads the row, verifies the AAD fingerprint matches the supplied AAD (rejects a re-bound row attack), asks the KeyProvider for the master, and runs AES-GCM verify-and-decrypt. A tampered ciphertext, a wrong nonce, or a mismatched AAD all surface as the same opaque `VaultError` — the caller can't distinguish, which is deliberate; we don't give an attacker a side-channel.
AAD binding is the security property most security tools skip. The AAD is the secret's purpose — `connector:wazuh:api_key`, `delivery:slack:webhook_url`, `ai:provider:anthropic`. An attacker who copies a vault row from `connector:wazuh:api_key` into `ai:provider:anthropic` to misuse the AI key as a Wazuh credential gets a VaultError, not a successful misuse, because the AAD fingerprint check fails before decryption.
Key rotation runs via `rotate(new_key_provider)` which re-encrypts every row under the new master without ever revealing the plaintext on disk — the new ciphertext is computed in memory and written back atomically per row. The audit log records the rotation as a single `vault_master_rotated` row carrying the count of rotated secrets but never any secret name or value content.
§4
Configure & observe
CLI
$ cybercli vault list Show every secret name + last_accessed_at, without ever revealing a value. Used as a 'what secrets does this install hold' inventory.
$ cybercli vault put <name> Add or replace a secret. Value comes from stdin so it never appears in shell history. AAD comes from the named secret's expected purpose.
$ cybercli vault delete <name> Hard-delete the row. The audit log keeps the delete event but the ciphertext is gone — no key-rotation can recover it.
Dashboard surfaces
Operator surface for adding webhook URLs, API keys, and SMTP credentials. The dashboard never re-displays a secret value once stored; every read in the engine goes through vault.get() with the matching AAD.
Forensic replay of every credential lifecycle event — adds, reads (rate-limited to one row per minute per secret), deletes, and master rotations.
Audit row shape
vault_secret_addedvault_secret_replacedvault_secret_readvault_secret_deletedvault_master_rotatedvault_decrypt_failed parameters.secret_name + parameters.aad_fingerprint + parameters.actor on every row. Read rows are rate-limited to one audit emit per secret per 60 seconds (high-frequency engine reads would otherwise drown the chain). Decrypt-failed rows carry an opaque parameters.failure_class (tampered_ciphertext / wrong_aad / wrong_master) without revealing which.
§5
Status
Stable — AES-GCM-256 + the AAD binding is the locked design. Pen-test in 2026-05 returned zero findings against the vault interface. The implementation is covered by ~80 unit tests including fuzz-test variants that try malformed AAD, truncated ciphertext, and mismatched nonces.
Known limit: only the file backend ships for the master KeyProvider in v1. KMS / HSM backends (AWS KMS, Azure Key Vault, on-prem PKCS#11) are the v1.5 + Enterprise add. The interface is already abstracted — the KMS backend slots in without any vault consumer changing.
Shipped 2026-06-07: `cybercli vault rotate --new-key-path <path>` + the `vault.rotate(new_master_key)` Python API. Rotation is all-or-nothing — a tampered or undecryptable entry aborts before touching disk. The remaining gap is an automated rotation scheduler that triggers on a cadence; that lands in v1.x.
How this engine talks to the rest