A NoFlare.org resource · Open license · No vendor lock-in · No Cloudflare in this stack
A NetSentinel Cookbook · in the NoFlare lineage

Defense
without
the gate.

Cloudflare-class protection answers a question civic platforms don't actually face. The threat shape is episodic harassment, not persistent commercial DDoS — angry mob spikes, scripted abuse waves, the occasional volumetric tantrum from someone who didn't like the post. The defense should match the threat. Always-on proxies tax every legitimate visitor to defend against attackers who show up for two hours a quarter. This cookbook is the alternative.

Standing cost
~$0/mo
All four layers, fully owned, before any attack
Spinup time
~90s
Episodic bastion, from script to live
Per-attack cost
~€0.50/hr
Hetzner CX22 pro-rated, torn down on completion
Latency tax
~0.1ms
In-process middleware. Zero new hops in steady state

The threat shape is episodic.
The defense should be too.

An always-on Cloudflare-class proxy is not a defense — it is a tax. It is paid by every legitimate neighbor on every page load to defend against attackers who, for civic platforms, represent a small fraction of traffic and arrive in concentrated bursts rather than continuous pressure. Three premises drive the architecture in this cookbook.

01

Always-on is a latency tax

Every extra hop costs your users milliseconds. A proxy in Falkenstein routing traffic to an origin in Virginia adds 30ms to every request — a constant cost paid by readers to defend against bots they never see. Civic platforms are mostly read traffic. The math doesn't work.

02

The proxy is the SPOF

A single proxy going down takes the whole platform with it. Mitigating that requires two proxies plus failover, which doubles the cost and operational surface. You're now maintaining a mini-CDN of your own — the very thing you were trying to avoid. The dependency moved; it didn't disappear.

03

Episodic ≠ persistent

The actual threat for most civic operators is bounded: an angry mob arrives, harasses for hours, leaves. The infrastructure to defeat that should match its shape — dormant by default, spun up when needed, torn down when the wave passes. That is achievable for less than a euro an hour.

You do not need a permanent fortress to weather an occasional storm. You need a roof that goes up in ninety seconds and comes down when the sky clears. The fortress is what was sold. The roof is what works.

Four layers. Three always-on. One on-demand.

The architecture separates three concerns that always-on WAF vendors bundle together — origin concealment, blocklist enforcement, and active filtering. Bundling them is what creates the SPOF and the latency tax. Separating them is what makes the cost collapse possible.

INTERNET Legitimate traffic Probes, scanners, abuse Volumetric attackers LAYER 0 · ALWAYS-ON · ONE-TIME SETUP Origin Concealment Fly.io / Hetzner firewall rules · Direct origin access denied LAYER 1 · ALWAYS-ON · EDGE LEVEL Null-Routed Blocklist Sentinel blocklist → Fly machines API / Hetzner BGP · Drops before Node LAYER 2 · DORMANT · ACTIVE ON DEMAND Episodic Bastion Hetzner CX22 + nginx + cloud-init · ~90s spinup, DNS cutover, teardown LAYER 3 · ALWAYS-ON · IN-PROCESS Express Sentinel Middleware Rate limiting · CIDR escalation · UA fingerprinting · Honeypot detection APP Routes serve the request
00
Origin Concealment

One-time platform config that makes direct-to-origin attacks impossible. Without this, every other layer can be bypassed by an attacker who finds the real IP.

cost $0
latency 0ms
setup 15 min
01
Null-Routed Blocklist

Known-bad IPs and CIDR ranges dropped at the platform edge before they reach Node.js. Sentinel publishes the list; Fly or Hetzner enforce it. No proxy.

cost $0
latency 0ms
refresh 5 min
02
Episodic Bastion

A scripted nginx proxy on a Hetzner CX22 that stands up in ninety seconds when an attack starts and tears down when it ends. DNS cutover via API. Pre-staged certs.

cost €0.50/hr
latency ~5–30ms
duration attack-only
03
Express Sentinel Middleware

In-process Node.js middleware. Rate limiting, CIDR-range auto-escalation, UA fingerprinting, honeypot detection. The existing NetSentinel stack, hardened.

cost $0
latency ~0.1ms
deps npm only
The principle

