57 API
Robin edited this page 2026-10-16 17:35:43 +00:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Web UI; 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 Web UI settings — saved views, theme, display (time zone, ban colour, the feed’s cadence and rows, the side panel) — 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 any of {agents: [...]}, {packs: [...]} — the machines running those packs — and {groups: [...]} — the machines in those groups or below them, resolved against the declarations as they stand), 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 | offenders, offenders?, block?, allow?, duration?} create a list (offenders {within, addresses, share} with asn: the network's offending prefixes — the ones its page lists — kept up to date; without asn, every network's, narrowed by countries/except_countries (the banned addresses') and asns/except_asns; several lists may judge one network; POST /v1/lists/preview {asn?, offenders} resolves them now — count prefixes, networks, attackers banned over the window, covered inside them, blocked_addresses, and every prefix (up to 5 000) with its attackers, share and, for a global list, asn/as_name) (the name defaults to as<n> / the country code / offenders); POST /v1/lists/preview {asn | country | prefixes} resolves a subject first — prefix count, addresses, a sample, the suggested name. GET /v1/lists/{name} of an offenders list adds parts (the prefixes standing: since, attackers, share, asn, reported_at) and past (the last hundred that left, with until)
PATCH /v1/lists/{name} {description?, block?, allow?, duration?, prefixes?, offenders?} (?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)
POST /v1/fleet/repair audit the fleet's sharing: for every machine that enforces it, the running shared sanctions it holds no copy of — lost on the way, or short because a longer end at the origin never followed — are sent again at its next syncs, each once (a lift on that machine stands); runs apart, the answer says it started, the count per machine goes to the log and the audit trail (fleet-repair); one at a time. A longer end at the origin is sent again on its own, without asking
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 Web UI's chart. since (RFC 3339, inside the window) answers the tail alone, on the same grid: the Web UI 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 Web UI 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, Web UI's file, a machine's declaration) and whether the Web UI may remove it; remove one the Web UI 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, user= and target= by one value or several comma-separated (any of a name's values, every name matched), rules=a,b by exact rules — each list keeps its own true counts while its filter narrows the rest
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= and target= keep those whose lines named one of the values (comma-separated; every name given must match), rules=a,b keeps these exact rules, 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/hits?agent=&value=&rule=&country=&asn=&field=target:…&since=&until=&regime=prod|test&exempt=1&evidence=1&limit= the hits HISTORY over a window — the below-threshold episodes the agents reported, kept past the live snapshot (a past window reads this, a live one /v1/watched; the shape matches so both render the same way): one row per continuous presence of an address under a rule (first, last, peak count of need, targets, test, exempt with the reason, gone_at once it left), lines with evidence=1 (open episodes' lines are read live); since/until RFC 3339 select by first appearance, regime= keeps one regime's episodes, exempt= as on /v1/watched — absent hides the exempt ones, except when value= asks for one address, whose episodes always show
?delta= on GET /v1/sanctions and GET /v1/watched differential sync for a live view: delta=1 answers in full and issues a delta_token; sending that token back answers only changed rows, removed keys and the full order of keys — the client rebuilds the list locally and prunes what fell out. A token answers one exact question for one credential, once, for two minutes: anything else falls back to the full answer with a fresh token
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); no fleet data, so an owner token with activity: read may read it too
GET /v1/networks/{asn}/owner, PATCH /v1/networks/{asn}/owner {reporting?, format?, contact?, note?, block?} the network's abuse desk and reporting: contact (the address the last report used, or the operator's), setting, blocks (every block a report went to, each with its desk, source, bounce state) and desks (how many differ — a wholesale network has one per customer), the recent reports and answers. PATCH with contact sets the operator's address for the whole network (empty: the registry's again); with block (a prefix) and contact, for that block alone — which wins over the network's; reporting on/off/"" and format xarf/text/"" per network
GET /v1/networks/{asn}/offenders the offenders lists blocking prefixes of the network right now — each list (own: the network's, else a global one), where it applies, and its parts inside the network with since, attackers, share, reported_at; the network's own lists holding nothing yet are listed too. What its page says beside the whole-network ban
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/sanctions/export?<the listing's filters>&format=csv txt
`GET /v1/sanctions/summary?<the listing's filters>&step=auto 1h
`GET /v1/networks/{asn}/report?since=48h 7d
GET /v1/me, PATCH /v1/me {email?, password?} who is asking — role admin (everything; called operator before 2026-09-03, still read) or owner (one client's scope and permissions, listed in the answer); a person changes their own password and email — an API token has neither
GET /v1/owners, POST /v1/owners {name, note?, permissions}, GET/PATCH/DELETE /v1/owners/{name}, POST/DELETE /v1/owners/{name}/resources {kind?, value}, GET /v1/permissions the clients (admin only): each holds machines and one permission per area — activity/logs (read), sanctions/exemptions/fleet (write), rules (read | switch | write); an account or token created with owner: holds exactly that. An owner credential reaches only the endpoints that apply its scope (default-deny); its lifts, bans and exemptions must name one of its machines and never widen
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=&before=&who=&action=&q=&since=&until=, GET /v1/audit/facets the trail, newest first, a page at a time: before=<id> continues below the last line shown, who and action match exactly, q is looked for in who, target and detail, since/until (RFC 3339) bound the time; facets lists every action and every actor with counts, and the total
POST /v1/join-tokens {uses?, expires?} a token for a new machine
GET|POST /v1/tokens, DELETE /v1/tokens/{name}, DELETE /v1/tokens/{name}?purge=1 API tokens: revoke one, or forget a revoked or expired one for good (a live one is refused — revoke it first)
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://shield.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
shield ban ADDR [-agent H] [-for 30d] | exemptions | unexempt ADDR | hits | watched
shield lists | list NAME [refresh|delete] | requests | request ID approve|deny | reports | report ID [handled]
shield owners | owner … | accounts | account … | api-tokens | api-token … | join-tokens
shield me | vitals | stats | config | maintenance | prune | vacuum | repair | updates | geoip
shield settings [AREA [key=value…]] | file PATH [-put [-validate]] | packs | parsers | policies | presets | templates
shield -json <verb>            # the answer as it came, for a script

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