Skip to content

tls_check: an opt-in TLS hardening check, with a badge on the H1 composite - #1296

Merged
MDA2AV merged 1 commit into
feat/tls-validationfrom
feat/tls-section
Aug 24, 2026
Merged

tls_check: an opt-in TLS hardening check, with a badge on the H1 composite#1296
MDA2AV merged 1 commit into
feat/tls-validationfrom
feat/tls-section

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Aug 24, 2026

Copy link
Copy Markdown
Owner

A new tls test: validation-only, opt-in, HTTP/1.1 only. Nothing is measured and no score moves — it is a hardening bar an entry chooses to be held to, and passing it earns a second badge on the H1 composite.

Built on top of #1292, which it reuses.

Why opt-in

Almost every check needs the entry to have done something deliberate. Binding a certificate once at startup — what nearly every entry does today — fails the first one. Measured before building any of this:

caddy:     did not rotate (still serving the old cert)
bun:       did not rotate
h2o-mruby: did not rotate

The checks

check what it requires
certificate rotation the pair at /certs is replaced under a running server; the new cert must be served without a restart and still answer
rotation keeps serving 30 requests across the swap, all must succeed
SNI handshakes both with a server name and without one
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, ~30s. Any HIGH/CRITICAL fails
the shared TLS checks cert identity, TLS 1.3, AEAD, ALPN, obsolete protocols, weak ciphers (from #1292)

Rotation is the centrepiece: a certificate is renewed roughly every 60 days in production, and a server needing a restart to pick one up is a weaker server. The usual way to pass is a per-handshake callback — Kestrel's ServerCertificateSelector, Go's GetCertificate, rustls' ResolvesServerCert.

Registration

A new VALIDATION_ONLY table in profiles.sh, not PROFILES. validate.sh accepts it in tests; the benchmark harness and leaderboard catalog ignore it, since there is no load generator behind it.

aspnet-minimal opts in and implements it

RotatingCertificate re-reads the PEM pair when its mtime moves, behind ServerCertificateSelector, which Kestrel calls per handshake. The mtime check is throttled to 1s — a stat is cheap next to a handshake but not next to a resumed one, and a second of staleness is nothing against a 60-day renewal.

PASS [tls certificate rotation] (new certificate served in 1s, original back in 1s, still answering)
PASS [tls rotation keeps serving] (30/30 requests answered across the swap)
PASS [tls SNI] · PASS [tls close_notify] · PASS [tls vulnerability suite]
NOTE [tls session resumption]: no session ticket issued

113 passed, section clean.

The badge

badge meaning
green shield TLS basics, on any entry with a TLS profile
gold shield opted into this section and passed it

Shown only on the H1 composite (view === 'composite' && scope === 'h1') — the section covers :8081, so a badge earned there must not follow the entry into h2/h3 views it says nothing about. Verified across all four view combinations.

Two details worth stating

Each verdict answers only for its own checks. aspnet-minimal fails an unrelated intermittent /fortunes assertion in the same run, and neither badge depends on it — a flaky check elsewhere says nothing about whether an entry rotates a certificate.

Certificates are restored on the way out, including when a check fails midway, and the replacement carries the mode of the pair it replaces. A 0600 file a non-root container cannot read looks exactly like a server that ignored the rotation — the same trap that produced false failures in #1272.

@MDA2AV MDA2AV changed the title tls: an opt-in hardening section, with a badge on the H1 composite tls_check: an opt-in TLS hardening check, with a badge on the H1 composite Aug 24, 2026
@MDA2AV
MDA2AV changed the base branch from main to feat/tls-validation August 24, 2026 14:29
@MDA2AV
MDA2AV force-pushed the feat/tls-validation branch from 4b2d714 to d071aec Compare August 24, 2026 14:56
…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.
@MDA2AV
MDA2AV merged commit 4399bb6 into feat/tls-validation Aug 24, 2026
33 of 135 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