levelrail-cli firewall ingress.
This is separate from the host firewall under Settings, which manages ports on the server itself (ufw), and from the per-domain limits described in Domain traffic controls, which now live in Firewall under Rules when a host is selected.
The four views
Pick a host in the scope selector to see one host, or leave it on All hosts. The time range applies to the Overview.
Evaluation order
For every request, in this order:- The per-domain shield allowlist (
allowon the domain’s limits) skips the per-domain shield only; the firewall below still runs. - The challenge endpoint (
/.well-known/lr-challenge) is handled by the firewall itself. - Rules, lowest position first, only those enabled and either global or scoped to this host. The first rule that decides ends the check:
allow(shown as Bypass): the request skips everything below, including bans and bot checks.deny: 403.challenge: the browser check, unless the client already holds a valid pass.rate_limit: counts matches per client IP in a fixed window. Under the limit the request continues to the next rule; over it the answer is 429 withRetry-After.log: counts the request as logged and continues.
- Temporary bans: a banned address is denied.
- Attack mode with challenge: every visitor without a pass gets the browser check.
- Bot lists: bad bots, then AI crawlers, each set to off, log, challenge or block.
- Attack mode limits: while attack mode is on, a per-client rate and connection limit applies to every host.
- The per-domain shield (rate, connections, body size, domain bot list) runs last, on routes that have one.
Rules
A rule has a name, an optional note, a scope (all hosts or one host), an enabled switch, an order, one or more conditions that must all match, and an action. For OR, add several rules.
A pattern uses
* for any run of characters, including slashes, and ? for one character, at most 8 wildcards per value. Paths and query values compare case sensitively; hosts, user agents and header values do not. Query parameters are compared as sent, without decoding. Rules are compiled when saved, so matching costs a few comparisons per rule and allocates nothing per request. At most APP_FIREWALL_MAX_RULES rules (default 500) can exist.
The IP blocking and Always allow lists are views over the same rules: a rule with a single IP condition and the deny or allow action. Allow entries are placed first and blocks right after them, so an allow always wins. Blocking an address that includes your own is refused unless you confirm it, because it would lock you out of this control plane.
Every rule shows how many requests it matched since the control plane started, with a one-hour sparkline.
The rule editor shows the rule exactly as stored under View code. The same checks run in the browser and on the server, so errors appear next to the field either way.
Temporary bans (persistent actions)
A client that is denied or rate limitedAPP_FIREWALL_BAN_STRIKES times (default 20) within APP_FIREWALL_BAN_WINDOW (default 5m) is denied everywhere for APP_FIREWALL_BAN_DURATION (default 1h). A client that matches an allow rule is never banned. The list shows the address, why, how many requests were blocked, when it started and when it ends, with a Lift button. Turn the feature off with the Automatic bans switch under Bot management. Bans are held in memory only, capped by APP_FIREWALL_MAX_BANS, and end on restart.
Browser challenge
A challenge serves a small page that needs no external service: JavaScript finds a counter so that the SHA-256 of a signed token and the counter hasAPP_FIREWALL_CHALLENGE_BITS leading zero bits (default 16, about a fraction of a second on a laptop), then posts the proof. The server checks the signature, expiry and proof and sets __lr_pass, an HttpOnly, SameSite=Lax cookie valid for APP_FIREWALL_PASS_TTL (default 30m).
- The token and the cookie are HMAC-SHA256 signed with a key derived from the control plane master key, bound to the client’s network (the /24 for IPv4, the /64 for IPv6), and the token expires after
APP_FIREWALL_CHALLENGE_TTL(default 2m). Without a master key a random per-process key is used and passes reset on restart. - The client address comes from the same trusted-proxy handling as the rest of the ingress, so the challenge works behind a CDN or load balancer. The cookie is marked Secure when the request arrived over TLS or with
X-Forwarded-Proto: https. - The page sets a strict Content-Security-Policy (only its own script, same-origin posts), is marked noindex and no-store, uses system colors so it follows the visitor’s light or dark setting, announces progress with
role="status", and shows a retry button when the check fails. - A challenge answers 403 with the page. It suits browsers navigating by GET. API clients and non-browser agents cannot pass it, so give them a bypass rule or an Always allow entry.
- A challenge proves a browser ran some JavaScript. It does not verify that a crawler is who it says it is. Verified-bot detection by reverse DNS is not implemented.
Bot management and AI crawlers
Each list has a mode: off, log only, challenge or block. The bad bot list is the curated list shipped with the shield (internal/trafficpolicy/shield_bots.txt, replaceable with APP_SHIELD_BOT_LIST_FILE). The AI crawler list is shipped as data (internal/edgefw/ai_bots.txt) and is editable under Edit list: one case-insensitive user agent substring per line, at most APP_FIREWALL_MAX_AI_BOT_AGENTS (default 256). An empty custom list means the shipped one is in use. Matching is by user agent text only.
Attack mode
Danger zone, Attack mode: for a chosen duration (up toAPP_SHIELD_ATTACK_MAX_DURATION) every host gets a low per-client request rate and connection cap (APP_SHIELD_ATTACK_RATE_PER_MINUTE, APP_SHIELD_ATTACK_BURST, APP_SHIELD_ATTACK_MAX_CONCURRENT) and the bad bot list is refused. Optionally every visitor must also pass the browser check. It ends on its own and can be turned off early. Per-domain under attack mode on the domain limits still works and is independent.
When denied, challenged and rate limited requests average APP_FIREWALL_SUGGEST_ATTACK_PER_MINUTE (default 120) or more per minute over APP_FIREWALL_SUGGEST_ATTACK_MINUTES (default 3) minutes and attack mode is off, the Overview shows an alert with a Turn on attack mode button, and an item appears in Attention for operators with the deploy ability. The test is a plain threshold, nothing else.
What is stored, and for how long
Query strings are never stored. Paths in the top table are reduced to a route template with identifiers replaced, at most three segments deep. Client addresses exist only in the capped, time-bounded in-memory tables above and in the rules you create; they are never written to a log or to the database by the firewall. Set
APP_FIREWALL_TOP_SAMPLE to N to feed the top tables from every Nth request on very busy instances. Requests the firewall denies never reach the per-app request statistics, which only see requests that were let through.
Reading client addresses (Traffic, Denied IPs, bans) needs the read:sensitive ability. Changing anything needs deploy. Reading counts, rules, settings and the audit view needs read.
Command line
--action rate_limit --rate 100 --window 60. A condition is FIELD OP VALUE, with header:NAME or query:NAME for named fields.
API
400 with {"error": "...", "fields": [{"field": "conditions[0].value", "message": "..."}]}. A rule that would block the caller’s own address answers 409 with "code": "self_lockout"; repeat with ?force=true to confirm. The app spec (app.yaml) does not carry firewall rules.
Not yet
- Managed rulesets such as the OWASP Core Rule Set. See the Coraza notes below.
- Countries and flags in the tables, ASN and network views, client fingerprints (JA3, JA4). Country appears in rules only when a GeoIP source is configured (
APP_GEOIP_DBor a trusted country header). Tables list addresses only. Showing flags needs the same source on the server side of the table; networks and fingerprints need an ASN database and TLS fingerprinting in the ingress, which Levelrail does not ship. - Verified bots (reverse DNS and published ranges). Matching is by user agent.
- Hit counters and charts across restarts. Everything in memory resets when the control plane restarts. A seven day view needs persisted aggregates.
- Describe a rule in words. The rule editor is complete; a box that drafts a rule from a sentence with the configured AI provider, limited to the same field and action catalog and never saved without your review, is planned. No AI is ever involved in handling a request.
- Match preview (“matches about N requests in the last hour”) needs per-rule dry-run counters that are not collected yet.
Could Coraza run as a Caddy module?
Coraza is a Go WAF engine that reads the OWASP Core Rule Set and has a Caddy module (coraza-caddy). Embedding it would be viable because the ingress already imports Caddy as a library, but the cost is real:
- Binary and build: one more module and its dependencies in the single static binary, and a pinned CRS version to ship and update.
- Latency and memory: CRS inspects headers and bodies with hundreds of regular expressions per request. Expect a measurable per-request cost and a body buffering limit, which interacts with streaming uploads and WebSockets.
- False positives: CRS needs per-app tuning (paranoia level, exclusions). Without a tuning workflow in the UI, a default install would block legitimate traffic. Detection-only mode with a review screen would have to ship first.
- Fit: the firewall’s counters, row menus and audit trail would need a second source of events from the WAF engine.
levelrail-cli bench, before any claim. Nothing of this is implemented.