iptables

The proposed way to ban IPs using iptables uses one reaction chain.

The IPs are banned on all ports, meaning banned IPs won't be able to connect on any service.

We use the ip46tables binary included alongside reaction, which permits to support both IPv4 and IPv6.

Start/Stop

We first need to create this chain on startup, and add it at the beginning of the INPUT iptables chain.

Docker & LXD users will need to add this rule to the FORWARD chain as well.

{
  start: [
    // create the `N`ew chain
    ['ip46tables', '-w', '-N', 'reaction'],
    // `I`nsert the chain at the beginning of INPUT & FORWARD
    ['ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction'],
    ['ip46tables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction'],
  ],
}

We want reaction to remove it when quitting:

{
  stop: [
    // `D`elete it from INPUT & FORWARD
    ['ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction'],
    ['ip46tables', '-w', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction'],
    // `F`lush it (delete all the items in the chain)
    ['ip46tables', '-w', '-F', 'reaction'],
    // Remove it completely
    ['ip46tables', '-w', '-X', 'reaction'],
  ],
}

Ban/Unban

Now we can ban an IP with this command:

{
  cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP']
  // or
  cmd: [ 'sh', '-c', 'ip46tables -w -A reaction -s <ip> -j DROP']
}

And unban the IP with this command:

{
  cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP']
  // or
  cmd: [ 'sh', '-c', 'ip46tables -w -D reaction -s <ip> -j DROP']
}

A good practice is to wrap the actions in a function with parameters:

local banFor(time) = {
  ban: {
    cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
  unban: {
    cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
    after: time,
  },
};

See how to merge different actions in JSONnet FAQ

Real-world example

local banFor(time) = {
  ban: {
    cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
  unban: {
    after: time,
    cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
};

{
  patterns: {
    // IPs can be IPv4 or IPv6
    // ip46tables (C program also in this repo) handles running the good commands
    ip: {
      regex: '...', // See patterns.md
    },
  },

  start: [
    ['ip46tables', '-w', '-N', 'reaction'],
    ['ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction'],
    ['ip46tables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction'],
  ],
  stop: [
    ['ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction'],
    ['ip46tables', '-w', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction'],
    ['ip46tables', '-w', '-F', 'reaction'],
    ['ip46tables', '-w', '-X', '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'),
        },
      },
    },
  },
}