fail2ban

The fail2ban utility provided the inspiration for this plugin. However, this approach isn't limited by simple string matching and supports various actions. It monitors events for failures using failed(). When a key exceeds its threshold within window seconds, ban() is called. Events always pass through downstream regardless of failure status.

Once a key is banned (i.e. ban() returns a non-null event), it is recorded permanently for the lifetime of the pipeline. Subsequent events matching that key pass through unchanged without calling ban() again.

The sandbox provides this.execute(key) which runs logbus-fail2ban <key> as a subprocess and returns the exit code (0 = success).

Config

Required:

  • failed: JS function (event) => { key, ts, limit } | null. Return an object if the event represents a failure; return null (or any falsy value) to ignore it. All three fields are required — if any field is missing or the wrong type the object is treated as a non-failure and a warning is printed to stderr.
    • key: string that groups related failures (e.g. "ssh~1.2.3.4")
    • ts: event timestamp in milliseconds — used for sliding-window eviction so out-of-order or replayed events are handled correctly
    • limit: number of failures within window before ban() is called for this key. Set per event so different failure types can have different sensitivity (e.g. limit: 1 for SSH probes, limit: 10 for iptables hits)
  • ban: JS function (event, key) => event | null. Called when a key exceeds its limit. Return the (possibly mutated) event to permanently ban the key and pass it downstream. Return null if the ban could not be applied (e.g. this.execute() failed) — the original event passes through unchanged, the key stays unbanned, and ban() will be called again on future failures.

Optional:

  • window: Sliding time window in seconds (default: 60). Eviction uses the ts values returned by failed(), not wall clock, so out-of-order or replayed events are handled correctly.

Example

Per-key limits allow for different ban sensitivities (e.g. by process):

pipeline:
  fail2ban:
    config:
      window: 300
      failed: |
        (event) => {
          if (event.process === 'sshd') {
            const m = event.message.match(/invalid user \S+ (?<ip>\S+)/u);
            if (m) return { key: `ssh~${m.groups.ip}`, ts: event.ts, limit: 1 };
          }
          if (event.source && event.event && event.event.dataset === 'iptables')
            return { key: `iptables~${event.source.ip}`, ts: event.ts, limit: 10 };
          return null;
        }
      ban: |
        function(event, key) {
          if (this.execute(key) !== 0) return null;
          event._banned = true;
          return event;
        }