원문 캡처 · github.com
원문 캡처 · github.com

fail2ban-notifier 저장소는 Fail2Ban에서 IP가 차단되거나 차단 해제될 때 여러 서비스로 알림을 보내는 오픈소스 도구다. Discord, Slack, Microsoft Teams, Telegram, 이메일, PagerDuty와 사용자 지정 웹훅을 지원하며, GeoIP 정보 조회와 알림 재시도 기능도 제공한다.

주요 기능과 설치

fail2ban-notifier는 Fail2Ban의 action 시스템에 연결해 차단·차단 해제 알림을 전달한다. 알림 서비스별 메시지 템플릿을 바꿀 수 있고, 설정된 커넥터를 자동으로 찾아내며, 스크립트나 HTTP 웹훅으로 새 커넥터를 추가할 수 있다. 실패한 알림을 위한 재시도 기능도 포함한다.

GeoIP 조회는 기본적으로 HTTPS를 사용하는 ipwho.is를 이용한다. 오프라인 MaxMind GeoLite2를 선택할 수도 있으며, 기존 ip-api.com 연동은 명시적으로 동의한 경우에만 사용할 수 있다. 조회 결과는 프로세스가 끝난 뒤에도 유지되는 캐시에 저장된다.

Linux에서는 다음 명령으로 설치할 수 있다. 설치 스크립트는 GitHub에서 최신 릴리스 태그를 가져오고, 시스템 아키텍처(amd64, arm64, 386, armv7)를 확인한 뒤 사전 빌드 바이너리를 내려받는다. 이어 바이너리와 설정 파일, 커넥터 스크립트를 설치하고 설정을 초기화한다.

sh
curl -sSL https://raw.githubusercontent.com/eyeskiller/fail2ban-notifier/refs/heads/main/install.sh | sudo bash

설치 과정에서 바이너리는 /usr/local/bin/fail2ban-notify에, 설정 파일은 /etc/fail2ban/action.d/에, 커넥터 스크립트는 /etc/fail2ban/connectors/에 복사된다. 설정 파일은 /etc/fail2ban/fail2ban-notify.json에 초기화되고, GeoIP 캐시 디렉터리 /var/cache/fail2ban-notifier도 만들어진다. 사전 빌드 바이너리는 Linux만 지원한다. 다른 운영체제에서는 소스에서 make build && sudo make install로 빌드해야 한다.

설치 후 대화형 설정 마법사를 실행하면 필요한 설정을 안내받을 수 있다.

sh
sudo fail2ban-notify -setup

마법사는 Fail2Ban 설치 여부와 권한을 확인하고, notify.conf action 설정을 설치하며, 애플리케이션 설정을 만든다. GeoIP 제공자(ipwho.is, GeoLite2, 또는 명시적으로 동의한 기존 ip-api.com)를 선택하고 Discord, Slack, Telegram, PagerDuty 등의 커넥터를 설정한 뒤 샘플 데이터로 시험한다. 마지막으로 Fail2Ban jail에 notify action을 추가한다. 저장소 설명에 따르면 설정은 대화형으로 진행되며 파일을 직접 편집할 필요가 없다.

설정과 GeoIP

수동 설정을 선택하면 /etc/fail2ban/fail2ban-notify.json을 편집해 알림 서비스를 활성화할 수 있다. 다음은 Discord 커넥터와 GeoIP 설정을 포함한 기본 구성 예시다.

json
{
 "connectors": [
 {
 "name": "discord",
 "type": "script",
 "enabled": true,
 "path": "/etc/fail2ban/connectors/discord.sh",
 "settings": {
 "DISCORD_WEBHOOK_URL": "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN",
 "DISCORD_USERNAME": "Fail2Ban",
 "DISCORD_AVATAR_URL": ""
 },
 "timeout": 30,
 "retry_count": 2,
 "retry_delay": 5,
 "description": "Send notifications to Discord via webhook"
 }
 ],
 "connector_path": "/etc/fail2ban/connectors",
 "geoip": {
 "enabled": true,
 "service": "ipwhois",
 "cache": true,
 "ttl": 86400,
 "cache_path": "/var/cache/fail2ban-notifier/geoip.json",
 "allow_insecure_http": false,
 "city_db_path": "/usr/share/GeoIP/GeoLite2-City.mmdb",
 "asn_db_path": "/usr/share/GeoIP/GeoLite2-ASN.mmdb"
 },
 "debug": false,
 "log_level": "info",
 "timeout": 30
}