Layers 0, 1, and 3 run continuously and cost nothing. Layer 2 is dormant until an attack arrives, and exists only as a script and a DNS plan until then. The only sustained infrastructure is the application itself. Cloudflare's bundle is unbundled — and the bundle was the leverage.

Five recipes. Hand them to your vibecoder.

Each recipe contains: a principle, the ingredient list, a sequence of steps, a representative code sample, and a Vibe Prompt — a copy-paste block formatted for Cursor, Claude Code, Replit, or any AI coding assistant that takes natural language and emits working code. The prompts are written to be self-contained.

01
Origin Concealment
One-time setup · Fly.io and Hetzner Cloud · ~15 minutes

Principle

Every other layer in this cookbook depends on this one. If an attacker can hit your application server's real IP directly, they bypass your null-routes, your bastion, and your middleware. Origin concealment closes that door — the application accepts traffic only from your platform's anycast layer or your bastion's IP. Anything else gets dropped at the network interface.

Ingredients

fly.tomlconfig
flyctlcli
Hetzner Cloud Firewallapi
hcloudcli

Sequence

# Fly.io path 1. Add machine-level firewall rules via the Fly machines API 2. Restrict ingress to Fly's anycast CIDRs only 3. Confirm: curl direct to a known Fly machine IP from a third-party host returns connection refused # Hetzner path 1. Create a Cloud Firewall, attach to the application server 2. Default deny inbound on 80/443 3. Allow only from: bastion IP (when active) + your monitoring/admin IPs 4. Confirm: same direct-IP curl test returns connection refused
Vibe Prompt · Origin Concealment
I am running a Node.js application on Fly.io. I want to lock down origin access so the application only accepts traffic from Fly's anycast layer — direct connections to a machine's public IP must be refused. Build me: 1. A `fly.toml` configuration block under `[services]` and `[[services.ports]]` that enforces this restriction using Fly's built-in firewall primitives. 2. A standalone Node.js script `scripts/lockdown-origin.mjs` that uses `fetch` against the Fly Machines API at `https://api.machines.dev/v1/apps/{APP_NAME}/machines` to: - List all machines for the app - For each machine, set firewall rules that deny all inbound traffic except from Fly's anycast layer - Use `FLY_API_TOKEN` from process.env (read from a `.env` file via dotenv) - Print a summary table of `machine_id | region | status | firewall_state` 3. A verification script `scripts/verify-lockdown.mjs` that: - Resolves the app's anycast hostname to its public IPs - Resolves each machine's direct public IP via the API - Attempts `curl -m 5` against each direct IP from the local environment - Reports PASS only if all direct-IP requests time out or are refused Use ESM, no Express, no extra deps beyond `dotenv`. Keep all files under 100 lines. Print clear human-readable output, color-coded with picocolors if available but optional. Verification criterion: after running, `curl --resolve myapp.fly.dev:443:<direct-machine-ip> https://myapp.fly.dev` must fail. `curl https://myapp.fly.dev` must succeed.
02
In-Process Hardening
Always-on · Express middleware · ~0.1ms latency cost

Principle

Most application-level attacks — credential stuffing, content scraping, scripted abuse — never trigger a honeypot path. They hit valid routes with high frequency or recognizable signatures. Three middleware layers close the gap: per-IP rate limiting, automatic CIDR-range escalation when a /24 keeps producing bad actors, and User-Agent fingerprinting against the small set of scanner tools that account for most automated probing.

Ingredients

express-rate-limitnpm
ipaddr.jsnpm
sentinel_blocklistdb
existing middlewareextend

Sequence

