34 API
Robin edited this page 2026-08-24 10:08:43 +00:00

API

JSON over HTTP on the API listener (127.0.0.1:17454 by default), authenticated by a token: Authorization: Bearer <token>. shieldlist-controller api-token -name NAME mints the first one at the console; POST /v1/tokens the next. Every write is audited under the token's name and is a write to the configuration files, reloaded whole or reverted with the loader's errors as the answer.

Endpoint What
GET /v1/version the controller's version and the oldest agent it serves
GET /v1/agents, GET /v1/agents/{host} the enrolled machines: versions, seen (and from which address), revision held and wanted, switches, enforcement summary, active bans — plus where the geo databases place the last address (country, city, lat/lon with the city database), which is where the map draws the server
GET|PATCH /v1/agents/{host}/declaration agents/<host>.yaml: level, state, lift, reporting, offer, enforce, description, packs: [{name, state, level}], logs: [{path|unit, log, ports}], rules: {name: {level|threshold, state, ban} | {} to add | null to drop} — created when absent
GET /v1/agents/{host}/rules what the machine runs: each rule with the packs that brought it (or added), its threshold there and why, its state, its outcome
GET /v1/packs, POST /v1/packs {name, description, rules}, PATCH /v1/packs/{name} {description, rules}, DELETE /v1/packs/{name} the packs — lists of rules — with the servers given each; a pack cannot go while a server has it
GET /v1/rules, POST /v1/rules {name, dir, category, description, log, parsers, threshold, ban, notify, report, count, distinct, state, share}, PATCH /v1/rules/{name}, DELETE /v1/rules/{name} the rules, each with its parsers ({parser, weight} or {regex, prefilter, inline}), threshold, ban, notify, report, count, state, share, the packs listing it, the servers running it. parsers replaces the list; ?preview=1 answers {path, before, after} without writing
GET /v1/parsers, POST /v1/parsers {name, dir, log, prefilter | prefilter_any, regex, time_field, time_format}, PATCH|DELETE /v1/parsers/{name}, POST /v1/parsers/test {regex, prefilter, prefilter_any, lines} the parsers (inline ones marked with their rule — edit the rule); the test answers each line gated, matched, with the fields captured
GET /v1/policies, POST /v1/policies {name, durations, ports (all | service | "80, 443"), memory, tighten, duration_start, reputation ("90/30d")}, PATCH|DELETE /v1/policies/{name} the ban policies, with the rules using each
GET /v1/reports, POST /v1/reports {name, to, when, categories, says, include}, PATCH|DELETE /v1/reports/{name} the report presets (the built-in default editable)
GET|PUT /v1/files/{path} one file of the tree whole; ?validate=1 on PUT tries it through the loader and puts it back
GET /v1/sanctions?state=&agent=&rule=&q=&country=&asn=&since=&until=&limit=&offset=&evidence=1&copies= the fleet's sanctions, newest first, bounded (limit ≤ 1000); asn takes one network or several comma-separated; state is active (banned now), bans (enforced, in force or not), recorded (detections), expired, lifted or all; rule contains; q is one search box — an address or its start, a rule, a server, a country code, an AS by number or name; country/asn filter on the origin; since/until are RFC 3339 on the start; evidence=1 adds each sanction's convicting lines (what the agent kept: 20 lines of 1 KiB at most). Each sanction carries country, asn, as_name when geolocation is configured. The fleet's copies — a sanction applied on a machine because a pack-mate decided it, rule fleet:<origin>/<rule> — are left out unless copies=include (beside the decisions) or copies=only; the facets and series follow the same switch
GET /v1/sanctions/facets?…&n=&hits=1 under the same filters, the top countries, networks, rules and usernames ({countries: [{key, n}], ases: [{key, label, n}], rules, users}, n ≤ 300); with hits=1 the offenders being counted (/v1/watched, the same filters) are tallied in — h on each row, rows sorted by n + h, hits the number of them — one answer to render a panel from
GET /v1/me/prefs, PUT /v1/me/prefs the person's own console settings — saved views, theme — kept with the account (a token has none); PUT writes only the keys sent
GET /v1/geoip what geolocation runs (databases, built date, source, the attribution due; with DB-IP: the edition held, when it was fetched, how the lists follow it)
PATCH /v1/geoip `{city: true
POST /v1/geoip/refresh fetch the month's DB-IP editions now and re-resolve the lists against them (the operator's own files do not refresh here)
POST /v1/agents/{host}/resync the machine sends its whole history again from its next sync, evidence included, a thousand a time — a machine enrolled again does so on its own
POST /v1/agents/{host}/revoke ends the agent's credential: its syncs are refused, and a new enrolment under that hostname is allowed at once (otherwise an enrolment under the name of an agent seen in the last two minutes is refused with 409 — a join token alone must not take a live machine's place)
GET /v1/lists, GET /v1/lists/{name} the lists (lists/.yaml): what each is made of (asn, country, prefixes), its block and allow scopes ({fleet: true} or {agents: […]}), duration, and where its block stands — entries resolved, orders placed, error when it cannot apply; one list adds a sample of its prefixes
POST /v1/lists {name?, description?, asn | country | prefixes, block?, allow?, duration?} create a list (the name defaults to as<n> / the country code); POST /v1/lists/preview {asn | country | prefixes} resolves a subject first — prefix count, addresses, a sample, the suggested name
PATCH /v1/lists/{name} {description?, block?, allow?, duration?, prefixes?} (?block=none, ?allow=none to clear a role), DELETE /v1/lists/{name} edit or remove a list; the blocks follow within seconds
GET /v1/stats?days=30&top=50 bans by country, asn, rule, agent over the window: key, label, total, one figure a day (from the daily tallies kept at ingestion; memoised a minute)
GET /v1/fleet the fleet's figures in one: banned_addresses (distinct addresses banned right now, each counted once however many machines apply its ban), banned_sanctions (the sum over the machines), servers, list_blocks (distinct prefixes the lists block), exempted (addresses and ranges never sanctioned: files, sources, lists)
GET /v1/sanctions/{id} one sanction by its fleet sequence, lines included, and facts: what its lines named, by field (the first value of each)
GET /v1/offenders/{address} banned where, by which rule, since when, until when — the whole history, lines included
POST /v1/lifts {value, agent?, reason?, withdraw_report?} lift wherever active, or on one machine; the agent applies it within seconds. withdraw_report asks the machine to withdraw what it reported about the address (AbuseIPDB clear-address: every report of the account about it)
POST /v1/exemptions {ip|range, for?, note?, lift?, withdraw_report?} written to exemptions-cli.yaml, pushed everywhere; lift also lifts the bans in force wherever they are
GET /v1/sanctions/series?…&window=24h&step=30m&since=… under the same filters, counts per step over the window (bans and detections apart) — the console's chart. since (RFC 3339, inside the window) answers the tail alone, on the same grid: the console asks for the whole once, then the last steps every tick
POST /v1/bans {value, agent?, duration?, reason?, report_as?, lines?} a ban by hand — one machine, or (no agent) the fleet: one order, applied by every machine at its next sync and by every machine enrolled later at its first, with what is left of the duration; 6h, 1y, permanent; through the usual checks (exemptions), recorded as rule manual with the reason as evidence, reported to AbuseIPDB when a preset is named. lines ([{at, raw, fields?}], 20 at most) are the detection's log lines the ban is given from: they become the sanction's evidence everywhere, and their fields are what the report says — only those the preset includes, as the rule's own report would. The console fills them in and presets the rule's duration and preset. A lift without agent cancels the fleet order too
GET /v1/bans the bans by hand still standing (orders not cancelled): value, agent (empty: the fleet), by, duration, reason, report preset, created — what Lists → Banned by hand shows
GET /v1/presets the report presets (name, categories)
GET /v1/reporting, PATCH /v1/reporting {key?, daily_limit?, enabled?} the controller's AbuseIPDB reporting: the key (hinted, never returned), the daily limit (auto learns the plan from the first answer; a number of your own may sit below it), the on/off switch (enabled: false keeps the key and sends nothing), who it reports for, and the day's two counts apart — what this controller sent (allowance.sent_today) and what the provider says is left on the whole account (allowance.provider_remaining, every reporter on the key counted)
GET /v1/exemptions, DELETE /v1/exemptions {value, agent?} every exemption in force with its origin (fleet file, source, console's file, a machine's declaration) and whether the console may remove it; remove one the console wrote
POST /v1/exemptions {…, agent?} with agent: one machine's own exemption, written to its declaration
PATCH /v1/sources {enable?, disable?, refresh?}, POST /v1/sources/custom {name, url|urls, format, note}, DELETE /v1/sources/custom/{name} sources on and off, the refresh interval, lists of your own — edits of controller.yaml, reloaded whole or reverted
GET /v1/sanctions/facets?…&facet=user&facet=path besides countries and networks: what the convicting lines named, per capture asked for (fields.user, …); field=user:admin filters the listing by one
GET /v1/maintenance, POST /v1/maintenance/prune {evidence_older_than?, samples_older_than?, sanctions_older_than?}, POST /v1/maintenance/vacuum what the store holds and the retention that applies; flush (retention's spans by default, now for everything); compact
GET /v1/updates the latest releases the forge publishes and whether this controller is behind
GET /v1/watched?agent=&q=&value=&country=&asn=&user=&exclude=&exempt=1&detect=&evidence=0&gone=1&limit= the offenders the agents are counting but have not convicted (what fail2ban shows as "found"): server, rule, threshold, count of need, first/last seen, origin (country, asn, city, lat/lon with the city database — the same shape the sanctions carry), the lines so far — as of each agent's last sync, a snapshot never stored; user= keeps those whose lines named that username, exclude= leaves rules out, exempt=1 shows the exempt ones too (exempt=only those alone), evidence=0 leaves the lines out, gone=1 appends the suspects that left the lists within the hour (gone_at set, gone count) — what was counted and came to nothing — and gone=only shows those alone
GET /v1/networks?q= autonomous systems by name or number (the biggest first; limit) — what the list dialog's picker asks
GET /v1/networks/{asn} an autonomous system from the ASN database: name, every prefix, and what they add up to (ipv4_addresses, ipv6_networks as /64s)
GET /v1/networks/{asn}/activity?since=30d&addr=, GET /v1/addresses/{addr}/network?since= what the network did over a window (24h, 7d, 30d, an RFC 3339 time, all): sanctions, distinct addresses, hits, bans in force, and counting_addresses/counting_hits — the addresses the servers are counting right now and have not banned (the rules' own windows plus the hour after; slow probing spread across a big network shows there) — the same per prefix of the network, plus the count of prefixes never seen (quiet_prefixes). With an address: containing (the prefix holding it) and address (its own line). The rules' own decisions only: the fleet's copies and bans of whole prefixes by hand are left out; sampled says the window held more than 200 000 sanctions. The origin and address dialogs are built on it
GET /v1/me, PATCH /v1/me {email?, password?} who is asking; a person changes their own password and email — an API token has neither
GET /v1/config, POST /v1/config/reload, POST /v1/config/validate the tree, reload it, or only check it
GET /v1/sources, POST /v1/sources/{name|all}/refresh published exemption sources
GET /v1/audit?limit= the trail
POST /v1/join-tokens {uses?, expires?} a token for a new machine
GET|POST /v1/tokens, DELETE /v1/tokens/{name} API tokens
GET|POST /v1/accounts, POST /v1/accounts/{name}/password, POST|DELETE /v1/accounts/{name}/totp, DELETE /v1/accounts/{name} people

Roles: every token today is an operator's; owner-scoped tokens come with the owner model. Refused tokens are rate-limited per address.

shield as the client

export SHIELD_CONTROLLER=https://controller.example.net SHIELD_TOKEN=…
shield agents | agent HOST | set HOST key=value… | bans | why ADDR
shield unban ADDR [-agent HOST] [-reason WHY] [-withdraw-report] | exempt ADDR [-for 30d] [-note WHY] [-lift [-withdraw-report]]
shield rules | rule NAME key=value… | reload | validate | audit | token | sources | refresh NAME

Same binary as on the machines; without -controller it reads the local agent. See the agent wiki's Commands.