GeoIP 정보는 알림에 덧붙이는 선택 정보다. 제공자 요청 시간이 초과되거나 호출 제한에 걸린 경우, 데이터베이스가 없거나 기능이 꺼진 경우에도 알림은 원래 IP 주소와 지리 정보 이외의 데이터를 포함해 전달된다. GeoIP 결과는 대략적인 정보이므로 인증이나 접근 제어를 위한 신뢰 신호로 사용해서는 안 된다.

제공자식별자전송 방식API 키기본값참고 사항
ipwho.isipwhoisHTTPS불필요예무료 요금제는 클라이언트 IP당 하루 약 1,000회 요청
MaxMind GeoLite2geolite2없음(로컬 MMDB)불필요아니요데이터베이스 설치 후 완전 오프라인으로 동작
ip-api.comipapiHTTP만 지원불필요아니요기존 제공자이며, 안전하지 않은 전송을 명시적으로 허용해야 함
ipgeolocation.ioipgeolocationHTTPS필요아니요기존 유료 연동 유지

새로 생성하는 설정은 기본적으로 ipwho.is를 선택한다. HTTPS를 사용하고 API 키가 필요하지 않으며 IPv4와 IPv6를 지원한다. 무료 API는 요청 수 제한이 있다. HTTP 429 응답을 받으면 Retry-After가 있을 경우 이를 따르는 대기 시간이 저장되므로, 이후 Fail2Ban 프로세스가 계속 재시도하지 않는다.

ipwho.is를 사용하는 설정은 다음과 같다.

json
{
 "geoip": {
 "enabled": true,
 "service": "ipwhois",
 "cache": true,
 "ttl": 86400,
 "cache_path": "/var/cache/fail2ban-notifier/geoip.json"
 }
}

MaxMind GeoLite2는 조회 과정에서 네트워크 요청을 하지 않는다. City 데이터베이스는 필수이고 ASN 데이터베이스는 선택 사항이다. ASN 데이터베이스가 없어도 City 데이터만으로 조회가 계속된다. 알림의 ISP 항목에는 ASN 조직명이 최선의 추정값으로 들어가며, 실제 소비자 ISP와 다를 수 있다. ASN 번호는 asn 또는 F2B_ASN으로 제공된다.

GeoLite2 데이터베이스는 이 저장소나 릴리스 바이너리에 포함되지 않는다. 무료 MaxMind 계정을 등록하고 GeoLite2 라이선스 키를 만든 뒤 geoipupdate를 설치해야 한다. 설정 파일 예시는 다음과 같다.

text
# Example /etc/GeoIP.conf
AccountID YOUR_ACCOUNT_ID
LicenseKey YOUR_LICENSE_KEY
EditionIDs GeoLite2-City GeoLite2-ASN
DatabaseDirectory /usr/share/GeoIP

sudo geoipupdate

데이터베이스를 최신 상태로 유지하려면 매주 systemd 타이머나 cron 작업 등을 이용해 geoipupdate를 주기적으로 실행한다. 데이터베이스 파일이 바뀌면 캐시된 결과는 자동으로 무효화된다. 이 제품에는 MaxMind가 만든 GeoLite2 데이터가 포함되며, MaxMind에서 제공한다. GeoLite2 데이터 사용에는 MaxMind의 라이선스와 출처 표시 요건이 적용된다.

기존 ip-api.com 제공자는 HTTP만 지원한다. 조회한 IP 주소와 응답은 암호화되지 않은 채 전송되므로 관찰되거나 변조될 수 있다. 이 제공자는 기본 선택 대상이 아니며 allow_insecure_http를 명시적으로 true로 설정해야만 사용된다.

json
{
 "geoip": {
 "enabled": true,
 "service": "ipapi",
 "allow_insecure_http": true,
 "cache": true,
 "ttl": 86400
 }
}

업그레이드 전 설정에 "service": "ipapi"가 남아 있어도 설정값은 유지되지만, 사용자가 동의하기 전에는 HTTP 요청을 보내지 않는다. 이때 경고가 로그에 기록되며 GeoIP 정보를 덧붙이지 않고 알림을 계속 전달한다. 권장 대안인 ipwhois로 바꾸거나 geolite2를 설치할 수 있다. 위험을 감수하려면 "allow_insecure_http": true를 명시해야 한다. HTTPS 연결을 HTTP로 자동 전환하지는 않는다.

