Skip to content

AVP

v0.9.0 Security

This release includes 1 security fix for security teams reviewing exposed deployments.

✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →
This release patches 1 known CVE

Topics

agentic-ai vaultwarden

Affected surfaces

auth rbac

Summary

AI summary

Updates docs/adrs/ADR-0029-stored-placeholders.md, scope, and 1/2/3 across a mixed release.

Full changelog

Added

  • Stored placeholders — the note pins what the consumer emits (ADR-0029). A binding note/annotation may now carry a placeholder: key; when present and valid it wins over salt derivation for that secret. avp binding new mints one by default (CSPRNG, 130-bit lowercase-base32 tail) and prints a consumer-wiring hint on stderr (export NAME='<placeholder>'); --no-placeholder keeps the legacy salt-derived flow. Three guardrails: generated-not-hand-chosen, a strict format gate at parse (avp-PLACEHOLDER- + ≥21-char lowercase-base32 tail — a weak string like token cannot parse, so a note can never aim injection at innocent traffic), and global uniqueness at resolve — fail-closed with thief-loses semantics, covering equal AND substring-overlapping claims (a stored placeholder colliding with another secret's derived or file placeholder drops only the thief, so a note-writer can't un-broker someone else's secret nor brick the reload via the merge validator's overlap raise; contests with no single legitimate claimant drop all claimants, every drop audited with both names and kept attributable at request time). avp env now reads notes and projects the stored placeholder where pinned, derived otherwise, so the projection always matches what the daemon enforces. Closes the onboarding gap where a freshly bound secret couldn't inject until someone with daemon-host access ran avp env and hand-wired the derived placeholder; stored placeholders also survive salt rotation and secret renames. Notes without the key are unchanged. 21 tests including an addon end-to-end (stored placeholder in, real secret on the wire, no value in the audit log).

  • avp binding new — deterministic binding-note generator (code before prompts). A new operator verb that builds the per-secret binding note/annotation from flags and validates it through the daemon's own parse_notes_binding before printing it — so it can only ever emit a binding the daemon will accept: the # avp-binding marker present, the generic {secret} token present, a real host (a URL or malformed host is refused with the parser's own diagnostic and a non-zero exit). --host (repeatable for multi-host), --header/--format (non-Bearer auth), --methods/--paths (scope), --name, --backend bws|gsm (prints the notes block or the gcloud secrets update … --update-annotations command). This is the security-critical artifact that two outages came from hand-authoring (a missing marker; a host-shaped description) — now structurally impossible to emit wrong. The bundled avp skill is rewritten to run this tool and hand the user its output rather than author YAML free-hand. avp setup now prints a next-steps block pointing at the tool and the skill install. 10 tests, including a round-trip smoke that re-parses the tool's output back into a valid ParsedBinding.

  • avp setup backend selection + graceful token handling. Setup no longer hard-defaults to Bitwarden and dead-ends on an unskippable token prompt. It now supports all three backends via mutually-exclusive flags — --bws, --gsm, --static — and, when none is given on a TTY, presents an interactive picker (1/2/3) that loops until a valid choice; off a TTY it defaults to BWS so Ansible/container runs stay non-interactive. --gsm writes a keyless, secure-by-default GSM config (self_check: deny, reject_ambient_key: true, no key-file field) and, following DRY, hands off to the separate privileged IAM helper avp gcp-setup for per-secret grants rather than duplicating it. The BWS token prompt is now graceful: empty input re-prompts once, then skips cleanly — it never writes a 0-byte token file (which previously produced confusing downstream daemon failures), and the message points at --static / --gsm; the entered token is whitespace-stripped. 11 tests (picker mapping + re-prompt, GSM keyless render + no token/static step, empty-token writes-nothing, mutual exclusion).

  • sigv4 injector — AWS Signature Version 4 request signing (ADR-0027, Slice 1). The first signing injector: AWS APIs don't take a static bearer credential, they require each request to be signed (AWS4-HMAC-SHA256 over method + canonical URI/query + signed headers + SHA-256 of the body). inject.type: sigv4 takes region, service, and vault-secret references access_key_id_secret / secret_access_key_secret / optional session_token_secret; the secret's placeholder (planted in Authorization) is the detection trigger, not a substituted value. The pure, dependency-free signer (injectors/sigv4.py) is verified byte-for-byte against the AWS SigV4 test-suite get-vanilla vector. Because SigV4 hashes the full request body (unavailable at requestheaders), the addon detects + stashes the verdict there and signs in a new request hook once the body has buffered, over the raw wire bytes; a sigv4 + body-injector combination on one host is rejected at config-load. End-to-end tested through the addon (valid signature applied, placeholder gone, session tokens signed, missing-credential fail-closed 503, credential values never in the audit log). Unblocks brokering AWS credentials (S3, execute-api, STS) so they never enter the agent's process. The sibling signing injectors (hmac, jwt_bearer) can now land on the same seam.

  • hmac + jwt_bearer signing injectors (ADR-0028). Two more auth mechanisms on the SigV4 request-hook seam. hmac (RFC 2104): signs an operator-declared signing_string — a template over {method} {path} {query} {host} {body_sha256} {timestamp} — with HMAC-<sha256/…> and writes the hex/base64 digest to a header (optional timestamp_header); body-hashing, so it signs in the request hook, and hmac+body-injector on one host is rejected at config-load. jwt_bearer (RFC 7519/7515): mints a signed JWT from a vault key + operator claims (iss/sub/aud, iat/exp from ttl_seconds, extra_claims) and injects it as Bearer {jwt}; supports HS256 (shared secret), RS256 and ES256 (PEM key via the existing cryptography dep; ES256 emits raw R||S per RFC 7518). Signers pinned to public vectors (HMAC-SHA256; jwt.io HS256 byte-for-byte; RS256/ES256 round-trips); end-to-end tested (signature applied, placeholder gone, fail-closed 503 on missing key, key never in the audit log). With SigV4 + OAuth2-refresh + static header/body, AVP now covers the major authentication mechanisms.

  • oauth2_client_credentials + github_app injectors (ADR-0030) — the injector taxonomy is complete. The last two reserved types, both network-exchange (resolved at requestheaders like oauth2_refresh) sharing a new SSRF-guarded token transport (injectors/_token_transport.py). oauth2_client_credentials (RFC 6749 §4.4): exchanges a vault client id + secret for an access token (body_post/basic auth, optional scope), caches it per-binding, injects Bearer {access_token}. github_app: mints an App JWT (RS256, reusing the jwt_bearer signer; iat backdated for skew, exp ≤10 min) and exchanges it at /app/installations/{id}/access_tokens for a short-lived installation token, injecting token {token}. Both fold the rotating credential into the token-cache key (rotation invalidates), reuse the token_exchange audit (no new event type), and fail closed with 503; the client secret / App private key and the minted token never reach the audit log. End-to-end tested against mocked endpoints (exchange, cache hit, 4xx, missing/malformed key). All eight injector types now ship — static header/body, oauth2_refresh, oauth2_client_credentials, github_app, sigv4, hmac, jwt_bearer, multi.

  • Zero-restart notes-refresh (ADR-0032). Notes-mode bindings re-resolve on a background timer (notes_refresh_seconds, default 60), so an added/removed/edited vault-note binding goes live without restarting the daemon. Value + derived-token caches stay warm across the atomic swap — the token-cache key folds in the secret value, so a changed binding can't reuse a stale token — and a transient vault-listing failure keeps the previous bindings rather than dropping them. Emits a notes_refreshed audit event on change; file-mode is untouched.