// Three middlewares, mounted in this order, before any route handler: app.use(rateLimitMiddleware()); // global cap + per-route cap app.use(uaFingerprintMiddleware()); // reject known scanner UAs app.use(cidrEscalationMiddleware()); // auto-block /24s with 3+ bad IPs app.use(sentinelMiddleware); // existing honeypot detection
Vibe Prompt · Express Hardening
I have an Express application written in TypeScript with an existing `sentinel-middleware.ts` that detects honeypot probes and writes to a `sentinel_blocklist` table (Postgres, columns: ip, blocked_at, reason, source_country, asn). Add three new middlewares to harden the in-process layer. All in `src/middleware/`: 1. `rate-limit.ts` — uses `express-rate-limit` with two tiers: - Global: 200 req/min per IP across all routes - Write endpoints (POST/PUT/DELETE on `/api/*`): 20 req/min per IP - On limit hit: write a row to `sentinel_blocklist` with reason='rate_limit_exceeded' if the IP exceeds the global limit 3 times in a 24h window - Use Redis if `REDIS_URL` is set, in-memory store otherwise - Whitelist any IP listed in `WHITELIST_IPS` env var (comma-separated) 2. `ua-fingerprint.ts` — rejects requests with empty User-Agent or matching scanner signatures. Hardcode the signature list as a `const SCANNER_PATTERNS: RegExp[]` array including: masscan, nuclei, zgrab, nmap, sqlmap, nikto, dirbuster, gobuster, wpscan, hydra, curl/8 with no Accept header, python-requests with no Accept-Language. Return 403 with body `forbidden`. Log to sentinel_blocklist with reason='ua_fingerprint_match'. 3. `cidr-escalation.ts` — runs every 10 minutes via setInterval (skip in test env). Query sentinel_blocklist for IPs blocked in the last 24h. Group by /24 (use `ipaddr.js`). For any /24 with ≥3 distinct blocked IPs, insert a CIDR row into a new `sentinel_cidr_blocklist` table (columns: cidr, blocked_at, member_count, escalated_from). The middleware itself, on each request, checks whether the source IP falls within any active CIDR and 403s if so. Cache the active CIDR list in memory, refresh every 60s. Mount order in app.ts before any route: app.use(rateLimit); app.use(uaFingerprint); app.use(cidrEscalation); app.use(existingSentinelMiddleware); Provide: - The three middleware files - A migration `db/migrations/cidr-blocklist.sql` for the new table - Updated `app.ts` showing the mount order - Unit tests for cidr-escalation grouping logic using vitest All files self-contained. No new infra dependencies beyond Redis (optional) and the existing Postgres connection.
03
Null-Routing the Blocklist
Always-on · Edge-level enforcement · 5-minute refresh

Principle

Once Sentinel knows an IP is bad, the IP should not be reaching your application server at all — not just being told 403. Null-routing pushes the blocklist down to the platform's network layer, where blocked traffic is dropped before any Node.js worker thread sees it. Two paths: Fly.io's machine firewall API, or Hetzner Cloud Firewall. The pattern is identical: cron job reads the blocklist, generates a deny ruleset, applies via API. No proxy. No new infrastructure.

Ingredients

node-cronnpm
Fly Machines APIapi
Hetzner Cloud APIapi
sentinel_blocklistdb
sentinel_cidr_blocklistdb

Sequence