유료 연동인 ipgeolocation.io도 계속 사용할 수 있다. "service": "ipgeolocation"과 "api_key": "..."를 설정하면 된다. API 요구 사항에 따라 키가 쿼리 문자열에 포함되지만, 오류 메시지에서는 키를 가린다.

GeoIP 결과는 Fail2Ban이 알림마다 짧게 실행되는 프로세스를 시작하기 때문에 JSON 파일에 저장한다. 메모리 캐시는 프로세스 실행 사이에 유지되지 않는다. 기본 파일 위치는 /var/cache/fail2ban-notifier/geoip.json이며 기본 TTL은 86400초다.

  • 항목은 정규화된 IP 주소와 제공자를 기준으로 저장한다. IPv4에 대응하는 IPv6 주소도 조회와 캐시 저장 전에 정규화한다.
  • GeoLite2 캐시 항목에는 데이터베이스 지문이 포함되며 geoipupdate 이후 무효화된다.
  • 파일 쓰기는 원자적으로 처리하고 프로세스 간 파일 잠금으로 보호한다. 캐시 파일 권한은 0600, 디렉터리 권한은 0750으로 생성된다.
  • 손상되거나 만료되었거나 유효하지 않은 결과, 조회 실패 결과는 성공한 조회 결과로 제공하지 않는다. 실패한 조회는 캐시에 저장하지 않는다.
  • 만료 항목을 정리하며 캐시는 최대 10,000개 항목으로 제한한다.
  • "cache": false로 설정하면 영구 캐시를 완전히 끈다.
  • 요청 제한 대기 시간은 같은 디렉터리 아래에서 제공자별로 따로 저장한다.

Fail2Ban은 보통 알림 도구를 root로 실행하므로 설치 프로그램은 캐시 디렉터리를 root 소유, 권한 0750으로 만든다. action을 root가 아닌 사용자로 실행하는 배포 환경에서는 해당 사용자에게 캐시 디렉터리 쓰기 권한을 주거나 cache_path를 적절한 위치로 바꿔야 한다.

문제 해결 시에는 다음 사항을 확인한다.

  • unsupported provider: ipwhois, geolite2, ipapi, ipgeolocation 중 하나를 사용한다. 제공자 이름은 조용히 다른 값으로 바뀌지 않고 검증된다. 지원되지 않는 이름은 경고로 기록되며 GeoIP 조회를 건너뛰지만 알림은 전달된다.
  • allow_insecure_http 경고: 기존 ipapi 제공자를 선택했지만 안전하지 않은 전송을 허용하지 않은 상태다. 위의 이전 안내를 따른다.
  • rate limited: 저장된 대기 시간이 끝날 때까지 기다리거나 다른 제공자를 선택한다. 이 동안에도 알림은 전달된다.
  • GeoLite2 데이터베이스를 사용할 수 없음: city_db_path와 asn_db_path의 경로 및 파일 권한을 확인하고 geoipupdate를 실행한다. mmdblookup으로 파일을 확인할 수 있다.
  • 캐시 권한 오류: 캐시 디렉터리와 파일의 소유자 및 권한을 확인한다. 캐시 오류는 경고로 기록되며 캐시 없이 계속 동작한다.
  • 디버깅: -debug를 실행하면 GeoIP 오류와 알림 데이터를 볼 수 있다. GeoIP 오류 메시지에는 API 키가 포함되지 않는다.

커넥터와 Fail2Ban 연결

사용 가능한 커넥터를 찾고 시험하려면 다음 명령을 실행한다.

sh
sudo fail2ban-notify -discover
sudo fail2ban-notify -test discord

첫 번째 명령은 커넥터 디렉터리를 검색해 사용 가능한 커넥터를 표시한다. 두 번째 명령은 지정한 커넥터로 시험 알림을 보낸다. 수동 설정에서는 /etc/fail2ban/fail2ban-notify.json을 편집해 원하는 커넥터의 "enabled": true를 설정한다. 대화형 마법사를 이용하려면 sudo fail2ban-notify -setup을 실행한다.

