Repository navigation
Releases: coo-quack/vigilant-heron
Release list
v0.4.0
- Added: the site directive
static_query_params <name> ...;lets a static asset
request with a cache-busting query, such as WordPress's?ver=6.8.3, skip the
backend. The query must be made only of the listed parameters, each with a plain
value of 1 to 64 letters, digits,.,_or-. Before, any query sent the
request to the backend, so a page view sent every such asset there and could hit
max_concurrency_per_client. Unset, the static skip still requires no query.
v0.3.0
- Changed: a prefix
locationand a^~location with the same path in one site
are now rejected at load as a duplicate, as nginx rejects them. Before, both were
accepted and the first one written was used. A configuration that has such a pair
must keep one of them. An=location may still share its path with a prefix
location.
v0.2.0
- Added:
check_request_with, the library form ofcheck_requestthat takes an
already loadedConfig, so a caller that checks many requests loads the
configuration once. - Added: the
$query_shapelog variable and aquery_shapefield in the built-in
jsonformat. It records the names of the query parameters and the kind of each
value (p=<num>&s=<text>), never the values, so a log shows which kinds of
requests a site receives. See Logging. - Added:
vigilant-heron suggest-skip-rules <access-log>reads ajsonaccess log
and suggests skip rules for request shapes that the model judged harmless many
times, from many clients, over several days, and never blocked. It only prints the
rules for review. See
Learn skip rules from the access log. - Added: each GitHub Release has Linux binaries for x86_64 and aarch64 (glibc 2.39
or newer) and aSHA256SUMSfile. See
Download a release binary. - Fixed:
docker build -t vigilant-heron .without--targetbuilt the e2e stub
decision backend instead of the router. The router is now the Dockerfile's last
stage. - Fixed: a request path with a NUL byte could select a different
locationfor
the block response, the headers and the WebSocket tunnel than for the limits,
because the decoded path kept the NUL byte and the other readings did not. NUL
bytes are now removed from every reading, so/%00adminselects the same
location everywhere. - Fixed: the
suggest-skip-rulesreport now says that a learned rule forwards
matching requests without the model, so the model no longer judges their
headers or body. Block rules, built-in and your own, still run first.
v0.1.0
-
Added:
vigilant-heron test-request <file> [--client <address>]reports what the
router would do with one raw HTTP/1.1 request before deployment: the site, the access
list,deny_path, the rules that match, and whether the request would go to the
decision backend. It runs the server's own checks, never calls the upstream or the
backend, and does not simulatelimit_reqorlimit_conncounts. See
Check a request with test-request. -
Fixed: a stale pid file could make
-s,stop,restartandstartact on an
unrelated process. When a router died without removing its pid file and the OS
gave its pid to another process,-s stoporstopsignalled that process, and
startrefused with "already running". A running router now holds an exclusive
lock on<pid file>.lock, and those commands treat a pid file as running only
while the lock is held. A stale file is never signalled. Also, two routers with
the samepidpath no longer overwrite each other's pid file: the second exits
withanother router is using the pid file <path>. -
Changed: releases now run the tests and the advisory check (
cargo denyand
pnpm audit) in a job without write access, and the publishing job keeps no
credential in its checkout. The version tag is created through the REST API. -
Fixed: the opt-in virtual patches
vh-cve-2026-9103(/api/v1/login/auto_login)
andvh-cve-2026-8481(/api/v1/validate/code) compared the path case-sensitively,
so a request such as/API/V1/LOGIN/AUTO_LOGINgot past them. Both rules now match
the path in any letter case. Which requests they block is otherwise unchanged. -
Security: more credentials are kept out of the decision backend. Headers named
withsignature(for examplex-hub-signature-256,svix-signatureand
x-slack-signature), withticketorauthenticationas a word, or ending in
key,authkeyorsessionkey(for examplex-functions-key,x-master-key
andx-admin-key) are now dropped. Body fields withsignaturein their name are
redacted. The rules forticket,authentication
and thekeysuffix apply to header names only, so an ordinarykeyorticket
body or query field is still sent. Multipart parts whosenameis sensitive have
their values replaced, and XML elements such as<password>and<wsse:Password>
have their text replaced. Form bodies split on;as well as&, so
a=1;password=xhides the password. A file part is redacted by its field name
only. Its content is not scanned. -
Fixed: a
proxy_websockettunnel was read as if it were HTTP requests. After the
101 Switching Protocols, a tunnelled chunk of more than 8 KiB without a line
break (a binary WebSocket frame, for example) was answered with 414 and the
connection was closed, and a tunnel open longer thanclient_header_timeout
(60s by default) received a 408 while it was still active. Tunnelled bytes are
now copied as they arrive, with no header limits and no header timer. -
Fixed: a zone keyed on a long client-chosen value, such as a request header,
could use far more memory than its declared size, because each state stored the
whole key. States now store a fixed-size digest of the key, so the declared size
bounds the memory again. -
Added: decision backend
strands(decision.backend strands;) for a self-hosted
Strands Decider server, which speaks the System One protocol.modeldefaults to
strands-decider,urlis required, andapi_token_envis optional: without it,
noAuthorizationheader is sent. -
Behaviour change: a client that holds its
max_concurrency_per_clientshare of
decision calls is now answered 429 with an empty body, and the backend is not
called. Before, that excess followedon_rate_limit, which defaults toallow,
so it was forwarded without the model. The new site directiveon_client_busy allow|block;(defaultblock) controls it, andon_rate_limitno longer
affects it. Seton_client_busy allowto keep the old forwarding. Shadow mode
records the 429 as the would-have action. The decision logreasonis
client_busy_allowedorclient_busy_blocked. -
The query string sent to the decision backend is redacted. A parameter whose
decoded name is sensitive, by the same rule as body fields, or named in
redact_field, has its value replaced by[REDACTED]. Other parameters are sent
as the client sent them.;separates parameters as&does, so
password=x;id=1hides the password and sendsid=1. -
Security (F2): short sensitive parts no longer match inside ordinary names.
ssn,card,cvvandpwdnow match only as whole words, soclass_name,
business_name,process_name,address_numberanddiscardare sent to the
decision backend unredacted.card_id,user_ssn,userSSN,cvvandnew_pwd
are still redacted.creditcardandcardnumbermatch as parts of a name. -
Security (F1): the commands that signal the router (
-s,start,stopand
restart) refuse a pid file in a directory that other users can write, such as
/tmp, when another user owns the file. A pid that is not a positive decimal
number, such as0or-1, is refused with an error. Before,-1could signal
every process of the user. The host guide now writes the pid file to a relative
path,pid vigilant-heron.pid;, which is resolved from the directory the command
runs in. -
Headers with a credential-like name are no longer sent to the decision backend,
for examplex-csrf-token,x-xsrf-token,x-amz-security-token,
x-access-tokenandx-session-id.host,user-agent,accept,referer,
origin,x-forwarded-forand the other headers the model uses are still sent. -
A configuration reload keeps the decision slots. Calls admitted before a reload
still count againstmax_concurrencyandmax_concurrency_per_client, which a
reload used to exceed. The slots are reused while their limit is unchanged. A
changed limit gets new slots, and calls in flight keep the slots they hold. -
startno longer signals a process it has already reaped when the router exits
while starting. The pid of a reaped process can belong to another process. -
The pid file's temporary file and the startup log are created exclusively. A file
planted at those paths, such as a symlink in a shared directory like/tmp, is
no longer written through. -
The docs said that a request whose
Hostmatches no site is always 403. It gets
default_status(421 by default) with reasonunknown_host, unless the top-level
access list denies it. The docs now say so. The security notes also used the
wrong namefail_closed_paths; the directive isfail_closed_path. -
stopwaitsworker_shutdown_timeoutplus 5 seconds, or 35 seconds when the
directive is not set. The docs now give that figure. -
start,stopandrestartrun the router in the background on a host.start
checks the file first, refuses when a router already runs, and prints
started (pid N).stopsends the graceful shutdown and waits until the process
has exited, and printsnot runningwhen none runs.restartchecks the file
first, so an invalid file leaves the running router alone. Under Docker or
systemd, run the router in the foreground, as before. -
The shutdown signals (SIGINT, SIGTERM, SIGQUIT) are registered before the pid
file is written. A stop sent right after the pid file appears now runs the
cleanup that removes it, where it used to end the process without that cleanup. -
docs/install.mdbuilds from source and from the Dockerfile. It no longer
mentions a published image, and it has a systemd unit example. -
SECURITY.mdlists the reporting channels: email and a GitHub security advisory. -
Command line, as nginx's:
-h/--helpand-v/--version,-c <path>as a
short form of--config,-T(test, then print the configuration with every
include expanded under its# configuration file <path>:header), and
-s reload|reopen|stop|quit.-ssends SIGHUP, SIGUSR1, SIGTERM or SIGQUIT to
the pid in the file thepiddirective names. An unknown option prints the
usage and exits 1.stopandquitare both graceful in this version. -
pid <path>;directive (default/run/vigilant-heron.pid). The router writes its
process id there once it is listening and removes it on exit. A write failure is
logged and the router keeps running. SIGQUIT now shuts down gracefully, like
SIGTERM and SIGINT. -
docs/cli.mddescribes the command line, andpidis documented in
docs/configuration.md. The migration notes from the README moved to
docs/configuration.md. -
README rewritten as a short introduction with install, a quick start, the
command line, the order of checks, and links to the documentation. The
reference stays indocs/. -
Removed
rustfmt.toml, which only repeatededition = "2024"fromCargo.toml.
cargo fmtoutput is unchanged. -
enable_rule <id> ...;at the top level and in asiteturns on built-in rules
that are off by default. A built-in rule written withdefault off;runs only
where it is enabled. The nearest level that names a rule decides, a site's lists
before the top level's, anddisable_rulewins overenable_rulewithin one level.
default off;is valid only in the built-in rules. -
Opt-in virtual patches
vh-cve-2026-9103(IBM Langflow OSS
/api/v1/login/auto_login, CVE-2026-9103) andvh-cve-2026-8481(IBM Langflow OSS
POST /api/v1/validate/code, CVE-2026-8481). Both are off by default. IBM's fix
for CVE-2026-9103 is Langflow 1.10.1; for CVE-2026-8481, upgrade past 1.10.0 and
check the bulletin. The docs page "From CVE advisories to rules" describes how
to write, roll out and remove such rules, and the policy for shipped ones. -
Host matching as in nginx
server_name: exact names, `*...