tls_check: re-land the opt-in TLS check that never reached main - #1301
Merged
Conversation
…osite
Opted into with a meta.json field:
"tls_check": true
It needs a TLS listener on :9000 reading /certs-tls, a directory mounted for
that entry alone. Nothing is measured; passing earns a badge on the HTTP/1.1
composite, and only there -- the check covers :8081-class HTTP/1.1 TLS, so a
badge earned on it must not follow the entry into h2 and h3 views it says
nothing about.
certificate rotation the pair at /certs-tls is replaced under a
running server; the new certificate must be
served without a restart and still answer.
Measured first: caddy, bun and h2o-mruby all
keep serving the old one.
rotation keeps serving 30 requests across the swap, all must succeed
SNI handshakes with a server name and without
session resumption reported, not required
close_notify closed at the TLS layer, not just the socket
vulnerability suite testssl.sh -U: Heartbleed, ROBOT, POODLE,
SWEET32, LUCKY13 and 14 more. HIGH or CRITICAL
fails.
the shared TLS checks certificate identity, TLS 1.3, AEAD, ALPN,
obsolete protocols and weak ciphers
The dedicated port and private directory are the point of the design: the
check rotates certificates under a running server, and doing that to the
shared /certs would move the ground under json-tls, static-tls and every h2
profile in the same run. /certs is verified byte identical across a run that
rotates twice.
aspnet-minimal opts in and implements it. RotatingCertificate re-reads the
pair when its mtime moves, behind Kestrel's ServerCertificateSelector, which
runs per handshake. 114 passed, 0 failed; rotation lands in 1s and 30/30
requests survive the swap.
nigrosimone
pushed a commit
to nigrosimone/HttpArena
that referenced
this pull request
Aug 26, 2026
tls_check (MDA2AV#1301) is a hardening bar rather than a profile: nothing is measured, and passing earns a badge on the H1 composite. It wants a TLS listener on :9000 reading /certs-tls, a directory belonging to this entry alone, because the section replaces the pair underneath the running server and doing that to /certs would move the ground under json-tls, static-tls and the h2 profiles in the same run. The listener is the 8081 one again, pointed at the other directory. What had to be worked out is the rotation. ## Why a new listener and not a new certificate µWS reads the pair once, when it builds the SSL context, and nothing points an existing context at a new file afterwards. addServerName() looks like the answer and is not: it replaces the certificate for one SNI name and leaves the default context alone, which is what answers a client that sends no server name. Measured, on this uWS: after addServerName('localhost', ...) an -servername handshake is served the new certificate and a -noservername handshake is still served the old one. Every cert-identity probe in validate.sh passes -servername, so the SNI-only rotation would have passed the section while leaving a real client that omits SNI on the expired pair. Rebuilding the listener is the rotation that reaches both, and it is verified here in both directions: after the swap SNI and no-SNI both serve the replacement, and after the restore both serve the original again. It costs no downtime because it does not have to. A worker binds the port shared — SO_REUSEPORT — so the replacement is accepting on 9000 beside the listener it replaces before that one is told to stop, and close() then closes the old listen socket, lets what it is already serving finish, and drops its idle keep-alives only after that. Nothing is refused, no response is cut. The watcher is armed inside the listen callback, which is what says this process owns a listener: the primary of a clustered app returns from listen() without binding, so only the workers arrive there and only they have a certificate to rotate. The stamp is inode, size and mtime of both files — the section swaps with mv, so the inode moves too, and a torn read mid-swap reports nothing rather than a new pair, which retries on the next tick instead of building a context out of half of each. ## Cost on a measured run: none /certs-tls is mounted by validate.sh alone. Verified with the directory absent: no second listener, no watcher, no stat, and :8080 and :8081 unaffected. ## Verified The tls_check probes lifted whole out of validate.sh and run against each entry's own app.js — 9 passed, 0 failed on both fulmine and fulmine-tuned: the mounted certificate, TLS 1.3, an AEAD suite, ALPN, TLS quality (refuses every obsolete protocol and weak cipher probed), SNI with and without a server name, close_notify, rotation (new certificate served in 1s, original back in 1s, still answering) and 30/30 requests answered across the swap. Session resumption reports NOTE — no ticket issued — which the section records rather than requires. Then the control, because a check nothing can fail is not a check: the same entry with the rotation stripped and the listener built once. It fails [tls_check certificate rotation] after 30s and passes everything else, which is the one line this change is responsible for. The vulnerability suite could not run here — testssl.sh needs docker and this sandbox has none — so that check is unverified rather than passing. It is the one thing in the section still to confirm on a runner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXewQKchJsgGrrXxFgGzQ5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Re-lands the
tls_checkwork from #1296, which merged but never reachedmain.What happened
#1296 was stacked on
feat/tls-validation(#1292) — I set that base to stop the two PRs showing the same diff twice. The timing then went wrong:mainas47b79d7c, and closedfeat/tls-validation— a branch whose PR had already closed 28 minutes earlierA squash merge collapses the branch into one new commit on
main; the branch itself is not fast-forwarded and its PR closes. So when #1296 landed on that branch afterwards, there was nothing left to carry it onward. It has been sitting infeat/tls-validationever since, merged and unreachable.That branch cannot simply be merged now either — it predates #1298, #1299 and #1300, so merging it would revert the gRPC streaming removal and the wtx/sark result restore.
This PR
The single
tls_checkcommit, cherry-picked onto currentmain. Same 8 files, nothing else moves:scripts/validate.sh— the opt-in section: certificate rotation, rotation-under-load, SNI, resumption, close_notify, the testssl vulnerability suite, on:9000with/certs-tlsscripts/gen_leaderboard_data.py— carries the verdict intometa.tlsChecksite/leaderboard/index.html— the gold badge, H1 composite onlyframeworks/aspnet-minimal/*—RotatingCertificate, the:9000listener,"tls_check": trueVerified on this branch
The parity count moved 522 → 530 because this branch sits on top of #1298/#1299 rather than beside them.
🤖 Generated with Claude Code