Getting started
Install
On Debian and its derivatives, the APT repository:
curl -fsSL https://repo.lrob.net/apt/shieldlist/shieldlist.gpg -o /usr/share/keyrings/shieldlist.gpg
echo "deb [signed-by=/usr/share/keyrings/shieldlist.gpg] https://repo.lrob.net/apt/shieldlist stable main" > /etc/apt/sources.list.d/shieldlist.list
apt update && apt install shieldlist-agent
The repository is signed with the Shieldlist key, fingerprint
B528 0C88 7016 8A94 40A9 05C4 3BB5 8113 CFAB 6DC5 (gpg --show-keys shieldlist.gpg); dev in
place of stable gets prereleases and development builds. The same .deb
files sit on the release page, one per architecture, for apt install ./file.deb.
The package puts the binaries under /usr/sbin, installs the unit, a
starter configuration that enforces nothing when there is none, and
enables the service — its own and nothing else: it does not start
nftables.service (on Debian that is nft -f /etc/nftables.conf,
which begins with flush ruleset and would wipe Docker's NAT and every
other firewall rule). The agent owns one table, inet shieldlist, and
touches nothing outside it. Elsewhere, the installer:
curl -fsSLO https://git.lrob.net/Shieldlist/shieldlist-agent/raw/branch/main/scripts/install.sh
less install.sh # one file of sh — read it before running it
sh install.sh
It works out this machine's architecture, downloads that build from the
latest release, checks it against the release's SHA256SUMS, installs
shieldlist-agent and shield into /usr/local/sbin, installs the
systemd unit, and validates before starting anything.
Re-run it to upgrade. It replaces the binaries, touches no configuration, and unbans nothing: the sets and their timeouts live in the kernel, so a restart is a gap in detection, not an opening.
Releases are not signed yet. A checksum served from the same host as the binary proves the download was not corrupted and nothing more.
Requirements
- Linux with nftables, and the
nftbinary on the path. The installer says so if it is missing; the agent will detect without it and enforce nothing. - Root — for programming the kernel's tables, and for reading logs it does not own.
journalctl, only if you watch journald units.
Building instead
git clone ssh://git@git.lrob.net:2222/Shieldlist/shieldlist-agent.git
cd shieldlist-agent && make build
install -m 0755 dist/shieldlist-agent dist/shield /usr/local/sbin/
make dist cross-compiles the whole release matrix — amd64, arm64,
armv7, riscv64 — and writes SHA256SUMS beside it. No cgo anywhere, so
there is no toolchain to install.
It enforces nothing yet
On a machine with no /etc/shieldlist, the installer writes a starter
configuration that watches sshd and is in test:
state: test
Every rule decides and records exactly as it would otherwise, and the data plane is never touched — no nftables table, no restore, not one element. That is deliberate. Nobody points a new blocker at a production machine and hopes: run it for a day beside fail2ban, CrowdSec or whatever already guards the box, and read what it would have done.
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
shield status answers enforcement DETECT-ONLY before any count, and
while the agent is in that mode the figures are the records it made
rather than the bans it holds.
Exempt yourself before going further, in
/etc/shieldlist/exemptions.yaml:
- ip: 203.0.113.7
note: the office
- range: 2001:db8:1234::/48
note: home
An exemption removes the sanction, never the observation: the traffic is
still parsed, still counted, still visible in shield why.
When the decisions look right, set state: on, validate, restart.
Nothing else changes.
What it watches
/etc/shieldlist/agent.yaml lists the log inputs:
logs:
- unit: ssh.service # a journald unit…
log: ssh
ports: [22]
- path: /var/log/nginx/access.log # …or a file…
log: http
- path: /var/www/vhosts/system/*/logs/proxy_access_ssl_log # …or many
log: http
A path may be a pattern, and on a machine hosting sites it should be: a panel gives every site its own log directory, and sites are added by people who are not thinking about this agent. The pattern is re-read every fifteen seconds, so a site created this afternoon is watched this afternoon.
category is what ties an input to its parsers. Read a category from
one kind of source only — a file and a journald unit means every line
counted twice.
Check before running
shieldlist-agent -t
It validates everything and reports every problem at once, each with its file and line, then exits, changing nothing. Run it after every edit. It also answers three questions that only this machine can:
shieldlist-agent: configuration in /etc/shieldlist is valid —
3 parsers, 1 rule, 1 policy, 0 trust entries, 2 exemptions, 3 log inputs
test: every rule records what it would do, nothing is enforced
/var/www/vhosts/system/*/logs/proxy_access_ssl_log matches 23 files
enforcement: this kernel takes the agent's table
- what each pattern matches here, now — the moment to find out that a path is wrong is before the agent starts, not in a log nobody reads;
- whether a category connects nothing to nothing — a log input no parser reads, or a parser nothing feeds, both of which are silent;
- whether this kernel will take the table. The agent builds its own table under a throwaway name, with no hook so nothing is evaluated against a packet, puts elements in it, and removes it again. A kernel that refuses something says so here rather than during an attack.
Run
systemctl enable --now shieldlist-agent
journalctl -u shieldlist-agent -f
shield status
A starting agent says what it loaded:
2026-08-14 17:08:57 INFO shieldlist-agent version=v0.0.1 config=/etc/shieldlist
2026-08-14 17:08:57 INFO loaded parsers=3 rules=1 policies=1 inputs="0 files, 1 journald"
2026-08-14 17:08:57 WARN test: every rule records what it would do, nothing is enforced
2026-08-14 17:08:57 INFO memory ceiling limit=956.6MB source="25% of machine memory"
and then very little, on purpose. A healthy agent is quiet: it speaks
when it decides something and when something is wrong. The log reflects
decisions, not traffic — a flood of a million lines produces a few
hundred lines of log, and the live view is shield status.
Before you trust a rule
Do not arm a rule and hope. Ask what it would have done to the logs you already have:
shieldlist-agent -dry-run 24h
shieldlist-agent -dry-run 24h -rule my-new-rule
It reads the last day, runs the rules over it, prints every crossing with the lines that caused it, and enforces nothing — judging by window and ban duration, so a crossing counts only if the sanction it earns would still be in force. See Replay.
Note that a dry run reports crossings, not decisions: it does not apply exemptions or trust, so an address you have exempted can appear in its output and still be spared when the agent runs.
Where next
- Configuration — every file and every key
- Writing rules — parsers, thresholds, weights, policies
- Commands — the full CLI
- Enforcement — how a ban actually stops traffic
- Sizing and limits — what it costs the machine
- Troubleshooting — when something looks wrong