Skip to content

agent-vault-proxy v0.8.0

Choose a tag to compare

@rathko rathko released this 18 Jul 21:26
v0.8.0
57dbecb

[0.8.0], 2026-07-18

Added

  • Google Secret Manager backend (backend.type: gsm, ADR-0018). Third built-in backend after bws and static. Reads secret values from GSM with one REST access call per cache miss (stdlib urllib for the call; google-auth — a single first-party dep in a new gsm extra — mints/refreshes access tokens only, no gRPC SDK). Keyless by design: ADC + service-account impersonation, Workload Identity Federation, or plain user ADC — there is deliberately no key-file config field, and reject_ambient_key refuses a downloaded service-account key surfaced via ADC. Secure-by-default is a boot invariant, not a doc note: self_check: deny refuses to start under an over-broad identity (enumeration + project-level versions.access + write/admin testIamPermissions probes), avp gcp-setup grants only per-secret secretAccessor and hard-fails a project/folder/org bind, and _assert_in_scope refuses to fetch any name outside secret_prefix. Host binding lives at the vault via the avp-binding annotation (binding_source: gsm_notes, generalized to notes), reusing the backend-agnostic notes parser. New operator CLIs: avp gcp-setup and read-only avp doctor --probe-gcp. Hardened across four cross-vendor security review passes.
  • avp-bindings skill + Claude Code plugin packaging. The repo ships an Anthropic-format skill at skills/avp-bindings/ that walks an AI agent through authoring notes/annotation bindings (propose-only — it never sees or stores the secret). Packaged as a plugin via .claude-plugin/plugin.json + marketplace.json, so Claude Code users install it with /plugin marketplace add inflightsec/agent-vault-proxy then /plugin install avp@agent-vault-proxy; a manual ~/.claude/skills/ copy/symlink path is documented for other agents.
  • Honeytoken tripwire + honeytoken_triggered audit event (audit contract v2 → v3, ADR-0019). Any secrets: entry can set honeytoken: true to become a canary: plant its placeholder somewhere tempting and bind it to a trap host you control. Because the agent only ever holds the placeholder, the honeytoken's real value never enters its address space — but any use of that placeholder (injected, denied, scope-violated, or aimed at a non-bound host) makes the proxy emit a second honeytoken_triggered record immediately after the normal inject_decision, under the same synchronous fsync (G6 ordering preserved per record). Shipped off-box, one trigger anywhere in a fleet means that machine is being walked for credentials — the signal fires on intent, before any real value moves. The event carries only request_id, binding_name, dest_host, and underlying_reason — no secret material, header, body, or query string, per the §4.4 minimization contract. Emission is centralized in AuditWriter (keyed on the operator-flagged secret names, rebuilt per config reload), so it covers every decision path with no call site missed and is a no-op when no honeytoken is configured. Bumps AUDIT_CONTRACT_VERSION to 3; docs/architecture.md §4.4 documents the event; bindings.example.yaml carries a worked DECOY_AWS_PROD example. 10 tests.
  • /healthz liveness/readiness endpoint (roadmap: Observability). A plain-HTTP request through the proxy to the reserved sentinel host healthz.agent-vault-proxy.invalid at path /healthz is answered by the addon directly — never proxied upstream, emits no audit (health polling would otherwise flood the log). Returns 200 {"status": "ok"} once config, backend client, and audit writer are all live; 503 {"status": "starting"} before that, so an orchestrator holds traffic until AVP can actually broker. The body carries no version/build identifier — an unauthenticated endpoint should not hand out fingerprint/CVE-matching surface. The .invalid TLD (RFC 6761) can never collide with a real binding, and the gate is host and path — a real proxied request to /healthz on a bound host still injects normally. Intercepted before the destination allow-list so the probe isn't 403'd as unmatched. The Docker HEALTHCHECK now uses it (pure-Python, no curl/shell), replacing the bare-TCP probe that only proved the socket was open — not that AVP could broker. Prometheus metrics surface still pending (see docs/ROADMAP.md).

