Getting started
The controller runs on one machine — usually not one it protects, though it may share a machine with an agent — and the agents connect to it. This page takes an empty machine to a Web UI with its first enrolled server.
Install
On Debian and its derivatives, the APT repository (the same one as the agent's; add it on this machine too, it is not there by itself):
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-controller
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 sits on the release
page, one per architecture, for apt install ./file.deb.
The package creates the shieldlist system user, puts the free rule
library under /usr/share/shieldlist-controller, writes a starter
tree under /etc/shieldlist/controller when there is none (local
listeners), installs the unit and starts the service. Elsewhere, or without packages, see
Installing — the binary, the user, the tree, the unit, by
hand.
Requirements
- Linux with systemd; one static binary, no database daemon (the
store is SQLite in
/var/lib/shieldlist/controller). - A name and a certificate for the Web UI, if anyone opens it from
elsewhere: the API and Web UI listen on
127.0.0.1:17454and expect a reverse proxy with TLS in front (below). - Port 17453 reachable by the agents — the controller mints its own certificate for it, agents pin its fingerprint; nothing else to set up.
Where it listens
/etc/shieldlist/controller/controller.yaml:
listen:
agents: ":17453" # the agents; TLS the controller signs itself
api: 127.0.0.1:17454 # the API and the Web UI; put nginx in front
nginx and certbot's nginx plugin first, if they are not there —
apt install nginx certbot python3-certbot-nginx on Debian and Ubuntu,
dnf install nginx certbot python3-certbot-nginx on Red Hat and its
family. Without the plugin, --nginx is not a flag certbot knows.
Then, on the same machine — in
/etc/nginx/sites-available/shield.example.net.conf on Debian and
Ubuntu, /etc/nginx/conf.d/shield.example.net.conf on Red Hat and its
family:
server {
server_name shield.example.net;
listen 80;
listen [::]:80;
location / {
proxy_pass http://127.0.0.1:17454;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
# Debian and Ubuntu: the symlink is what enables the site
ln -s /etc/nginx/sites-available/shield.example.net.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d shield.example.net --redirect
certbot adds the certificate, the listen 443 ssl lines and the
redirection from 80 in place, and renews on its own; the name must
already resolve here, since it proves you own it over port 80.
The controller believes X-Forwarded-* from a loopback peer only.
The first person
shieldlist-controller account -name you # asks for a password twice
Twelve characters at least, and there is a reason it is strict: an account is never locked out, however many times its password is refused. Locking accounts would hand an attacker a way to lock you out; the refusals are logged instead, and a rule of your own can ban whoever collects them. A short password with no lockout behind it is the one combination that does not work.
Sign in at the Web UI with that name and password. Later, under
Settings → Access, add people; a one-time code can be enrolled per
account. For scripts and for shield from your desk:
shieldlist-controller api-token -name deploy
Run them as root: they open the controller's store directly, and the binary drops to the service user itself, so what it creates belongs to the service and not to root.
The rules it has to give
The tree is an agent's tree — parsers/, rules/, policies/,
reports/ (the library: every parser, rule, policy and preset,
once), exemptions.yaml at the root (what every machine gets),
packs/<name>.yaml for each pack (a named list of rules — what a
machine is given) — plus controller.yaml and one
agents/<hostname>.yaml per machine: its packs, what makes it differ.
The package provides the free library under
/usr/share/shieldlist-controller, read with the tree: openssh,
postfix-dovecot, bind, mariadb, web-essentials (the web rules
that convict on the request itself) and shieldlist-console (the
console's own). Rules → Packs shows them; every rule says there what it
blocks, what it applies to, what it needs and what to watch out for. An
upgrade replaces the library — that is how a corrected rule reaches
you — and touches nothing in the tree; a file under the same name in
the tree replaces the package's. Add your own from the Web UI (Rules →
New pack…, then rules and parsers in it), or copy a working machine's
/etc/shieldlist language into the tree (minus agent.yaml and
secrets.yaml, which stay on the machine). Then:
shieldlist-controller -t # validates the tree AND every machine's assembly
systemctl reload shieldlist-controller # SIGHUP: loaded whole or refused whole, pushed to the agents
-t says, per declared machine, what it would receive and where it
differs from the defaults. A machine runs the packs its declaration
names — give it packs in the Web UI (Servers → the server → Rules) or
packs: [...] in its file; a machine with no declaration runs the packs
controller.yaml names, the shipped ones to begin with, in test.
See Configuration for packs, levels, overrides and
per-machine switches.
shieldlist-controller init writes the library from the binary where no
package put it, and a controller.yaml when there is none; it never
replaces a file.
Enrol the first machine
On the controller — or in the Web UI, Servers → Add a server:
shieldlist-controller token # a join token: one use, valid a day
On the machine, running the agent (0.0.4 or newer; 0.1.0 for the command):
shield enrol -controller https://shield.example.net:17453 <join-token>
Within seconds the machine appears under Servers with the revision it
holds, its bans and detections start arriving, and shield status on
the machine shows controller_sync. It runs the packs its declaration
names — or the shipped ones — in test until you arm it (Servers →
the server, two clicks): every rule records, nothing new is banned.
The machine keeps its own agent.yaml, secrets and local exemptions;
the rule language, the state and the level now come from the
controller. Back on its own at any time: shield leave — the local
files were never touched.
Then
- The Web UI: the feed, the map, the panel, bans by hand.
- Configuration: what each machine runs (levels, overrides, offer/enforce, who may lift), published exemption sources, geolocation, retention, updates.
- The API, and
shield -controller URL -token Tfor the fleet from any machine.
Report offenders to AbuseIPDB
Reporting is one switch and one key: Settings → Reporting → AbuseIPDB in the Web UI
(or abuseipdb: in the controller's secrets.yaml — key,
daily_limit, enabled). The controller reports for every server whose
declaration says reporting: controller (the default); a server saying
reporting: agent reports with the key in its own secrets.yaml.
- One report per ban, composed from the rule's preset — categories and named fields only, never the raw log line. Recidivism is reported once per episode. Test bans are never reported.
- The daily limit is your plan's: leave it on
autoand the first answer of the day teaches it, or set a number of your own — deliberately below the plan when several machines share one account. - Two counts on the page, kept apart on purpose: what this controller sent today, and what AbuseIPDB says is left on the whole account. On a shared account the second moves faster than the first — every machine and tool reporting with the key draws on it.
- Off keeps the key and sends nothing until it is turned back on. A
server reporting with its own key follows its own
secrets.yaml(enabled: falsethere).