Skip to content

static, static-tls: reference-only for frameworks; drop them from the entries that fail them - #1290

Merged
MDA2AV merged 1 commit into
mainfrom
chore/unscore-static-h1
Aug 24, 2026
Merged

static, static-tls: reference-only for frameworks; drop them from the entries that fail them#1290
MDA2AV merged 1 commit into
mainfrom
chore/unscore-static-h1

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Two changes, both from the full sweep in #1285.

1. static and static-tls stop feeding the framework composite

Both go to scored=False — the shape pipelined already has. Still measured, still shown, faded on the board, out of the sum.

infraScored stays True: serving files fast from a tuned cache is what a proxy is for, and that league is unaffected. engineScored was already False.

CATALOG is the single source of truth here — the board reads it through data.js (PROFD.profiles) and the badge port reads it through _scored_for(). The two scoring implementations cannot disagree about this, so check_badge_parity.js stays green.

2. Twelve entries drop the two profiles

These fail validation on static and nothing else, and are flagship / emerging / experimental:

django, fletch, genhttp-11, genhttp-11-kestrel, genhttp-kestrel,
mq-bridge, mq-bridge-py, pyronova, rage, rails, true-async-server, userver

Each loses static / static-tls from its tests array and the result keys those profiles produced — 69 keys across the twelve.

They stay enabled: static was their only failure, and everything else they publish is unaffected.

Scope

  • No other profile's results are touched.
  • static-h2 and static-h3 are left alone — the failures were on the H1 pair.
  • Entries failing static that are not flagship/emerging/experimental are untouched: fastpysgi-asgi, fastpysgi-wsgi, uvicorn (engine) and h2o, varnish (infrastructure).
  • frankenphp-trueasync and h2o-mruby are not here either. They failed the sweep because their containers never started on missing shared libraries, which json-tls coverage for flagship/emerging entries on the H1 table #1273 fixes — a different problem.

… entries that fail them

Two changes from the sweep in #1285.

The profiles stop feeding the framework composite. static and static-tls go
to scored=False, which is the shape Pipelined already has: still measured,
still shown, faded on the board, out of the sum. infraScored stays True --
serving files fast from a tuned cache is what a proxy is for, and that league
is unaffected. engineScored was already False.

CATALOG is the single source: the board reads it through data.js and the
badge port reads it through _scored_for(), so the two scoring implementations
cannot disagree about this and check_badge_parity.js stays green.

Twelve entries fail validation on static and on nothing else, and are
flagship, emerging or experimental. They lose the two profiles from their
tests array and the 69 result keys those profiles produced:

  django, fletch, genhttp-11, genhttp-11-kestrel, genhttp-kestrel,
  mq-bridge, mq-bridge-py, pyronova, rage, rails, true-async-server, userver

Nothing else is touched: every other profile keeps its results, and
static-h2 and static-h3 are left alone -- the failures were on the H1 pair.

The entries stay enabled, since static was their only failure.
@github-actions

Copy link
Copy Markdown
Contributor

👋 Heads up! This PR modifies the following frameworks:

@MDA2AV
MDA2AV merged commit 1f878dd into main Aug 24, 2026
12 of 15 checks passed
MDA2AV added a commit that referenced this pull request Aug 24, 2026
)

The last group from the sweep in #1285: twelve entries that fail checks
other than the static probe, so neither #1288 (build), #1289 (fragmentation)
nor #1290 (static reference-only) covers them.

  deno-websocket, fleck        reject non-upgrade GET /ws
  node-websocket               text echo, multi-message, dies during the run
  dogrider                     binary echo
  sark                         upload chunked, upload truncated body,
                               StreamSum over h2+TLS
  swerver                      HTTP/2 negotiation and h2c prior-knowledge
  wtx-grpc, wtx-grpc-tls       GetSum over h2c / h2+TLS
  iris, typev                  upload chunked, plus static
  zio-http, zix                upload chunked

