-
v0.0.9 Stable
released this
2026-08-26 21:35:03 +02:00 | 1 commits to main since this releaseShieldlist Agent 0.0.9
What it is
The agent is what actually protects a machine: it tails its logs, scores what it reads against your rules, and blocks the offenders in the kernel — by itself, with no database and no daemon beside it. It works alone with local files, and it can be enrolled with a controller to be driven with the rest of a fleet.
What you gain over 0.0.3
Mostly speed and steadiness: a large ban set no longer costs you the loop, a flood no longer stalls detection while the firewall is being written, and a restart's restore takes seconds instead of minutes.
Then what came with the fleet: enrolled mode, a simpler configuration format, rules that count by site or by origin, lists that block whole networks, and a test mode that decides like production and bans nobody.
Everything that changed
Faster, and steadier under attack
- Single addresses live in plain firewall sets and only ranges in interval sets: listing an interval set dumps all of it before any change, so with a hundred thousand bans every call cost the best part of a second, stalled each round and turned a restore into minutes.
- A refused batch is retried without the element the kernel names, then bisected — never one process per element — and the reason it was refused is logged.
- Orders and lifts from a controller are applied as one pass: one transaction, one firewall batch, one log line. Thousands applied one at a time used to freeze evaluation for minutes.
- The sync sends what changed instead of scanning the store, and the machine's own counters are published rather than recomputed.
- A query that walked the whole sanctions table on every sync was split into two indexed halves merged in order.
- The bans in force come from the published aggregates, not a table scan on every sync.
WatchKeepin two passes: count every group, build lines and progress only for the survivors.- The control loop's questions are answered the moment they arrive, not on the next snapshot.
- A permanent ban's timeout is written in days — the kernel refuses nine digits of seconds, and long bans were silently refused.
- Lifting keeps a kernel element another live sanction still needs; every value entering the store is canonical, so two rows can never fight over one element.
- The unit asks for a CPU weight instead of a hard quota: a quota rationed the restore and the enforcement exactly under attack.
- The restore says where its time went — store, kernel, waiting for the loop.
- One named drop counter read by reset, instead of listings that dump the sets.
- Aho-Corasick prefilters for the rules whose patterns are long lists of literals.
Detection
- Hits survive a restart: the rules' windows are snapshotted to disk and come back, and they cross an in-place reload.
{target}in a log input's path names the site every line of that file is about — a field, a filter and a facet, without allocating; a fixed file can declare its target.- Rules count by origin:
countries,asnsand their exceptions, with the geo databases pulled from the controller or named locally. targets/except_targets: count only what was aimed at a site, or everything but.- A severer rule's conviction extends and renames an existing sanction instead of stacking a second one.
tighten: a returning offender needs less evidence per past episode.- Evidence lines keep what the parser captured, by name — the username tried, the path asked for, the site aimed at.
- Lines two web servers write for one request are counted once.
- A hit is a line some rule scores against the offender: a successful login weighed against a ban is not counted as one.
- Several thresholds per rule, and a verdict restarts every threshold's counter for that offender.
- The machine is never an offender: loopback and its own interface addresses are exempt in the core, and a non-routable address is never reported.
Enrolled mode
shield enrol <token>attaches the machine: the bundle is validated before it is swapped in and reloaded in place — the pipeline is rebuilt between rounds, the store and the kernel untouched.- A received configuration that no longer loads is set aside at start; the machine runs its own files until a good one arrives.
- The sync carries what it decided, who it is counting and how close they are, its running view (memory, lines and hits per minute, pending verdicts, shed, errors) and its clock.
- It is nudged the moment a round bans or hits, so the console shows this second's decisions, and falls back to its interval on a quiet machine.
- Suspects travel as a delta, with their lines; a suspect new to the list sends its last lines whole, so the controller always holds lines for what it shows.
- Bans the operator orders are applied like any sanction — same checks, same enforcement, recorded as manual with the reason as evidence, reported when a preset is named.
- Blocked lists arrive as data and load into their own firewall sets off the main loop, carved around the exemptions;
shield whyandshield statussay when an address is covered by one. - The controller's log inputs replace the local ones; exemptions by country or AS are assembled for the machine.
- Detect-only from the controller is remembered across a restart, and arming says when a restart is needed.
- Nothing executable ever comes down that link: a controller sends configuration, never code.
The configuration made simple
- One rule, one file, five blocks; parsers named or written inline; thresholds as
5/10m, as a list, or scaled by level (instant, strict, standard, lenient). - Packs group rules;
-convertrewrites a 0.0.x tree once. state: teston a rule or a machine: decides like production, bans nothing.share: nokeeps a rule's bans off the fleet.- An old-format file is named once and dropped, not half-validated.
Reporting
- The comment ends with the episode's minute, first report and renewal alike, so a repeat offence is actually stored by the provider.
- Sends may run in parallel; an unreachable provider is no longer read as a spent daily allowance; reporting can be switched off without losing the key.
- The rule is unchanged: one chance at the moment of the decision, and
shield report -last <duration>is the manual catch-up.
Commands and diagnostics
shield status <key>prints one value alone;new_todaycounts from midnight in the machine's own zone.shield tailfollows decisions and their evidence live;shield whyexplains one address;shield rangesshows the busiest buckets.SHIELDLIST_DEBUG=1writes a debug log with each sync's cost;SHIELDLIST_PPROF=<addr>opens Go's profiler on a loopback address.- Per-parser and per-rule cost sampling, published in health and shown by
shield stats— a rule at zero hits is visible.
Operations
- Debian packages for amd64, arm64, armhf and riscv64 from our APT repository; a hardened unit; logrotate; man pages; third-party licence notices.
- The unit never wants
nftables.service: Debian's own unit flushes the whole ruleset when it starts, taking every other firewall's rules with it.
Security
- A hardening pass on the geolocation database reader against crafted files.
https://required for a controller address.- The packaged unit no longer blocks
shield exempt.
Early days
Formats can still move between releases and an upgrade can ask for a re-enrolment. Start on test servers, keep what protects you today, and tell us what breaks.
Credits
Geolocation: DB-IP Lite (CC BY 4.0). Go modules: THIRD_PARTY_LICENSES. Detection rules translated from public sources keep their attribution in the rule files.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.0.3 Pre-release
released this
2026-08-15 16:07:48 +02:00 | 121 commits to main since this releaseShieldlist agent v0.0.3 — the reporting release. The agent can now tell the world about the offenders it bans, watch itself do it, and post its decisions to your own endpoints, while several sharp edges found by running it armed on a production host were filed off.
Abuse reporting (AbuseIPDB)
- The agent submits the offenders it bans to AbuseIPDB under your API key. Off unless you arm it:
reporting.providerin agent.yaml is the only switch, the key and yourdaily_limitlive in secrets.yaml (0600), and what each report says is a preset file you own —reports/*.yaml, referenced by a rule'sreport_as:. An agent in detect-only never reports anybody. - Redaction is default-deny: a preset's
include:list is the only way a parsed field can leave the machine, the comment is composed the moment the ban is decided and stored with it (shield whyshows the exact text), and the validator refuses a preset naming a field its rule never captures. A comment describes the offender's behaviour — one sample per included field with a distinct count — and never your thresholds, your server's configuration, or anyone else's usernames and mailboxes. - A report gets one chance, at the moment of the detection, and it leaves on the ban's heels: the decision loop nudges the sender as soon as a round's bans are durable. Whatever stops a report — allowance spent, provider down, key refused — the reason is one log line and the sanction stays on record, still owed. Nothing is retried on a cadence and no backlog drains by itself.
shield report -last 6hsends the past on purpose: it counts what is owed in the window you name, shows the estimate, asks, then submits freshest first at full speed, stopping cleanly when the provider says the allowance is spent. The allowance is watched from both ends — your daily_limit binds absolutely, the provider's rate-limit headers are read on every response, the reset is midnight UTC, and a bounded probe notices an upgraded plan or another machine freeing room.- Addresses that are not publicly routable are never submitted, and the machine's own addresses are never offenders in the first place (see below).
Outbound hooks
hooks:in agent.yaml POSTs one JSON document per enforced ban to any URL you name — a chat relay, a SIEM, your own dashboard — carrying the ban's facts and the same default-deny comment a report would, never a raw log line. Hooks are local configuration by invariant: a controller can never push one. One delivery attempt each, right behind the ban, outcome logged, nothing in detect-only.
Watching it: shield tail, and a store that counts returns
shield tailfollows decisions live, each with the log lines that convicted it, plusagainlines when a sanctioned offender keeps coming back,reportedlines when the report leaves (with the provider's abuse score), and the provider's own words when it refuses. One terminal shows the whole story.- Sanctions now carry
hitsandlast seen(schema 11, migrated automatically): every absorbed return is counted, evidenced (bounded per sanction), and said — an address that keeps coming back is visible coming back.shield bansgrows HITS, LAST and REPORTED columns;shield whytells each sanction's full story including its report's fate;last seenis the log's clock, not the verdict's. shield statusanswers more without costing more: reporting counters (owed, sent, duplicate, refused) and the exemption count in the default output, published by the agent's own aggregates pass — plus every loaded rule listed, the zero-ban ones included, because a rule that never fires should be visibly at zero, not absent. Reports piling past ten thousand owed is one edge-triggered WARN.
The machine itself is never an offender
- Loopback and the machine's own interface addresses cannot be sanctioned by any rule, built into the core rather than left to an exemption file — no configuration of this program can firewall the machine it runs on. Loopback events are dropped at evaluation, before any state exists: a local service authenticating against another (a webmail against its IMAP server) is plumbing, not an attack, and the real client is in the web application's own log.
Faster list parsers
prefilter_any:— a parser may now gate on a list of tokens instead of a single literal, answered by a multi-literal automaton in under a microsecond. This is what makes big list parsers affordable: a 683-entry User-Agent list that cost ~920µs per line ungated now costs ~1µs on the lines it ignores, a measured ~3300× improvement. Exactly one gate per parser, and the gate only ever admits lines the pattern then judges.
Also
- Log lines about reports read like the decision lines they follow — rule, action, bare copyable address — and every egress outcome is one line with the provider's exact words where they matter.
- The systemd-less quick start, examples and wiki are updated; docs/CONFIG.md gains the full reporting, hooks and prefilter_any reference.
Upgrading from v0.0.2: replace the binaries and restart; the store migrates itself (schema 9 → 11). The reporting block, presets and hooks are opt-in — nothing changes until you write them.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The agent submits the offenders it bans to AbuseIPDB under your API key. Off unless you arm it:
-
v0.0.2 Pre-release
released this
2026-08-15 00:30:20 +02:00 | 151 commits to main since this releaseThree fixes from putting v0.0.1 on a real, busy production machine — a
hosting node with a database, twenty PHP pools and twenty-three sites.
Every one of them was invisible on a quiet test box.The agent's memory ceiling no longer follows the machine
The agent used to subtract a reserve from the machine's currently
available memory, so that it would "give room back" as the protected
service grew. On a machine that genuinely uses its memory it did the
opposite of its intent: a nightly job pushed available memory under the
reserve, and the agent's budget collapsed from 929 446 buffered events
to 16 384 — while the process was holding 16 MB. It shed a capacity it
had never taken, because of what something else was doing at midnight.limits.memoryis now a fixed ceiling and the whole of the policy.
The agent readsMemTotal— enough to resolve25%— and nothing else.
It does not watch free memory and does not shrink when something else
grows.A program whose behaviour depends on the rest of the box is one whose
behaviour cannot be reproduced, and one that competes with every other
program for the same slack. Tell this one what it may have; dividing the
rest is the administrator's job, andMemoryMaxin the unit is the tool
for it.Exemptions were being silently discarded
loadListdecoded straight into the destination slice, and yaml.v3
replaces a slice rather than appending to it. So the second
exemption file read discarded the first: the momentshield exempt
createdexemptions-cli.yaml, every exemption written by hand stopped
existing — an operator's own infrastructure included — and nothing said
so. The count inshieldlist-agent -twas the only trace, and only if
you knew what it should have been.If you have ever run
shield exempt, check the count:shieldlist-agent -texemptions.d/
The useful exemption lists are generated: the ranges Google and Bing
publish run to a couple of thousand entries and are refreshed from a
source. Pasted intoexemptions.yamlthey bury the dozen a person
maintains, and the next refresh has to preserve them./etc/shieldlist/ exemptions.yaml yours exemptions.d/*.yaml one file per generated source exemptions-cli.yaml what `shield exempt` wroteAll of them load, and each entry remembers which file it came from.
The agent writes its own log
Under systemd the log goes to the journal, which is right — but
/var/logis the first place anyone looks after starting a security
daemon, and finding nothing there reads as "it did not start".log_file: /var/log/shieldlist/agent.logSet in the shipped configuration, with the same lines as
journalctl -u shieldlist-agent. The lines written before the configuration was read
are replayed into it, so it holds the whole run. No logrotate snippet
ships with it: the file is identified by (device, inode) like every
other file the agent follows, so a rename is noticed and the file
reopened. The unit gainsLogsDirectory=shieldlist, without which
ProtectSystem=strictleaves/var/logunwritable.Upgrading
install.shagain — it replaces the binaries and touches no
configuration. Which means an existing install needslog_fileadded by
hand if you want the file.Still pre-1.0 and still unsigned: the checksums prove the download was
not corrupted and nothing more.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
v0.0.1 Pre-release
released this
2026-08-14 19:11:16 +02:00 | 153 commits to main since this releaseThe first real release of the standalone agent.
It tails the log inputs you declare, evaluates your rules, applies
sanctions to the machine's own nftables, and keeps every decision in a
local store that survives restarts. One binary, plain YAML, no database
daemon, no external service on the machine it protects.shieldships
alongside forstatus,bans,why,unban,exemptandreplay.Install
curl -fsSLO https://git.lrob.net/Shieldlist/shieldlist-agent/releases/download/v0.0.1/install.sh less install.sh # one file of sh — read it before running it sh install.shIt enforces nothing until you say so
A fresh install writes a starter configuration in
detect-only: every
rule records what it would have done, and nftables is never touched.
Run it beside fail2ban, CrowdSec or whatever already guards the machine,
read a day of it —shield status # what it would be holding shield bans # which addresses, by which rule shield why 203.0.113.9 # the log lines that convicted one of them— then set
state: onin/etc/shieldlist/agent.yamland restart.Exempt yourself first, in
/etc/shieldlist/exemptions.yaml.What is in it
- Detection: literal-prefiltered RE2 parsers, rules that score
weighted evidence across parsers within a window, negative weights so
good traffic can exculpate,distinctcounting,group_by ipand
range:/n. - Sanctions: duration ladders that climb only when an offender
returns after a ban ran out, port scopes per rung, decay measured from
a ban's end, exemptions (withuntil:), trust weights on origins. - Enforcement: one
inettable, prerouting,ct direction original
so machine-initiated replies are spared; persist-first, so losing the
agent never unbans; a restart rebuilds the data plane from the store
in the background while the agent already watches. - Reading the past:
shieldlist-agent -dry-run 24h [-rule NAME]
judges by window and ban duration — a crossing counts only if the
sanction it earns would still be in force now. - Bounds: a memory ceiling the agent holds itself to (half the
machine by default), measured per-rule and per-parser cost, ordered
shedding, and figures for all of it inshield stats.
Proven on a 2-core VPS: ~68k lines/s at one core, and 5.1M distinct
addresses banned without losing one. What nothing can bound — nftables
set memory is kernel memory — is written down indocs/RESOURCES.md.It checks the kernel before trusting it
At startup the agent builds its own table under a throwaway name, with
no hook — so nothing in it is ever evaluated against a packet — puts
elements in it with timeouts and concatenated address·port keys, and
removes it again. What it proves is what the agent will actually ask
for, because the probe renders the same script.A kernel that refuses
gc-intervalgets a warning and a working agent;
an expired element stops matching traffic whether or not it has been
collected. A kernel that refuses anything else stops the agent at
startup with one sentence naming what it refused, rather than a parse
error out of a child process in the middle of an attack.
shieldlist-agent -truns the probe too — that is where an operator
asks whether a machine can enforce, and the only place an agent in
detect-onlywould ever find out.Verified on Debian 12 (kernel 6.1, nft 1.0.6) and Debian 13 (kernel
6.12, nft 1.1.3).Its log is where you will look for it
Under systemd the log goes to the journal, which is right — but
/var/logis the first place anyone looks after starting a security
daemon, and fail2ban and CrowdSec both write a file there. So does this
now:log_file: /var/log/shieldlist/agent.log, set in the shipped
configuration, with the same lines asjournalctl -u shieldlist-agent.No logrotate snippet ships with it. The agent identifies the file by
(device, inode) like every other file it follows, notices when logrotate
has renamed it away, and reopens.Pre-1.0, and honest about it
- Releases are not signed. The checksums are served from the same
host as the binaries: they prove the download was not corrupted, and
nothing more. Signing is an open decision (SPEC §11). - The configuration format may still change.
- Shipped rules cover sshd only. Rules for web, mail and panels are
being written;docs/CONFIG.mdand the wiki's Writing rules page
are how to write your own meanwhile. - The central controller does not exist yet. Standalone is a mode, not a
fork: the same files carry over when it does.
Requires Linux with nftables. Builds for amd64, arm64, armv7 and
riscv64; no cgo.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Detection: literal-prefiltered RE2 parsers, rules that score