nftables
The proposed way to ban IPs with nftables uses its own reaction
table.
Inside, there are two sets and two rules.
One set/rule couple is for IPv4 and the other one is for IPv6.
The IPs are banned on all ports, meaning banned IPs won't be able to connect on any service of the host.
We don't make use of
nftables
timeouts because we need reaction to handle the lifecycle of a ban. If you choose to unban withnftables
timeouts, you won't have access to all of reaction features, as it won't know what's currently banned, nor how to unban an IP: showing bans withreaction show
and unbanning withreaction flush
can't be supported.
Start/Stop
We create the table with relevant rules and filters
{
start: [
['nft', |||
table inet reaction {
set ipv4bans {
type ipv4_addr
flags interval
auto-merge
}
set ipv6bans {
type ipv6_addr
flags interval
auto-merge
}
chain input {
type filter hook input priority 0
policy accept
ip saddr @ipv4bans drop
ip6 saddr @ipv6bans drop
}
}
||| ],
],
}
We want reaction
to delete all its setup when quitting:
{
stop: [
['nft', 'delete table inet reaction'],
],
}
🚧 auto-merge has been reported not to work well with nftables < 1.0.7
Ban/Unban
IPv4
Now we can ban an IPv4 address with this command:
{
cmd: ['nft', 'add element inet reaction ipv4bans { <ipv4> }']
}
And unban the IP with this command:
{
cmd: ['nft', 'delete element inet reaction ipv4bans { <ipv4> }']
}
IPv6
IPv6 works the same way:
{
cmd: ['nft', 'add element inet reaction ipv6bans { <ipv6> }']
}
{
cmd: ['nft', 'delete element inet reaction ipv6bans { <ipv6> }']
}
IPv4/IPv6
A very small utility, nft46
, has been written to unify ipv4 and ipv6 commands:
{
cmd: ['nft46', 'add element inet reaction ipvXbans { <ip> }']
}
{
cmd: ['nft46', 'delete element inet reaction ipvXbans { <ip> }']
}
The X
in the command will be changed to 4 or 6 at runtime depending on the IP provided.
There must be a X
before the curly brackets, then this sequence: {
, at least one space, exactly one IP (v4 or v6), at least one space, a }
.
You can do it!
Wrapping this in a reusable JSONnet function
local banFor(time) = {
ban: {
cmd: ['nft46', 'add element inet reaction ipvXbans { <ip> }'],
},
unban: {
cmd: ['nft46', 'delete element inet reaction ipvXbans { <ip> }'],
after: time,
},
};
Real-world example
local banFor(time) = {
ban: {
cmd: ['nft46', 'add element inet reaction ipvXbans { <ip> }'],
},
unban: {
cmd: ['nft46', 'delete element inet reaction ipvXbans { <ip> }'],
after: time,
},
};
{
patterns: {
ip: {
regex: '...', // See patterns.md
},
},
start: [
['nft', |||
table inet reaction {
set ipv4bans {
type ipv4_addr
flags interval
auto-merge
}
set ipv6bans {
type ipv6_addr
flags interval
auto-merge
}
chain input {
type filter hook input priority 0
policy accept
ip saddr @ipv4bans drop
ip6 saddr @ipv6bans drop
}
}
||| ],
],
stop: [
['nft', 'delete table inet reaction'],
],
streams: {
// Ban hosts failing to connect via ssh
ssh: {
cmd: ['journalctl', '-fn0', '-u', 'sshd.service'],
filters: {
failedlogin: {
regex: [
@'authentication failure;.*rhost=<ip>',
@'Connection reset by authenticating user .* <ip>',
@'Failed password for .* from <ip>',
],
retry: 3,
retryperiod: '6h',
actions: banFor('48h'),
},
},
},
},
}