Each fails something the entry claims to implement, so the numbers it
publishes for those profiles do not describe a working server.

zio-http is the only flagship here and the only one whose failure is a
single profile -- upload chunked. It is included because the instruction was
the remaining failures, but it is the one worth a second look before this
merges: it loses 23 result keys over one check.
MDA2AV added a commit that referenced this pull request Aug 24, 2026
…o 800s

Two things the full sweep on this PR exposed.

/data/static was mounted only when an entry subscribed to a static profile.
benchmark.sh mounts it unconditionally (scripts/lib/framework.sh), and the
entries that read the directory at startup cannot boot without it: rage and
rails copy it with FileUtils.cp_r, userver builds an fs-cache from it. So
when #1290 dropped the static profiles from those three, "does not serve
static" quietly became "does not start" -- all three failed the sweep with
the container dead before the first check.

It is a read-only bind of a small directory and there is nothing to be gained
by leaving it out, so validate.sh now matches benchmark.sh and always mounts
it. rage goes from a dead container to 44 passed, 0 failed.

The watchdog goes 300s -> 800s. The entries with the widest profile sets --
humming-bird and the three web-framework-* entries -- were killed mid-run
rather than failing anything, so their results said nothing at all.
MDA2AV added a commit that referenced this pull request Aug 24, 2026
…1294)

* drop the static profiles from the four entries that fail only static

From the sweep on #1292. These four fail validation on the static staleness
probe and on nothing else, so the entries stay enabled and keep every other
profile they publish; they lose static, static-tls and the 21 result keys
those produced.

  fastpysgi-wsgi, uvicorn   engine
  h2o, varnish              infrastructure

Same treatment #1290 gave the flagship, emerging and experimental entries in
this position, applied to the tiers that were left out of it -- with one
difference worth stating: for infrastructure this is a real result rather
than a formality. h2o and varnish are proxies whose rule explicitly allows
open_file_cache and mmap, and they were given a 30s window against the
framework tiers' 2s. They still serve the old bytes after it.

* also drop the static profiles from fastpysgi-asgi

It fails static and only static against main -- 53 passed, 4 failed, every
failure a staleness probe -- so it belongs here rather than in #1295, where
it was first put. The sweep that classified it ran on the #1292 branch and
showed a fragmentation failure that main does not reproduce.
MDA2AV added a commit that referenced this pull request Aug 24, 2026
…o 800s

Two things the full sweep on this PR exposed.

/data/static was mounted only when an entry subscribed to a static profile.
benchmark.sh mounts it unconditionally (scripts/lib/framework.sh), and the
entries that read the directory at startup cannot boot without it: rage and
rails copy it with FileUtils.cp_r, userver builds an fs-cache from it. So
when #1290 dropped the static profiles from those three, "does not serve
static" quietly became "does not start" -- all three failed the sweep with
the container dead before the first check.

It is a read-only bind of a small directory and there is nothing to be gained
by leaving it out, so validate.sh now matches benchmark.sh and always mounts
it. rage goes from a dead container to 44 passed, 0 failed.

The watchdog goes 300s -> 800s. The entries with the widest profile sets --
humming-bird and the three web-framework-* entries -- were killed mid-run
rather than failing anything, so their results said nothing at all.
MDA2AV added a commit that referenced this pull request Aug 24, 2026
* validate: check TLS quality and TLS fairness, not just ALPN

Until now the only thing checked about TLS was which protocol ALPN settled
on. Two questions went unasked.

What did this connection negotiate -- the fairness half, tls_posture_probe:

  * the certificate must be the one mounted at /certs. Every handshake costs
    the server one signature with it, and on this box RSA-2048 signs 3,052/s
    against 77,124/s for ECDSA P-256. An entry that quietly generates its own
    EC certificate gets 25x cheaper handshakes and nothing would have
    noticed. Verified against a synthetic entry serving its own EC pair.
  * TLS 1.3, against a client offering it. The 1.2 handshake costs an extra
    round trip.
  * one of the three TLS 1.3 AEAD suites, which rules out NULL, anon, export
    and RC4 making "TLS" free.
  * ALPN must never name a protocol the client did not offer. Selecting
    nothing is fine and common -- bun does it, and the client falls back.

