feat(web): TLS offload via VERVAIN_CERT_URL reverse-proxy cert fetch (VS-27) #39

Merged
David merged 1 commit from feat/tls-offload-cert-url-VS-27 into main 2026-06-05 20:59:56 +02:00
Owner

Implements VS-27: run the server behind a TLS-terminating reverse proxy (Traefik, Caddy, nginx) without forcing TCP passthrough (serversTransport). Mirrors upstream MeshCentral's certUrl concept via the VERVAIN_CERT_URL env var.

What changed

  • New meshcentral-web/src/proxy_cert.rs: ProxyCertCache dials VERVAIN_CERT_URL over TLS (accept-any verifier, mirroring the dangerous-config pattern in meshcentral-peering::cert_pinning; the fetched bytes only RECOGNIZE what agents see, they are never a trust anchor), captures the proxy's leaf cert, and stores its SPKI SHA-384. Startup fetch plus on-mismatch refetch rate-limited to one attempt per 60 seconds, so a rotated proxy cert heals on the agent's automatic reconnect without a restart.
  • agent_handshake.rs: the handshake accepts extra server hashes alongside the web signer's own, records the hash the agent reported seeing, and reconstructs the agent-signature digest from that recorded hash in both the RSA and ECDSA verify paths (the agent signs over what it saw). Unknown hashes are a new UnknownServerHash error. With no proxy configured the recorded hash equals the signer's, so behavior is unchanged.
  • agent.rs: snapshots the cache into each handshake; an UnknownServerHash rejection spawns the rate-limited refetch off the connection path.
  • main.rs: reads VERVAIN_CERT_URL; a failed startup fetch logs server warning id 102 and the server boots normally (per VS-27 decision).
  • meshcentral-pki: sha384_of_subject_public_key is now public so the fetcher hashes the proxy cert with the exact recipe agents use.
  • Docs: README section + .env.example entry.

Unchanged on purpose

  • ServerID in meshsettings.ashx and the server's own AuthRequest hash stay the web signer's pubkey hash; the server signing identity is unaffected by offload.
  • Dev mode behavior is untouched.

Pre-existing breakage fixed in passing

cargo test --workspace did not compile on main: VS-26 added setup_enabled to ServerConfig but missed the admin.rs, login.rs, and tls_smoke.rs integration tests. Both that field and the new proxy_cert field are now populated there.

Tests

  • 6 new handshake tests: offload happy path (RSA + ECDSA), direct connections still working with extras configured, signature over the wrong hash rejected, unknown-hash error shape.
  • 8 new proxy_cert tests including an end-to-end fetch against a local tokio-rustls listener serving a throwaway cert, and rate-limiter behavior.
  • Full workspace suite green (was not compiling on main, see above).

🤖 Generated with Claude Code

Implements VS-27: run the server behind a TLS-terminating reverse proxy (Traefik, Caddy, nginx) without forcing TCP passthrough (serversTransport). Mirrors upstream MeshCentral's certUrl concept via the `VERVAIN_CERT_URL` env var. ## What changed - New `meshcentral-web/src/proxy_cert.rs`: `ProxyCertCache` dials `VERVAIN_CERT_URL` over TLS (accept-any verifier, mirroring the dangerous-config pattern in `meshcentral-peering::cert_pinning`; the fetched bytes only RECOGNIZE what agents see, they are never a trust anchor), captures the proxy's leaf cert, and stores its SPKI SHA-384. Startup fetch plus on-mismatch refetch rate-limited to one attempt per 60 seconds, so a rotated proxy cert heals on the agent's automatic reconnect without a restart. - `agent_handshake.rs`: the handshake accepts extra server hashes alongside the web signer's own, records the hash the agent reported seeing, and reconstructs the agent-signature digest from that recorded hash in both the RSA and ECDSA verify paths (the agent signs over what it saw). Unknown hashes are a new `UnknownServerHash` error. With no proxy configured the recorded hash equals the signer's, so behavior is unchanged. - `agent.rs`: snapshots the cache into each handshake; an `UnknownServerHash` rejection spawns the rate-limited refetch off the connection path. - `main.rs`: reads `VERVAIN_CERT_URL`; a failed startup fetch logs server warning id 102 and the server boots normally (per VS-27 decision). - `meshcentral-pki`: `sha384_of_subject_public_key` is now public so the fetcher hashes the proxy cert with the exact recipe agents use. - Docs: README section + `.env.example` entry. ## Unchanged on purpose - `ServerID` in `meshsettings.ashx` and the server's own `AuthRequest` hash stay the web signer's pubkey hash; the server signing identity is unaffected by offload. - Dev mode behavior is untouched. ## Pre-existing breakage fixed in passing `cargo test --workspace` did not compile on main: VS-26 added `setup_enabled` to `ServerConfig` but missed the `admin.rs`, `login.rs`, and `tls_smoke.rs` integration tests. Both that field and the new `proxy_cert` field are now populated there. ## Tests - 6 new handshake tests: offload happy path (RSA + ECDSA), direct connections still working with extras configured, signature over the wrong hash rejected, unknown-hash error shape. - 8 new proxy_cert tests including an end-to-end fetch against a local tokio-rustls listener serving a throwaway cert, and rate-limiter behavior. - Full workspace suite green (was not compiling on main, see above). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): accept reverse-proxy cert hash in agent handshake (VERVAIN_CERT_URL)
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Failing after 3s
Create release / Create release from merged PR (pull_request) Has been skipped
8ac485df38
When a TLS-terminating reverse proxy fronts the server, agents see the proxy's certificate, so the strict handshake rejected their reported cert hash and the only workarounds were TCP passthrough (Traefik serversTransport) or dev mode, which skips signature verification entirely. Mirror upstream MeshCentral's certUrl concept: VERVAIN_CERT_URL points at the proxy, the server dials it over TLS at startup (chain and hostname validation intentionally skipped, the bytes only RECOGNIZE what agents see), and the leaf cert's SPKI SHA-384 is accepted in the handshake alongside the web signer's own hash.

The handshake now records the hash the agent reported seeing and reconstructs the agent-signature digest from it (RSA and ECDSA paths), since that is what the agent actually signs over; for direct connections the recorded hash equals the signer's, so behavior is unchanged when no proxy is configured. An unknown reported hash is a new UnknownServerHash error which triggers a refetch rate-limited to one attempt per 60 seconds, so a rotated proxy cert (Let's Encrypt renewal) heals on the agent's automatic reconnect without a server restart. A failed startup fetch logs a server warning (id 102) and boots normally. ServerID in meshsettings.ashx and the server's own AuthRequest hash stay the web signer's, the server identity is unaffected.

Also adds the setup_enabled and proxy_cert fields to the ServerConfig literals in the admin/login/tls_smoke integration tests; the first was missed by VS-26 and cargo test --workspace did not compile on main.

#VS-27

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
David merged commit 349088b9fc into main 2026-06-05 20:59:56 +02:00
David deleted branch feat/tls-offload-cert-url-VS-27 2026-06-05 20:59:56 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!39
No description provided.