Fail2Ban에 수동으로 연결하려면 jail 설정에 notify action을 추가한다. 설정 마법사는 이 작업을 자동으로 할 수 있다. 예를 들어 /etc/fail2ban/jail.local에서 다음과 같이 구성한다.

ini
[ssh]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 3600
action = iptables-multiport[name=ssh, port="ssh", protocol=tcp]
         notify[name=ssh]

명령줄 사용법

명령줄 옵션은 다음과 같다.

옵션설명예시
-action string수행한 동작(ban/unban)-action="unban"
-config string설정 파일 경로-config="/path/to/config.json"
-debug디버그 로그 활성화-debug
-discover사용 가능한 커넥터 검색-discover
-failures int실패 횟수-failures=5
-init설정 파일 초기화-init
-ip string차단하거나 차단 해제한 IP 주소-ip="192.168.1.100"
-setup대화형 설정 마법사 실행-setup
-jail stringFail2Ban jail 이름-jail="ssh"
-status커넥터 상태 표시-status
-test string지정한 커넥터 시험-test="discord"
-version버전 정보 표시-version

커넥터 상태를 확인하려면 다음 명령을 사용한다. 활성화·비활성화·유효하지 않은 상태를 포함해 설정된 모든 커넥터의 상태를 표시한다.

sh
sudo fail2ban-notify -status

모든 활성 커넥터에 알림을 수동으로 보내거나 기본 설정 파일을 만들 수도 있다.

sh
sudo fail2ban-notify -ip="192.168.1.100" -jail="ssh" -action="ban" -failures=5
sudo fail2ban-notify -init

첫 번째 명령은 예시 IP가 ssh jail에서 차단되고 실패 횟수가 5회였다는 알림을 보낸다. 두 번째 명령은 샘플 커넥터를 포함한 기본 설정 파일을 만든다. 전체 구성과 배포를 안내하는 마법사는 다음과 같이 실행한다.

sh
sudo fail2ban-notify -setup

지원 서비스와 사용자 지정 커넥터

지원 서비스에는 Discord와 Slack 채널(웹훅), Microsoft Teams 채널(웹훅), Telegram 채팅(봇 API), SMTP 이메일, PagerDuty(Events API v2), 사용자 지정 웹훅이 포함된다. PagerDuty에서는 ban 동작이 인시던트를 발생시키고 unban 동작이 이를 해결한다. 사용자 지정 커넥터는 Bash나 Python 등의 스크립트 또는 HTTP 웹훅으로 만들 수 있다. 스크립트 커넥터는 환경 변수와 표준 입력으로 알림 데이터를 받으며, HTTP 커넥터는 JSON 페이로드를 받는다.

스크립트 커넥터를 만들려면 /etc/fail2ban/connectors/에 스크립트 파일을 만들고 실행 권한을 부여한 뒤 환경 변수와 표준 입력을 이용해 연동 로직을 작성한다. Bash 커넥터 예시는 다음과 같다.

bash
#!/bin/bash
# MyService Connector for fail2ban-notify
# Place this file in /etc/fail2ban/connectors/myservice.sh

set -euo pipefail

# Configuration - set via environment variables from main config
API_KEY="${MYSERVICE_API_KEY:-}"
API_URL="${MYSERVICE_API_URL:-https://api.example.com/notify}"

# Validation
if [[ -z "$API_KEY" ]]; then
 echo "Error: MYSERVICE_API_KEY not set" >&2
 exit 1
fi

# Read JSON data from stdin
JSON_DATA=""
if [[ -p /dev/stdin ]]; then
 JSON_DATA=$(cat)
fi

# Get data from environment variables (set by main program)
IP="${F2B_IP:-unknown}"
JAIL="${F2B_JAIL:-unknown}"
ACTION="${F2B_ACTION:-ban}"
TIME="${F2B_TIME:-$(date -Iseconds)}"
COUNTRY="${F2B_COUNTRY:-}"
REGION="${F2B_REGION:-}"
CITY="${F2B_CITY:-}"
ISP="${F2B_ISP:-}"
FAILURES="${F2B_FAILURES:-0}"

# Create your service-specific payload
PAYLOAD=$(cat <<EOF
{
 "event": "fail2ban_${ACTION}",
 "ip": "$IP",
 "jail": "$JAIL",
 "time": "$TIME",
 "location": "${CITY:+$CITY, }${COUNTRY}",
 "failures": $FAILURES
}
EOF
)

