Skip to content

feat(tls): certificate hot reload without listener restart - #417

Merged
gg582 merged 5 commits into
devfrom
feat/tls-hot-reload
Oct 18, 2026
Merged

gg582 merged 5 commits into
devfrom
feat/tls-hot-reload

Conversation

@gg582

@gg582 gg582 commented Oct 18, 2026

Copy link
Copy Markdown
Member

Summary

Adds hot TLS certificate + private-key reload for live HTTPS (TCP) and HTTPS/3 (QUIC) listeners without restarting the listener or dropping in-flight connections (closes #404).

Scope & Design

  • Public APIs:
    • cwist_app_reload_tls(app, cert_path, key_path): reloads the application's HTTPS context and, when HTTPS/3 is enabled, its HTTP/3 QUIC context, then updates app->cert_path / app->key_path.
    • cwist_https_reload_context(ctx, cert_path, key_path, app): builds and validates a new SSL_CTX via cwist_https_build_ssl_ctx(), carries over the shared STEK and ALPN callback, and swaps ctx->ctx under ctx_lock. Readers hold the read lock only across the pointer load + SSL_new() (which takes BoringSSL's own SSL_CTX reference).
    • cwist_http3_reload_context(ctx, cert_path, key_path): builds and validates a new QUIC SSL_CTX, carries over the wrapper-owned STEK, and swaps ctx->ssl_ctx under ssl_ctx_lock. ssl_ctx_handout pins the SSL_CTX returned from lsquic's ea_get_ssl_ctx callback across the window before lsquic calls SSL_new().
  • STEK rotation decision: documented in docs/decisions/0002-tls-hot-reload-stek.md (and indexed in docs/README.md). Certificate reload keeps the existing STEK so session resumption (and QUIC 0-RTT) continues seamlessly across a cert swap.
  • HTTP/3 shutdown lifecycle fix: app_listen_serve() now joins the HTTP/3 listener thread and closes udp_fd after clearing app->h3_ctx->running, preventing a shutdown use-after-free when cwist_app_destroy() runs immediately after cwist_app_listen_ex() returns.

Verification

  • tests/test_tls_hot_reload.c (wired into make test):
    • Serves HTTPS, holds a keep-alive connection on cert A, reloads to cert B mid-traffic, verifies new handshakes see cert B and the pre-reload connection completes another request with zero drops.
    • Verifies failed reloads leave the previous certificate active.
    • Runs 200 (and 1000 via CWIST_TLS_RELOAD_ITERS=1000) alternating reloads and asserts zero fd drift and zero ASan/LSan leaks.
    • Serves HTTPS/3 and verifies CA-pinned QUIC requests switch from cert A to cert B on reload.
  • make SANITIZE=address,undefined WERROR=1 test_tls_hot_reload + CWIST_TLS_RELOAD_ITERS=1000 passes cleanly.
  • make api-check, make env-check, check_api_test_coverage.py, check_test_wiring.py, and malloc_lint.sh all pass.

Closes #404

gg582 and others added 2 commits October 18, 2026 20:15
Add hot certificate reload for live HTTPS (TCP) and HTTPS/3 (QUIC)
listeners via cwist_app_reload_tls(), cwist_https_reload_context(), and
cwist_http3_reload_context():

- Build and validate the new SSL_CTX before swapping it under a
  reader-writer lock (ctx_lock / ssl_ctx_lock). In-flight handshakes and
  established connections keep their own BoringSSL SSL_CTX reference and
  finish on the previous certificate; new handshakes present the new one.
- Preserve the shared session-ticket key (STEK) across reloads so TLS 1.3
  and QUIC session resumption survive a certificate swap without a
  full-handshake cliff (documented in
  docs/decisions/0002-tls-hot-reload-stek.md).
- Join the HTTP/3 UDP listener thread and close the UDP socket on
  app_listen_serve() shutdown so cwist_app_destroy() cannot race with an
  exiting QUIC loop thread.
- Add tests/test_tls_hot_reload.c covering mid-traffic HTTPS swap,
  HTTP/3 CA-verified swap, rollback on invalid cert/key, context-level
  APIs, and a 200/1000-iteration fd/ASan leak check.
@gg582

gg582 commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Some functions are deleted. But I don't think we can decide to delete those functions so easily.

gg582 added 3 commits October 18, 2026 21:55
# Conflicts:
#	docs/README.md
#	docs/api/v4.0-api-baseline.txt
- https_hs_shard.running is read lock-free by the shepherd loop while
  start/stop write it from other threads; make it _Atomic bool.
- cwist_shutdown_request() now only claims the UDP fd instead of closing
  it: the HTTP/3 loop thread can be inside recvmmsg() on it, and closing
  under it raced with the read and could later close a reused descriptor
  number. The loop exits on g_cwist_running within its poll timeout and
  the owning server path closes the fd after joining the thread.
- cwist_multiport_h3_stop_group() now joins the listener thread before
  closing its udp_fd, same race.
…ls-hot-reload

# Conflicts:
#	docs/README.md
#	docs/api/v4.0-api-baseline.txt
@gg582
gg582 merged commit 8625120 into dev Oct 18, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant