The Ur Project

Documenting the craft of stacking things so they remain stacked. For a very long time.

flagger(8)

NAME

flagger - ban hosts that abuse services, by watching the systemd journal

SYNOPSIS

flagger [--config PATH] [--rules-dir DIR]... [--dry-run]

flagger --check [--config PATH] [--rules-dir DIR]...

flagger --test LINE [--rules-dir DIR]...

DESCRIPTION

Put sshd on a public address and within minutes something is guessing passwords at it. It won't get in if your passwords are any good, but it fills your journal, it burns CPU on key exchanges, and it never stops. flagger makes it stop, for a while.

It reads the systemd journal, picks out the messages that say somebody is up to no good, and once an address has done enough of that, it drops every packet from it in the firewall. The journal does the filtering, regular expressions do the matching, and nftables does the blocking. That's the whole design, and it's the same one tallow(1) had. flagger is a rewrite of it.

How does it decide?

Each rule file names a service and a list of patterns. A message from that service that matches a pattern gives up an IP address, and that address gets the rule's score added to its running total. When the total reaches 1.0, the address is banned: it goes into an nftables set, and packets from it are dropped until the set entry expires.

More than one rule can match a message, and then all of their scores count. An sshd root password failure matches both the general failure rule and the root-specific one, and that's on purpose.

A rule can also carry a ban. That drops the address for a short fixed time on every match, without touching the score. tallow(1) called this a throttle. It's for messages that are suspicious on their own but not conclusive.

The rule format is in flagger.rules(5).

Doesn't it just come back an hour later?

Usually, yes. So repeat offenders get longer bans each time. The first ban lasts expires, and each one after that is escalate.factor times longer, up to escalate.max. An address is forgotten escalate.forget after its last ban ended, and then its ladder starts from the bottom again.

Offenders are also grouped by network: an IPv4 /24 and an IPv6 /64 by default. Bans already earned in a network make every other address in it score higher, and push that address further up its own ladder. A first offense next to three banned neighbours is banned as if it were a fourth offense.

That never bans an address that has done nothing itself. The penalty is a multiplier, and any multiple of no activity is still none. See [subnet] in flagger.toml(5).

Both the ladder and the neighbourhood history are kept in a state file, so a restart or a reboot doesn't hand every offender a clean slate.

What does it do to my firewall?

It creates one nftables table and manages it for as long as the daemon runs:

table inet flagger chain input -- type filter hook input priority filter; policy accept set v4 -- ipv4_addr, with timeouts set v6 -- ipv6_addr, with timeouts

Two rules in the chain drop traffic whose source address is in the matching set. A banned address is a set element with its own timeout, so the kernel expires it, not flagger.

The table is created at startup and deleted on a clean shutdown. If you'd rather own the ruleset yourself, set manage_rules = false in flagger.toml(5). That page lists the exact commands to build it by hand.

flagger needs CAP_NET_ADMIN and an nft binary, and nothing else that's privileged. The shipped unit drops everything else. If nft doesn't work at startup, the daemon refuses to start rather than run without teeth.

OPTIONS

With no mode flag, flagger runs as the daemon. The two modes exist so you can work on rules and configuration without root, without the journal, and without touching the firewall.

--check
Parse flagger.toml(5) and every rule file, print the resolved configuration and the rule inventory, and exit. It exits non-zero if anything fails to parse, or if no rules were loaded at all. Run this after every edit, before you restart the service.
--test LINE
Run LINE past every loaded rule, whatever service the rule is for, and report each one that matches: its score, its ban, the address it pulled out, and the pattern as written in its file. See EXIT STATUS.
--config PATH
Read the configuration from PATH instead of /etc/flagger/flagger.toml. A missing default config isn't an error, you just get the built-in defaults. A missing PATH that you named here is an error, because you asked for it by name.
--rules-dir DIR
Read rules from DIR. Can be repeated. Directories are read in order, and a file name in a later directory replaces the same file name from an earlier one. Passing this at all replaces the default search path entirely, and a directory you name here has to exist.
--dry-run
Score messages and log every decision, but never create firewall rules, ban anything, or tear anything down. The state file is still read and written. Use this to find out what flagger would have done to your users before you let it do it.
-V, --version
Print the version and exit.
-h, --help
Print usage and exit.

SIGNALS

SIGTERM, SIGINT
Write the state file, remove the firewall table created at startup, and exit. The bans leave the firewall with it, but they come back from the state file when flagger starts again.
SIGUSR1
Write the tracking table to the journal: every address being watched, its score, its offense count, how long any ban has left, and the neighbourhood it's in, followed by the ban count of each tracked network. Unlike tallow(1), this works in ordinary builds.

EXIT STATUS

In daemon and --check mode, 0 on success and 1 on failure.

--test has three outcomes, so you can script a rule test suite around it:

0
A rule matched and pulled out a valid address.
1
No rule matched the line. That's an answer, not necessarily a fault.
2
A rule matched, but what it captured isn't an IP address. That's a bug in the rule: its pattern is matching text it shouldn't.

FILES

/etc/flagger/flagger.toml
Configuration. Optional. See flagger.toml(5).
/etc/flagger/rules.d/*.toml
Your own rules, and overrides of shipped ones by file name.
/usr/share/flagger/rules.d/*.toml
Rules shipped with the package. Don't edit these. A file with the same name under /etc replaces one here entirely.
/var/lib/flagger/state.toml
Scores, offense counts and per-network history. Written on exit and periodically while running. Losing it costs accuracy, not correctness.

EXAMPLES

Check the configuration after editing it:

flagger --check

Find out whether a log line would be caught, and by which rule:

$ flagger --test 'Invalid user admin from 192.0.2.10 port 55232' sshd.toml:3 score 0.2 ban 10s ip 192.0.2.10 Invalid user .* from %ip% port \d+ 1 of 12 rules matched

The :3 is the rule's position in its file, counting [[rule]] blocks from 1.

Watch what it would do for a day, without doing any of it:

flagger --dry-run

Ask a running daemon what it's tracking:

systemctl kill -s USR1 flagger journalctl -u flagger -n 50

BUGS

flagger is NOT A SECURITY SOLUTION. It cuts down log clutter and the resources wasted on hostile traffic, and it pays for that by sometimes denying access to valid users. It does nothing about a password guess that succeeds.

Think about what it is: a program whose entire job is to create a denial of service. Put it on a host whose real users connect from lots of changing addresses, and sooner or later it will lock one of them out. A mistyped username is enough. Put the addresses that must never be banned in allow in flagger.toml(5), and run with --dry-run before you trust it with a service that matters.

SEE ALSO

flagger.toml(5), flagger.rules(5), nft(8), systemd.journal-fields(7), sd-journal(3), systemd-journald.service(8)