Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 with nftables 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 with reaction show and unbanning with reaction flush can’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'),
        },
      },
    },
  },
}