Changed

  • Note parser tolerates uniformly-indented bodies. A hand-pasted note whose body is indented (copied out of a rendered/quoted block, or tab-indented — which raw YAML rejects) now parses: the common leading whitespace is stripped before YAML load. Safe because the note grammar is flat with flow-style lists, so dedent never alters key nesting; genuinely inconsistent indentation still fails loud.
  • Skill renamed avp-bindingsavp (skills/avp/): its scope now covers the full onboarding ceremony — authoring the binding, minting the stored placeholder, and wiring the consumer — not just bindings. Plugin name (/plugin install avp@agent-vault-proxy) is unchanged; the in-plugin invoke becomes /avp:avp. Update any ~/.claude/skills/ symlink to point at skills/avp.

Security

  • TLS termination scoped to bound hosts (ADR-0026). New tls_termination: bound | all (default bound). In bound mode AVP MITM-terminates and injects ONLY for hosts the config binds; every other CONNECT is an opaque TCP passthrough — no leaf cert minted, no decryption, the client validates the upstream's real certificate end-to-end. So AVP never holds plaintext for traffic it does not broker, and a stolen AVP CA cannot decrypt unbound flows. Decided per-connection at tls_clienthello (SNI, CONNECT-host fallback) against the live config snapshot, so hot-reload applies to new connections. Each tunneled connection emits a tls_passthrough audit event (destination host + reason only — no secret material) to preserve exfil visibility without interception. Deny-the-unbound (egress allow-list) was considered and rejected — it would force a per-deployment allow_unbound list maintained alongside the vault, breaking the "drop a secret, done" model; blocking stays with the host egress layer (nftables/OpenSnitch). BEHAVIOUR CHANGE: unbound HTTPS is no longer decrypted by default — set tls_termination: all to keep full termination. Orthogonal to unmatched_destination_policy: deny, which still 403s an unbound CONNECT before TLS.
  • Notes-binding marker required (ADR-0025) — a note is only a binding when it says so. A note/annotation is now parsed as a binding ONLY when its first non-blank line is exactly # avp-binding; anything unmarked is a human description and yields NoBinding — it cannot bind, cannot be judged malformed, and can never fail-closed-exclude the same secret's file bindings (the structural close of the v0.8.0 incident: ambient vault prose could previously become a "malformed binding" that silently un-brokered the secret under binding_source: both). Unmarked notes that look host-shaped log a load-time warning naming the secret, so a forgotten marker is self-diagnosing. Marked notes fail LOUD: marker with no body, or a marked scalar that is not hostname-shaped, is InvalidBinding (audited). Uniform across BWS notes and the GSM avp-binding annotation. BREAKING (note bindings only): prepend a # avp-binding line to any existing note/annotation binding, or it becomes inert (with the warning) on first reload; file bindings.yaml is unaffected.

Breaking Changes

  • Notes-binding marker `# avp-binding` is now required; unmarked notes become inert with a warning.

Security Fixes

  • TLS termination scoped to bound hosts (`tls_termination: bound` by default) prevents unbound HTTPS decryption, reducing MITM exposure.

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track AVP

Get notified when new releases ship.

Sign up free

Related context

Beta — feedback welcome: [email protected]