Fleet Bans
One agent blocking an address is an observation. Forty agents blocking the same address is a different kind of fact — and the second one is invisible to any agent looking only at its own traffic.
Fleet bans close that gap. Each agent reports what it decided to block, the platform corroborates across the deployment, and every agent pulls back the aggregated result.
Out of the box a fleet ban does not install a block. The set is consulted, the outcome counted and logged, and traffic is unaffected.
That is deliberate. Acting on what other machines concluded is a large step, and it should be justified by the false-positive rate that advisory mode exists to measure. Run it, read the numbers, then turn on enforcement for the agents where they hold up.
Two halves of one loop
| Direction | Config | Scope | |
|---|---|---|---|
| Ban ledger | Upload — what this agent decided to block | platform.ban_ledger | fleet_bans:write |
| Fleet bans | Download — what the fleet corroborated | platform.fleet_bans | fleet_bans:read |
They are separate stores with opposite provenance, and kept separate on purpose. The ledger holds first-party evidence — decisions this agent made from its own events. The fleet set holds bans this agent has no local evidence for. Merging them is how a fleet ban gets re-reported upward as fresh evidence and corroboration becomes an echo of itself.
What a fleet ban carries
| Field | |
|---|---|
agents | Distinct agents that reported banning this address |
agent_count_total | Live agents in the workspace — the denominator |
confidence | Platform confidence, 0–100 |
score | Platform score, 0–99, directly comparable to a threat-MMDB score |
first_banned_unix | When the fleet first banned this address |
expires_unix | When the ban lapses; absent means no expiry |
layers | Bitmask of the enforcing layers that reported it |
agent_count_total is the field that makes the rest readable. agents: 3 is unanimity in
a three-agent fleet and noise in a three-thousand-agent one. Any threshold you write should be
a ratio, not a count.
The layer bitmask records what kind of agreement it is — access_rules, smart_firewall,
ids, waf, threat_intel, captcha, rate_limit. Three agents agreeing via three
different layers is stronger corroboration than three agreeing via the same one, which is
usually the same rule firing three times.
The platform is not trusted
Both local safety predicates are re-applied to everything the platform serves, and anything failing either is dropped regardless of what arrived:
| Predicate | Drops |
|---|---|
| Ban-safe | Anything not unambiguously global-unicast. Fails closed. |
| Never-ban | Your own infrastructure — HOME_NET, and the node's own IPs |
The never-ban check is the one that matters most in practice. A public load balancer VIP is
global-unicast, so the first predicate would permit it; banning it blackholes every client
behind it. It is protected through HOME_NET: in Kubernetes the Synapse operator puts the VIPs
there, and elsewhere you list them in ids.address_vars.HOME_NET yourself.
The asymmetry is intentional: the agent is permissive about what it uploads — an unsafe ban is worth reporting so the fleet can see it — and strict about what it accepts.
Rejections are counted separately as rejected_unsafe, rejected_protected and
rejected_malformed, so a set that is arriving but being discarded is visible rather than
looking like an empty fleet. A malformed record is skipped and counted rather than failing the
whole payload.
Configuration
platform:
ban_ledger:
enabled: true
interval_secs: 60
fleet_bans:
enabled: true
refresh_secs: 60
advisory: true
advisory_dedup_secs: 300
enforce: false
Both inherit platform.base_url, so neither normally states a URL.
interval_secs governs how fast a ban propagates, not how much traffic this generates: a
tick whose snapshot has not changed sends nothing at all, and a forced resend happens only
every twelfth suppressed tick. refresh_secs is a cheap version check — the artifact is
downloaded only when the version changes.
The shipped configuration sets both to 60 seconds (left out, each defaults to 300), so the loop
is symmetric rather than having a slow leg. Raise refresh_secs if a large fleet makes the
version endpoint hot.
Enforcing the set
With enforce: true an agent stops only advising and blocks what the fleet corroborated:
- In the kernel (Linux, unless
firewall.mode: none). Each fleet ban goes into the same IP filter local bans use, and comes out again when its deadline passes, when the fleet withdraws it, when your own configuration starts to allow the address, or when it no longer ranks within the fleet's share. While the agent runs, a new set reaches the kernel as soon as it is downloaded, and a ban whose deadline passes comes out within about half a minute. - At the proxy. Behind a load balancer the kernel sees only the balancer's address, so a
kernel ban on the client never matches. With
proxy.protocol.enabled: trueand a balancer that sends PROXY protocol headers, the proxy knows the real client and refuses a fleet-banned one with403andX-Block-Reason: fleet_ban. It does not useX-Forwarded-Forfor this. The proxy refuses every enforceable fleet ban, whether or not the kernel holds it.
What is never enforced, whatever the set says:
- An address you allow.
access_rules.allowbeats a fleet ban, including when the allowed entry is a wider range than the banned address. - Your own infrastructure. The same never-ban checks as above — HOME_NET, which is where load-balancer VIPs belong, and the node's own IPs — and, on Unix, any address the host holds on any interface.
- An address your configuration already blocks. A configured
access_rules.blockentry covering it drops it anyway. On nftables the fleet's entry would even keep that range from being installed, so a held fleet ban a configured range comes to cover is lifted for it. - A ban without a deadline. Every fleet ban the platform publishes expires unless agents keep reporting it; a ban that does not is never installed.
- More than its share, in the kernel. Fleet bans take at most a quarter of each address
family's IP filter (
firewall.max_ip_filter_entries_v4/_v6), leaving at least three quarters for this agent's own bans and rules. When the set is larger, the most widely corroborated bans win the kernel's quarter, and the proxy still refuses the rest. The quarter is of the filter the kernel actually has: a resized filter takes effect only once the agent's pinned BPF state is removed.
A fleet ban and a local ban on the same address do not interfere: lifting either leaves the other in place.
An enforced fleet ban is never reported as this agent's own. The ban ledger leaves it out, so it is never uploaded, and the platform ignores it when corroborating. Without that, one agent's ban would come back as every other agent's evidence and never expire.
Enforcement is Linux-only in the kernel; the proxy refusal works wherever the proxy runs. Each
install is a security event with source fleet_ban and rule id fleet_bans.enforce, and so is
an address's first refusal at the proxy in each advisory_dedup_secs window (at most 50 such
events a second); every refusal is counted. Each lift is a synapse.fleet_ban.lifted
OpenTelemetry log record, which your own collector keeps (the platform stores only block
events). The synapse.fleet_bans.* metrics report the set's size and age, what is enforced,
and what was skipped and why.
Limits worth knowing
- Advisory means advisory. Consulted, counted, logged. No traffic is affected until enforcement is turned on.
- An unloaded set is not an empty one. A miss before the first set has been published means we do not know yet, not not banned. Enforcement never lifts a ban for that reason: until a set arrives, only bans past their own deadline come out.
- A stopped agent lifts nothing. While it is down, what it installed stays in the filter, past its deadline if need be; only nftables expires an entry by itself, within the hour. The agent records what it installed, and on the next start it lifts whatever the fleet withdrew or what expired in the meantime.
- Turning enforcement off takes a restart.
enforceis read at startup. The first start with it off lifts everything the agent installed. - A ban the kernel will not take waits. An address the IP filter refuses (a full filter, for one) is tried again after five minutes. After three refusals in one address family, the rest of that family waits for the next pass; bans already held, and the other family, are unaffected.
- Corroboration is not proof. Agents in one deployment often run one policy, so agreement can mean the same rule fired everywhere rather than independent confirmation. Read the layer mask before treating a count as consensus.
- It needs both scopes.
fleet_bans:writeto contribute,fleet_bans:readto receive. An agent with only read is a passenger.
Use cases
- Measure before enforcing — run advisory for a period and read the false-positive rate against your own traffic, then enforce where it holds up.
- Close the window on a scanner — once enforcing, an address the fleet agrees on is blocked on agents it has not reached yet.
- Spread a detection across a fleet — one agent seeing a scanner means every agent knows about it within about a minute.
- Corroborate a marginal signal — a source your local threshold nearly caught is easier to judge when forty other agents already did.
- Spot a bad rule — a sudden spike in reported bans across one layer is usually a rule, not an attack.
See also
- Access Rules and Smart Firewall — layers that report bans
- Threat Detection — the platform-wide feed, distinct from your own fleet's decisions
- Security Event Export — the local record of what this agent blocked
- Configuration — the full
platform.ban_ledgerandplatform.fleet_bansblocks