Abuse reporting
The agent can submit the offenders it bans to AbuseIPDB, under your name and your API key. It is off unless you arm it, an observation window never reports anybody, and what a report says is a data file you own — not strings inside the program.
Arming it takes three pieces
Nothing leaves until all three exist. A machine can hold two of them for weeks — that is preparation, not a mistake.
1. The switch, in agent.yaml. provider is the only thing that
turns reporting on:
reporting:
provider: abuseipdb
2. The credential, in secrets.yaml (mode 0600, never in version
control, never in a deploy script):
abuseipdb:
key: your-api-key
daily_limit: 1000
# enabled: false — keep the key, send nothing (absent means on)
enabled: false switches reporting off without losing the key — the
agent sends nothing and shield report refuses, until it is removed or
set back to true.
daily_limit is your plan — 1000 free, 10000 basic, 50000 premium — or
deliberately less than it, which is how one account is shared
between machines: a box allowed fifty a day stops at fifty however much
the account still holds. auto learns the plan from the provider's
first answer and follows an upgrade by itself.
3. A report preset, in reports/, named by each rule that reports:
# reports/ssh-intrusion.yaml
report: ssh-intrusion
categories: [brute-force, ssh]
says: "Unauthorised SSH login attempt on a key-only host"
include: []
# rules/my-ssh.yaml — the rule opts in
report: ssh-intrusion
What a comment says
The rule: the reported party must see why they were banned — and never how it was measured, and never anyone else's names.
- Banned for their User-Agent → the User-Agent is in the report.
- Banned for junk requests → at least one full example (method, path, query).
- Banned for SSH user enumeration → the usernames tried are NOT.
- Banned for failed mail logins → the mailboxes tried are NOT.
- Never a hit count or a window: attackers read their own reports, and "20 hits in 1m" hands over the exact rate to stay under.
- Never the server's own configuration: "on a key-only host" told every reported attacker how that host authenticates. The comment describes the offender's behaviour — nothing about the machine, its policies or its defences.
The shape is one sample per included field with a distinct count:
Web application probing | method: GET | path: /wp-admin/css/ (+11 more) | ua: Mozilla/5.0 …
What can never leave
include is a whitelist and an empty one is normal. Only the fields it
names may appear in the report's comment. Everything else the parser
captured — usernames tried, mailbox names, which of your sites was hit,
the raw log line — stays on the machine. The validator refuses a preset
that names a field its rule never captures, so a typo cannot silently
turn into an always-empty redaction.
The comment is composed at the moment the ban is decided and stored
with the sanction, so shield why <address> shows you the exact text
before or after it leaves.
One chance per report
A report is sent at the moment of the detection it describes, or not at all. If it cannot leave — allowance spent, provider down, key refused — the agent logs the reason on the report's own line and moves on; the sanction stays on record, still owed. There is no retry cadence and no automatic backlog drain: an agent that drains a backlog by itself is describing last week as if it were happening now, and spending your allowance without asking.
The log reads like this, one line per attempt:
INFO my-ssh reported 203.0.113.9 score=100 remaining=49381
INFO my-ssh already-reported 203.0.113.7 detail="You can only report the same IP address once in 15 minutes."
INFO my-ssh not-reported 203.0.113.5 reason="allowance spent"
The report follows its ban within a second: the decision loop nudges
the sender the moment a round's bans are durable; the interval in
the reporting block is only a fallback tick.
A wrong or revoked key stops the sender for the rest of the run — a key
does not start working by itself. Fix it, restart, and use
shield report for what accumulated meanwhile.
Sending the past, on purpose
$ shield report -last 6h
217 offenders owed a report in the last 6h
send? [y/N]
You name the window; there is no default. The count is shown before
anything leaves so you can weigh it against the day's allowance and
abstain. -n 100 caps it, -y skips the question, and the freshest go
first — providers discount a report that arrives long after the events
it describes. The run stops by itself when the provider says the
allowance is spent, refuses the key, or cannot be reached.
Watching it
$ shield status reports
reports_owed 217
reports_sent 1043
reports_duplicate 12
duplicate means somebody on the same account — another of your
machines, or you — reported that address within the provider's
fifteen-minute window. It is not a failure; the report exists.
The allowance is watched from both ends: your daily_limit on the
agent's own count, and the provider's rate-limit headers on every
response. The daily reset is midnight UTC. While the provider says the
day is spent, one probe every five minutes notices an upgraded plan or
another machine freeing room — twelve requests an hour, not a hammer.
What it never does
- Report in test. What was not enforced is not submitted.
- Report a range. Only single addresses are sent.
- Slow down a ban. The sender runs apart from the decision path; a provider that is slow or down costs reports, never enforcement.
- Compose from the raw line. Only named fields, only whitelisted ones.
Enrolled: the controller may report for you
Enrolled with a controller, who reports is the controller's word — the
machine's declaration says reporting: agent (this machine, with the
key in its own secrets.yaml — everything above), controller (the
controller reports on the machine's behalf, with its own key and
allowance, the same state machine and the same presets: this machine's
sender pauses and its rows are settled there — the console says
"reported by the controller"), or off. Detect-only never reports,
whoever would. See the controller wiki,
Configuration → Who reports.
Your own endpoints: hooks
Beyond AbuseIPDB, hooks: in agent.yaml POSTs one JSON document per
enforced ban to any URL you name — a chat relay, a SIEM, your own
dashboard. Hooks are local configuration by design: a controller can
never push one. Each delivery gets one chance, right behind the ban,
and its outcome is one log line (hook, hook-failed, hook-refused
— named by the endpoint's host, never the full URL, which may carry
your token). The body carries the ban's facts and the same default-deny
comment a report would — never a raw log line. Reference in
CONFIG.md.
Watching it all from shield status: reports_owed / reports_sent /
reports_duplicate (and reports_refused when it is not zero) are in
the default output, published by the agent — plus a single WARN if owed
ever piles past ten thousand with a provider armed.