flagger.toml(5)
NAME
flagger.toml - configuration file for flagger(8)
SYNOPSIS
/etc/flagger/flagger.toml
DESCRIPTION
You don't need this file. The built-in defaults run a working sshd setup, and an absent file isn't an error. You write one when you want to change how long bans last, how hard repeat offenders are hit, which networks are never touched, or where state is kept. The rules themselves live in separate files, see flagger.rules(5).
The format is TOML, and every key is optional.
What happens when I make a typo?
flagger(8) refuses to start, and tells you the file and the key. Every key is validated. An unknown key, a misspelled one, or a value that doesn't parse is a startup failure, never something quietly ignored. A ban daemon running on settings you didn't mean is worse than one that doesn't run.
Run flagger --check after every edit. It prints the resolved configuration, including what each setting actually works out to.
Where do top-level keys go?
Before the first [table]. TOML binds a bare key to the most recent table header, so a manage_rules written under [escalate] becomes escalate.manage_rules, and is rejected as unknown. This one bites everybody once.
Durations
Anywhere a duration is accepted, write a string with units: "30s", "15m", "1h", "2h30m". The units are s, m, h and d, and they can be combined. A bare number is read as seconds, so tallow(1)'s expires=3600 still means an hour. Anything over ten years is rejected.
TOP-LEVEL KEYS
expires= duration- How long a score record lives, and how long a first offense is banned for. Later offenses multiply this, see
[escalate]. Must be more than zero. Default"1h". ipv6=true|false|"auto"- Whether to create the IPv6 set and drop IPv6 traffic.
"auto"checks whether/proc/sys/net/ipv6exists. With IPv6 off, v6 addresses are still scored and logged, they just aren't blocked. Default"auto". manage_rules=true|false- Whether flagger(8) creates its nftables table at startup and removes it on exit. Set it to
falseto manage the ruleset yourself, see MANUAL FIREWALL SETUP. Defaulttrue. allow= [ cidr, ... ]Networks that are never banned. Entries are CIDR in either family, and a bare address means just that host. An address in here is never scored and never counts toward a neighbourhood.
Host bits below the prefix are masked off, and
--checkshows you the masked form: write192.168.1.0/16and you'll see192.168.0.0/16back.Default:
127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16. Setting this replaces the defaults, so include them if you still want them. An empty list is allowed, but flagger(8) warns about it.tallow(1) matched leftmost strings here, so
whitelist=10.worked. That isn't CIDR and it's rejected. Write10.0.0.0/8.
[escalate]
Why do bans get longer?
Because a one-hour ban is a coffee break for a bot. An address that keeps coming back gets a longer ban every time:
ttl(n) = min(expires * factor^(n-1), max)
With the defaults that's 1h, 2h, 4h, 8h, 16h, 1d8h, 2d16h, 5d8h, and then 7d from there on. flagger --check prints this ladder for your own settings.
factor= integer- Multiplier for each further offense.
1turns escalation off, which is tallow(1)'s flat behaviour.0is rejected. Default2. max= duration- Ceiling on an escalated ban. Must be at least
expires. Default"7d". forget= duration- How long an address has to stay quiet, counted from the end of its last ban, before its history is dropped and its ladder starts over. Default
"7d".
[subnet]
Why does my neighbour count against me?
Abuse clusters. An address that starts probing from a network that has already earned bans is rarely a coincidence. So offenders are grouped by network, and the bans already earned there count against everything else in it.
That works two ways. Scores are multiplied by 1 + neighbours * multiplier, capped at max. And the neighbour count is added to the address's rung on the escalation ladder, so a first offense next to three banned neighbours is banned as if it were a fourth.
It can never ban an address that has done nothing. The penalty is a multiplier, and any multiple of no activity is still none. All it does is make an address in a bad neighbourhood reach the threshold sooner, and stay banned longer when it gets there.
An address's own repeat offenses don't count as its own bad neighbours. A lone persistent bot climbs its own ladder, and no faster.
v4= prefix length- IPv4 grouping width,
0to32.0turns grouping off for IPv4. Default24. v6= prefix length- IPv6 grouping width,
0to128.0turns it off. Default64, because a/64is the standard single allocation. Tracking single addresses is close to useless against IPv6: an attacker holding a/64has more addresses than you will ever see, and each one looks new. multiplier= float- Score gain for each ban a neighbour has already earned.
0turns score scaling off and leaves the ladder stacking in place. Can't be negative. Default0.5. max= float- Ceiling on the multiplier. Must be at least
1.0. Default4.0.
Isn't the default a bit harsh?
Yes, on purpose. Rule scores add up within one message: an sshd root password failure matches both the general failure rule (0.2) and the root-specific one (0.5), for 0.7. One banned neighbour scales that to 1.05, so it's banned on the first message. A direct root login attempt next door to a known offender doesn't get a second chance. If you want it to get one, set multiplier = 0.25.
[state]
Why keep state at all?
Without it, an attacker resets their own escalation ladder by waiting for you to restart the service. So scores, offense counts and per-network history are written to a file, and a restart or a reboot doesn't hand every offender a clean slate. Bans still in force are pushed back into the firewall at startup, before the service reports itself ready.
The file is TOML, so you can read it. It stores time remaining rather than timestamps, so downtime is subtracted on load, and anything that ran out while flagger(8) wasn't running stays gone. A missing, corrupt or future-version file is a warning and an empty tracker. It's never a reason to refuse to start, because a host with no filtering at all is worse than one that forgot.
path= path- Where to keep it. An empty string turns persistence off entirely. Default
"/var/lib/flagger/state.toml". interval= duration- How often to write it while running, which bounds what an unclean shutdown costs you.
0writes only on exit. Default"15m". max_records= integer- Ceiling on tracked addresses, and separately on tracked networks.
0means no limit. Default25000.
How big does it get?
Retention bounds how long a record lives, not how many there are. A banned address is kept for its ban plus escalate.forget, one that was only scored for expires. With the defaults that's a window of about two weeks for banned addresses. But a hard enough scan can produce any number of distinct addresses inside one window, and that's what max_records is for.
When it's reached, the least valuable records go first: never-banned before banned, then fewer offenses, then less time left. flagger(8) logs that it has started doing so.
It's roughly 170 bytes per tracked address. The default of 25000 caps the file at about 4 MB, and its load at under two seconds. For scale, ten thousand addresses is about 1.7 MB, and a hundred thousand about 17 MB, which takes some seconds to read at startup. That's why the cap exists.
Raise it if you'd rather keep the history and pay for the size. Lower escalate.forget if you'd rather have fewer records to begin with.
[paths]
nft= path- The nft(8) binary. Default
"/usr/sbin/nft".
Where's the backend key?
There isn't one. nftables is the only backend. tallow(1) also carried firewall-cmd and iptables, but firewalld has used the nftables backend for years, and those paths were the least tested code in it.
MANUAL FIREWALL SETUP
With manage_rules = false, flagger(8) adds and removes set elements, and creates nothing. Build the table yourself before you start it, with your own expires in place of the timeouts:
nft -f - <<'EOF' add table inet flagger add set inet flagger v4 { type ipv4_addr; flags timeout; timeout 3600s; } add set inet flagger v6 { type ipv6_addr; flags timeout; timeout 3600s; } add chain inet flagger input { type filter hook input priority filter; policy accept; } add rule inet flagger input ip saddr @v4 drop add rule inet flagger input ip6 saddr @v6 drop EOF
And to remove it afterwards:
nft delete table inet flagger
Leave out the two v6 lines if IPv6 is off. This is exactly what flagger(8) sends when it manages the ruleset itself, except that it also flushes the chain first, so a restart doesn't stack up a second copy of the drop rules.
EXAMPLES
The defaults written out, plus one office address that must never be banned:
expires = "1h" ipv6 = "auto" manage_rules = true
allow = [ "127.0.0.0/8", "::1/128", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "203.0.113.7", # the office ]
[escalate] factor = 2 max = "7d" forget = "7d"
[subnet] v4 = 24 v6 = 64 multiplier = 0.5 max = 4.0
[state] path = "/var/lib/flagger/state.toml" interval = "15m"
[paths] nft = "/usr/sbin/nft"
Note that allow sits above [escalate]. Move it below and it's escalate.allow, which is rejected.
SEE ALSO
flagger(8), flagger.rules(5), nft(8)