Security

  • Bare-note shorthand gated on hostname shape (live-incident fix). The ADR-0018 §4 Tier 0 bare-string shorthand (a note that is just api.example.com) treated ANY string note as a host. Real vaults carry free-text human descriptions in the note field ("HackerOne API identifier", "GCP project ID"); under binding_source: both each description became a garbage host binding that shadowed the secret's real bindings.yaml host — silently un-brokering the whole fleet (caught by the release smoke test on the first 0.8.0 deploy; the daemon was healthy but injecting nothing). The shorthand now fires only when the note is actually shaped like a hostname (≥2 dot-separated DNS labels, FQDN/IPv4/*.-wildcard); descriptive text returns NoBinding — no binding, not a garbage one. Explicit host: mappings are untouched. Regression tests feed the parser real-world description notes and pin both directions.
  • File-side notes host allowlist — annotations may only narrow, never add a host (ADR-0024). Closes the confused-deputy ADR-0018's fourth cross-vendor pass surfaced: on GCP, secretmanager.secrets.update (edit a secret's avp-binding annotation) and versions.access (read its value) are independently grantable, so a principal who can write the annotation but not read the secret can point it at a host they control — AVP then reads with its own identity and injects there, exfiltrating a value the attacker could never read directly. New opt-in top-level notes_host_allowlist bounds where notes/annotation-sourced bindings may route. Absent (the default) is byte-identical to prior behaviour — the zero-config North Star is untouched, pinned by a no-allowlist regression test. When set and binding_source is notes/both, a notes-supplied host outside the list has its binding entry dropped fail-closed at config-build, and a later request carrying that placeholder toward the dropped host is denied under the distinct host_not_in_allowlist audit reason (kept apart from invalid_binding_metadata). Annotations may still narrow methods/paths within an allowed host; file-secrets: hosts are trusted by definition and never rejected (including both-mode unions); multi-host fan-outs (ADR-0021) are judged per host; wildcard entries ride the existing allow_wildcard_hosts opt-in and a note can never broaden an exact entry. Source-agnostic — structural on GSM where the deputy exists, defense-in-depth on BWS. avp doctor --probe-gcp downgrades its annotation-trust WARN to OK once the allowlist is set.
  • ADR-0017 deferred hardening series closed (Amendment). All five items the v0.7.0 notes tracked as "hardening patch series remaining": (1) the SSRF guard's IP-literal short-circuit can no longer swallow its own block verdict — blocked literals raise without any DNS consultation; (2) token-exchange redirects are refused at the opener — any 3xx maps to token_endpoint_status:<code> with no retry and the Location target is never contacted (replaces the post-call geturl() check, which could only distrust a redirect after the redirected host had been visited); (3) refresh_token_rotated:pending is fsynced before the vault PUT (crash-window ordering — Oracle F2); (4) write-back value-precondition: an operator's manual mid-flight rotation is detected and never clobbered (new BackendWriteConflictError → outcome write_back_conflict; implements Oracle F3 as a stronger value variant, since the BWS SDK has no conditional PUT and revisionDate false-fires on note edits); (5) per-binding write_back_min_interval_seconds floor (default 60 s, 0 disables) bounds vault PUT pressure under forced-rotation storms (outcome write_back_rate_limited — Oracle F4). New refresh_token_rotated outcome values only — no new event types, no audit contract bump. ~20 new tests including an on-the-wire proof that the opener refuses redirects.
  • Concurrency audit + cross-vendor review hardening. A concurrency review of the oauth2_refresh write-back path closed a read-modify-write race on the per-binding write-back rate-limit bookkeeping — now serialized under a threading.Lock (_write_back_lock), so two in-flight rotations of the same binding can no longer both slip past the write_back_min_interval_seconds floor. A cross-vendor review pass separately caught a case-normalization bug: notes_host_allowlist entries are now lowercased at config-load, so a mixed-case allowlist entry can no longer silently fail to match the already-lowercased note host and drop a legitimately-allowed binding. New tests/test_concurrency_audit.py (five concurrency surfaces) and tests/test_addon_oauth2_concurrent.py (cold-start OAuth race).
  • Closed audit event-type set + emit() choke-point guard (ADR-0023). AUDIT_EVENT_TYPES in audit.py enumerates every event type the audit stream may carry; AuditWriter.emit() refuses (raises) any unlisted type. A new event type now requires a conscious edit to the closed set — the review point where it must prove no-secret-material and gain stateful no-leak coverage. Type-level guard; the per-type field allowlist is pinned in tests/test_audit_event_type_contract.py.
  • GSM read-only enforcement + annotation-trust advisory (ADR-0018, 4th cross-vendor pass). self_check now runs a third testIamPermissions probe for write/admin permissions (secretmanager.secrets.update / versions.add / secrets.delete) and refuses to start (deny) when AVP's identity holds any — AVP is a read-only broker, so a write grant is over-privileged (a compromised proxy could tamper with the vault or rewrite the routing annotations). Separately, avp doctor --probe-gcp now emits an annotation-trust WARN whenever binding_source is notes/both: because secretmanager.secrets.update (edit avp-binding) and versions.access (read the value) are independently grantable on GCP, a metadata-only writer can redirect an in-namespace secret to a host they control — a confused-deputy that abuses AVP's legitimate read access. Operators must lock annotation-write to the value-read trust tier. Both probes are best-effort (an inconclusive testIamPermissions never hard-blocks). 5 tests.

Deprecated

  • agent_vault_proxy.bws_notes module path. The note/annotation parser was renamed to agent_vault_proxy.notes_binding — it is backend-agnostic (BWS notes and GSM avp-binding annotations), so the bws_ prefix misled. The old path stays a re-export shim for back-compat but now emits a DeprecationWarning; import from notes_binding instead. Removal in a future release.