はじめに
本記事はサーバのセキュリティ対策として、fail2banを用いた不正アクセスを防止する方法について記載しています。
インターネット上に公開されたサーバは、アタックサーフェスとして常にスキャンや攻撃のリスクにさらされているため、手動で対応する場合は運用が大変です。
fail2banを導入することで、このような脅威に自動で対処し、運用負荷を大幅に軽減できます。
背景
筆者はGoogle CloudのCompute Engineを利用してWebサーバを運用しています。
基本的に無料枠内で使用しているため、課金は発生していませんでしたが、ある時期から課金が発生していました。原因は既に特定していて、VM インスタンスからの下りネットワークの通信が原因です。
公式ドキュメントの無料枠の使用量上限の通りにアウトバウンドのデータ量が1GBを超えたためです。
北米から全リージョン宛ての 1 GB のアウトバウンド データ転送(1 か月あたり、中国とオーストラリアを除く)
以下は2025年4月分のレポートより、SKU ID:03A5-1B2A-3485(Network Internet Data Transfer Out from Americas to South America)でフィルタした例です。
実態としては、海外からのボットを用いたクローリングなどの偵察行為による無差別なアクセスが原因と考えられますが、2024年12月頃からこうしたアクセスが顕著に増加していました。
fail2ban
fail2banは、Pythonで実装されているオープンソースのセキュリティツールです。
システムが出力するログファイルを基にフィルターを適用し、ログイン失敗回数などの条件により不正とみなされるIPアドレスを検出して、ファイアウォールのルールを自動的に更新します。
例えば、ApacheやNginxなどを利用してWebサーバを運用している場合、fail2banを導入することで、短時間に大量アクセスを行うIPアドレスからのアクセスを禁止(ban)することができます。
fail2banのインストール
Fail2banは多くのLinuxディストリビューションで公式にパッケージ化されているため、Debian系やRed Hat系などのパッケージマネージャを用いて簡単にインストールできます。
Debian系Linuxの場合は、APTコマンドでインストールを行います。
$ sudo apt install fail2ban
fail2banの設定
fail2banの設定方法を以下に記載します。
本記事ではOSとしてDebianを使用していますが、fail2banの設定ファイルは、/etc/fail2ban/
ディレクトリ配下に格納されています。
.
├── action.d
├── fail2ban.conf
├── fail2ban.d
├── filter.d
├── jail.conf
├── jail.d
├── jail.local
├── paths-arch.conf
├── paths-common.conf
├── paths-debian.conf
└── paths-opensuse.conf
jail.conf
ファイルは、デフォルトのテンプレートファイルです。パッケージのアップグレードによって上書きされる可能性があります。また、変更内容について将来のバージョンと互換性がない可能性があるため、直接変更することは推奨されていません。
新しく設定を行う場合は、jail.conf
ファイルを直接編集せずに、jail.local
ファイルやjail.d/*.conf
ファイルをカスタマイズする方法が推奨されています。
従ってjail.local
ファイルを新たに作成するか、/etc/fail2ban/jail.d/
ディレクトリ配下に個別の.conf
ファイルを追加することで、設定の上書きが可能です。
デフォルト動作
デフォルトでは、sshdに関するジェイルが設定されています。
sshdに関する設定は、/etc/fail2ban/jail.conf
に記述されています。また、当ファイルはbantime
(禁止する時間)、findtime
(検出する時間)、maxretry
(最大失敗回数)などのデフォルト値も含まれています。
[sshd]
# To use more aggressive sshd modes set filter parameter "mode" in jail.local:
# normal (default), ddos, extra or aggressive (combines all).
# See "tests/files/logs/sshd" or "filter.d/sshd.conf" for usage example and details.
#mode = normal
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
そして、/etc/fail2ban/jail.d/defaults-debian.conf
ファイルによって、sshdのJailが有効化されています。
[sshd]
enabled = true
全てのジェイルに共通する設定は、[DEFAULT]セクションに記述します。
[DEFAULT]セクションは通常、/etc/fail2ban/jail.conf
ファイルに記述されているため、bantime、findtime、maxretryなどの値は、各ジェイルの設定で明示的に指定しない限り、全てのジェイルに適用されます。
従って/etc/fail2ban/jail.local
ファイルの[DEFAULT]セクションを上書きすることで、ジェイルに適用されるデフォルト値を変更します。
設定ファイルの作成
本記事では、偵察行為と思われる不正アクセスを検出することを目的に、Nginxのアクセスログより、以下のルールを作成します。
設定ファイル | 目的 |
---|---|
nginx-abuse-access | 過剰アクセスのリクエストを防ぐため、すべてのGETまたはPOSTリクエストを対象とする |
nginx-badbots | 自動化ツールや悪質なBotのアクセスを防ぐため、User-Agentにbotと思われるリクエストを対象とする |
はじめにフィルター設定に関する設定ファイルを作成します。
/etc/fail2ban/filter.d/nginx-abuse-access.conf
[Definition]
failregex = ^<HOST> -.*"(GET|POST).*HTTP.*"
ignoreregex =
ignoreip =
/etc/fail2ban/filter.d/nginx-badbots.conf
[Definition]
failregex = ^<HOST> -.*"(GET|POST).*HTTP.*"(?:Mozilla|libwww|python|curl|php|wget|java|ruby|scrapy|bot).*$
ignoreregex =
ignoreip =
ignoreregex
は除外するログの条件、ignoreip
については除外するIPアドレスを記述します。これらの値は空でも問題ありません。
次にフィルター設定を適用するための設定ファイルを作成します。
以下の例では、60秒以内で10回以上のアクセスがログにあった場合、当該IPアドレスをBanにします。また、BanされたIPアドレスは1時間、ファイアウォール(iptables及びnftables)でアクセスが遮断されます。
/etc/fail2ban/jail.local
[nginx-abuse-access]
enabled = true
filter = nginx-abuse-access
port = http,https
logpath = /var/log/nginx/access.log
findtime = 60
maxretry = 10
bantime = 3600
action = iptables-multiport[name=abuse, port="http,https", protocol=tcp]
/etc/fail2ban/jail.d/nginx-badbots.local
[nginx-badbots]
enabled = true
filter = nginx-badbots
logpath = /var/log/nginx/access.log
findtime = 60
maxretry = 10
bantime = 3600
action = iptables-multiport[name=badbots, port="http,https", protocol=tcp]
HTTPとHTTPSは区別されるため、action
の値についてiptables-multiport
を使用しない場合は、iptablesを用いてそれぞれルールを作成する必要があります。
action = iptables[name=HTTP, port=http, protocol=tcp]
設定変更
設定変更を行う際は、サービスの再起動または設定ファイルの再読み込みを行います。
- サービスの再起動
$ sudo systemctl restart fail2ban
- 設定ファイルの再読み込み
$ sudo fail2ban-client reload
正常にサービスが起動していること及び各種ルールの状態については、以下のコマンドを実行して確認します。
- サービスの確認
$ systemctl status fail2ban
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
Active: active (running) since Tue 2025-06-10 10:14:34 JST; 50min ago
Docs: man:fail2ban(1)
Main PID: 478 (fail2ban-server)
Tasks: 9 (limit: 1107)
Memory: 47.1M
CPU: 5.114s
CGroup: /system.slice/fail2ban.service
└─478 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
Jun 10 10:14:34 webserver systemd[1]: Started fail2ban.service - Fail2Ban Service.
Jun 10 10:14:35 webserver fail2ban-server[478]: 2025-06-10 10:14:35,912 fail2ban.configreader [478]: WARNING 'allowipv6' not defined in 'Definition'. Using defaul>
Jun 10 10:14:38 webserver fail2ban-server[478]: Server ready
- fail2ban-clientの確認
$ sudo fail2ban-client status
Status
|- Number of jail: 3
`- Jail list: nginx-abuse-access, nginx-badbots, sshd
引数に設定したルールを付与することで、個別に状態を確認できます。
$ sudo fail2ban-client status nginx-abuse-access
Status for the jail: nginx-abuse-access
|- Filter
| |- Currently failed: 0
| |- Total failed: 32
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
BanされたIPが発生すると、Banned IP listに表示されます。
Status for the jail: nginx-abuse-access
|- Filter
| |- Currently failed: 0
| |- Total failed: 214
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 1
|- Total banned: 3
`- Banned IP list: [REDACTED]
- fail2ban-の設定値確認
$ sudo fail2ban-client -d
2025-06-10 11:06:47,728 fail2ban.configreader [1459]: WARNING 'allowipv6' not defined in 'Definition'. Using default one: 'auto'
['set', 'syslogsocket', 'auto']
['set', 'loglevel', 'INFO']
['set', 'logtarget', '/var/log/fail2ban.log']
['set', 'allowipv6', 'auto']
['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3']
['set', 'dbmaxmatches', 10]
['set', 'dbpurgeage', '1d']
['add', 'sshd', 'auto']
['set', 'sshd', 'usedns', 'warn']
['set', 'sshd', 'prefregex', '^<F-MLFID>(?:\\[\\])?\\s*(?:<[^.]+\\.[^.]+>\\s+)?(?:\\S+\\s+)?(?:kernel:\\s?\\[ *\\d+\\.\\d+\\]:?\\s+)?(?:@vserver_\\S+\\s+)?(?:(?:(?:\\[\\d+\\])?:\\s+[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?|[\\[\\(]?sshd(?:\\(\\S+\\))?[\\]\\)]?:?(?:\\[\\d+\\])?:?)\\s+)?(?:\\[ID \\d+ \\S+\\]\\s+)?</F-MLFID>(?:(?:error|fatal): (?:PAM: )?)?<F-CONTENT>.+</F-CONTENT>$']
['set', 'sshd', 'maxlines', 1]
['multi-set', 'sshd', 'addfailregex', ['^[aA]uthentication (?:failure|error|failed) for <F-USER>.*</F-USER> from <HOST>( via \\S+)?(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^User not known to the underlying authentication module for <F-USER>.*</F-USER> from <HOST>(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^Failed publickey for invalid user <F-USER>(?P<cond_user>\\S+)|(?:(?! from ).)*?</F-USER> from <HOST>(?: (?:port \\d+|on \\S+)){0,2}(?: ssh\\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)', '^Failed (?:<F-NOFAIL>publickey</F-NOFAIL>|\\S+) for (?P<cond_inv>invalid user )?<F-USER>(?P<cond_user>\\S+)|(?(cond_inv)(?:(?! from ).)*?|[^:]+)</F-USER> from <HOST>(?: (?:port \\d+|on \\S+)){0,2}(?: ssh\\d*)?(?(cond_user): |(?:(?:(?! from ).)*)$)', '^<F-USER>ROOT</F-USER> LOGIN REFUSED FROM <HOST>', '^[iI](?:llegal|nvalid) user <F-USER>.*?</F-USER> from <HOST>(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^User <F-USER>\\S+|.*?</F-USER> from <HOST> not allowed because not listed in AllowUsers(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^User <F-USER>\\S+|.*?</F-USER> from <HOST> not allowed because listed in DenyUsers(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^User <F-USER>\\S+|.*?</F-USER> from <HOST> not allowed because not in any group(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^refused connect from \\S+ \\(<HOST>\\)', '^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>(?: (?:port \\d+|on \\S+)){0,2}:\\s*3: .*: Auth fail(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^User <F-USER>\\S+|.*?</F-USER> from <HOST> not allowed because a group is listed in DenyGroups(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', "^User <F-USER>\\S+|.*?</F-USER> from <HOST> not allowed because none of user's groups are listed in AllowGroups(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$", '^<F-NOFAIL>pam_[a-z]+\\(sshd:auth\\):\\s+authentication failure;</F-NOFAIL>(?:\\s+(?:(?:logname|e?uid|tty)=\\S*)){0,4}\\s+ruser=<F-ALT_USER>\\S*</F-ALT_USER>\\s+rhost=<HOST>(?:\\s+user=<F-USER>\\S*</F-USER>)?(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^maximum authentication attempts exceeded for <F-USER>.*</F-USER> from <HOST>(?: (?:port \\d+|on \\S+)){0,2}(?: ssh\\d*)?(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^User <F-USER>\\S+|.*?</F-USER> not allowed because account is locked(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*', '^<F-MLFFORGET>Disconnecting</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\\S+</F-USER> <HOST>(?: (?:port \\d+|on \\S+)){0,2}:\\s*Change of username or service not allowed:\\s*.*\\[preauth\\]\\s*$', '^Disconnecting: Too many authentication failures(?: for <F-USER>\\S+|.*?</F-USER>)?(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*$', '^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>(?: (?:port \\d+|on \\S+)){0,2}:\\s*11:', '^<F-NOFAIL><F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)(?: (?:invalid|authenticating) user <F-USER>\\S+|.*?</F-USER>)? <HOST>(?:(?: (?:port \\d+|on \\S+|\\[preauth\\])){0,3}\\s*|\\s*)$', '^<F-MLFFORGET><F-MLFGAINED>Accepted \\w+</F-MLFGAINED></F-MLFFORGET> for <F-USER>\\S+</F-USER> from <HOST>(?:\\s|$)', '^<F-NOFAIL>Connection from</F-NOFAIL> <HOST>']]
['set', 'sshd', 'datepattern', '{^LN-BEG}']
['set', 'sshd', 'addjournalmatch', '_SYSTEMD_UNIT=sshd.service', '+', '_COMM=sshd']
['set', 'sshd', 'maxretry', 5]
['set', 'sshd', 'maxmatches', 5]
['set', 'sshd', 'findtime', '10m']
['set', 'sshd', 'bantime', '10m']
['set', 'sshd', 'ignorecommand', '']
['set', 'sshd', 'logencoding', 'auto']
['set', 'sshd', 'addlogpath', '/var/log/auth.log', 'head']
['set', 'sshd', 'addaction', 'iptables-multiport']
['multi-set', 'sshd', 'action', 'iptables-multiport', [['actionstart', "{ <iptables> -C f2b-sshd -j RETURN >/dev/null 2>&1; } || { <iptables> -N f2b-sshd || true; <iptables> -A f2b-sshd -j RETURN; }\nfor proto in $(echo 'tcp' | sed 's/,/ /g'); do\n{ <iptables> -C INPUT -p $proto -m multiport --dports ssh -j f2b-sshd >/dev/null 2>&1; } || { <iptables> -I INPUT -p $proto -m multiport --dports ssh -j f2b-sshd; }\ndone"], ['actionstop', "for proto in $(echo 'tcp' | sed 's/,/ /g'); do\n<iptables> -D INPUT -p $proto -m multiport --dports ssh -j f2b-sshd\ndone\n<iptables> -F f2b-sshd\n<iptables> -X f2b-sshd"], ['actionflush', '<iptables> -F f2b-sshd'], ['actioncheck', "for proto in $(echo 'tcp' | sed 's/,/ /g'); do\n<iptables> -C INPUT -p $proto -m multiport --dports ssh -j f2b-sshd\ndone"], ['actionban', '<iptables> -I f2b-sshd 1 -s <ip> -j <blocktype>'], ['actionunban', '<iptables> -D f2b-sshd -s <ip> -j <blocktype>'], ['port', 'ssh'], ['protocol', 'tcp'], ['chain', '<known/chain>'], ['name', 'sshd'], ['actname', 'iptables-multiport'], ['blocktype', 'REJECT --reject-with icmp-port-unreachable'], ['returntype', 'RETURN'], ['lockingopt', '-w'], ['iptables', 'iptables <lockingopt>'], ['blocktype?family=inet6', 'REJECT --reject-with icmp6-port-unreachable'], ['iptables?family=inet6', 'ip6tables <lockingopt>']]]
['add', 'nginx-abuse-access', 'auto']
['set', 'nginx-abuse-access', 'usedns', 'warn']
['set', 'nginx-abuse-access', 'addfailregex', '^<HOST> -.*"(GET|POST).*HTTP.*"']
['set', 'nginx-abuse-access', 'maxretry', 10]
['set', 'nginx-abuse-access', 'maxmatches', 10]
['set', 'nginx-abuse-access', 'findtime', '60']
['set', 'nginx-abuse-access', 'bantime', '3600']
['set', 'nginx-abuse-access', 'ignorecommand', '']
['set', 'nginx-abuse-access', 'logencoding', 'auto']
['set', 'nginx-abuse-access', 'addlogpath', '/var/log/nginx/access.log', 'head']
['set', 'nginx-abuse-access', 'addaction', 'iptables-multiport-abuse']
['multi-set', 'nginx-abuse-access', 'action', 'iptables-multiport-abuse', [['actionstart', "{ <iptables> -C f2b-abuse -j RETURN >/dev/null 2>&1; } || { <iptables> -N f2b-abuse || true; <iptables> -A f2b-abuse -j RETURN; }\nfor proto in $(echo 'tcp' | sed 's/,/ /g'); do\n{ <iptables> -C INPUT -p $proto -m multiport --dports http,https -j f2b-abuse >/dev/null 2>&1; } || { <iptables> -I INPUT -p $proto -m multiport --dports http,https -j f2b-abuse; }\ndone"], ['actionstop', "for proto in $(echo 'tcp' | sed 's/,/ /g'); do\n<iptables> -D INPUT -p $proto -m multiport --dports http,https -j f2b-abuse\ndone\n<iptables> -F f2b-abuse\n<iptables> -X f2b-abuse"], ['actionflush', '<iptables> -F f2b-abuse'], ['actioncheck', "for proto in $(echo 'tcp' | sed 's/,/ /g'); do\n<iptables> -C INPUT -p $proto -m multiport --dports http,https -j f2b-abuse\ndone"], ['actionban', '<iptables> -I f2b-abuse 1 -s <ip> -j <blocktype>'], ['actionunban', '<iptables> -D f2b-abuse -s <ip> -j <blocktype>'], ['name', 'abuse'], ['port', 'http,https'], ['protocol', 'tcp'], ['actname', 'iptables-multiport-abuse'], ['chain', 'INPUT'], ['blocktype', 'REJECT --reject-with icmp-port-unreachable'], ['returntype', 'RETURN'], ['lockingopt', '-w'], ['iptables', 'iptables <lockingopt>'], ['blocktype?family=inet6', 'REJECT --reject-with icmp6-port-unreachable'], ['iptables?family=inet6', 'ip6tables <lockingopt>']]]
['add', 'nginx-badbots', 'auto']
['set', 'nginx-badbots', 'usedns', 'warn']
['set', 'nginx-badbots', 'addfailregex', '^<HOST> -.*"(GET|POST).*HTTP.*"(?:Mozilla|libwww|python|curl|php|wget|java|ruby|scrapy|bot).*$']
['set', 'nginx-badbots', 'maxretry', 10]
['set', 'nginx-badbots', 'maxmatches', 10]
['set', 'nginx-badbots', 'findtime', '60']
['set', 'nginx-badbots', 'bantime', '3600']
['set', 'nginx-badbots', 'ignorecommand', '']
['set', 'nginx-badbots', 'logencoding', 'auto']
['set', 'nginx-badbots', 'addlogpath', '/var/log/nginx/access.log', 'head']
['set', 'nginx-badbots', 'addaction', 'iptables-multiport-badbots']
['multi-set', 'nginx-badbots', 'action', 'iptables-multiport-badbots', [['actionstart', "{ <iptables> -C f2b-badbots -j RETURN >/dev/null 2>&1; } || { <iptables> -N f2b-badbots || true; <iptables> -A f2b-badbots -j RETURN; }\nfor proto in $(echo 'tcp' | sed 's/,/ /g'); do\n{ <iptables> -C INPUT -p $proto -m multiport --dports http,https -j f2b-badbots >/dev/null 2>&1; } || { <iptables> -I INPUT -p $proto -m multiport --dports http,https -j f2b-badbots; }\ndone"], ['actionstop', "for proto in $(echo 'tcp' | sed 's/,/ /g'); do\n<iptables> -D INPUT -p $proto -m multiport --dports http,https -j f2b-badbots\ndone\n<iptables> -F f2b-badbots\n<iptables> -X f2b-badbots"], ['actionflush', '<iptables> -F f2b-badbots'], ['actioncheck', "for proto in $(echo 'tcp' | sed 's/,/ /g'); do\n<iptables> -C INPUT -p $proto -m multiport --dports http,https -j f2b-badbots\ndone"], ['actionban', '<iptables> -I f2b-badbots 1 -s <ip> -j <blocktype>'], ['actionunban', '<iptables> -D f2b-badbots -s <ip> -j <blocktype>'], ['name', 'badbots'], ['port', 'http,https'], ['protocol', 'tcp'], ['actname', 'iptables-multiport-badbots'], ['chain', 'INPUT'], ['blocktype', 'REJECT --reject-with icmp-port-unreachable'], ['returntype', 'RETURN'], ['lockingopt', '-w'], ['iptables', 'iptables <lockingopt>'], ['blocktype?family=inet6', 'REJECT --reject-with icmp6-port-unreachable'], ['iptables?family=inet6', 'ip6tables <lockingopt>']]]
['start', 'sshd']
['start', 'nginx-abuse-access']
['start', 'nginx-badbots']
- ファイアウォール設定確認(
nff
)
$ sudo nft list ruleset
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
chain f2b-abuse {
ip saddr [REDACTED] counter packets 37 bytes 4966 reject
counter packets 2760 bytes 255469 return
}
chain INPUT {
type filter hook input priority filter; policy accept;
meta l4proto tcp tcp dport { 80, 443 } counter packets 2842 bytes 266088 jump f2b-abuse
}
}
- ファイアウォール設定確認(
iptables
)
$ sudo iptables -L -n --line-numbers
Chain INPUT (policy ACCEPT)
num target prot opt source destination
1 f2b-abuse 6 -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443
Chain FORWARD (policy ACCEPT)
num target prot opt source destination
Chain OUTPUT (policy ACCEPT)
num target prot opt source destination
Chain f2b-abuse (1 references)
num target prot opt source destination
1 REJECT 0 -- [REDACTED] 0.0.0.0/0 reject-with icmp-port-unreachable
2 RETURN 0 -- 0.0.0.0/0 0.0.0.0/0
actionで指定するnameの値について、本記事では管理がしやすいように分けて設定しています。例えば、共通の値にすることでiptablesのチェインを減らしてルールを見やすくすることもできますが、どのジェイルのルールでbanされたかが分かりにくいなどのデメリットがあります。
fail2banの運用
fail2banのログの見方及びログの分析方法を以下に記載します。
また、ログの分析結果を踏まえて、アクセスの傾向に応じた応用的な対策について紹介します。
ログの見方
fail2banが出力するログは、デフォルトの場合、/var/log/
ディレクトリ配下に出力されます。
-rw-r----- 1 root adm 281508 Jun 10 10:41 /var/log/fail2ban.log
-rw-r----- 1 root adm 1027167 Jun 8 00:00 /var/log/fail2ban.log.1
-rw-r----- 1 root adm 18159 May 31 23:54 /var/log/fail2ban.log.2.gz
-rw-r--r-- 1 root root 32064 Jun 10 10:10 /var/log/faillog
ログの確認は、/var/log/fail2ban.log
ファイルを参照します。/var/log/faillog
ファイルはバイナリファイルです。
/var/log/fail2ban.log
ファイルについて、監視対象のログファイルがフィルターの条件に一致した場合、Found
のメッセージが出力されます。
2025-06-10 20:25:22,273 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:21
2025-06-10 20:25:22,274 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:22
2025-06-10 20:25:22,458 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:22
2025-06-10 20:25:22,780 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:22
2025-06-10 20:25:23,091 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:23
2025-06-10 20:25:23,396 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:23
2025-06-10 20:25:23,711 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:23
2025-06-10 20:25:24,025 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:24
2025-06-10 20:25:24,332 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:24
2025-06-10 20:25:24,643 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:24
2025-06-10 20:25:24,956 fail2ban.filter [478]: INFO [nginx-abuse-access] Found [REDACTED] - 2025-06-10 20:25:24
その後、findtime
で設定した時間内にmaxretry
回以上のしきい値を超えた場合、当該IPアドレスに関するBan
のメッセージが出力されます。
2025-06-10 20:25:25,197 fail2ban.actions [478]: NOTICE [nginx-abuse-access] Ban [REDACTED]
設定した時間が経過すると、制限は解除されるるため、Unban
のメッセージが出力されます。
2025-06-10 21:25:24,315 fail2ban.actions [478]: NOTICE [nginx-abuse-access] Unban [REDACTED]
参考までにNginxのアクセスログより、上記BanされたIP addressからのアクセスは以下の通りです。
[REDACTED] - - [10/Jun/2025:20:25:21 +0900] "GET /contact-us HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:22 +0900] "GET /.env HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:22 +0900] "GET /contact HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:22 +0900] "GET /contactus HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:23 +0900] "GET /contactus.html HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:23 +0900] "GET /contactus.php HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:23 +0900] "GET /contact_us HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:24 +0900] "GET /_profiler/phpinfo HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:24 +0900] "GET /phpinfo HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:24 +0900] "GET /phpinfo.php HTTP/1.1" 404 146 "-" "-"
[REDACTED] - - [10/Jun/2025:20:25:24 +0900] "GET /info.php HTTP/1.1" 404 146 "-" "-"
ログの分析1
fail2banが出力するfail2ban.log
ファイルを基にログの分析を行ないます。
以下のコマンドは実行すると、fail2ban.log
ファイルよりBanされたIPアドレスを抽出し、IPアドレスでソートした結果を踏まえて、IPアドレスとタイムスタンプの情報を出力します。
$ cat /var/log/fail2ban.log* | grep 'Ban' | awk '{print $NF, $1, $2}' | sort -t . -k1,1n -k2,2n -k3,3n -k4,4n
[REDACTED] 2025-06-01 18:00:31,767
コマンド結果から大きく以下の傾向が確認できました。
基本的なパターンは、同一IPアドレスからの大量アクセスです。時間帯に規則性はなく、Banが行われても連日による不定期なアクセスが確認できます。
出力例
[REDACTED] 2025-06-01 01:16:36,507
[REDACTED] 2025-06-01 03:11:53,954
[REDACTED] 2025-06-01 04:58:18,768
[REDACTED] 2025-06-01 06:58:28,149
[REDACTED] 2025-06-01 09:10:04,594
[REDACTED] 2025-06-01 10:59:44,230
[REDACTED] 2025-06-01 12:33:34,875
[REDACTED] 2025-06-01 16:43:22,353
[REDACTED] 2025-06-01 18:21:42,393
[REDACTED] 2025-06-01 20:16:25,674
[REDACTED] 2025-06-01 22:03:12,942
[REDACTED] 2025-06-01 23:56:54,248
[REDACTED] 2025-06-02 02:00:27,605
[REDACTED] 2025-06-02 10:37:51,744
[REDACTED] 2025-06-02 12:45:06,020
[REDACTED] 2025-06-02 14:18:25,681
[REDACTED] 2025-06-02 16:19:53,124
[REDACTED] 2025-06-02 17:52:00,927
[REDACTED] 2025-06-02 19:40:17,349
[REDACTED] 2025-06-02 21:26:44,324
[REDACTED] 2025-06-02 23:19:33,013
[REDACTED] 2025-06-03 01:05:32,369
[REDACTED] 2025-06-03 02:33:19,008
[REDACTED] 2025-06-03 04:28:22,373
[REDACTED] 2025-06-03 06:19:26,595
[REDACTED] 2025-06-03 08:12:12,016
[REDACTED] 2025-06-03 09:45:44,198
[REDACTED] 2025-06-03 11:31:08,914
[REDACTED] 2025-06-03 13:35:09,127
[REDACTED] 2025-06-03 15:12:22,771
[REDACTED] 2025-06-03 17:07:27,365
[REDACTED] 2025-06-03 18:55:27,159
[REDACTED] 2025-06-03 20:36:30,714
[REDACTED] 2025-06-03 22:30:24,300
[REDACTED] 2025-06-04 00:46:15,650
[REDACTED] 2025-06-04 02:40:09,902
[REDACTED] 2025-06-04 04:37:05,981
[REDACTED] 2025-06-04 06:12:36,854
[REDACTED] 2025-06-04 07:54:19,516
[REDACTED] 2025-06-04 09:38:54,252
[REDACTED] 2025-06-04 11:35:34,746
[REDACTED] 2025-06-04 13:19:11,169
[REDACTED] 2025-06-04 15:10:09,320
[REDACTED] 2025-06-04 16:46:29,451
[REDACTED] 2025-06-05 10:54:00,421
[REDACTED] 2025-06-05 12:41:49,167
[REDACTED] 2025-06-05 14:36:50,218
[REDACTED] 2025-06-05 16:37:24,671
[REDACTED] 2025-06-05 18:39:55,324
[REDACTED] 2025-06-05 20:41:53,408
[REDACTED] 2025-06-06 03:14:32,613
[REDACTED] 2025-06-06 05:27:38,586
[REDACTED] 2025-06-06 07:20:12,388
[REDACTED] 2025-06-06 09:17:20,735
[REDACTED] 2025-06-06 11:30:15,449
[REDACTED] 2025-06-06 13:23:49,431
[REDACTED] 2025-06-06 15:21:12,685
[REDACTED] 2025-06-06 17:10:25,922
[REDACTED] 2025-06-06 19:30:35,334
[REDACTED] 2025-06-06 21:17:07,349
[REDACTED] 2025-06-06 23:27:26,388
[REDACTED] 2025-06-07 01:07:09,374
[REDACTED] 2025-06-07 03:33:42,778
[REDACTED] 2025-06-07 05:15:15,442
[REDACTED] 2025-06-07 07:21:57,055
[REDACTED] 2025-06-07 09:10:49,671
[REDACTED] 2025-06-07 11:10:10,013
[REDACTED] 2025-06-07 13:08:19,036
[REDACTED] 2025-06-07 14:53:44,249
[REDACTED] 2025-06-07 17:07:20,829
[REDACTED] 2025-06-08 01:51:46,809
対策
対策として2つの方法を紹介します。
1つ目の対策は、設定ファイルのチューニングです。findtime
やmaxretry
の値を小さくし、bantime
の値を伸ばすことで、アクセスを低減することができます。
2つ目の対策は、recidiveジェイルを併用することで、過去に何度もBanされているIPアドレスを長期的にBanすることができます。
recidiveジェイルの使用する場合は、/etc/fail2ban/jail.local
ファイルに以下のような設定を記述します。以下の例では24時間以内に3回以上Banされていたら1週間Banにします。
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
findtime = 86400
maxretry = 3
bantime = 604800
backend = auto
設定変更後、サービスの再起動などを行います。
recidiveジェイルは、単一のジェイルに限らず、複数のジェイルによってBanされた履歴も含めて判断します。
ログの分析2
次のパターンは、IPアドレスのレンジを変えることによる大量アクセスのパターンです。
[REDACTED].12 2025-06-04 12:20:16,196
[REDACTED].16 2025-06-04 12:19:38,014
[REDACTED].17 2025-06-04 12:18:06,569
[REDACTED].21 2025-06-04 12:16:29,715
[REDACTED].24 2025-06-06 09:40:30,168
[REDACTED].29 2025-06-06 09:36:16,022
[REDACTED].32 2025-06-06 09:39:43,984
[REDACTED].35 2025-06-06 09:38:17,743
[REDACTED].41 2025-06-07 16:51:15,552
[REDACTED].45 2025-06-07 16:49:38,100
[REDACTED].46 2025-06-07 16:53:29,273
[REDACTED].49 2025-06-07 16:52:46,999
[REDACTED].68 2025-06-01 11:13:22,790
[REDACTED].74 2025-06-01 11:10:59,764
[REDACTED].76 2025-06-01 11:16:25,621
[REDACTED].79 2025-06-01 11:15:23,285
広範囲によるアクセスを低減するためには、ファイアウォールでIPアドレスの範囲を制限することが有効ですが、正当なユーザまで遮断するリスクがあるため、実際にシステム運用を行なっている場合などは十分に注意する必要があります。
なお、IPアドレスの範囲で制限を行う場合、Linuxのファイアウォールで制限を行う方法と、クラウドなどのプラットフォーム側のファイアウォールで制限を行う方法が可能ですが、それぞれ動作は異なります。
例えば、iptablesで制限する場合、iptablesはTCPのパケットを基に制御していることから、TCPのパケットは仮想マシンまで届いています。しかし、プラットフォーム側のファイアウォールなどを利用する場合は、仮想マシンまでパケットが届きません。
対策
ipset
を活用して、IPアドレスの範囲で制限を行う例を以下に紹介します。
ipset
は、IPアドレスをグループとして管理することができるツールです。ipset
をインストールしていない場合は、以下のコマンドを実行して、インストールを行います。
$ sudo apt install ipset
事前に以下のコマンドを実行して、IPアドレスのデータセットを作成します。blacklist
はセットする名前になるため、任意で指定します。(timeoutの値は以下の場合、1日を指定)
$ ipset create blacklist hash:net timeout 86400
以下のコマンドを実行して、送信元がblacklist
の場合は全てDROP
するルールを追加します。
$ sudo iptables -I INPUT -m set --match-set blacklist src -j DROP
/etc/fail2ban/action.d/ipset-blacklist.conf
ファイルを作成します。IPアドレスの範囲は、CIDRで指定します。
[Definition]
actionstart = ipset create blacklist hash:net timeout 86400 -exist
actionstop = ipset flush blacklist
actioncheck = ipset list blacklist
actionban = ipset add blacklist N.N.N.0/24 timeout 86400 -exist
actionunban = ipset del blacklist N.N.N.0/24
[Init]
name = blacklist
/etc/fail2ban/jail.local
ファイルの設定するジェイルに関するaction
の値を変更します。
[nginx-abuse-access]
enabled = true
filter = nginx-abuse-access
port = http,https
logpath = /var/log/nginx/access.log
findtime = 60
maxretry = 10
bantime = 120
action = ipset-blacklist[name=abuse, port="http,https", protocol=tcp]
上記設定完了後、サービスの再起動などを行い設定を反映します。以降、アクセス元のIPアドレスが特定のネットワークからのアクセスであり、条件を満たす場合はblacklist
に追加されることで制限されます。
制限が行われると、以下のようなN.N.N.0/24 timeout 60
の行が追加されるため、blacklist
に登録されたことが確認できます。
$ ipset list blacklist
Name: blacklist
Type: hash:net
Revision: 7
Header: family inet hashsize 1024 maxelem 65536 timeout 60 bucketsize 12 initval 0xade94787
Size in memory: 520
References: 1
Number of entries: 0
Members:
N.N.N.0/24 timeout 60
ipsetを併用する場合は、/etc/fail2ban/jail.local
ファイルで設定したbantime
の値と、/etc/fail2ban/action.d/ipset-blacklist.conf
ファイルで指定するtimeout
の値は揃えるのが望ましいです。なぜならば、bantime
の値が長くても、ipsetのtimeout
の値が短かいと、適切にブロックできないためです。
プラットフォーム側のファイアウォールについては、Google Cloudを例にすると、VPCファイアウォールのファイアウォール ルールより、ソースフィルタのIP 範囲を制限することで、一括でアクセスを低減できます。
おわりに
fail2ban導入後、また無料枠で利用することができました。
パブリッククラウドのサービスを利用してWebサーバを公開している場合、インバウンドの通信は無料であるものの、アウトバウンドの通信はデータ量に応じて課金されることがほとんどです。
従ってクラウド環境においては、不正アクセスを防ぐことは意図しない課金を抑制し、結果的にコスト削減にもつながります。