# Every 5 minutes: 1. Read all active rows from sentinel_blocklist + sentinel_cidr_blocklist 2. Generate platform-specific deny rules: - Fly.io: PATCH machine config with firewall.deny entries - Hetzner: PUT firewall ruleset with deny rules per CIDR 3. Apply via API 4. Log a digest line: applied N IPs, M CIDRs, took Xms 5. Expose GET /api/sentinel/export/{platform} for inspection
Vibe Prompt · Null-Route Sync
I have an Express/TypeScript application running on Fly.io with a Postgres database containing two tables: `sentinel_blocklist` (columns: ip, blocked_at, reason) and `sentinel_cidr_blocklist` (columns: cidr, blocked_at, member_count). Build a null-route sync system that pushes the blocklist down to Fly's network layer so blocked traffic never reaches Node.js. Files to create: 1. `src/null-route/fly-sync.ts` — async function `syncToFly()` that: - Reads all rows from both blocklist tables (cap at 5000 IPs + 500 CIDRs) - Calls the Fly Machines API `PATCH /v1/apps/{APP_NAME}/machines/{MACHINE_ID}` with updated config - Specifically updates `services[].ports[].handlers` and machine-level firewall to deny the listed CIDRs - Handles batching since Fly has rule-count limits; if >500 rules, deduplicate /24s first - Returns `{ applied_ips, applied_cidrs, took_ms, errors }` 2. `src/null-route/hetzner-sync.ts` — same shape but targeting Hetzner Cloud Firewall: - Use `https://api.hetzner.cloud/v1/firewalls/{FIREWALL_ID}/actions/set_rules` - Hetzner allows up to 50 rules per firewall — the rules are CIDR-based, so collapse to /24 or /16 before applying - Read `HETZNER_CLOUD_TOKEN` from env 3. `src/null-route/scheduler.ts` — uses `node-cron` to run sync every 5 minutes. Reads `NULL_ROUTE_TARGET` env var (one of: `fly`, `hetzner`, `both`, `none`). Logs a single line per run. 4. `src/api/sentinel/export.ts` — Express route `GET /api/sentinel/export/:format` returning the active blocklist in three formats: - `nginx`: lines like `deny 1.2.3.4;` and `deny 1.2.3.0/24;` - `fly-json`: the JSON payload that fly-sync would POST - `hetzner-json`: same for Hetzner - Auth via `?token=` matching env var `SENTINEL_EXPORT_TOKEN` 5. `src/null-route/dedupe.ts` — utility module: `dedupeToCidrs(ips: string[], maxRules: number): string[]` — uses ipaddr.js to collapse individual IPs into /24 ranges when 3+ IPs share a /24, falling back to /16 if total rule count would exceed maxRules. Add `NULL_ROUTE_TARGET=fly` to the example .env. Wire the scheduler into app startup. All TypeScript strict mode, no `any`. Tests for `dedupe.ts` only.
04
The Episodic Bastion
Dormant by default · ~90s spinup · ~€0.50 per attack-hour

Principle

When the always-on layers are not enough — a sustained volumetric attack, an organized harassment campaign, anything that overwhelms in-process middleware — you stand up a proxy in front of the origin for the duration of the attack. The bastion is a CX22 with nginx, cloud-init, and the current sentinel blocklist. DNS cuts over to its IP. The origin firewall (Recipe 01) is updated to accept only from the bastion. When the wave passes, you tear it down. The whole cycle costs less than a euro.

Ingredients

hcloud CLIcli
cloud-initconfig
Bunny DNS APIapi
pre-staged TLScert
nginx configtemplate

The two prerequisites people forget

Prerequisite 1 · TTL discipline

DNS records that need to fail over must already be set to a 60-second TTL. If you discover the attack at 2 AM and your TTL is 3600 seconds, your fastest possible cutover is an hour. Set this on day one for every record that protects a public-facing service.

Prerequisite 2 · Pre-staged certificates

Let's Encrypt cold-start adds 30+ seconds and can fail under attack. Stage a wildcard cert in your secret store, encrypted, and have cloud-init drop it into nginx on first boot. The bastion is serving valid TLS within seconds, not minutes.

Sequence

