Installing
For the walk-through — install, first person, rules, first machine — see Getting started. This page is the reference: the package, and the same by hand on a system without it.
One static binary, one directory of files, one directory of state. No database daemon: state is SQLite inside the state directory.
Debian and derivatives — the APT repository, added on this machine (it is not there because an agent's machine has it):
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
Key fingerprint B528 0C88 7016 8A94 40A9 05C4 3BB5 8113 CFAB 6DC5;
dev in place of stable for our development builds; or
the package from the release page, one per architecture,
apt install ./shieldlist-controller_<version>_amd64.deb.
The package creates the shieldlist user, a starter tree under
/etc/shieldlist/controller (local listeners, no rules yet), installs
the unit and starts the service; then sudo -u shieldlist shieldlist-controller api-token -name you for the first operator token.
Upgrading restarts it; purging removes the tree and the store. An agent
may live on the same machine (its own package, its own files).
By hand, elsewhere:
# 1. the binary, from the release page (SHA256SUMS beside it)
install -m 0755 shieldlist-controller_linux_amd64 /usr/local/sbin/shieldlist-controller
# 2. a user, and the configuration tree it may edit
useradd --system --home /var/lib/shieldlist/controller --shell /usr/sbin/nologin shieldlist
mkdir -p /etc/shieldlist/controller/{agents,parsers,rules,policies,reports,exemptions.d}
chown -R shieldlist:shieldlist /etc/shieldlist/controller
# 3. the service
install -m 0644 shieldlist-controller.service /etc/systemd/system/
systemctl daemon-reload && systemctl enable --now shieldlist-controller
Put the fleet's rule language in the tree — the same files an agent
reads: parsers/, rules/, policies/, reports/, exemptions.yaml
— and validate it: shieldlist-controller -t -config /etc/shieldlist/controller reads the tree with the agent's own loader
and shows what every declared machine would get. Copying a working
machine's /etc/shieldlist (minus agent.yaml and secrets.yaml) is a
fine start.
Where it listens
By default the agent transport answers every address on 17453 with a
self-signed certificate the controller mints under its state directory
(agents pin its fingerprint, carried by the join token), and the API and
the console answer 127.0.0.1:17454 over plain HTTP. To reach the
console from elsewhere, put a TLS-terminating proxy on the same machine
in front of 17454 — the controller believes X-Forwarded-For and
X-Forwarded-Proto only from a loopback peer. With nginx and Let's
Encrypt:
server {
server_name controller.example.net;
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;
}
}
certbot --nginx -d controller.example.net --redirect does the rest.
Agents keep talking to 17453 directly; a machine whose egress allows
only 443 can have listen.agents bound there, and tls.cert/tls.key
name the operator's own certificate when the controller sits on a
public name.
Why the X-Forwarded-For line matters (do not skip it)
The public unblock page treats the visitor as their address: a
person unblocks only the IP their request came from, and the page's
abuse limits are counted per address. The controller learns that
address from X-Forwarded-For, and — because the header is trivially
forgeable — it believes it only from a loopback peer and takes the
last element of the list (the one the proxy in front appended).
So the proxy in front must set that last element to the real client and
never let a client-supplied value be the last one. proxy_set_header X-Forwarded-For $remote_addr; (above) does exactly that: it replaces
the header with the connecting peer, discarding anything the client
sent. $proxy_add_x_forwarded_for is also safe (it appends the real
peer at the end). What is not safe:
- passing the client's header through unchanged (e.g.
proxy_set_header X-Forwarded-For $http_x_forwarded_for;), or omitting the line while another upstream already trusts a client header — then the last element is attacker-chosen; - a second proxy or CDN hop in front of nginx: whatever that hop puts
in
X-Forwarded-Forbecomes the trailing element nginx forwards. If you run behind a CDN, terminate its forwarded-IP header explicitly and re-setX-Forwarded-Forto the real client at the nginx that talks to the controller.
Get this wrong and a visitor can set X-Forwarded-For: <someone-else> to
lift another address's ban and to spend someone else's rate budget. Get
it right — one trusted nginx on loopback that overwrites the header — and
a client cannot choose its own address. The same rule governs
X-Forwarded-Proto (used to know the request arrived over TLS).
The first person, the first machine
sudo -u shieldlist shieldlist-controller account -name you # asks for a password; -totp enrols a one-time code
sudo -u shieldlist shieldlist-controller api-token -name deploy # a token for scripts and for shield
sudo -u shieldlist shieldlist-controller token # a join token for one machine, valid a day
Console commands open the store directly, hence the service user. Then on the machine to enrol — running an agent that speaks the protocol (0.0.4 and newer):
# /etc/shieldlist/agent.yaml
mode: enrolled
controller: https://controller.example.net:17453
systemctl restart shieldlist-agent
shield enrol <join-token>
Within seconds the machine appears in the console with the revision it
holds; the file agents/<hostname>.yaml is optional — a machine without
one runs the defaults.
Reloading
Edit any file, then systemctl reload shieldlist-controller (SIGHUP),
shield reload, or the console's Reload and push: the tree is loaded
whole or refused whole with every error, and a new revision reaches
every connected machine at once. Edits made through the console, the
API or shield are edits of the same files, so a git diff in the tree
shows them like anyone else's.