前置き
サーバへの余計なトラフィックを遮断します。
サーバへの攻撃があった場合、「ポートを非標準の番号へ移動して防御する」という記述がよそ様のサイトに散見されますが、敵も然る者引っ掻くもの。
どのサービスもポートを移動した程度では一時しのぎにしかなりません。
特に公開の Well known を使うサービスでは、そもそもポートを移動させることができません。
前に SSHGuard のインストールを書きましたが、今回は Fail2ban です。
Fail2ban は sshd ポートだけでなく、様々な攻撃 (プロトコル) に対して防御します。
0. インストール要件
OS AlmaLinux release 9.4 (Seafoam Ocelot) 本稿記述時の最新版 防御ソフト Fail2ban 1.0.2 本稿記述時の最新版 firewalld 1.3.4 AlmaLinux 9.4 に付属のもの テスト用
サーバインタプリタ Python 3.9.18 フレームワーク Flask 3.0.3 本稿記述時の最新版 ログファイル /tmp/access_log ここから Flask のアクセス状況を Fail2ban に読み込ませる
1. firewalld を確認
$ LANG=C dnf info firewalld | egrep 'Name|Version|Release'
Name : firewalld Version : 1.3.4 Release : 1.el9
2. Fail2ban のインストールと起動
2-1. EPEL をインストールする。
$ su2-2. Fail2ban をインストールする。
# dnf install epel-release
# LANG=C dnf info epel-release \
| egrep 'Name|Version|Release'
Name : epel-release Version : 9 Release : 7.el9
# dnf install fail2ban2-3. Fail2ban の動作ログの出力先を確認する。
# LANG=C dnf info fail2ban \
| egrep 'Name|Version|Release'
Name : fail2ban Version : 1.0.2 Release : 12.el9
# cat /etc/fail2ban/fail2ban.conf \2-4. 組み込み済みのフィルタを確認する。(今回は使わない)
| grep 'logtarget ='
# ls -1 /var/log/ | grep fail2ban.log | wc -l
logtarget = /var/log/fail2ban.log
0
# ls -1 /etc/fail2ban/filter.d/ | head -3 ; \2-5. テスト用 Web サーバ向けにフィルタを作成する。(ここでは DoS を Ban するだけなので複雑にしない)
echo ' --- 省略 ---' ; \
ls -1 /etc/fail2ban/filter.d/ | tail -3
3proxy.conf apache-auth.conf apache-badbots.conf --- 省略 --- xinetd-fail.conf znc-adminlog.conf zoneminder.conf
# cat << EOF > /etc/fail2ban/filter.d/flask-ddos.conf2-6. 作成したフィルタを使って監視条件を定義する。
[Definition]
failregex = ^<HOST> - .+(GET|POST).*$
ignoreregex = \.(?i)(jpg|jpeg|gif|png|pdf|js|txt)
EOF
# cat /etc/fail2ban/filter.d/flask-ddos.conf
*1「ignoreregex = …」は対象外とする文字列を記述する。(今回はテキトー)
[Definition] failregex = ^<HOST> - .+(GET|POST).*$ ignoreregex = \.(?i)(jpg|jpeg|gif|png|pdf|js|txt) *1
(?i) は後に続く文字列の大文字・小文字を区別しない指定。
# cat << EOF > /etc/fail2ban/jail.d/flask-8080.conf2-7. Fail2ban を起動する。
[flask-8080]
filter = flask-ddos
enabled = true
port = 8080
logpath = /tmp/access_log
backend = polling
maxretry = 3
findtime = 1s
bantime = 1h
EOF
# cat /etc/fail2ban/jail.d/flask-8080.conf
*2「filter = …」で上記 2-5 で作成したフィルタを指定している。
[flask-8080] filter = flask-ddos *2 enabled = true port = 8080 *3 logpath = /tmp/access_log backend = polling maxretry = 3 findtime = 1s bantime = 1h *4
*3「port = …」で遮断するポート番号を指定する。Fail2ban はポートを監視しない。(普通はやらないが) 全く関係ないポートを遮断することもできる。
*4遮断期間を 1h (1 時間) としている。永久にしたければ -1 を指定する。
# systemctl enable fail2ban
# systemctl start fail2ban
Created symlink /etc/systemd/system/multi-user.target.wants/fail2ban.service → /usr/lib/systemd/system/fail2ban.service.
# systemctl status fail2ban
# cat /var/log/fail2ban.log
● fail2ban.service - Fail2Ban Service Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: disabled) Active: active (running) since Sun 2024-06-23 21:11:10 JST; 6s ago Docs: man:fail2ban(1) Process: 56842 ExecStartPre=/bin/mkdir -p /run/fail2ban (code=exited, status=0/SUCCESS) Main PID: 56843 (fail2ban-server) Tasks: 5 (limit: 7752) Memory: 11.0M CPU: 148ms CGroup: /system.slice/fail2ban.service └-56843 /usr/bin/python3 -s /usr/bin/fail2ban-server -xf start 6月 23 21:11:10 MyHost systemd[1]: Starting Fail2Ban Service... 6月 23 21:11:10 MyHost systemd[1]: Started Fail2Ban Service. 6月 23 21:11:10 MyHost fail2ban-server[56843]: Server ready
# fail2ban-client status*5Fail2ban はブロックリストの管理に SQLite3 を使用している。初期状態にしたければ、このファイルを削除してから Fail2ban を起動する。
2024-06-23 21:11:10,655 fail2ban.server [56843]: INFO -------------------------------------------------- 2024-06-23 21:11:10,655 fail2ban.server [56843]: INFO Starting Fail2ban v1.0.2 2024-06-23 21:11:10,656 fail2ban.observer [56843]: INFO Observer start... 2024-06-23 21:11:10,662 fail2ban.database [56843]: INFO Connected to fail2ban persistent database '/var/lib/fail2ban/fail2ban.sqlite3' *5 2024-06-23 21:11:10,664 fail2ban.database [56843]: WARNING New database created. Version '4' 2024-06-23 21:11:10,664 fail2ban.jail [56843]: INFO Creating new jail 'flask-8080' 2024-06-23 21:11:10,665 fail2ban.jail [56843]: INFO Jail 'flask-8080' uses poller {} 2024-06-23 21:11:10,665 fail2ban.jail [56843]: INFO Initiated 'polling' backend 2024-06-23 21:11:10,666 fail2ban.filter [56843]: INFO maxRetry: 3 2024-06-23 21:11:10,667 fail2ban.filter [56843]: INFO findtime: 1 2024-06-23 21:11:10,667 fail2ban.actions [56843]: INFO banTime: 3600 2024-06-23 21:11:10,667 fail2ban.filter [56843]: INFO encoding: UTF-8 2024-06-23 21:11:10,667 fail2ban.filter [56843]: INFO Added logfile: '/tmp/access_log' (pos = 0, hash = 3bdda2956f60e31c7ec7f4fa328a3e890b36bb9f) 2024-06-23 21:11:10,685 fail2ban.jail [56843]: INFO Jail 'flask-8080' started
# fail2ban-client status flask-8080
Status |- Number of jail: 1 `- Jail list: flask-8080
*6まだ相手ホストをブロックしていないのでリスト Banned IP list が空になっている。
Status for the jail: flask-8080 |- Filter | |- Currently failed: 0 | |- Total failed: 0 | `- File list: /tmp/access_log `- Actions |- Currently banned: 0 |- Total banned: 0 `- Banned IP list: *6# exit $
$ python --version
$ pip install flask
Python 3.9.18
$ pip show flask
*7who は Flask を実行するユーザのホームディレクトリ。
Name: Flask Version: 3.0.3 Summary: A simple framework for building complex web applications. Home-page: Author: Author-email: License: Location: /home/who/.local/lib/python3.9/site-packages *7 Requires: click, Jinja2, blinker, itsdangerous, Werkzeug, importlib-metadata Required-by:
4. 実証
別のホストから疑似攻撃してみる。
意図的に DoS を発生させるので、よそのホストには実行しないこと。
間違えて管轄外のホストを攻撃すると、そのホストだけでなく自分のプロバイダからもブロックされる恐れがあります。
攻撃相手の IP アドレスをよく確認して実行する。 可能なら、インターネットを切断して LAN だけで実行する。
攻/防 ホスト名 IP アドレス ポート 攻撃側 Villain 10.2.1.99 - 防御側 MyHost 10.2.1.10 8080
4-1. テストサーバを実行 (バナー表示のみ)
• Fail2ban に渡すため /tmp/access_log にアクセスログを記録する。
$ python << EOF 2>&1 | tee /tmp/access_log
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, Fail2ban.\n'
app.run(host='10.2.1.10', port=8080)
EOF
* Serving Flask app '<stdin>' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://10.2.1.10:8080 Press CTRL+C to quit 10.2.1.99 - - [23/Jun/2024 22:27:27] "GET / HTTP/1.1" 200 - 10.2.1.99 - - [23/Jun/2024 22:27:27] "GET / HTTP/1.1" 200 - 10.2.1.99 - - [23/Jun/2024 22:27:27] "GET / HTTP/1.1" 200 - 10.2.1.99 - - [23/Jun/2024 22:27:27] "GET / HTTP/1.1" 200 -
4-2. 疑似攻撃
4-2-1. MyHost の 8080 番ポートに接続可能か確認する。
Villain$ telnet 10.2.1.10 80804-2-2. MyHost の 8080 番ポートに DoS をかけてみる。
Trying 10.2.1.10... Connected to 10.2.1.10. Escape character is '^]'. ^C Connection closed by foreign host.
← 接続できている。
jail.d/flask-8080.conf で 4 回以上/1 秒 のアクセスで Ban となる設定にしている。(maxretry = 3)
Ban されるよう、連続 4 回のアクセスを実施する。
Villain$ for i in {1..4} ; do curl http://10.2.1.10:8080/ ; done
正常にアクセスできている。
Hello, Fail2ban. Hello, Fail2ban. Hello, Fail2ban. Hello, Fail2ban.
4-2-3. MyHost の 8080 番ポートに接続可能か確認する。
Villain$ telnet 10.2.1.10 8080
Trying 10.2.1.10... telnet: connect to address 10.2.1.10: Connection refused
← 接続できなくなった。
4-3. 防御の様子
MyHost$ su
MyHost# cat /var/log/fail2ban.log | tail
短期間に 4 回のアクセスがあったので、アクセス元を 3600 秒の Ban (banTime = 1h) にした。
2024-06-23 22:27:27,556 fail2ban.filter [56843]: INFO [flask-8080] Found 10.2.1.99 - 2024-06-23 22:27:27
2024-06-23 22:27:27,557 fail2ban.filter [56843]: INFO [flask-8080] Found 10.2.1.99 - 2024-06-23 22:27:27
2024-06-23 22:27:27,557 fail2ban.filter [56843]: INFO [flask-8080] Found 10.2.1.99 - 2024-06-23 22:27:27
2024-06-23 22:27:27,557 fail2ban.filter [56843]: INFO [flask-8080] Found 10.2.1.99 - 2024-06-23 22:27:27
2024-06-23 22:27:27,756 fail2ban.actions [56843]: NOTICE [flask-8080] Ban 10.2.1.99
このとき Fail2ban では以下の状態になっている。
MyHost# fail2ban-client status flask-8080
Status for the jail: flask-8080 |- Filter | |- Currently failed: 0 | |- Total failed: 4 | `- File list: /tmp/access_log `- Actions |- Currently banned: 1 |- Total banned: 1 `- Banned IP list: 10.2.1.99
4-4. ブロックの解除
MyHost# fail2ban-client set flask-8080 unbanip 10.2.1.99MyHost# fail2ban-client status flask-8080MyHost# exitCurrently banned と Banned IP list はクリアされるが、Total banned は変化しない。
Status for the jail: flask-8080 |- Filter | |- Currently failed: 0 | |- Total failed: 4 | `- File list: /tmp/access_log `- Actions |- Currently banned: 0 |- Total banned: 1 `- Banned IP list:
MyHost$
5. Ban 条件の作成 (Web サーバの認証失敗のケース)
Fail2ban は、各種ログから正規表現にマッチしたアクセス元を遮断するので、
正規表現とログを用意すれば、様々なポートを防御することができる。
• ログの種類は以下のいずれかを選択可能。(backend = で指定する)
- polling (=ファイル)
- pyinotify (=python によるファイル変更の監視)
- gamin (=gamin によるファイル変更の監視)
- systemd (=ジャーナル)
ここでは BASIC 認証に対するブルートフォースを想定して Ban 条件を作成する。
サーバの IP アドレス等は上記 4 と同じ。
5-1. テスト用に Flask の BASIC 認証モジュールをインストールする。
MyHost$ pip install flask_httpauth
MyHost$ pip show flask_httpauth
*8who は Flask を実行するユーザのホームディレクトリ。
Name: Flask-HTTPAuth Version: 4.8.0 Summary: HTTP authentication for Flask routes Home-page: https://github.com/miguelgrinberg/flask-httpauth Author: Miguel Grinberg Author-email: miguel.grinberg@gmail.com License: Location: /home/who/.local/lib/python3.9/site-packages *8 Requires: flask Required-by:
5-2. テストサーバを実行 (BASIC 認証)
• Fail2ban に渡すため /tmp/access_log にアクセスログを記録する。
MyHost$ python << EOF 2>&1 | tee /tmp/access_log
from flask import Flask
from flask_httpauth import HTTPBasicAuth
from markupsafe import escape
auth = HTTPBasicAuth()
@auth.get_password
def member_auth(name):
return {
'user1':'password',
}.get(name)
app = Flask(__name__)
@app.route('/restricted/')
@auth.login_required
def hello():
name = auth.username()
return f'Hello, {escape(name)}.\n'
app.run(host='10.2.1.10', port=8080)
EOF
* Serving Flask app '<stdin>' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://10.2.1.10:8080 Press CTRL+C to quit 10.2.1.99 - - [25/Jun/2024 15:32:23] "GET /restricted/ HTTP/1.1" 200 - 10.2.1.99 - - [25/Jun/2024 15:32:23] "GET /restricted/ HTTP/1.1" 200 - 10.2.1.99 - - [25/Jun/2024 15:32:23] "GET /restricted/ HTTP/1.1" 200 - 10.2.1.99 - - [25/Jun/2024 15:33:17] "GET /restricted/ HTTP/1.1" 401 - 10.2.1.99 - - [25/Jun/2024 15:33:17] "GET /restricted/ HTTP/1.1" 401 - 10.2.1.99 - - [25/Jun/2024 15:33:17] "GET /restricted/ HTTP/1.1" 401 -
5-3. 成功と失敗のログから正規表現を作成する。
A. BASIC 認証を成功させるアクセス:
Villain$ curl http://user1:password@10.2.1.10:8080/restricted/
結果:
B. BASIC 認証を失敗させるアクセス: (Fail2ban ではこちらを検出する)
応答メッセージ Hello, user1. /tmp/access_log 10.2.1.99 - - [25/Jun/2024 00:23:28] "GET /restricted/ HTTP/1.1" 200 -
上記 A, B から、BASAIC 認証の失敗を検出する正規表現は以下にすれば良いということが分かる。Villain$ curl http://user1:12345678@10.2.1.10:8080/restricted/
結果:
応答メッセージ Unauthorized Access /tmp/access_log 10.2.1.99 - - [25/Jun/2024 00:23:35] "GET /restricted/ HTTP/1.1" 401 -
• 本当はダブルクォート (") も指定したいが、fail2ban の regex 処理にバグ (?) があって、ダブルクォートを含めると正常に動作しない。
failregex = ^<HOST> - .+(GET|POST).+ 401 .*$
(fail2ban-regex -D ではダブルクォートも matched になるが実際はヒットしないので、ま·ぎ·ら·わ·し·い)
•「POST」はおまけ。普通は付けるじゃろ。
5-4. テスト用 BASIC 認証向けに監視条件を作成する。
5-4-1. 上記 5-3 で得た正規表現でフィルタを作成する。
5-4-2. 作成したフィルタを使って監視条件を追記する。MyHost# cat << EOF > /etc/fail2ban/filter.d/flask-auth.conf[Definition]
failregex = ^<HOST> - .+(GET|POST).+ 401 .*$
ignoreregex = \.(?i)(jpg|jpeg|gif|png|pdf|js|txt)
EOF
MyHost# cat /etc/fail2ban/filter.d/flask-auth.conf
[Definition] failregex = ^<HOST> - .+(GET|POST).+ 401 .*$ ignoreregex = \.(?i)(jpg|jpeg|gif|png|pdf|js|txt)
MyHost# cat << EOF >> /etc/fail2ban/jail.d/flask-8080.conf# ---
[flask-auth]
filter = flask-auth
enabled = true
port = 8080
logpath = /tmp/access_log
backend = polling
maxretry = 2
findtime = 10s
bantime = 1h
EOF
MyHost# cat /etc/fail2ban/jail.d/flask-8080.conf上記 2-6 で作成した flask-8080.conf に Ban 条件 [flask-auth] が追加された。
[flask-8080] filter = flask-ddos enabled = true port = 8080 logpath = /tmp/access_log backend = polling maxretry = 3 findtime = 1s bantime = 1h # --- [flask-auth] filter = flask-auth enabled = true port = 8080 logpath = /tmp/access_log backend = polling maxretry = 2 findtime = 10s bantime = 1h
5-5. Fail2ban の再起動と防御の確認
5-5-1. Fail2ban を再起動する。
MyHost# systemctl restart fail2ban5-5-2. 防御の確認をする。
MyHost# systemctl status fail2ban
MyHost# cat /var/log/fail2ban.log
● fail2ban.service - Fail2Ban Service Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: disabled) Active: active (running) since Tue 2024-06-25 14:04:46 JST; 20s ago Docs: man:fail2ban(1) Process: 62331 ExecStartPre=/bin/mkdir -p /run/fail2ban (code=exited, status=0/SUCCESS) Main PID: 62333 (fail2ban-server) Tasks: 7 (limit: 7752) Memory: 13.1M CPU: 138ms CGroup: /system.slice/fail2ban.service └-62333 /usr/bin/python3 -s /usr/bin/fail2ban-server -xf start 6月 25 14:04:46 MyHost systemd[1]: Starting Fail2Ban Service... 6月 25 14:04:46 MyHost systemd[1]: Started Fail2Ban Service. 6月 25 14:04:46 MyHost fail2ban-server[62333]: Server ready
*9flask-8080, flask-auth の二つが動作を開始した。
2024-06-25 14:11:25,857 fail2ban.server [62376]: INFO -------------------------------------------------- 2024-06-25 14:11:25,857 fail2ban.server [62376]: INFO Starting Fail2ban v1.0.2 2024-06-25 14:11:25,858 fail2ban.observer [62376]: INFO Observer start... 2024-06-25 14:11:25,860 fail2ban.database [62376]: INFO Connected to fail2ban persistent database '/var/lib/fail2ban/fail2ban.sqlite3' 2024-06-25 14:11:25,861 fail2ban.jail [62376]: INFO Creating new jail 'flask-8080' 2024-06-25 14:11:25,861 fail2ban.jail [62376]: INFO Jail 'flask-8080' uses poller {} 2024-06-25 14:11:25,861 fail2ban.jail [62376]: INFO Initiated 'polling' backend 2024-06-25 14:11:25,862 fail2ban.filter [62376]: INFO maxRetry: 3 2024-06-25 14:11:25,862 fail2ban.filter [62376]: INFO findtime: 1 2024-06-25 14:11:25,862 fail2ban.actions [62376]: INFO banTime: 3600 2024-06-25 14:11:25,862 fail2ban.filter [62376]: INFO encoding: UTF-8 2024-06-25 14:11:25,863 fail2ban.filter [62376]: INFO Added logfile: '/tmp/access_log' (pos = 0, hash = 3bdda2956f60e31c7ec7f4fa328a3e890b36bb9f) 2024-06-25 14:11:25,863 fail2ban.jail [62376]: INFO Creating new jail 'flask-auth' 2024-06-25 14:11:25,863 fail2ban.jail [62376]: INFO Jail 'flask-auth' uses poller {} 2024-06-25 14:11:25,863 fail2ban.jail [62376]: INFO Initiated 'polling' backend 2024-06-25 14:11:25,864 fail2ban.filter [62376]: INFO maxRetry: 2 2024-06-25 14:11:25,864 fail2ban.filter [62376]: INFO findtime: 10 2024-06-25 14:11:25,864 fail2ban.actions [62376]: INFO banTime: 3600 2024-06-25 14:11:25,864 fail2ban.filter [62376]: INFO encoding: UTF-8 2024-06-25 14:11:25,864 fail2ban.filter [62376]: INFO Added logfile: '/tmp/access_log' (pos = 0, hash = 3bdda2956f60e31c7ec7f4fa328a3e890b36bb9f) 2024-06-25 14:11:25,865 fail2ban.jail [62376]: INFO Jail 'flask-8080' started *9 2024-06-25 14:11:25,866 fail2ban.jail [62376]: INFO Jail 'flask-auth' started *9
5-5-2-1. 正常/異常アクセスをそれぞれ実施する。
5-5-2-1-1. 正常アクセスを実施する。(1秒以内に 3 回以下のアクセスなので Ban の対象にならない: maxRetry = 3)5-5-2-2. Fail2ban のログとステータスを確認する。
Villain$ for i in {1..3} ; do curl http://user1:password@10.2.1.10:8080/restricted/ ; done
5-5-2-1-2. 異常アクセスを実施する。(10秒以内に 3 回の認証失敗なので Ban の対象になる: maxRetry = 2)
Villain$ for i in {1..3} ; do curl http://user1:12345678@10.2.1.10:8080/restricted/ ; done
5-5-2-2-1. Fail2ban のログを確認する。
5-5-2-2-2. Fail2ban のステータス (サマリ) を確認する。MyHost# cat /var/log/fail2ban.log• 最初の 3 回の攻撃は flask-8080 で検出されたものの、しきい値以下のアクセス回数だったので Ban の対象にならなかった。
2024-06-25 15:32:23,316 fail2ban.filter [62376]: INFO [flask-8080] Found 10.2.1.99 - 2024-06-25 15:32:23 2024-06-25 15:32:23,317 fail2ban.filter [62376]: INFO [flask-8080] Found 10.2.1.99 - 2024-06-25 15:32:23 2024-06-25 15:32:23,318 fail2ban.filter [62376]: INFO [flask-8080] Found 10.2.1.99 - 2024-06-25 15:32:23
2024-06-25 15:33:17,565 fail2ban.filter [62376]: INFO [flask-auth] Found 10.2.1.99 - 2024-06-25 15:33:17 2024-06-25 15:33:17,566 fail2ban.filter [62376]: INFO [flask-auth] Found 10.2.1.99 - 2024-06-25 15:33:17 2024-06-25 15:33:17,567 fail2ban.filter [62376]: INFO [flask-auth] Found 10.2.1.99 - 2024-06-25 15:33:17 2024-06-25 15:33:17,569 fail2ban.actions [62376]: NOTICE [flask-auth] Ban 10.2.1.99
• 次の 3 回の攻撃は flask-auth で検出され、しきい値を超過するアクセス回数だったので Ban の対象になった。(Ban 10.2.1.99)
MyHost# fail2ban-client status
Status |- Number of jail: 2 `- Jail list: flask-8080, flask-auth
MyHost# fail2ban-client status flask-8080
Status for the jail: flask-8080 |- Filter | |- Currently failed: 0 | |- Total failed: 3 | `- File list: /tmp/access_log `- Actions |- Currently banned: 0 |- Total banned: 0 `- Banned IP list:
MyHost# fail2ban-client status flask-auth
Status for the jail: flask-auth |- Filter | |- Currently failed: 0 | |- Total failed: 3 | `- File list: /tmp/access_log `- Actions |- Currently banned: 1 |- Total banned: 1 `- Banned IP list: 10.2.1.99