# waf-up.sh — execute when an attack starts: 1. hcloud server create --type cx22 --image ubuntu-24.04 \ --user-data ./cloud-init.yaml --name bastion-$(date +%s) 2. Wait for cloud-init completion (~60s) — nginx running, certs in place 3. Pull current blocklist from /api/sentinel/export/nginx, scp to bastion 4. Reload nginx with the deny rules 5. Update Bunny DNS A record: example.com → bastion IP (60s TTL) 6. Update origin firewall: deny all except bastion IP 7. Verify: curl https://example.com from a third party returns 200, blocked IP returns 403 # waf-down.sh — when attack subsides: 1. Update Bunny DNS A record back to origin 2. Update origin firewall: restore prior allowlist 3. Wait DNS TTL (60s) for cutover 4. hcloud server delete bastion-* 5. Total cost: ~€0.50 per attack-hour
Vibe Prompt · Episodic Bastion
I want a CLI tool that stands up an emergency nginx WAF proxy on Hetzner Cloud (CX22 server) when my civic platform is under attack, and tears it down when the attack ends. The setup: - Origin runs at app.example.com on Fly.io - DNS managed via Bunny DNS API (`BUNNY_DNS_API_KEY`, `BUNNY_ZONE_ID`) - Hetzner via `HETZNER_CLOUD_TOKEN` - Pre-staged wildcard TLS cert + key stored in `~/.waf/certs/` as `fullchain.pem` + `privkey.pem` - The Sentinel API publishes the current blocklist at `https://app.example.com/api/sentinel/export/nginx?token=...` Build a Node.js CLI tool `waf-bastion` (single repo, ESM, TypeScript) with two commands: `waf-bastion up [--domain example.com]`: 1. Calls Hetzner API to create a CX22 in fsn1, image ubuntu-24.04, with cloud-init userdata that installs nginx, drops in cert files (passed as base64 in user-data), writes an nginx config that proxies all traffic to the Fly origin via Host header preservation, and applies the current Sentinel blocklist as a `geo $blocked` map plus `if ($blocked) { return 403; }` 2. Polls cloud-init completion via SSH (uses `~/.ssh/id_ed25519`), max 120s 3. Verifies the bastion responds 200 to `curl --resolve $DOMAIN:443:$BASTION_IP https://$DOMAIN/` 4. Updates Bunny DNS: PATCH the A record for $DOMAIN to point to bastion IP, set TTL 60 5. Calls a configurable `ORIGIN_LOCKDOWN_HOOK` URL (POST with `{ "allow_only_ip": "$BASTION_IP" }`) to update origin firewall — this hook is implemented separately by the user 6. Saves state to `~/.waf/state.json`: `{ bastion_id, bastion_ip, domain, started_at, original_a_record }` 7. Prints: `Bastion live at $BASTION_IP. Originals saved. Run 'waf-bastion down' to tear down.` `waf-bastion down`: 1. Reads state.json 2. Restores Bunny DNS A record to original value 3. Calls ORIGIN_LOCKDOWN_HOOK with `{ "allow_only_ip": null }` 4. Waits 65 seconds for DNS TTL 5. Deletes the Hetzner server 6. Removes state.json 7. Prints duration and estimated cost: `Bastion ran for $DURATION. Cost ~ €$COST.` Cloud-init template should be a separate file `templates/cloud-init.yaml` with handlebars-style `{{...}}` substitutions for cert content, blocklist URL, origin hostname, and a token to fetch the blocklist. Include a `waf-bastion status` command that just prints state.json contents if it exists. Single-file output is fine if it stays under 400 lines. Use `commander` for the CLI, `node-fetch`, `ssh2` for the cloud-init readiness check. Tests not required.
05
Sentinel Federation Protocol
Covenant-governed shared blocklist · The force multiplier

Principle

A single operator's blocklist is bounded by what their honeypots have seen. A federated blocklist — many operators sharing fingerprints under a covenant — is bounded by what the entire network has seen. This is the network effect Cloudflare uses to justify its pricing. The covenant version makes that effect available to everyone, with no central party who can decide who deserves protection. Each operator runs their own Sentinel instance. Each instance publishes a signed feed. Each instance subscribes to peers it trusts. Trust is established by covenant attestation, not by paid contract.

The protocol surface

# Each Sentinel instance exposes: POST /api/sentinel/federation/report <- peer-signed report of newly observed bad IPs/CIDRs -> 202 Accepted, ingested into local provisional pool GET /api/sentinel/federation/feed -> current local blocklist, signed with operator's covenant key -> format: JSON with { ip|cidr, first_seen, last_seen, signatures[], reasons[] } GET /api/sentinel/federation/peers -> list of subscribed peer feeds with trust gradient (0–100) POST /api/sentinel/federation/subscribe <- request to subscribe to this instance's feed -> covenant attestation flow

Trust gradient

Not all peers are equally trusted. A new peer joins at trust 50 — their reports require corroboration from another peer before promoting an IP from the provisional pool to the active blocklist. Trust rises as their reports are corroborated by trusted peers and falls when their reports turn out to be false positives. The gradient prevents a single compromised or malicious peer from poisoning the entire network — the same mechanism by which a covenant community keeps faith without central enforcement.

