Shieldlist

Enrolment

Cette page n'existe qu'en anglais pour le moment.

An agent is complete on its own: it reads its logs, decides and enforces from the files under /etc/shieldlist. Enrolled to a controller it is the same program with a conversation added — its rules come from the controller, what it decides goes up, the fleet's decisions come down — and that is where the product is at its best: one place to write rules for every machine, the bans one machine decides applied by the others, the whole fleet on one screen. Standalone is a mode, not a fork: the evaluator, the enforcement and the store are the same either way.

Who manages what

On its own Enrolled
Parsers, rules, policies, report presets, trust /etc/shieldlist/{parsers,rules,policies,reports}/, trust.yaml the controller — assembled for this machine, held under /var/lib/shieldlist/received/; the local ones are not read and the journal says so
The machine's state (on / test) and level agent.yaml the controller's declaration of this machine — held in /var/lib/shieldlist/machine.yaml; agent.yaml's keys are commented out
Fleet switches (offer, enforce), who may lift, who reports, sync cadence — the controller's declaration, in machine.yaml too
Log inputs and their ports agent.yaml agent.yaml, unless the declaration names them (logs:)
Memory ceiling, log file, secrets, hooks agent.yaml, secrets.yaml, hooks/ the same — a controller can never push a hook
The machine's own exemptions exemptions.yaml, exemptions.d/, exemptions-cli.yaml the same, added to the fleet's

shield status says which side applies, in two lines:

config    controller (revision 0d6633e2761c668f, received 2026-09-13 07:47 UTC) — local rules, parsers and policies not read
state     on (controller, heard 2026-09-13T07:47:52Z)

or, on its own, config local — agent.yaml and the rules under it decide and state on (agent.yaml).

Joining

On the controller, mint a join token — at the Web UI (Servers → Add a server), with shield token, or at the controller's terminal (shieldlist-controller token). It is a secret, valid for a day and one machine by default, and it may carry the controller's certificate fingerprint (secret@fingerprint).

On the machine, one command:

shield enrol -controller https://shield.example.net:17453 <join-token>

What it does to the files, and says as it does:

  • agent.yaml gets mode: enrolled and controller: — set where they are, commented out included, appended when absent; nothing else in the file moves.
  • state: and level: in agent.yaml are commented out, their values kept in the comment (# state: detect-only # decided by the controller since this machine enrolled — shield status says what applies). From now on the controller decides both, and a line that said otherwise would only be a contradiction waiting to be believed. A file that still carries them — written before this command learned to, or edited since — is put right the same way when the agent starts, with a warning in the journal; a live state: a person writes back is said by shieldlist-agent -t and never obeyed.
  • The agent is restarted, presents the token, keeps the credential it receives in its state store, pins the fingerprint, and from then on dials the controller itself: a long-lived connection over which the controller pushes what changed the moment it changes, and a sync every thirty seconds that also uploads what this machine decided. Nothing listens on the machine; where a long connection cannot be held, the sync alone does the job.
  • The first answer brings the machine's configuration (/var/lib/shieldlist/received/) and its declaration (/var/lib/shieldlist/machine.yaml). Until it has arrived the machine watches — every rule records, nothing new is banned: arming is the controller's act, never a default's. The data plane is up meanwhile (the bans already on record stay enforced), so the controller's on arms the rules at its first sync, with no restart in between.

Enrolment needs the agent that speaks the protocol (0.0.4 and newer) and a controller 0.0.1 and newer; a pair too far apart is refused with a message that names both versions. The controller's address must be an https:// URL — the agent pins the certificate it meets and refuses to speak in the clear.

Enrolling again (a reinstall, a lost state store) works with a new join token, with one guard: while an agent under that hostname is still talking to the controller — seen in the last two minutes — the enrolment is refused (an agent by that name is active — revoke it first), so a join token alone cannot take a live machine's place. Stop the old agent and wait, or revoke it first (Servers → the machine → Revoke, or POST /v1/agents/{host}/revoke with an operator token), then enrol.

Leaving

shield leave

The machine runs on its own again, and goes on doing exactly what it did: state: and level: are written back into agent.yaml as the controller last set them (from machine.yaml; a machine that never heard from its controller keeps watching), mode: local, and the agent is restarted on its own files. The controller's bundle stays on disk under received/ and is no longer read; the rules under /etc/shieldlist/rules/ are — the command says how many there are, and says out loud when there are none (shield init writes the set the package ships with). The credential and machine.yaml are forgotten, and controller: is kept as a comment (the loader refuses a controller on a machine in local mode): enrolling again — here or elsewhere — is the one command above, which revives the line, and the machine watches until its controller has spoken. Nothing is revoked on the controller: revoke the machine there so nothing waits for it.

What comes from the controller

Parsers, rules, policies, report presets, trust and the fleet's exemptions — the tree the controller assembled for this machine at its level, with its overrides applied. It lands under /var/lib/shieldlist/received/, a directory the agent replaces whole on every new revision (a marker file, .shieldlist-received, says it is the agent's — the agent never replaces a directory it did not write). shieldlist-agent -t reads it beside your local files, so what you validate is what runs.