The cipher is reported rather than required. The field is already split:
some entries pick AES-128-GCM, some AES-256-GCM, and those measure ~17%
apart on bulk encryption at static-file block sizes (11.1 vs 9.5 GB/s at
16 KB). Not every framework exposes cipher preference, so this is on the
record instead of invisible.

What will the server negotiate at all -- the quality half, tls_quality_scan.
A server can hand a modern client TLS 1.3 and still accept TLS 1.0 or a NULL
cipher from anything that asks. Fails on SSLv2, SSLv3, TLS 1.0, TLS 1.1 and
on NULL, anonymous, export, 64-bit/DES/RC4/MD5 and 3DES cipher lists; notes
obsolete CBC and AEAD-without-forward-secrecy, which are weak but shipped as
defaults by much of the field.

That half is testssl.sh rather than a hand-rolled sweep: it already knows
every suite, it is the reference tool, and -p -s answers in ~5s. Its full run
also produces an SSL Labs grade, but costs ~48s per port and caps every entry
at B on the self-signed certificate the harness mounts, so the graded run is
left for audits. Skippable with HTTPARENA_SKIP_TLS_SCAN=1, and it skips
itself rather than failing an entry when the scanner image is unavailable.

validate: bun 70 -> 79 passed 0 failed, caddy 71/0. bun draws the note --
it accepts CBC and non-FS suites; caddy accepts neither.

* docs: document the TLS checks on the json-tls validation page

The checks added alongside this were not written down anywhere. The json-tls
page now carries them, since that is the profile whose whole point is TLS:

  * the certificate must be the one mounted at /certs, with the numbers that
    make it matter -- RSA-2048 signs 3,052/s against ECDSA P-256's 77,124/s,
    and every handshake is one signature, so a self-generated EC certificate
    is a 25x discount on that step
  * TLS 1.3, because the 1.2 handshake costs an extra round trip
  * one of the three TLS 1.3 AEAD suites, and why the choice among them is
    reported rather than required: the server picks, the field is split, and
    AES-128 against AES-256 is ~17% on bulk encryption
  * ALPN may select nothing, but never something the client did not offer
  * the testssl.sh scan: what fails, what is only noted, how to skip it, and
    why the graded run is left for audits rather than the gate

static-tls, baseline-h2 and static-h2 get a short pointer instead of a copy
-- the checks run there too, and one description is easier to keep true than
four.

* validate: ask about obsolete protocols with openssl rather than a scanner

Same question, asked directly: offer SSLv3, TLS 1.0 and TLS 1.1, and the
NULL, anonymous, export, RC4, DES and 3DES cipher families, and fail when a
handshake actually completes. An alert, a reset or a timeout is a refusal.

OpenSSL will not make those offers at its default security level, so the
probes carry @SECLEVEL=0; a protocol this openssl cannot offer at all is
reported as unprobed rather than counted as refused.

testssl.sh agrees with these results and is what found the first real failure
here -- humming-bird completes a TLS 1.0 handshake with
ECDHE-RSA-AES256-SHA. What decides it for the gate is cost: ~70ms for the
whole set against ~5s, and nothing outside the base image to pull or hang.
testssl.sh stays the better tool for an audit, where its wider suite coverage
and SSL Labs style grade earn the time, and the docs say so.

Verified on a clean host: humming-bird accepts tls1 and tls1_1, bun and caddy
accept neither.

* board: a TLS shield beside entries whose TLS was checked and clean

