Configure attested routes (the attestation floor)
Diátaxis quadrant: How-to. Audience: integrators and operators running humanymous Gate (“Gate” after first mention) who want to raise the cost of automated access to specific high-value routes.
This guide shows how to put the attestation floor on an operator-marked route: what to change, the two configuration preconditions that Gate refuses to start without, how a real user clears the floor, and how to confirm it works. It is written against the reference implementation; for the boundary with production see Production vs reference.
What the floor does, and what it does not
Past the T4 ceiling, a coherent engine-level spoof cannot be told apart from a human by detection alone — that is a design boundary, not a bug (see What is Gate?). The attestation floor does not try to detect the spoof. On a route you mark attested, it prices the ALLOW: a scoring-ALLOW no longer takes the free fast-path. The request must carry one of
- possession — a WebAuthn, Privacy Pass, or Web Bot Auth trust-upgrade (the possession pre-gate forwards it before scoring runs), or
- a step-up proof — the
hmn_sucookie, minted when the user solves a humanymous Pass challenge.
Without either, the ALLOW is demoted to a Pass challenge. The demotion is always CHALLENGE→Pass, never DENY: an unattested human solves the same Pass an anonymous visitor already solves. The floor never blocks a human categorically, and it is not a lockout.
It converts a spoof’s unlimited free high-value passes into either possession it cannot mint or a per-session human Pass solve. It does not close the ceiling: a real human on their own connection, and a single one-shot coherent request, remain open by construction. Compute cost (PoW) is deliberately not the friction — a native solver clears any false-positive-safe difficulty faster than a real browser, so it would tax real, no-JS, and assistive-technology users first. The friction is the interactive Pass.
Before you start
You need a Gate in front of an origin (see Deployment & policy operations) and the Core engine serving the Pass challenge. Decide which routes are high-value and mutating — a checkout, a funds transfer, a password change, an API-key mint. Do not floor a whole site; browse traffic belongs on balanced.
Step 1 — set a shared token key (required)
The Pass runs on the Core; the floor is enforced on the Gate. When a user solves the Pass, the Core mints a short-lived step-up receipt; the Gate verifies it and mints the hmn_su cookie. Both sides must sign and verify with the same key. Set the same value on the Core and the Gate:
export HMN_TOKEN_KEY=$(openssl rand -hex 32) # the SAME value on Core and Gate
If any route uses the attested preset and HMN_TOKEN_KEY is unset (or malformed), both the Gate and the Core refuse to start:
routes: route "/checkout" uses preset "attested" but no shared HMN_TOKEN_KEY is set —
the step-up receipt from the Core Pass could never verify at the Gate, making the route
an unredeemable Pass loop for humans; set a shared HMN_TOKEN_KEY (hex, ≥16 bytes) across
Core and Gate
This is deliberate. Without a shared, persistent key the Gate’s per-boot random key could never verify a Core-minted receipt, so /__hmn/stepup could never mint hmn_su, and the route would become a Pass loop no human could clear — worse than the lockout the no-lockout rule forbids. Failing at boot makes the misconfiguration impossible to ship.
Step 2 — mark the route
Add the route to your -routes file, one <prefix> <preset> per line:
# routes.conf
/checkout attested
/transfer attested
/health off
Start the Gate with -routes routes.conf. Longest-prefix match wins; everything unmatched stays balanced.
The attested preset is refused on a catch-all prefix. / attested (or an empty prefix) is a configuration error at load time, and the same refusal is enforced a second time inside NewServer so a programmatic route table cannot bypass it:
routes ...: preset "attested" is refused on the catch-all prefix "/" —
mark specific high-value routes (e.g. /checkout), not the whole site
Step 3 — wire a credential verifier (recommended)
With a credential verifier configured, a returning user who holds possession (a passkey, a Privacy Pass token, or an allow-listed agent signature) skips the Pass entirely — the possession pre-gate forwards them before scoring. Without one, the floor still holds, but every user (including real humans) must solve a Pass to clear it, and the Gate logs a startup warning:
WARNING: route "/checkout" uses preset "attested" but NO possession verifier
(-agent-keys/-pat-issuers/-webauthn-creds) is configured — every user (incl. returning
humans) must solve a Pass to clear the floor (still no-lockout, but no attestation fast-path).
See Configure credential verifiers for wiring WebAuthn, Privacy Pass, and Web Bot Auth.
Step 4 — keep the Pass in the Gate’s cookie jar
The step-up receipt is bound to the session id (hsid). The Core mints it against the Core’s hsid; the Gate verifies it against the request’s hsid. Those are the same value only when the browser sends one hsid cookie to both. Two topologies satisfy this:
- Co-serve the Pass through the Gate (recommended) — the Pass is same-host, so one
hsidcookie covers both. Different ports are fine; cookies are not port-scoped. - Share a cookie
Domain— if the Pass is on a sibling subdomain, set an explicitDomainonhsidso both hosts receive it.
A split-domain Pass with no shared cookie makes hsid diverge. The receipt then never matches, /__hmn/stepup returns 403, hmn_su is never minted, and a real human who genuinely solved the Pass loops. Gate makes this self-diagnosing (see below) rather than silent, but the fix is the cookie jar. On a multi-host deployment, also run NTP — the receipt carries only a ±120 s clock-skew leeway. See Supported topologies.
How a user clears the floor
flowchart TD
A["request to /checkout"] --> P{"possession? (WebAuthn / PAT / Web Bot Auth)"}
P -->|"yes"| F["forward (possession pre-gate)"]
P -->|"no"| S{"valid hmn_su step-up proof?"}
S -->|"yes"| F
S -->|"no"| C["CHALLENGE → humanymous Pass"]
C --> SV["user solves Pass"]
SV --> R["Core mints step-up receipt in solve JSON"]
R --> ST["client POSTs receipt to /__hmn/stepup (X-Hmn-Stepup-Receipt)"]
ST --> MU["Gate mints hmn_su (re-bound to the live socket)"]
MU --> F
The user solves the Pass once per window. hmn_su is TTL’d and rides subsequent requests, so a multi-step checkout does not re-challenge on every step.
Confirm it works
- An unattested ALLOW to the attested route returns a challenge (HTTP 401), and the origin is not contacted. On any other (
balanced/strict) route the same session still passes — the floor touches only marked routes. - After a Pass solve and a successful
POST /__hmn/stepup, the next request to the attested route passes. - A possession-bearing request passes without a Pass.
Watch the audit log for the diagnostic reasons at /__hmn/stepup:
| Audit reason | Meaning | Fix |
|---|---|---|
sid_mismatch |
A validly-signed receipt for a different hsid — the cookie jar diverged. |
Co-serve the Pass behind the Gate or share the hsid cookie Domain (Step 4). |
expired |
The receipt aged out beyond the skew leeway. | Check Core↔Gate clock skew / NTP; the client can re-solve for a fresh receipt. |
bad_sig |
Absent, forged, or key-mismatched receipt. | Confirm the same HMN_TOKEN_KEY on Core and Gate. |
Bounding a shared credential (ceiling-guard #2)
Possession is not unlimited. A single WebAuthn credential that fast-paths from more than CredFanoutCap distinct /24 subnets (default 8) within the tracking window — a stolen or shared passkey fronting a proxy farm — has its trust-upgrade withheld on attested routes and reverts to Pass. Fan-out is counted on every route but withheld only where the operator priced the ALLOW, so a roaming human on one passkey is never newly challenged on ordinary browse traffic. The revert is to CHALLENGE, never DENY.
Related
- Configure credential verifiers — give possession-holders the fast-path past the floor.
- CLI, configuration & policy reference — the
attestedpreset,HMN_TOKEN_KEY, and the step-up surface. - Supported topologies — the cookie-jar and shared-key prerequisites.
- Why am I seeing this? (humanymous Pass) — the challenge from the user’s side.