Vibe Prompt · Federation Protocol
I want to add federation to my existing NetSentinel instance — a covenant-governed shared blocklist where multiple Sentinel operators can subscribe to each other's feeds and benefit from collective threat intelligence without a central authority. Existing context: - Express + TypeScript + Postgres - Tables: `sentinel_blocklist` (ip, blocked_at, reason), `sentinel_cidr_blocklist` (cidr, blocked_at, member_count) - Operator has an Ed25519 keypair at `~/.sentinel/operator-key.pem` for signing Build the federation layer: 1. New tables (migration `db/migrations/sentinel-federation.sql`): - `sentinel_peers` (peer_id, covenant_url, public_key, trust_score, subscribed_at, last_pulled_at) - `sentinel_provisional` (ip_or_cidr, first_seen, peer_id, signature, status: 'provisional'|'corroborated'|'active'|'rejected') - `sentinel_corroborations` (provisional_id, corroborating_peer_id, observed_at) 2. Endpoints in `src/routes/federation.ts`: - `POST /api/sentinel/federation/report` — accepts a signed report from a peer. Body: `{ peer_id, observations: [{ ip, reason, observed_at }], signature }`. Verifies signature against peer's public key. If peer trust ≥ 70, promotes directly to provisional with status='corroborated'; otherwise inserts as 'provisional'. When the same IP is reported by 2+ distinct provisional peers, promote to 'active' (i.e., add to local blocklist). - `GET /api/sentinel/federation/feed` — returns the operator's current active blocklist signed with the operator key. Format `{ feed_id, generated_at, entries: [...], signature }`. Cache for 60s. - `GET /api/sentinel/federation/peers` — returns the operator's peer list with trust scores (no secrets). - `POST /api/sentinel/federation/subscribe` — covenant attestation flow: accepts `{ covenant_url, public_key, attestation }` where attestation is a signed message containing the covenant statement of intent. Verify signature; if valid, insert peer at trust_score=50 and start polling their feed. 3. Background worker `src/federation/poller.ts` — every 15 min, for each subscribed peer, fetches their feed, verifies the signature, ingests new entries via the same logic as `/report`. 4. Trust adjustment `src/federation/trust.ts` — exposes `bumpTrust(peer_id, delta)`. Hooks: every time a peer's report is corroborated by another peer, +1. Every time it's rejected (false positive flag), -10. Trust capped at [0, 100]. Trust < 30 = peer feed ignored. 5. CLI command `sentinel federation export-key` — prints the operator's public key block formatted for inclusion in a covenant attestation. 6. Document the covenant attestation format in `FEDERATION.md` — what the attestation message must contain (operator name, jurisdiction, date, statement of covenant adherence, public key fingerprint), and how it's signed. Use `tweetnacl` for Ed25519 signing/verification. Strict TypeScript. No new transitive deps beyond tweetnacl.

Or hand the whole thing to your vibecoder

If you'd rather not implement recipe-by-recipe, the prompt below is the cookbook compressed into a single instruction set. Drop it into Cursor or Claude Code with your repo open. It produces all five recipes as one cohesive PR. Verify each layer before going to production.

Vibe Prompt · Full Cookbook
Implement a sovereign WAF defense for this Node.js/Express/Postgres application. Five layers, structured per the NetSentinel cookbook (sovereign-waf-cookbook.html). Layer 0 — Origin concealment: - fly.toml restricting machine ingress to Fly anycast layer - scripts/lockdown-origin.mjs and scripts/verify-lockdown.mjs Layer 1 — Null-route sync (Fly + Hetzner targets): - src/null-route/{fly-sync,hetzner-sync,scheduler,dedupe}.ts - GET /api/sentinel/export/:format endpoint - Cron every 5 min, controlled by NULL_ROUTE_TARGET env var Layer 2 — Express in-process hardening: - src/middleware/{rate-limit,ua-fingerprint,cidr-escalation}.ts - New table sentinel_cidr_blocklist via migration - Mounted before existing sentinel-middleware Layer 3 — Episodic bastion CLI: - waf-bastion up | down | status commands - Hetzner CX22 + nginx + pre-staged TLS via cloud-init - Bunny DNS A-record cutover with 60s TTL - State persisted in ~/.waf/state.json Layer 4 — Federation protocol: - sentinel_peers, sentinel_provisional, sentinel_corroborations tables - POST /report, GET /feed, GET /peers, POST /subscribe endpoints - Ed25519 signatures via tweetnacl, trust gradient with corroboration rules - 15-min poller for subscribed peer feeds Architectural rules: - TypeScript strict mode throughout - No new infrastructure dependencies beyond what's in package.json + Hetzner Cloud + Bunny DNS - Each layer must be independently disable-able via env var (LAYER_0_ENABLED, LAYER_1_ENABLED, etc.) - All env vars documented in .env.example - README.md updated with a "Sovereign WAF" section explaining how to enable each layer in order - One PR per layer, in this order: 0 → 2 → 1 → 4 → 3 Verification criteria for each layer: - Layer 0: direct-IP curl times out - Layer 1: GET /api/sentinel/export/nginx returns deny lines for known-blocked IPs - Layer 2: 201 requests in 60s from same IP returns 429 on the 201st - Layer 3: waf-bastion up completes in ≤120s, blocked IPs get 403, valid requests pass - Layer 4: a test peer subscription survives signature verification and yields entries in /feed Read the existing sentinel-middleware.ts before starting; preserve its honeypot detection and RIPE/AbuseIPDB reporting. Do not break the existing block-list behavior. The new layers extend, not replace.