A small green shield after the framework name, on the table rows and in the
detail modal, for an entry that passed the TLS checks: serves the certificate
the harness mounts, negotiates TLS 1.3 with an AEAD cipher, and refuses
SSLv3, TLS 1.0/1.1 and the NULL, anonymous, export, RC4, DES and 3DES cipher
families.

The verdict is earned rather than declared. validate.sh writes
site/data/tls/<entry>.json when it actually runs those probes, the generator
folds it into meta, and the board reads meta[fw].tls. It is deliberately not
a meta.json field: that would let an entry claim the shield without being
checked, which is the opposite of the point.

Three states, and only one of them shows anything. "pass" gets the shield;
"fail" and absent both get nothing, because most entries have no TLS profile
at all and a red mark against them would say something untrue.

Seeded with the eight entries verified on a clean host this session -- bun,
caddy, elysia, genhttp-11-ioxide, h2o-mruby, hono-bun and slimeweb pass,
humming-bird fails on accepting TLS 1.0 and 1.1. Every other entry stays
unmarked until it is checked.

Hidden below 640px, where the name column is tightest.

check_badge_parity.js: 533 ranks match.

* ci: keep the TLS verdicts a validation run produces

validate.sh writes site/data/tls/<entry>.json when it runs the TLS probes,
and the board turns that into the shield -- but nothing collected the files,
so a full sweep computed 177 verdicts and threw all of them away.

One artifact per entry rather than a commit from inside the job: 177 matrix
jobs writing one directory would race, and a validation run committing to the
PR it is validating is a loop nobody wants. Collected after the run and
committed deliberately.

Uploaded with if: always(), because an entry that fails its TLS checks is
exactly the verdict worth keeping.

* ci: stop the detect step aborting on a disabled entry

all_entries() ends on a jq that exits non-zero for a disabled entry, so the
function inherited that status and `changed=$(all_entries)` aborted the whole
step under `bash -e` -- before a single line was echoed, which is why the
failure looked like nothing at all had run.

Whether it triggers depends on which entry sorts last and whether it happens
to be enabled, so it passed on the run that introduced it and failed on the
next one, after #1288, #1289 and #1291 disabled a batch of entries.

An explicit `return 0` on the function.

* validate: always mount the static directory, and raise the watchdog to 800s

Two things the full sweep on this PR exposed.

/data/static was mounted only when an entry subscribed to a static profile.
benchmark.sh mounts it unconditionally (scripts/lib/framework.sh), and the
entries that read the directory at startup cannot boot without it: rage and
rails copy it with FileUtils.cp_r, userver builds an fs-cache from it. So
when #1290 dropped the static profiles from those three, "does not serve
static" quietly became "does not start" -- all three failed the sweep with
the container dead before the first check.

It is a read-only bind of a small directory and there is nothing to be gained
by leaving it out, so validate.sh now matches benchmark.sh and always mounts
it. rage goes from a dead container to 44 passed, 0 failed.

The watchdog goes 300s -> 800s. The entries with the widest profile sets --
humming-bird and the three web-framework-* entries -- were killed mid-run
rather than failing anything, so their results said nothing at all.

* board: drop the TLS shield; the tls_check badge replaces it

Two badges for one subject was one too many, and the green shield was the
weaker of the two: it marked TLS basics an entry got right by default, while
the gold badge marks a hardening bar an entry opted into and wrote code for.

Removes the renderer, its CSS, the three render sites, the meta field the
board read and the loader that filled it. validate.sh still writes
site/data/tls/<entry>.json -- the verdict is what the opt-in badge is built
on -- but the files themselves are no longer committed, since nothing here
consumes them any more.

It also repairs a real break. The modal insertion had produced

    '</h3>'+tlsShield(fw)<span class="badge b-'+t+'"

-- the +' was lost, which is a syntax error in the board's only script block,
so the page would not have rendered at all. The earlier check evaluated
data.js and the shield function in isolation and never parsed index.html,
which is exactly the gap that let it through. Both are parsed now:

    all script blocks parse
    badge parity ok — 533 ranks match
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