# Send the notification to your service
HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null \
 -H "Content-Type: application/json" \
 -H "Authorization: Bearer $API_KEY" \
 -d "$PAYLOAD" \
 "$API_URL")

if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then
 echo "MyService notification sent successfully (HTTP $HTTP_CODE)"
 exit 0
else
 echo "MyService notification failed (HTTP $HTTP_CODE)" >&2
 exit 1
fi

Python으로 작성하는 예시는 다음과 같다.

python
#!/usr/bin/env python3
# MyService Connector for fail2ban-notify
# Place this file in /etc/fail2ban/connectors/myservice.py

import os
import sys
import json
import requests
from datetime import datetime

# Configuration from environment variables
api_key = os.environ.get('MYSERVICE_API_KEY', '')
api_url = os.environ.get('MYSERVICE_API_URL', 'https://api.example.com/notify')

# Validation
if not api_key:
 print("Error: MYSERVICE_API_KEY not set", file=sys.stderr)
 sys.exit(1)

# Get data from environment variables
ip = os.environ.get('F2B_IP', 'unknown')
jail = os.environ.get('F2B_JAIL', 'unknown')
action = os.environ.get('F2B_ACTION', 'ban')
time_str = os.environ.get('F2B_TIME', datetime.now().isoformat())
country = os.environ.get('F2B_COUNTRY', '')
region = os.environ.get('F2B_REGION', '')
city = os.environ.get('F2B_CITY', '')
isp = os.environ.get('F2B_ISP', '')
failures = int(os.environ.get('F2B_FAILURES', '0'))

# Read JSON data from stdin (optional)
try:
 if not sys.stdin.isatty():
 json_data = json.load(sys.stdin)
except:
 json_data = {}

# Create payload for your service
location = f"{city}, {country}" if city and country else country
payload = {
 "event": f"fail2ban_{action}",
 "ip": ip,
 "jail": jail,
 "time": time_str,
 "location": location,
 "failures": failures
}

# Send notification
headers = {
 "Content-Type": "application/json",
 "Authorization": f"Bearer {api_key}"
}

try:
 response = requests.post(api_url, json=payload, headers=headers)
 response.raise_for_status()
 print(f"MyService notification sent successfully (HTTP {response.status_code})")
 sys.exit(0)
except Exception as e:
 print(f"MyService notification failed: {str(e)}", file=sys.stderr)
 sys.exit(1)

커넥터에 전달되는 환경 변수는 다음과 같다.

변수설명
F2B_IP차단하거나 차단 해제한 IP 주소
F2B_JAILFail2Ban jail 이름
F2B_ACTION수행한 동작(ban/unban)
F2B_TIME이벤트 시각(ISO 8601 형식)
F2B_TIMESTAMP이벤트의 Unix 타임스탬프
F2B_COUNTRYIP의 국가(GeoIP 활성화 시)
F2B_REGIONIP의 지역 또는 주
F2B_CITYIP의 도시
F2B_ISPIP의 ISP 또는 ASN 조직(최선의 추정값)
F2B_ASNASN 번호(예: AS15169)
F2B_HOSTNAMEIP의 호스트 이름(확인 가능한 경우)
F2B_FAILURES차단을 유발한 실패 횟수

HTTP 커넥터는 fail2ban-notify.json에 다음과 같은 항목을 추가해 설정한다.

json
{
 "name": "mywebhook",
 "type": "http",
 "enabled": true,
 "path": "",
 "settings": {
 "url": "https://your-api.com/webhook",
 "header_Content-Type": "application/json",
 "header_Authorization": "Bearer YOUR_TOKEN"
 },
 "timeout": 30,
 "retry_count": 2,
 "retry_delay": 5,
 "description": "Send notifications to MyService API"
}

스크립트를 만든 뒤 sudo fail2ban-notify -discover로 검색하고, sudo fail2ban-notify -test myservice로 시험한다. 설정 파일에서 해당 커넥터의 "enabled": true를 지정하거나 sudo fail2ban-notify -setup으로 대화형 설정을 진행할 수 있다. 프로젝트는 MIT 라이선스로 배포된다. 변경 내역과 릴리스 정보는 CHANGELOG.md에서 확인할 수 있다.