A new revision is applied in place: parsers, rules, policies and the decider are rebuilt between two rounds, without a restart, and read under the controller's word on this machine's state. The rules' windows start over; sanctions, the store and the kernel's sets are untouched. A revision this agent's loader refuses is kept on disk for you to look at, the previous one keeps running, and the refusal is logged and shown to the controller.

The declaration — machine.yaml — is written by the agent at every sync where something in it changed: the state, the level, whether this machine offers its bans to the fleet and enforces the fleet's, who reports its sanctions (the local reporter stands down when the controller does it), the sync cadence. It is read again at start, so a restart with the controller away begins the way the controller left things; it is not yours to edit — the controller's declaration of the machine is where these are set. The state applies at once, both ways: on arms the rules, test relaxes them, and the journal says so. The data plane's own mode — armed, or watching — is decided at start, so a machine started watching and armed since bans at the next restart; the journal says that too. shield status prints rules_armed 23/23, and the console shows Test whenever the rules record, whatever was declared.

What goes up, what comes down

Every sanction the machine decides is reported to the controller (with its rule, the threshold that fired, hits, report state), and every change to one afterwards. The fleet's sanctions come down when the machine enforces the fleet's: each passes the local exemptions and the machine's own addresses first, extends rather than duplicates an active ban, and is recorded with its origin (fleet:web01/ssh-bruteforce) so shield why says where it came from. Lifts decided on the controller — Web UI, API, shield unban — arrive within seconds and are applied like a local shield unban.

Reading the state of the conversation

shield status agent
config                controller (revision 5f3038a21eb2de3a, received 2026-09-13 07:47 UTC) — local rules, parsers and policies not read
state                 on (controller, heard 2026-09-13T07:47:52Z)
controller_version    0.1.0
controller_revision   5f3038a21eb2de3a
controller_sync       9s_ago
controller_error      …               # only when there is one

The controller's agent list shows the same from its side, plus the machine's active bans and enforcement summary. When the controller is away the agent says so once, keeps enforcing on the last configuration received, and says once more when it is back.

Every sync also compares clocks: past thirty seconds of disagreement the agent warns in its log (once, and again when it is over) and shield status shows controller_clock; the Web UI warns on its side. Fix NTP.

What travels up

On every sync the agent also sends the offenders it is counting but has not convicted — the nearest to a threshold first, two hundred at most, then the most recently hit among the rest, two hundred at most — so the Web UI shows who is knocking before the ban, the fresh ones included. An offender exempt on this machine travels marked as such (why), past its count too — it is hammering and will never be convicted; the Web UI hides those unless asked. A snapshot: the controller keeps the latest. Beside it, the hits the rules scored since the last sync, by offender — a flow the controller keeps per minute and draws over time; a sync that fails puts them back, nothing is lost. When the controller holds none of this machine's flow yet, the agent sends, once, what its windows still hold, minute by minute — the past as far as the longest window reaches.

Every sanction the machine decides, with the log lines that convicted it (bounded: twenty lines of a kilobyte), and every change to one — a return counted, a report settled, an end brought forward. When the controller holds less than the agent believes — the machine was enrolled again, or an operator asked for its history from the Web UI — the agent sends everything again from what is really held, a thousand a sync, evidence included; nothing doubles, the controller keys each sanction by the machine, its own id and its start.