10 Enrolment
Robin edited this page 2026-08-22 23:15:50 +00:00

Enrolment

An agent is complete on its own. Enrolled to a controller it stays the same program: it keeps reading, deciding and enforcing locally, and adds a conversation — its configuration comes from the controller, what it decides goes up, the fleet's decisions come down. Standalone is a mode, not a fork: agent.yaml, secrets.yaml, your hooks and your own exemptions do not change when a machine enrols.

Joining

On the controller, mint a join token — at the console (PeopleMint a join token), 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:

# /etc/shieldlist/agent.yaml
mode: enrolled
controller: https://controller.example.net:17453
systemctl restart shieldlist-agent      # mode is read at start
shield enrol <join-token>

The agent 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. 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 (POST /v1/agents/{host}/revoke with an operator token), then enrol.

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. 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.

Also from the controller, per machine: whether this machine offers its bans to the fleet and enforces the fleet's, who may lift its bans, who reports its sanctions (the local reporter stands down when the controller does it), and test — which relaxes the machine and never arms it.

What stays yours

agent.yaml (mode, controller, state, level, log inputs, limits, reporting), secrets.yaml, hooks, and exemptions.yaml, exemptions.d/, exemptions-cli.yaml — which add to the fleet's. A hook can never be pushed by a controller.

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 — console, API, shield unban — arrive within seconds and are applied like a local shield unban.

Reading the state of the conversation

shield status agent
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 console 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 console 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 console 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 console — 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.