#
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 failure limit within minutes time window, ban() is called. Events pass through regardless of failure status. The following definitions are available in addition to the standard this context:
ipset(...args): Wraps the ipset command-line utility for IP-based banning.
#
Config
Required:
failed: Return failures in the form{key: string, ts: number, limit: number}where ts is minutes since epoch. Return{key: null, ts: null, limit: null}if there is no failure.ban: Called when failure limit reached. Return true to confirm the ban.minutes: Time window for failure counting
#
Example
TODO: remove failures since handled by the plugin?
pipeline:
fail2ban:
config:
failures: {} # to avoid banning the same ip multiple times
minutes: 5
failed: !!js/function >-
function(event) {
let key
let ts
let limit
if (event.ts && event.message) {
if (!event.client) {
event.client = {}
}
if (!event.labels) {
event.labels = {}
}
ts = event.ts.unix() / 60 // convert to minutes since epoch
let match
if (event.source?.ip && event.event?.action === 'dropped' && event.event?.dataset === 'iptables') {
// blacklist anything probing the firewall too much
key = `iptables~${event.source.ip}`
limit = 10
} else if (event.process === 'sshd') {
match = event.message.match(/invalid user (?<user>\S+) (?<ip>\S+) port (?<port>\d+)/ui)
if (match) {
const {user, ip, port} = match.groups
event.client.ip = ip
event.client.port = port
event.labels.user = user
key = `ssh~${ip}`
limit = 1
}
} else if (event.client.ip && event.event?.category?.includes('web') && event.event?.type?.includes('access')) {
// assuming web access log parsed by us
const path = event.url?.path ?? ''
const status = event.http?.response?.status_code
if (path.endsWith('.env') || path.includes('\\x')) {
key = `web~${event.client.ip}`
limit = 1
} else if (status >= 400 && status < 500) {
key = `web~${event.client.ip}`
limit = status !== 403 ? 5 : 10 // 5 failures is too easy to ban legit users
}
}
if (key) {
this.config.failures[key] = event
}
}
return {key, ts, limit}
}
ban: !!js/function >-
async function ban(event, key) {
if (!this.config.failures[key]) {
return false
}
const [service, ip] = key.split('~')
this.logbus.stats({banned: 1})
const {stderr, stdout} = await this.ipset('add', '-!', `blacklist-${service}`, ip)
this.logbus.log.warn({ip, service, stderr, stdout}, 'banned')
event.tags.push('banned')
delete this.config.failures[key]
return true
}