Skip to content

Add ACME TLS-ALPN-01 challenge with an ALPN gate on 443 - #128

Merged
Shine-neko merged 3 commits into
mainfrom
feat/tls-alpn-01
Jul 20, 2026
Merged

Add ACME TLS-ALPN-01 challenge with an ALPN gate on 443#128
Shine-neko merged 3 commits into
mainfrom
feat/tls-alpn-01

Conversation

@Shine-neko

Copy link
Copy Markdown
Contributor

Adds the ACME TLS-ALPN-01 challenge (RFC 8737): Sōzune can now obtain Let's Encrypt certificates with neither port 80 (HTTP-01) nor DNS API credentials (DNS-01). The validation happens over the TLS handshake on 443 itself. Built on cheti's new TlsAlpn01Solver.

Usage

acme:
  enabled: true
  email: ops@example.com
  resolvers:
    edge:
      challenge: tls-alpn-01

An entrypoint (or Docker label) points at the resolver: sozune.http.app.acme.resolver=edge.

How it works

Answering the challenge means intercepting the handshake on 443, which the HTTPS worker can't do alone. So — only when a tls-alpn-01 resolver is configured — Sōzune fronts 443 with an ALPN-aware gate:

  • the gate binds public 443 and prereads each ClientHello's ALPN;
  • acme-tls/1 handshakes go to a local rustls responder that presents cheti's challenge certificate;
  • everything else is spliced, undecrypted, to the HTTPS worker (now on a loopback port).

The real client IP is preserved: the gate prepends a PROXY-v2 header the worker consumes (with_expect_proxy), so client-IP allow-lists, access logs and metrics stay accurate. Without a tls-alpn-01 resolver, none of this happens — 443 stays a direct HTTPS listener, byte-for-byte the old path.

Three commits

  1. Bump cheti to the revision with the TLS-ALPN-01 solver.
  2. The responder + resolver wiring (rustls loopback listener, Resolver::TlsAlpn01, config). Safe in isolation — nothing routes to it yet.
  3. The 443 gate that routes acme-tls/1 to the responder. This is the part that touches the production HTTPS path.

Verified end-to-end

A Pebble-based harness (tests/e2e/acme/run-alpn.sh) issues a real certificate over a real TLS-ALPN-01 challenge, hermetically, and asserts normal HTTPS still flows through the gate and reaches the backend.

Hardening

The 443 gate is the riskiest code here — it fronts all HTTPS traffic — so it went through two Codex review rounds, nine issues fixed:

  • client IP loss → PROXY-v2 header + with_expect_proxy, destination taken from local_addr();
  • ClientHello fragmented across TLS records → handshake reassembly;
  • per-read timeout (slowloris) → single absolute preread deadline;
  • silent gate bind failure → error propagates and fails the process;
  • unbounded handshake framing → walk bounded by the ClientHello's declared length;
  • byte-cap tuning and honest documentation of its one theoretical (non-reachable) edge.

The hand-rolled ClientHello ALPN parse has unit tests for each case (fragmentation, trailing-byte rejection, non-TLS, truncation, PROXY-v2 encoding).

Limitation

Wildcards are not supported (a TLS-ALPN-01 validator connects to a concrete name). Use DNS-01 for *.example.com.

@Shine-neko
Shine-neko merged commit b37af13 into main Jul 20, 2026
3 checks passed
@Shine-neko
Shine-neko deleted the feat/tls-alpn-01 branch July 20, 2026 22:12
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