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
nftablestimeouts because we need reaction to handle the lifecycle of a ban. If you choose to unban withnftablestimeouts, 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 showand unbanning withreaction flushcan’t be supported.
⚠️ There is no chain for forwarded packets, so Docker containers (for example) are unprotected! Any contribution welcome to add this forward chain here. See #85.
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
{
cmd: ['nft', 'add element inet reaction ipv4bans { <ip> }'],
ipv4only: true,
}
{
cmd: ['nft', 'add element inet reaction ipv6bans { <ip> }'],
ipv6only: true,
}
See the documentation on ipv4only and ipv6only for more details.
Wrapping this in a reusable JSONnet function
local banFor(time) = {
ban4: {
cmd: ['nft', 'add element inet reaction ipv4bans { <ip> }'],
ipv4only: true,
},
ban6: {
cmd: ['nft', 'add element inet reaction ipv6bans { <ip> }'],
ipv6only: true,
},
unban4: {
cmd: ['nft', 'delete element inet reaction ipv4bans { <ip> }'],
after: time,
ipv4only: true,
},
unban6: {
cmd: ['nft', 'delete element inet reaction ipv6bans { <ip> }'],
after: time,
ipv6only: true,
},
};
Real-world example
local banFor(time) = {
ban4: {
cmd: ['nft', 'add element inet reaction ipv4bans { <ip> }'],
ipv4only: true,
},
ban6: {
cmd: ['nft', 'add element inet reaction ipv6bans { <ip> }'],
ipv6only: true,
},
unban4: {
cmd: ['nft', 'delete element inet reaction ipv4bans { <ip> }'],
after: time,
ipv4only: true,
},
unban6: {
cmd: ['nft', 'delete element inet reaction ipv6bans { <ip> }'],
after: time,
ipv6only: true,
},
};
{
patterns: {
ip: {
type: 'ip',
},
},
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>',
// More specific auth fail
@'Failed password for .* from <ip>',
// Other auth failures
@'Connection from <ip> port [0-9]*: invalid format',
@'Invalid user .* from <ip>',
],
retry: 3,
retryperiod: '6h',
actions: banFor('48h'),
},
},
},
},
}