Releases: aaronckj/vaultproxy
Releases · aaronckj/vaultproxy
v1.11.1 — security hardening (mTLS key 0600, trailer + pseudo-header strip)
Security
- mTLS server key 0600 enforcement.
--transparent-mtls-server-key
is now mode-checked at startup, mirroringTransparentCa::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_HEADERSextended 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_strippedcovers the
extendedFORBIDDEN_HEADERSlist.
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)
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 viaSendStream::send_trailers. End-of-stream flags on
the response HEADERS / body DATA frames are computed so the h2
framing is correct (end_streamon DATA = false when trailers
follow). h2_upstream::ParsedH2Responsegains 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.rsspins 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
Added
- Upstream HTTP/2 connection pool.
AppState.h2_upstream_pool
is aDashMap<(host, port), h2::client::SendRequest<Bytes>>that
reuses h2 sessions across many transparent requests instead of
opening a fresh connection per stream.SendRequestisClone
and thread-safe, so concurrent requests against the same upstream
share one frame multiplexer + one flow-control budget. - New
h2_upstream::try_h2_pooledconsults the pool first; on a
miss it handshakes, stores the newSendRequest, 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 calltry_h2_pooledso all four
agent↔upstream wire combinations benefit from the pool.
Refactored
h2_upstreamsplit the prior single-shot helper into
handshake_tls/handshake_plain/drive_handshake/
send_request_on. The pooled and non-pooled entry points share
thesend_request_onpath so a cachedSendRequestand a fresh
one issue identical requests.
Tests
tests/transparent_h2_upstream_pool.rsdrives 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
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 sameh2_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
recomputedContent-Length,Connection: close, and the
connection-specific h2-forbidden headers dropped.
Tests
tests/transparent_cross_protocol_h2_upstream.rsdrives 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
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
addsh2_upstream::try_h2which 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 — orVP_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.rsmodule. HttpRequestis nowCloneso 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.rsspins 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=1is 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. ADashMap<(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
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: extractedParsedHttp1type
alias forparse_http1_response's return shape
(clippy::type_complexity).src/security/audit_sinks.rs: switched thelibc::syslog
format-string ptr fromb"%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)
Added
- Native HTTP/2 transparent MITM. The MITM leaf cert now
advertises bothh2andhttp/1.1on ALPN (was http/1.1-only in
v1.4.1+). Post-handshake,mitm::runinspects the negotiated
protocol and dispatches to either the existing
mitm::run_http1(HTTP/1.1) or the newh2_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_h2helper (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.rsmodule. Directh2 = "0.4"
andhttp = "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_downgradetest 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.rsdrives a hand-rolled rustls +
h2::clientend-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
Added
- Custom-field OAuth refresh-token writeback. The v1.5.0 writeback
path was limited torefresh_token_field = "password". v1.6.0 adds
VaultManager::update_field_for_item(and the pure
merge_field_into_cipherhelper 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_itemwhenrefresh_token_field == "password"
andupdate_field_for_itemotherwise. 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
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.
- The Transparent HTTPS_PROXY section now covers all three listener
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
Added
- OAuth refresh-token vault writeback. New
oauth_writeback = true
flag onauth = "oauth_refresh"services. When the IdP returns a
rotatedrefresh_tokenin the response, the proxy writes it back
to the vault item viaupdate_password_for_item. Concurrent
refreshes are serialised via a per-vault_itemMutexheld 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_fieldis 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.
- Default
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_tokenis nowpub(was
pub(crate)) so integration tests can drive it directly.VaultManager::seed_test_passwordis production-visible (was cfg-
gated totest-utils). The companion readertest_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.rsE2Es both legs:
writeback ON persists rotated RT to the stub map and the next
refresh uses it; writeback OFF leaves the stub untouched.