Skip to content

Releases: aaronckj/vaultproxy

v1.11.1 — security hardening (mTLS key 0600, trailer + pseudo-header strip)

26 May 04:42

Choose a tag to compare

Security

  • mTLS server key 0600 enforcement. --transparent-mtls-server-key
    is now mode-checked at startup, mirroring TransparentCa::load_byo's
    treatment of the MITM CA key. A world-readable mTLS server key now
    refuses to start instead of silently loading. The mTLS server key is
    a Tier-1 secret (SECURITY.md) — a leak lets an attacker impersonate
    the proxy to every agent that trusts the corresponding server cert.
  • HTTP/2 trailer sanitisation. The h2 MITM path now drops
    pseudo-header names (:-prefixed) and connection-specific names
    (connection, keep-alive, proxy-connection, transfer-encoding,
    upgrade, te, trailer, host, content-length) from upstream-
    supplied trailers before re-emitting on the agent stream. h2
    enforces this on send, but failing there would abort the stream
    after we'd already sent the response body. Drop quietly instead.
  • FORBIDDEN_HEADERS extended with h2 pseudo-headers (:authority,
    :scheme, :method, :path, :status) and trailer-control fields
    (trailer, te). Defence-in-depth against a confused or hostile
    agent smuggling these inline on an http/1.1 request.

Tests

  • New h2_pseudo_headers_and_trailer_fields_stripped covers the
    extended FORBIDDEN_HEADERS list.

Source

  • Findings from a v1.2.5..v1.11.0 security audit (cavecrew-reviewer
    subagent). Two additional 🟡 findings (seed_test_password
    ungating, oauth-writeback cache-write ordering) inspected and
    determined to be either defence-in-depth deferrable or already
    correct as implemented. No 🔴 critical findings.

v1.11.0 — HTTP/2 trailers pass-through (gRPC)

26 May 04:26

Choose a tag to compare

Added

  • HTTP/2 trailers pass-through. gRPC carries its status in
    trailers (grpc-status / grpc-message), so transparent gRPC
    proxying needs them. The h2 MITM path now drains the upstream's
    TRAILERS frame after the body, then re-emits it on the agent
    stream via SendStream::send_trailers. End-of-stream flags on
    the response HEADERS / body DATA frames are computed so the h2
    framing is correct (end_stream on DATA = false when trailers
    follow).
  • h2_upstream::ParsedH2Response gains a fourth tuple element:
    Option<Vec<(String, String)>> for trailers. Callers that don't
    speak h2 to the agent (the http/1.1 MITM path) get a startup
    WARN when the upstream returns trailers — gRPC over plain
    http/1.1 isn't supported and the trailers are dropped.

Tests

  • tests/transparent_h2_trailers.rs spins up an h2c upstream that
    sends {body: "grpc-body", trailers: {grpc-status: "0", grpc-message: "OK"}}; drives an h2 agent through the proxy;
    asserts the agent receives both the body and the trailers
    end-to-end.

Not implemented

  • HTTP/2 server push is intentionally not supported. Browsers
    have removed it (Chrome 106+, Firefox 113+); it's effectively
    dead in modern stacks.

v1.10.0 — upstream HTTP/2 connection pool

26 May 04:26

Choose a tag to compare

Added

  • Upstream HTTP/2 connection pool. AppState.h2_upstream_pool
    is a DashMap<(host, port), h2::client::SendRequest<Bytes>> that
    reuses h2 sessions across many transparent requests instead of
    opening a fresh connection per stream. SendRequest is Clone
    and thread-safe, so concurrent requests against the same upstream
    share one frame multiplexer + one flow-control budget.
  • New h2_upstream::try_h2_pooled consults the pool first; on a
    miss it handshakes, stores the new SendRequest, and runs the
    request. On send error (GOAWAY / RST_STREAM / connection-died)
    the entry is evicted so the next request re-handshakes against a
    healthy upstream.
  • Both MITM paths (h2 agent via h2_mitm, http/1.1 agent via
    mitm::run_http1) now call try_h2_pooled so all four
    agent↔upstream wire combinations benefit from the pool.

Refactored

  • h2_upstream split the prior single-shot helper into
    handshake_tls / handshake_plain / drive_handshake /
    send_request_on. The pooled and non-pooled entry points share
    the send_request_on path so a cached SendRequest and a fresh
    one issue identical requests.

Tests

  • tests/transparent_h2_upstream_pool.rs drives 3 sequential h2
    requests through the proxy against a counting h2c upstream;
    asserts the upstream observed exactly 1 h2 connection (not 3).
  • All 19 prior transparent E2E tests still pass.

v1.9.0 — cross-protocol upstream h2 for http/1.1 agents

26 May 04:26

Choose a tag to compare