The cookbook does not specify a runtime for the bastion. Hetzner is the cheapest standing option in Europe, but the same cloud-init script runs on DigitalOcean, Vultr, Linode, or any cloud that accepts arbitrary user-data. Substitute the API client; the architecture holds.

The network effect, without the gatekeeper

Cloudflare's pricing is justified, in part, by their threat intelligence — every attack against any Cloudflare customer becomes data that protects every other customer. That is real value. The covenant question is whether that value should accrue to a single private entity or to the network of operators who produce it. Federation answers: to the network.

One operator, bounded view

A single Sentinel instance only knows what its own honeypots have seen. A scanner that probes ten unrelated civic platforms gets caught ten times in isolation, with each operator independently figuring out it's bad.

Federated, shared view

Operators publish signed feeds. Other operators subscribe to peers they trust. The first hit at any peer becomes provisional intelligence. The second corroboration promotes it to active. The scanner gets caught at the first portal — not the tenth.

Covenant trust, not contract

Trust is established by covenant attestation — a signed statement of operator identity and adherence — not by paying a vendor. A peer's trust score rises and falls based on the quality of their reports, not their bill. False positives cost trust. Corroborations earn it.

No single point of capture

There is no central registry. There is no party that can revoke a peer, throttle a feed, or sell the intelligence to a state actor. Every operator runs their own instance, signs their own feed, and decides their own trust gradient. The protocol is open.

The network effect is the asset. The gate around the network is the leverage. Federation keeps the asset and removes the leverage. That is what was always supposed to be possible.

What you stop paying. What you start owning.

Always-on WAF vendors price for an availability scenario most civic operators don't actually live in. Below is the comparison at the size where most civic platforms operate — a few dozen domains, modest traffic, occasional harassment events. Numbers are illustrative; verify against current vendor pricing.

Layer Cloudflare-class always-on Sovereign episodic stack
DNS Bundled · gateway dependency Bunny DNS · ~$0/mo
Always-on WAF / DDoS $20–$200/mo per zone, more at scale Express middleware · $0
Origin concealment Implicit · vendor-controlled Fly/Hetzner firewall · $0
Edge null-routing Implicit · vendor-controlled Sentinel sync · $0
Standing infra cost $240–$2,400+/yr ~$0/yr
Per-attack cost Bundled, but you cannot leave during one ~€0.50/hr · CX22 pro-rated
Threat intelligence Captive · sold back to you Federated · covenant-governed
Off-switch leverage Vendor holds the switch You hold the switch
The honest accounting

The sovereign stack is not free in the sense of zero effort — implementing five recipes requires a developer afternoon (or a vibecoder hour) and operational discipline to maintain TTL settings and pre-staged certs. What it removes is the standing rent and the off-switch leverage. Those are the two things the always-on bundle was selling. They are now optional purchases — and most civic operators will never need to buy them.

This cookbook is open.
So is what it produces.

This document is published under MIT license. The reference implementations are open source. The federation protocol is openly specified, implementable by anyone, in any language. There is no contributor agreement, no premium tier, no enterprise contact form. If you build it, you own it. If you improve it, contribute back if you wish, or don't. The point is not who gets credit. The point is that the gate is no longer the only path through.