Added

  • Cross-protocol HTTP/2 upstream for HTTP/1.1 agents. v1.8.0
    added upstream h2 only when the agent itself spoke h2. v1.9.0
    closes the matrix: the http/1.1 MITM path now also tries h2
    against the upstream first (via the same h2_upstream::try_h2
    helper). When the upstream picks h2, the parsed response is
    re-serialised back to http/1.1 wire bytes
    (h2_upstream::serialise_as_http1) for the agent. When the
    upstream picks http/1.1, the path falls back to the existing
    http/1.1 forwarder unchanged.
  • New h2_upstream::serialise_as_http1(status, headers, body)
    helper: produces a complete HTTP/1.1 response with a
    recomputed Content-Length, Connection: close, and the
    connection-specific h2-forbidden headers dropped.

Tests

  • tests/transparent_cross_protocol_h2_upstream.rs drives a
    vanilla reqwest http/1.1 agent through the proxy against an
    h2c upstream, asserts the upstream sees the vault-injected
    Bearer (not the agent's smuggled one) and the agent gets the
    body back as a normal http/1.1 response.
  • All four agent↔upstream wire combinations now have E2E coverage:
    http/1.1 ↔ http/1.1 (v1.1.0), h2 ↔ http/1.1 (v1.7.0),
    h2 ↔ h2 (v1.8.0), http/1.1 ↔ h2 (v1.9.0).

Limitations

  • Still no upstream h2 connection pool — every request opens a
    fresh h2 session to the upstream. Tracked as v1.10 "HTTP/2
    upstream pool".

v1.8.0 — native HTTP/2 to upstream

26 May 04:26

Choose a tag to compare

Added

  • Native HTTP/2 to the upstream too. The v1.7.0 h2 MITM spoke h2
    to the agent and re-framed as HTTP/1.1 to the upstream. v1.8.0
    adds h2_upstream::try_h2 which the h2 MITM path calls first: it
    opens a TLS connection to the upstream with ALPN
    ["h2", "http/1.1"] and, when the upstream picks h2, runs a
    single h2 request and returns the parsed response shape
    (status + headers + body) for direct re-framing back to the agent.
    When the upstream picks http/1.1 — or VP_TRANSPARENT_TEST_HTTP=1
    is set (test affordance) — the path falls back to the existing
    http/1.1 forwarder + parse step. End-to-end native h2 now works
    when both the agent and the upstream speak h2.
  • New src/proxy/transparent/h2_upstream.rs module.
  • HttpRequest is now Clone so the h2 MITM can hand the same
    injected request to the h2-try then (on fallback) the http/1.1
    forwarder.

Tests

  • tests/transparent_h2_upstream.rs spins up a hand-rolled h2c
    upstream that records the headers it received, drives an h2 agent
    through the proxy, and asserts (a) the upstream got the
    vault-injected Bearer (not the agent's smuggled one) and (b) the
    agent got the upstream's h2 response back over h2.
  • VP_TRANSPARENT_TEST_FORCE_H2=1 is a new test-only env knob that
    flips the upstream h2 client to plain TCP (h2c with prior
    knowledge) so tests don't need a TLS dance against a stub cert.

Limitations

  • No upstream h2 connection pool yet. Every agent stream still
    opens its own h2 connection to the upstream. A DashMap<(host, port), SendRequest<Bytes>> is the natural v1.9 follow-up.
  • The http/1.1 MITM path (agent speaks HTTP/1.1) still always
    forwards to the upstream over http/1.1. Mixing agent-http/1.1
    with upstream-h2 needs a separate response-shape converter and
    is not in scope for v1.8.0.

v1.7.1 — clear rust 1.95 clippy errors

26 May 04:26

Choose a tag to compare

Fixed (CI)

  • Three clippy errors surfaced on rust 1.95.0 (CI's stable channel)
    that the older 1.94.x local toolchain didn't emit. All purely
    stylistic; no runtime behaviour change.
    • src/proxy/transparent/h2_mitm.rs: extracted ParsedHttp1 type
      alias for parse_http1_response's return shape
      (clippy::type_complexity).
    • src/security/audit_sinks.rs: switched the libc::syslog
      format-string ptr from b"%s\0".as_ptr() to the modern
      c"%s".as_ptr() (clippy::manual_c_str_literals).
    • src/security/audit_sinks_http.rs: reflowed a multi-line doc
      list-item so the continuation line aligns at 4 spaces
      (clippy::doc_overindented_list_items).
  • Root cause for the v1.4.4 → v1.7.0 Docker-publish failures was
    the same three lints; they tripped CI on every release since
    v1.4.4 but didn't affect crates.io publication (which doesn't run
    clippy) so the released artefacts were and remain correct.

v1.7.0 — native HTTP/2 transparent MITM (agent side)

26 May 04:26

Choose a tag to compare

Added

  • Native HTTP/2 transparent MITM. The MITM leaf cert now
    advertises both h2 and http/1.1 on ALPN (was http/1.1-only in
    v1.4.1+). Post-handshake, mitm::run inspects the negotiated
    protocol and dispatches to either the existing
    mitm::run_http1 (HTTP/1.1) or the new h2_mitm::run_h2
    (HTTP/2) path. Agent-side framing is native h2 with per-stream
    concurrency; upstream-side still speaks HTTP/1.1 via the shared
    forward_to_upstream_for_h2 helper (the proxy synthesises an
    HTTP/1.1 request from the h2 headers + body, runs the existing
    injectors, then re-frames the HTTP/1.1 response as h2 back to the
    agent).
  • New src/proxy/transparent/h2_mitm.rs module. Direct h2 = "0.4"
    and http = "1" deps (already in tree via reqwest/hyper).

Behaviour changes

  • ALPN contract: a client that offers ["h2", "http/1.1"] now
    ends up on h2 (was http/1.1 in v1.4.1+). A client that offers only
    ["http/1.1"] still negotiates http/1.1. A client that demands
    only ["h2"] now succeeds (was a clean ALPN-mismatch error in
    v1.4.1+).
  • The transparent_alpn_downgrade test file was renamed in spirit:
    the two tests now cover the v1.7.0 contract (mixed offer →
    picks h2; http/1.1-only → picks http/1.1).

Tests

  • New tests/transparent_h2_mitm.rs drives a hand-rolled rustls +
    h2::client end-to-end against the MITM listener: outer ALPN
    negotiation lands on h2, h2 server framing reads the agent
    request, vault-injected Bearer reaches the wiremock upstream, and
    the upstream's HTTP/1.1 response is re-framed back as h2.

Limitations (will tighten in follow-up releases)

  • Upstream still HTTP/1.1; an h2-required upstream (rare in
    practice — most accept HTTP/1.1) won't work via the h2 MITM yet.
  • Trailers + server push not supported.

v1.6.0 — custom-field OAuth RT writeback

26 May 04:26

Choose a tag to compare

Added

  • Custom-field OAuth refresh-token writeback. The v1.5.0 writeback
    path was limited to refresh_token_field = "password". v1.6.0 adds
    VaultManager::update_field_for_item (and the pure
    merge_field_into_cipher helper that powers it) so OAuth services
    whose RT lives in a custom Vaultwarden field can now persist
    rotated tokens too. The helper merges only the named field; every
    other encrypted field (including the credential blobs) stays
    byte-for-byte unchanged so the cipher PUT diff is minimal.
  • Routing: the OAuth writeback path now picks
    update_password_for_item when refresh_token_field == "password"
    and update_field_for_item otherwise. Behaviour for the default
    case is identical to v1.5.0.

Tests

  • New unit tests cover the merge helper end-to-end (existing-field
    update + untouched-field byte invariance, plus append-when-absent).

v1.5.1 — SECURITY.md sweep

26 May 04:26

Choose a tag to compare

Docs

  • SECURITY.md sweep. The transparent-mode section was last touched
    at v1.2.5 and still claimed "no listener-side authentication" — out
    of date since v1.3.1 (UDS + SO_PEERCRED) and v1.4.0 (mTLS-fronted
    listener). Rewritten:
    • The Transparent HTTPS_PROXY section now covers all three listener
      variants (TCP, UDS, mTLS), v1.4.1 ALPN downgrade behaviour, and
      the Tier-1 status of the mTLS server cert + key.
    • New "OAuth tokens" sub-section covers the in-memory token cache,
      refresh-token vault writeback (oauth_writeback = true), and the
      custom-field limitation.
    • New "Audit log + SIEM sinks" sub-section covers v1.4.2 sync sinks
      and v1.4.4 network sinks, including the Tier-2 sensitivity of the
      SIEM-side API key / HEC token and the rationale for sourcing them
      from env vars rather than argv.

No code changes — version bumped to v1.5.1 so the docs ship via the
standard publish flow.

v1.5.0 — OAuth refresh-token vault writeback

26 May 04:26

Choose a tag to compare

Added

  • OAuth refresh-token vault writeback. New oauth_writeback = true
    flag on auth = "oauth_refresh" services. When the IdP returns a
    rotated refresh_token in the response, the proxy writes it back
    to the vault item via update_password_for_item. Concurrent
    refreshes are serialised via a per-vault_item Mutex held from
    cache-check through POST through writeback, so a rotating IdP
    doesn't deal two grants the second of which uses an already-
    invalidated RT.
    • Default false (preserves v1.3.2 behaviour: log + discard).
    • Only supported when refresh_token_field is the default
      "password". Custom-field writeback logs a WARN and discards
      the rotation (tracked as a v1.6 follow-up).
    • Public OAuth flows that don't return a rotated RT see no
      behaviour change.

Changed

  • AppState.oauth_writeback_locks (new) holds the per-vault_item
    serialisation mutexes. Lazily populated; one entry per OAuth
    refresh-token service for the process lifetime.
  • proxy::get_or_refresh_oauth_refresh_token is now pub (was
    pub(crate)) so integration tests can drive it directly.
  • VaultManager::seed_test_password is production-visible (was cfg-
    gated to test-utils). The companion reader test_item_password
    was already production-visible; symmetry is preserved. No new
    external API: the only way to populate the test_passwords map in
    production is to call this from inside the proxy process itself.

Tests

  • tests/transparent_oauth_refresh_writeback.rs E2Es both legs:
    writeback ON persists rotated RT to the stub map and the next
    refresh uses it; writeback OFF leaves the stub untouched.