サーバーセキュリティ (Fail2ban)

ネット上に公開されているサーバーは常に何らかのスキャンを受けています。ほとんどは無害なものが多いと思われますが、中には踏み台探しや、設定ミスによる API キーやデータベースのパスワード漏洩等を狙ったアクセスなどもあります。

このブログでは開発手法に関する記事が多いためか、そのようなアクセスがだんだん増えてきましたので何らかの対応をする必要性が出てきました。

結論から言うと、これらのアクセスを完全になくすことはできませんが、ある程度制限することで不要な負荷やリスクを減らすことは可能です。今回は備忘録も兼ねて「Fail2ban」というパケットフィルタリングツールを使用した動的防御の設定方法と、Apache・UFW などを組み合わせた多層防御の構成方法をメモとして残します。Fail2ban は VPS だけではなく、AWS などでも使用可能です。

この記事ではおもに

  • Fail2ban の基本設定
  • WordPress ログイン攻撃の遮断方法
  • .env スキャン対策
  • メールブルートフォース防御
  • recidive の実運用設定

などについてご説明しています。

Fail2ban とは

Fail2ban は、サーバーやシステムのログを監視して攻撃パターンを検出し、nftables などのファイアウォールに自動で Ban ルールを追加することで、不正アクセスを抑止するセキュリティツールです。

デフォルト設定では SSH の不正アクセスを検知できるようになっていますが、Web サーバーやメールサーバーに限らず、以下の条件を満たすログファイルであれば、どのようなものでも監視することが可能です。

  • テキスト形式のファイルであること
  • IP アドレスが記録されていること
  • 正規表現で攻撃パターンを判別できること

次のセクションからはインストールと設定方法をご説明します。

※このブログサイトにはすでに設定済みなので、記事作成用に WSL2 上の Ubuntu 24.04 を使用してご説明しています。

Fail2ban のインストール

以下のコマンドをコンソール画面で入力して Fail2ban をインストールします。

sudo apt update
sudo apt install -y fail2ban

インストールが完了すると以下の位置にフォルダとファイルが作成されます。「filter.d」フォルダ内には正規表現の検知条件が書かれたフィルターファイルがいくつも登録されています。必要であれば自分でオリジナルの検知用ファイルを追加することもできます。

「jail.conf」設定ファイルで使用する検知用ファイルを有効化できますが、バージョンアップなどで更新される可能性がありますので、新たに「jail.local」というファイルを作成してこちらに設定を記述していきます。

etc/
└─ fail2ban/
    ├── action.d/
    ├── fail2ban.d/
    ├── filter.d/ ← ※検知用のファイルが保存されています
    ├── jail.d/
    ├── fail2ban.conf
    ├── jail.conf
    ├── jail.local ← ※新たにファイルを作成します
    ├── paths-arch.conf
    ├── paths-common.conf
    ├── paths-debian.conf
    └── paths-opensuse.conf

フィルターファイルは以下のように記述されています。各設定項目の意味は次の通りです。どちらも正規表現で記述できます。

  • failregex・・・検知条件を記述します。
  • ignoreregex・・・除外条件を記述します。
    検知条件にマッチしてもこの条件にマッチした場合は除外されます。
[Definition]
failregex = ^<HOST> - - \[.*\] "(GET|POST) (?:/[^"]*)?wp-login\.php(?:\?.*)? HTTP/[^"]+" 403 .*
ignoreregex =

「jail.local」ファイルの記述例は以下のようになります。各セクションと設定値の意味については後ほどご説明いたします。

[DEFAULT]
backend  = auto
maxretry = 3
findtime = 10m
bantime  = 1h
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 7d
banaction         = nftables

ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
backend = systemd

[apache-botsearch]
enabled  = true
port     = http,https
filter   = apache-botsearch
logpath  = /var/log/apache2/access.log
maxretry = 5
findtime = 5m
bantime  = 24h
Fail2ban の設定方法

このセクションからは実際の攻撃パターン毎の設定方法についてご説明していきます。Fail2ban の設定方法だけではなく、WordPress や Apache との連携についてもご説明します。

・jail.local ファイルのひな型を作成

まずは「jail.local」というファイルを作成して、以下の設定を書き込みます。[DEFAULT] セクションは各セクションのデフォルト値を定義しています。各セクションで特に指定がない場合はこのセクションの設定値が使用されます。

[DEFAULT]
backend  = auto
maxretry = 3
findtime = 10m
bantime  = 1h
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 7d
banaction         = nftables

各設定項目の意味は以下のようになっています。

設定項目意味
backendログの監視方法を設定します。以下の種類があります。
auto : 自動判別 (通常はこれでOK)
systemd : systemd-journald から直接読む
polling : ファイルを定期スキャン
pyinotify : inotifyでリアルタイム監視
maxretryfindtime 内に maxretry 回失敗すると Ban
findtime失敗回数をカウントする時間範囲
bantime初回 Ban 時間 以下の指定が可能です。
・指定なし : 秒 (bantime = 600 の場合は 10 分)
m : 分
h : 時間
d : 日
w : 週
mo : 月 (概算)
y : 年
-1 または permanent : 永久 Ban (手動解除まで)
bantime.increment再遮断時に Ban 時間を延長する (true = 有効)
bantime.factor再遮断時の延長倍率 (例:1h → 2h → 4h → 8h …)
bantime.maxtimeBan の最大時間 (これ以上は延長しない)
banactionIP の遮断方法を設定します。以下の種類が代表的です。
nftables : 近代的な Linux 標準ファイアウォール
iptables-multiport : 従来の iptables を使用

以下の設定も追加して自分が Ban されないようにします。必要に応じてリモート設定用の IP アドレスなども除外しておきます。

# ローカルや管理用 IP を誤って Ban しないよう除外する
# 複数指定する場合はスペース区切り
ignoreip = 127.0.0.1/8 ::1

一応念のために SSH のセクション (フィルター) も追加しておきます。

[sshd]
enabled = true
backend = systemd

設定が完了したら以下のコマンドを実行して設定ファイルのチェックを行います。

sudo fail2ban-client -t

ファイルの文字コードなどが合っていないと以下のようなエラーが出ることがあります。

user01@DESKTOP-CQEA49P:~$ sudo fail2ban-client -t
2026-02-20 11:46:41,130 fail2ban                [2862]: ERROR   Failed during configuration: File contains no section headers.
file: '/etc/fail2ban/jail.local', line: 1
'\ufeff[DEFAULT]\n'
2026-02-20 11:46:41,130 fail2ban                [2862]: ERROR   ERROR: test configuration failed

そのような場合は設定ファイル (jail.local) を削除してから、以下のコマンドをターミナル画面にコピー&ペーストしてください。(ターミナル画面でマウスを右クリックするとペースト可能です。)

sudo tee /etc/fail2ban/jail.local > /dev/null << 'EOF'
[DEFAULT]
backend  = auto
maxretry = 3
findtime = 10m
bantime  = 1h
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 7d
banaction         = nftables

ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
backend = systemd
EOF

特に問題なければ Fail2ban を再起動します。

sudo systemctl restart fail2ban

「sudo fail2ban-client status」コマンドを実行すると現在のステータスが表示されます。

user01@DESKTOP-CQEA49P:~$ sudo fail2ban-client status
Status
|- Number of jail:      1
`- Jail list:   sshd
user01@DESKTOP-CQEA49P:~$
・WordPress へのログインを試みるアクセスを検知

WordPress へのログインを試みるアクセスを検知して遮断するように設定します。Apache のログファイル (/var/log/apache2/access.log) を見てみると、私の場合は以下のように記録されていました。(実際の IP アドレスはマスクしています。) 短時間に何回もログインを試みています。

xxx.xxx.xxx.xxx - - [01/Feb/2026:13:53:55 +0900] "GET /wp-login.php HTTP/1.1" 302 3419 "https://twitter.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:53:56 +0900] "GET /blog/wp-login.php HTTP/1.1" 403 486 "https://twitter.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:29 +0900] "POST /wp-login.php HTTP/1.1" 302 920 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.88 Safari/537.36"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:30 +0900] "GET /blog/wp-login.php HTTP/1.1" 403 486 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.88 Safari/537.36"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:30 +0900] "POST /wp-login.php HTTP/1.1" 302 920 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.133"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:30 +0900] "GET /blog/wp-login.php HTTP/1.1" 403 486 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.133"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:30 +0900] "POST /wp-login.php HTTP/1.1" 302 920 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.88 Safari/537.36"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:31 +0900] "GET /blog/wp-login.php HTTP/1.1" 403 486 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.88 Safari/537.36"
xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:31 +0900] "POST /wp-login.php HTTP/1.1" 302 920 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.130 Safari/537.36"
...

このアクセスを検知するフィルターは標準では用意されていないようなので自分で作成します。

まず「/etc/fail2ban/filter.d/」フォルダ内に「wordpress-auth.conf」というファイルを作成して以下の内容を書き込みます。正規表現の中にある <HOST> は Fail2ban が内部的に展開する特別なマクロになっています。IPv4、IPv6、ホスト名などにマッチするようです。それ以降は GET または POST でログインが失敗 (403 Forbidden) した場合にマッチします。

[Definition]
failregex = ^<HOST> - - \[.*\] "(GET|POST) (?:/[^"]*)?wp-login\.php(?:\?.*)? HTTP/[^"]+" 403 .*
ignoreregex =

「jail.local」ファイルに以下のセクションを追加します。セクション名は任意の名称が使用できます。

[wordpress-auth]
enabled  = true
port     = http,https
filter   = wordpress-auth
logpath  = /var/log/apache2/access.log
maxretry = 3
findtime = 10m
bantime  = 24h

各設定項目の意味は以下のようになっています。

設定項目意味
enabledフィルターの有効化フラグ
port遮断対象ポート 数値またはプロトコル名 (小文字) で指定します。複数ある場合はカンマで区切ります。
・http : 80
・https : 443
・smtp : 25
・smtps : 465
・submission : 587 など
/etc/services 内にあるサービス名が使用できます。
filter使用するフィルター名 (フォルダ名と拡張子を除いたもの)
logpath監視対象ログファイル名

それ以外は 10 分以内に 3 回失敗した IP アドレスを 24 時間 Ban するように設定しておきます。3 回目で Ban されます。

設定が完了したら「sudo fail2ban-client -t」と入力してエラーが無いか確認します。OK の場合は以下のように入力して正常に検知できるかチェックします。

sudo fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/wordpress-auth.conf

実際のログファイルを使用して確認したところ以下のようになりました。155 件マッチしたようです。

user01@DESKTOP-CQEA49P:~$ sudo fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/wordpress-auth.conf

Running tests
=============

Use   failregex filter file : wordpress-auth, basedir: /etc/fail2ban
Use         log file : /var/log/apache2/access.log
Use         encoding : UTF-8


Results
=======

Failregex: 155 total
|-  #) [# of hits] regular expression
|   1) [155] ^<HOST> - - \[.*\] "(GET|POST) (?:/[^"]*)?wp-login\.php(?:\?.*)? HTTP/[^"]+" 403 .*
`-

Ignoreregex: 0 total

Date template hits:
|- [# of hits] date format
|  [6295] Day(?P<_sep>[-/])MON(?P=_sep)ExYear[ :]?24hour:Minute:Second(?:\.Microseconds)?(?: Zone offset)?
`-

Lines: 6295 lines, 0 ignored, 155 matched, 6140 missed
[processed in 0.36 sec]

Missed line(s): too many to print.  Use --print-all-missed to print all 6140 lines
user01@DESKTOP-CQEA49P:~$

特に問題なければ Fail2ban を再起動します。

sudo systemctl restart fail2ban

「sudo fail2ban-client status wordpress-auth」と入力すると現在の状態が表示されます。

user01@DESKTOP-CQEA49P:~$ sudo fail2ban-client status wordpress-auth
Status for the jail: wordpress-auth
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
|  `- File list:        /var/log/apache2/access.log
`- Actions
   |- Currently banned: 1
   |- Total banned:     1
   `- Banned IP list:   xxx.xxx.xxx.xxx
user01@DESKTOP-CQEA49P:~$

「sudo nft list table inet f2b-table」と入力すると現在のすべてのフィルターの状態が一覧表示されます。(まだ何も遮断されていない場合はエラーが表示されます)

user01@DESKTOP-CQEA49P:~$ sudo nft list table inet f2b-table
table inet f2b-table {
        set addr-set-wordpress-auth {
                type ipv4_addr
                elements = { xxx.xxx.xxx.xxx }
        }

        chain f2b-chain {
                type filter hook input priority filter - 1; policy accept;
                tcp dport { 80, 443 } ip saddr @addr-set-wordpress-auth reject with icmp port-unreachable
        }
}
user01@DESKTOP-CQEA49P:~$

手動で Ban、Unban したい場合は以下のように入力します。

# 手動 Ban
sudo fail2ban-client set wordpress-auth banip xxx.xxx.xxx.xxx

# 解除
sudo fail2ban-client set wordpress-auth unbanip xxx.xxx.xxx.xxx

WordPress のログイン検知ですが、ログインに失敗しても通常は 403 などのエラーコードは返りません。

xxx.xxx.xxx.xxx - - [01/Feb/2026:13:54:30 +0900] "GET /blog/wp-login.php HTTP/1.1" 403 486 "https://mimura-soft.jp/wp-login.php" "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.88 Safari/537.36"

これはログインに失敗したからではなくて、Apache の .htaccess ファイルで管理用のファイルにアクセスできる IP を制限しているために 403 (Forbidden) エラーが返っています。

<FilesMatch "^(wp-config\.php|wp-cron\.php|xmlrpc\.php|wp-login\.php)">
  order deny,allow
  deny from all
  allow from 127.0.0.1
  allow from xxx.xxx.xxx.xxx
</FilesMatch>

このような設定ができない環境の場合は、ログインの失敗をログファイルから判定するのは非常に困難です。(ログイン失敗でも 302 や 200 が返ってしまうため) WordPress のプラグインでログイン失敗時にアクセス制限できるものもありますが、簡単なプラグインを作成して Fail2ban で判定する方法を次にご紹介します。

・WordPress へのログイン試行を検知 (自作プラグイン使用)

まず WordPress のプラグインフォルダ 「wp-content/plugins/」フォルダ内に「my-fail2ban」フォルダを作成して、「my-fail2ban.php」というファイルを作成します。(任意の名称に変更できますが「wp-fail2ban」という名称はすでに公開されているプラグインがあるため使用できないようです。)

<wordpress>/
└─ wp-content/
    └─ plugins/
        └─ my-fail2ban/
            └─ my-fail2ban.php

「my-fail2ban.php」ファイルに以下の内容を記述して保存します。WordPress のログインが失敗した時に発生する「wp_login_failed」イベントをフックしてエラーログに記録するようになっています。

<?php
/*
Plugin Name: My Fail2ban
Author: <your name>
Description: Log WordPress login failures to syslog for fail2ban
Version: 1.0
*/
add_action('wp_login_failed', function ($username) {
    $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
    error_log("WP-LOGIN-FAIL user={$username} ip={$ip} ua={$ua}");
});

ターミナル画面などでプラグインフォルダ (plugins) に移動してから、以下のコマンドで自作プラグインの所有者を変更します。

sudo chown -R www-data:www-data ./my-fail2ban

あとは WordPress で「My Fail2ban」プラグインを有効化しておきます。以降はログインに失敗するたびに「/var/log/apache2/error.log」ファイルに以下のような内容が記録されます。

[Mon Feb 23 10:49:04.192500 2026] [php:notice] [pid 425] [client 172.29.112.1:51660] WP-LOGIN-FAIL user=@yuuji_mimura ip=172.29.112.1 ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36, referer: http://mimura-soft.jp/blog/wp-login.php?loggedout=true&wp_lang=ja

次に「/etc/fail2ban/filter.d/」フォルダ内に「wordpress-auth-hook.conf」というファイルを作成して以下の内容を書き込みます。

[Definition]
failregex = WP-LOGIN-FAIL user=.* ip=<HOST> .*
ignoreregex =

「jail.local」ファイルに以下のセクションを追加します。セクション名は任意の名称が使用できます。

[wordpress-auth-hook]
enabled  = true
port     = http,https
filter   = wordpress-auth-hook
logpath  = /var/log/apache2/error.log
maxretry = 3
findtime = 10m
bantime  = 24h

作成が終了したら設定チェックを行い、さらにフィルターが実際にヒットするか確認します。

sudo fail2ban-client -t
sudo fail2ban-regex /var/log/apache2/error.log /etc/fail2ban/filter.d/wordpress-auth-hook.conf

特に問題なければ Fail2ban を再起動しておきます。

sudo systemctl restart fail2ban

実際にログインに失敗して「/var/log/fail2ban.log」というログファイルに以下のようなメッセージが表示されたら正常に動作しています。

2026-02-23 11:08:21,934 fail2ban.filter         [2919]: INFO    [wordpress-auth-hook] Found 172.29.112.1 - 2026-02-23 11:08:21
・設定ミスを狙ったスキャンを検知

次に危険度が高そうなスキャンを検知します。ときどき以下のような「本物」のスキャンをされることがあります。長いので折りたたんで表示します。(IP アドレスはマスクしてあります。)

設定ファイル・秘密情報の総当たりスキャナ
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:43 +0900] "GET / HTTP/1.1" 200 73639 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:44 +0900] "GET /.env HTTP/1.1" 301 3278 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:45 +0900] "GET /.env.local HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:46 +0900] "GET /.env.production HTTP/1.1" 301 3288 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:47 +0900] "GET /.env.development HTTP/1.1" 301 3291 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:48 +0900] "GET /.env.dev HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:48 +0900] "GET /.env.prod HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:49 +0900] "GET /.env.test HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:50 +0900] "GET /.env.staging HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:51 +0900] "GET /.env.backup HTTP/1.1" 301 3286 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:52 +0900] "GET /.env.save HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:53 +0900] "GET /.env.old HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:54 +0900] "GET /.env.bak HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:55 +0900] "GET /.env.tmp HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:55 +0900] "GET /.env.swp HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:56 +0900] "GET /.env~ HTTP/1.1" 301 3279 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:57 +0900] "GET /env HTTP/1.1" 301 3276 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:58 +0900] "GET /env.js HTTP/1.1" 301 3280 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:59 +0900] "GET /env.json HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:00 +0900] "GET /app/.env HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:01 +0900] "GET /src/.env HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:02 +0900] "GET /config/.env HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:02 +0900] "GET /backend/.env HTTP/1.1" 301 3286 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:03 +0900] "GET /frontend/.env HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:04 +0900] "GET /api/.env HTTP/1.1" 301 3281 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:05 +0900] "GET /server/.env HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:06 +0900] "GET /client/.env HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:07 +0900] "GET /web/.env HTTP/1.1" 301 3281 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:08 +0900] "GET /public/.env HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:09 +0900] "GET /private/.env HTTP/1.1" 301 3286 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:10 +0900] "GET /var/.env HTTP/1.1" 301 3281 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:10 +0900] "GET /docker-compose.yml HTTP/1.1" 301 3292 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:11 +0900] "GET /docker-compose.yaml HTTP/1.1" 301 3292 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:12 +0900] "GET /docker-compose.override.yml HTTP/1.1" 301 3301 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:13 +0900] "GET /docker-compose.dev.yml HTTP/1.1" 301 3297 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:14 +0900] "GET /docker-compose.prod.yml HTTP/1.1" 301 3297 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:15 +0900] "GET /config.js HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:16 +0900] "GET /config.json HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:17 +0900] "GET /settings.js HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:17 +0900] "GET /settings.json HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:18 +0900] "GET /secrets.json HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:19 +0900] "GET /credentials.json HTTP/1.1" 301 3290 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:20 +0900] "GET /app.js HTTP/1.1" 301 3279 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:21 +0900] "GET /main.js HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:22 +0900] "GET /index.js HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:23 +0900] "GET /server.js HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:24 +0900] "GET /bundle.js HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:25 +0900] "GET /app.bundle.js HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:25 +0900] "GET /main.bundle.js HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:26 +0900] "GET /vendor.js HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:27 +0900] "GET /chunk.js HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:28 +0900] "GET /static/js/main.js HTTP/1.1" 301 3290 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:29 +0900] "GET /static/js/app.js HTTP/1.1" 301 3291 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:30 +0900] "GET /static/js/bundle.js HTTP/1.1" 301 3293 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:31 +0900] "GET /dist/main.js HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:32 +0900] "GET /dist/app.js HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:32 +0900] "GET /dist/bundle.js HTTP/1.1" 301 3288 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:33 +0900] "GET /build/static/js/main.js HTTP/1.1" 301 3298 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:34 +0900] "GET /assets/index.js HTTP/1.1" 301 3290 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:35 +0900] "GET /assets/app.js HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:36 +0900] "GET /_next/static/chunks/main.js HTTP/1.1" 301 3302 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:37 +0900] "GET /_next/static/chunks/app.js HTTP/1.1" 301 3300 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:38 +0900] "GET /js/app.js HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:39 +0900] "GET /js/main.js HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:40 +0900] "GET /js/config.js HTTP/1.1" 301 3286 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:40 +0900] "GET /settings.py HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:41 +0900] "GET /config.py HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:42 +0900] "GET /secrets.py HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:43 +0900] "GET /config/secrets.yml HTTP/1.1" 301 3292 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:44 +0900] "GET /config/master.key HTTP/1.1" 301 3291 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:45 +0900] "GET /wp-config.php.bak HTTP/1.1" 301 3291 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:46 +0900] "GET /wp-config.php.old HTTP/1.1" 301 3290 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:47 +0900] "GET /wp-config.php.save HTTP/1.1" 301 3293 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:47 +0900] "GET /wp-config.php.txt HTTP/1.1" 301 3291 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:48 +0900] "GET /wp-config.php~ HTTP/1.1" 301 3288 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:49 +0900] "GET /config/app.php HTTP/1.1" 301 3288 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:50 +0900] "GET /application.properties HTTP/1.1" 301 3296 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:51 +0900] "GET /application.yml HTTP/1.1" 301 3288 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:52 +0900] "GET /application.yaml HTTP/1.1" 301 3289 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:53 +0900] "GET /application-dev.properties HTTP/1.1" 301 3300 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:54 +0900] "GET /application-prod.properties HTTP/1.1" 301 3302 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:54 +0900] "GET /appsettings.json HTTP/1.1" 301 3290 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:55 +0900] "GET /appsettings.Development.json HTTP/1.1" 301 3302 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:56 +0900] "GET /appsettings.Production.json HTTP/1.1" 301 3301 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:57 +0900] "GET /config.yaml HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:58 +0900] "GET /config.yml HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:47:59 +0900] "GET /config.toml HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:00 +0900] "GET /serverless.yml HTTP/1.1" 301 3288 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:01 +0900] "GET /serverless.yaml HTTP/1.1" 301 3289 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:02 +0900] "GET /config/production.json HTTP/1.1" 301 3296 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:02 +0900] "GET /config/development.json HTTP/1.1" 301 3297 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:03 +0900] "GET /config/default.json HTTP/1.1" 301 3292 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:04 +0900] "GET /config/local.json HTTP/1.1" 301 3290 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:05 +0900] "GET /conf/settings.json HTTP/1.1" 301 3292 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:06 +0900] "GET /data/config.json HTTP/1.1" 301 3291 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:07 +0900] "GET /actuator/env HTTP/1.1" 301 3286 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:08 +0900] "GET /actuator/configprops HTTP/1.1" 301 3294 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:09 +0900] "GET /debug/vars HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:09 +0900] "GET /admin/config HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:10 +0900] "GET /api/config HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:11 +0900] "GET /api/settings HTTP/1.1" 301 3287 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:12 +0900] "GET /api/env HTTP/1.1" 301 3280 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:13 +0900] "GET /.env.example HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:14 +0900] "GET /.env.sample HTTP/1.1" 301 3285 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:15 +0900] "GET /.env.dist HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:16 +0900] "GET /.env.template HTTP/1.1" 301 3286 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:17 +0900] "GET /backup/.env HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:17 +0900] "GET /backups/.env HTTP/1.1" 301 3286 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:18 +0900] "GET /old/.env HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:19 +0900] "GET /temp/.env HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:20 +0900] "GET /tmp/.env HTTP/1.1" 301 3281 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:21 +0900] "GET /.streamlit/secrets.toml HTTP/1.1" 301 3296 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:22 +0900] "GET /.streamlit/config.toml HTTP/1.1" 301 3296 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:23 +0900] "GET /app/.streamlit/secrets.toml HTTP/1.1" 301 3301 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:24 +0900] "GET /home/appuser/.streamlit/secrets.toml HTTP/1.1" 301 3310 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:25 +0900] "GET /root/.streamlit/secrets.toml HTTP/1.1" 301 3301 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:25 +0900] "GET /src/.streamlit/secrets.toml HTTP/1.1" 301 3302 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:26 +0900] "GET /.streamlit/credentials.toml HTTP/1.1" 301 3301 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:27 +0900] "GET /@fs/app/.env?raw?? HTTP/1.1" 301 3439 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:28 +0900] "GET /@fs/root/.env?raw?? HTTP/1.1" 301 3439 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:29 +0900] "GET /@fs/proc/self/environ?raw?? HTTP/1.1" 301 3448 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:30 +0900] "GET /@fs/.env?raw?? HTTP/1.1" 301 3435 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:31 +0900] "GET /@fs/..%2f..%2f..%2f..%2f..%2froot/.env?raw?? HTTP/1.1" 404 2996 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:32 +0900] "GET /@fs/..%2f..%2f..%2f..%2f..%2fproc/self/environ?raw?? HTTP/1.1" 404 2997 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:32 +0900] "GET /@fs/..%2f..%2f..%2f..%2f..%2fapp/.env?raw?? HTTP/1.1" 404 2996 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:33 +0900] "GET /@fs/..%252f..%252f..%252f..%252f..%252froot/.env?raw?? HTTP/1.1" 301 3475 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:34 +0900] "GET /@fs/..%252f..%252f..%252f..%252f..%252fproc/self/environ?raw?? HTTP/1.1" 301 3483 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:35 +0900] "GET /..%2f..%2f..%2f..%2f..%2f..%2fproc/self/environ HTTP/1.1" 404 2997 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:36 +0900] "GET /../../../../../../../proc/self/environ HTTP/1.1" 400 3029 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:36 +0900] "GET /../../../../../../../app/.env HTTP/1.1" 400 3028 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:48:37 +0900] "GET /../../../../../../../root/.env HTTP/1.1" 400 3029 "-" "Mozilla/5.0"

以下のような攻撃を試みています。

  • .env 全バリエーションの読み取り
  • docker-compose.yml
  • application.yml / properties (Spring)
  • appsettings.json (.NET)
  • wp-config.php.*
  • serverless.yml
  • actuator/env
  • proc/self/environ : 環境変数の読み取り
  • @fs/… (Vite / Dev Server系)
  • パストラバーサル など

まだどれも成功した形跡がないので早速フィルターを書きたいですが、その前に 1 つ問題があります。「.env」ファイルの読み取りで何かにリダイレクト (301) されています。おそらく WordPress の 404 ページが表示されていると思われますが、実際にサーバールート以下に「.env」ファイルなどがあった場合は、一回目のアクセスで情報を取得されてしまう可能性があります。ドットファイルなどは Web サーバー側で 403 または 404 を返すように修正しておいた方が安全です。

xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:43 +0900] "GET / HTTP/1.1" 200 73639 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:44 +0900] "GET /.env HTTP/1.1" 301 3278 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:45 +0900] "GET /.env.local HTTP/1.1" 301 3284 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:46 +0900] "GET /.env.production HTTP/1.1" 301 3288 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:47 +0900] "GET /.env.development HTTP/1.1" 301 3291 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:48 +0900] "GET /.env.dev HTTP/1.1" 301 3283 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [08/Feb/2026:09:46:48 +0900] "GET /.env.prod HTTP/1.1" 301 3282 "-" "Mozilla/5.0"
...

以降は Ubuntu 24.04 上の Apache2 を使用している前提でご説明します。他の環境でもおおまかな設定方法は同じと思われます。

まず「/etc/apache2/conf-available/」フォルダ内に「hardening-scans.conf」というファイルを作成して以下の設定を書き込みます。

# Block common secret files / bot scans early

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Allow .well-known (Let's Encrypt etc.)
  RewriteRule "^/\.well-known(/.*)?$" "-" [L]

  # Block Vite/Dev Server abuse
  RewriteRule "^/@fs(/|$)" "-" [F,L,NC]

  # Block environ leak path
  RewriteRule "proc/self/environ" "-" [F,L,NC]

  # Block dotfiles and dot directories anywhere (/.env, /.git, /.aws, /.streamlit, etc.)
  RewriteRule "(^|/)\." "-" [F,L]

  # Block non-dot secret/config filenames often leaked accidentally
  RewriteRule "(?i)(^|/)(docker-compose(\..*)?\.ya?ml|serverless\.ya?ml|secrets(\..*)?\.(json|ya?ml|toml)|credentials\.json|appsettings(\..*)?\.json|application(-.*)?\.(properties|ya?ml)|wp-config\.php(\..*)?)$" "-" [F,L]

  # Block traversal-like encoded patterns
  RewriteCond %{THE_REQUEST} "(?i)(\.\./|%2e%2e%2f|%2f\.\.|%252f)" [OR]
  RewriteCond %{REQUEST_URI}  "(?i)(\.\./|%2e%2e%2f|%2f\.\.|%252f)"
  RewriteRule ".*" "-" [F,L]
</IfModule>

rewrite モジュールが入っていない場合は有効化します。

sudo a2enmod rewrite

作成した conf ファイルを有効化して Apache 再読込みします。

sudo a2enconf hardening-scans
sudo apachectl configtest
sudo systemctl reload apache2

ブラウザーを起動して URL 欄に「<ホスト名>/.env」と入力するか、WSL2 などから以下のコマンドを実行して 403 または 404 が返ってきたら正常に設定されています。(自分のサーバーからも実行できます。)

curl -i http://<ホスト名>/.env
curl -i http://<ホスト名>/@fs/
curl -i http://<ホスト名>/proc/self/environ
curl -i "http://<ホスト名>/%2e%2e%2f"

環境によっては (ローカル環境の WSL2 などでは) ブロックされないようなので、その場合は「hardening-scans.conf」ファイルを以下のように変更するとブロックされるようです。

# Block common secret files / bot scans early

# URLとして来る攻撃パスをブロック (静的 / WordPress / リバースプロキシ 全部に対応)
<LocationMatch "(?i)^/(?:@fs(?:/|$)|@vite(?:/|$)|vite-client$|proc/self/environ$)">
  Require all denied
</LocationMatch>

# パストラバーサルをブロック
<LocationMatch "(?i)(?:\.\./|%2e%2e%2f|%2f\.\.|%252f)">
  Require all denied
</LocationMatch>

#ファイル実体の漏えいをブロック (/var/www/html 配下のサブフォルダ内のファイルにも有効)
<Directory /var/www/html>
  # dotfiles: .env, .git, .aws, ...
  <FilesMatch "(?i)^\.(?!well-known$).+">
    Require all denied
  </FilesMatch>

  # 狙われやすい設定ファイルへのアクセスをブロック
  <FilesMatch "(?i)^(wp-config\.php(?:\..*)?|docker-compose(\..*)?\.ya?ml|compose\.ya?ml|serverless\.ya?ml|credentials\.json|appsettings(\..*)?\.json|application(-.*)?\.(properties|ya?ml|yaml)|secrets(\..*)?\.(json|ya?ml|yaml|toml))$">
    Require all denied
  </FilesMatch>
</Directory>

# Let's Encrypt などを使用している場合は .well-known を許可
<Directory "/var/www/html/.well-known">
  Require all granted
</Directory>

以上で Web サーバー側の設定は終了です。続いて Fail2ban 側の設定を行います。

「/etc/fail2ban/filter.d/」フォルダ内に「apache-envscan.conf」というファイルを作成して以下の内容を書き込みます。ドットファイルや設定ファイルに対して読み取りなどが失敗した (400、403、404 が返った) 場合にマッチします。

[Definition]
allowipv6 = auto
failregex = ^<HOST> .* "(GET|POST|HEAD) (?:https?://[^ ]+)?/(?:@fs(?:/|$)|.*proc/self/environ|.*\.env(?:[^ ]*)?|.*\.git|.*\.aws|.*\.streamlit|docker-compose(?:\..*)?\.ya?ml).*" (?:400|403|404) .*$
ignoreregex =

「jail.local」ファイルに以下のセクションを追加します。セクション名は任意の名称が使用できます。危険なアクセスなので、24 時間以内に 1 回アクセスすると 1 週間 (7 日間) Ban します。その後に再度遮断されるたびに、2 週間 → 4 週間と伸びてゆき最終的には最大で 30 日間 Ban されます。

[apache-envscan]
enabled  = true
port     = http,https
filter   = apache-envscan
logpath  = /var/log/apache2/access.log
maxretry = 1
findtime = 24h
bantime  = 7d
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 30d
・メールサーバーへのブルートフォースを検知

このサイトではメールサーバーも運用しているので以下のようなアクセスもあります。

2026-02-08T00:00:12.443401+09:00 mimura-soft postfix/smtpd[17033]: warning: unknown[xxx.xxx.xxx.xxx]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=thunder@mimura-soft.jp
2026-02-08T00:00:13.594634+09:00 mimura-soft postfix/smtpd[17033]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-08T00:00:29.857238+09:00 mimura-soft postfix/smtpd[17033]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-08T00:00:36.498676+09:00 mimura-soft postfix/smtpd[17033]: warning: unknown[xxx.xxx.xxx.xxx]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=tiger@mimura-soft.jp
2026-02-08T00:00:37.556327+09:00 mimura-soft postfix/smtpd[17033]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-08T00:00:55.107838+09:00 mimura-soft postfix/smtpd[17237]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-08T00:01:01.121826+09:00 mimura-soft postfix/smtpd[17237]: warning: unknown[xxx.xxx.xxx.xxx]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=tmbecker@mimura-soft.jp
2026-02-08T00:01:01.814733+09:00 mimura-soft postfix/smtpd[17237]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-08T00:01:20.852168+09:00 mimura-soft postfix/smtpd[17033]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-08T00:01:26.264459+09:00 mimura-soft postfix/smtpd[17033]: warning: unknown[xxx.xxx.xxx.xxx]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=tmp@mimura-soft.jp
2026-02-08T00:01:27.561799+09:00 mimura-soft postfix/smtpd[17033]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-08T00:01:46.084281+09:00 mimura-soft postfix/smtpd[17237]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-08T00:01:52.016981+09:00 mimura-soft postfix/smtpd[17237]: warning: unknown[xxx.xxx.xxx.xxx]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=tomcat4@mimura-soft.jp
2026-02-08T00:01:52.305199+09:00 mimura-soft postfix/smtpd[17237]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
...

典型的なメール認証ブルートフォースと思われますが、放置しておくとスパムメールの踏み台にされかねませんので、こちらのアクセスも検知できるように設定していきます。

メール送信に Postfix を使用する構成であれば、標準で「postfix」というフィルターが用意されていますのでこれを使用します。「jail.local」ファイルを開いて以下の設定を追加します。

[postfix]
enabled  = true
port     = smtp,submission,smtps
filter   = postfix
logpath  = /var/log/mail.log
maxretry = 2
findtime = 1h
bantime  = 24h

また標準のフィルターでは「sasl_username=server」などのように、メールアドレス形式ではないユーザー名にマッチしないので「/etc/fail2ban/filter.d」フォルダ内に「postfix-sasl.conf」というファイルを作成して以下の内容を書き込みます。

[Definition]
failregex = ^.*warning: .*\[<HOST>\]: SASL (?:LOGIN|PLAIN) authentication failed:.*$
ignoreregex =

「jail.local」ファイルに以下のセクションを追加します。

[postfix-sasl]
enabled  = true
filter   = postfix-sasl
logpath  = /var/log/mail.log
maxretry = 2
findtime = 1h
bantime  = 24h

さらに通信手順を無視して直接バイナリデータを送り込んでくる以下のようなアクセスも多数ありましたので、追加のフィルターを作成します。

2026-02-08T11:27:52.614220+09:00 mimura-soft postfix/submission/smtpd[21771]: improper command pipelining after CONNECT from xxxx [xxx.xxx.xxx.xxx]: \026\003\001\000{\001\000\000w\003\003\344\027\261o\204ca\222~\353\306\026p\207\374H>N\361\032\330\226mAa&\301\366\2411\365r\000\000\032\300/\300+\300\021\300\a\300\023\300\t\300\024\300\n\000\005\000/\0005\300\022\000\n\001\000\0004\000\005\000\005\001\000\000\000\000\000\n\000\b\000\006\000\027\000\030\000\031\000\v\000
2026-02-08T11:27:52.614557+09:00 mimura-soft postfix/submission/smtpd[21771]: warning: non-SMTP command from xxxx [xxx.xxx.xxx.xxx]: \001\000\0004\000\005\000\005\001\000\000\000\000\000

「/etc/fail2ban/filter.d」フォルダ内に「postfix-protocol.conf」というファイルを作成して以下の内容を書き込みます。failregex に 2 つ以上の条件がある場合は、いずれかの条件にマッチすると一致したとみなされます。

[Definition]
failregex =
    ^.* postfix/(?:submission/)?smtpd\[\d+\]: warning: non-SMTP command from \S*\[<HOST>\].*$
    ^.* postfix/(?:submission/)?smtpd\[\d+\]: improper command pipelining after CONNECT from \S*\[<HOST>\]:.*$
ignoreregex =

「jail.local」ファイルに以下のセクションを追加します。

[postfix-protocol]
enabled  = true
port     = smtp,submission,smtps
filter   = postfix-protocol
logpath  = /var/log/mail.log
maxretry = 3
findtime = 1h
bantime  = 24h
・繰り返し Ban される IP アドレスがある場合 (recidive)

Fail2ban は設定されている「bantime」を過ぎると制限を解除 (Unban) します。しかし何度も同じ IP が Ban されるような場合は「recidive (レシディブ)」という特別なセクションを用意します。

通常の Ban 動作が Web やメールサーバーのログを監視しているのに対して、recidive は自分のログファイル (fail2ban.log) を監視します。他のログファイルを指定することも可能ですが、recidive のフィルタは 「Ban」を含む行を検知するのであまり意味がありません。

「jail.local」ファイルに以下のように記述すると有効になります。検知条件や bantime は自由に設定できます。用途を考えると少し長め (30d ~ 90d くらい) に設定しておいても良いかもしれません。

[recidive]
enabled  = true
logpath  = /var/log/fail2ban.log
maxretry = 2
findtime = 1d
bantime  = 30d

設定が完了したら Fail2ban をリロード (または再起動) します。

sudo fail2ban-client reload

Fail2ban の主な機能のご説明はここで終了です。次のセクションからは UFW やメールサーバーの設定で、もう少し制限をかける方法を考察します。

UFW で不要なポートを閉じる

Fail2ban を使用すると動的にアクセス制限できましたが、「UFW」と呼ばれるファイアウォールを使用すると静的にアクセス制限をかけることが可能です。デフォルトですべてのポートを閉じて、使用するポートのみを開放または制限をかける方式で設定していきます。

まずはターミナル画面に以下のコマンドを入力してバージョンを確認します。

ufw --version

インストールされていない場合は、以下のコマンドを入力して UFW パッケージをインストールします。

sudo apt update
sudo apt install ufw

次に以下のコマンドを入力して、デフォルトで外部からの接続をすべて拒否します。内側からの発信は許可します。(UFW を起動するまで設定は有効になりません。)

sudo ufw default deny incoming
sudo ufw default allow outgoing

IPv6 を使用しない場合は「/etc/default/ufw」ファイルを開いて「IPV6=no」に設定します。

# Set to yes to apply rules to support IPv6 (no means only IPv6 on loopback
# accepted). You will need to 'disable' and then 'enable' the firewall for
# the changes to take affect.
IPV6=no

SSH を使用している場合は以下のように設定します。アクセスできる IP アドレスを制限しない場合は、安全のためにこちらの記事などでご紹介している、公開鍵で認証する方法をおすすめします。

# すべてのアドレスに公開する場合
sudo ufw allow 22/tcp

# アクセスできる IP アドレスを指定する場合
sudo ufw allow from xxx.xxx.xxx.xxx to any port 22 proto tcp

# アクセスできる IP アドレスの範囲を指定する場合
sudo ufw allow from xxx.xxx.xxx.xxx/24 to any port 22 proto tcp

あとは必要に応じて開放するポートを追加していきます。

# Web サーバー
sudo ufw allow 80/tcp   # 必要に応じて
sudo ufw allow 443/tcp

# メールサーバー
sudo ufw allow 25/tcp   # smtp       (MTA 受信用)
sudo ufw allow 465/tcp  # smtps      (送信用)
sudo ufw allow 587/tcp  # submission (送信用)
sudo ufw allow 993/tcp  # imaps      (受信用)
# など必要に応じて

すべて設定したら以下のように入力して UFW を有効化します。(SSH を使用している場合は締め出されていないか確認するため、現在のターミナルを閉じる前に他のターミナルからも接続できるか確認してください。)

sudo ufw enable

無効化するには以下のように入力します。

sudo ufw disable

現在の状態を調べるには以下のように入力します。

sudo ufw status

# 番号付きで見たい場合は以下のように入力します
sudo ufw status numbered

ルールは最後尾に追加されていくため、任意の場所に追加したい場合は「insert」オプションを使用します。

# 先頭に追加したい場合
sudo ufw insert 1 allow 80/tcp

ルールを削除したい場合は以下のように入力します。

sudo ufw delete <ルール番号>

余談ですが、このサイトでは管理やメールの送受信は固定 IP からしか行わないため、以下のような設定になっています。(SSL 経由で VPN 接続するように設定すれば、3 番目以降のルールも不要になります。)

root@mimura-soft:/# ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 25/tcp                     ALLOW IN    Anywhere
[ 2] 443/tcp                    ALLOW IN    Anywhere
[ 3] 22/tcp                     ALLOW IN    xxx.xxx.xxx.xxx
[ 4] 465/tcp                    ALLOW IN    xxx.xxx.xxx.xxx
[ 5] 993/tcp                    ALLOW IN    xxx.xxx.xxx.xxx

root@mimura-soft:/#
メールサーバーの設定を変更する

自前でメールを受信するサーバーである以上 25 番ポートを塞ぐことは不可能なので、このポートのセキュリティについて考察していきます。

・25 番ポートの AUTH を停止する

実際にメールサーバーに Telnet などで接続すると、「250-AUTH」などと表示されることがあります。これはこのポートで認証 (ユーザー名とパスワードでログインする) 機能が有効であることを示しています。

user01@DESKTOP-CQEA49P:~$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 mail.mimura-soft.jp ESMTP
EHLO test
250-mail.mimura-soft.jp
250-PIPELINING
250-SIZE 10240000
250-ETRN
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
QUIT
221 2.0.0 Bye
Connection closed by foreign host.
user01@DESKTOP-CQEA49P:~$

この機能が有効になっていると以下のようなアクセスが増える場合があります。

2026-02-23T05:56:21.419105+09:00 mimura-soft postfix/smtpd[61963]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T05:56:24.389950+09:00 mimura-soft postfix/smtpd[61963]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-23T05:56:51.159511+09:00 mimura-soft postfix/smtpd[61963]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T05:56:53.785480+09:00 mimura-soft postfix/smtpd[61963]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-23T05:57:17.946607+09:00 mimura-soft postfix/smtpd[61963]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T05:57:20.962418+09:00 mimura-soft postfix/smtpd[61963]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-23T05:57:44.731129+09:00 mimura-soft postfix/smtpd[61963]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T05:57:48.503407+09:00 mimura-soft postfix/smtpd[61963]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-23T05:58:12.698825+09:00 mimura-soft postfix/smtpd[61963]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T05:58:15.407663+09:00 mimura-soft postfix/smtpd[61963]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
2026-02-23T05:58:39.791862+09:00 mimura-soft postfix/smtpd[61963]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T05:58:42.506298+09:00 mimura-soft postfix/smtpd[61963]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 rset=1 quit=1 commands=3/4
...

仮にこの認証を突破されると、そのセッションで任意のメールが転送可能になります。直ちに危険ではありません (他の送信用ポートも危険度は同じです) が、適切に設定されているメールサーバーであればオープンリレーなどは出来ないようになっていると思われますので、不要な攻撃の糸口を塞ぐためにこの機能を停止します。

Postfix + Dovecot + SASL という構成を前提にしてご説明します。

まず初めにオープンリレーになっていないか確認します。「/etc/postfix/main.cf」というファイルを開いて「smtpd_recipient_restrictions」項目に「reject_unauth_destination」という設定があればリレーされません。(設定が他の場所で上書きされていない場合)

smtpd_recipient_restrictions = 
  permit_mynetworks,
  permit_sasl_authenticated,
  reject_unauth_destination

実際に確認したい場合は、「telnet <ホスト名> 25」などと外部から接続して、以下の順番で入力します。

  • EHLO test
  • MAIL FROM:<test@example.com>
  • RCPT TO:<someone@gmail.com>
  • QUIT と入力して終了します。

554 5.7.1 <someone@gmail.com>: Relay access denied などの応答が返ってくれば中継されていません。(WSL2 の場合は 454 で返ってくるようです。)

220 mail.mimura-soft.jp ESMTP
EHLO test
250-mail.mimura-soft.jp
250-PIPELINING
250-SIZE 10240000
250-ETRN
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
MAIL FROM:<test@example.com>
250 2.1.0 Ok
RCPT TO:<someone@gmail.com>
454 4.7.1 <someone@gmail.com>: Relay access denied

次に AUTH 機能を停止します。「/etc/postfix/master.cf」というファイルを開いて smtp 設定の 13 行目辺りに以下の設定を追加します。

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (no)    (never) (100)
# ==========================================================================
smtp      inet  n       -       y       -       -       smtpd
  -o smtpd_sasl_auth_enable=no
#smtp      inet  n       -       y       -       1       postscreen
#smtpd     pass  -       -       y       -       -       smtpd
#dnsblog   unix  -       -       y       -       0       dnsblog
#tlsproxy  unix  -       -       y       -       0       tlsproxy

設定が完了したらメールサーバーの設定を更新します。

sudo postfix reload

設定が反映されたか確認します。Telnet などでメールサーバーにログインして、「EHLO test」などと入力すると使用可能な機能や情報が表示されますので、「250-AUTH」項目がない事を確認します。「QUIT」と入力すると終了します。

user01@DESKTOP-CQEA49P:~$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 mail.mimura-soft.jp ESMTP
EHLO test
250-mail.mimura-soft.jp
250-PIPELINING
250-SIZE 10240000
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
QUIT
221 2.0.0 Bye
Connection closed by foreign host.
user01@DESKTOP-CQEA49P:~$
・Postscreen と DNSBL (RBL) を有効化する

Postfix には「Postscreen」という接続前フィルターが組み込まれています。この機能を有効化すると、メールサーバーに接続するスパムメールなどのあやしい接続をセッションが開始する前に切断してくれます。同様に DNSBL (DNS-based Blackhole List) と呼ばれる仕組みを使用すると、接続してきた IP アドレスを外部の専用サイトに DNS 形式で問い合わせて、ブラック度合いのスコアを取得します。取得されたスコアが設定した基準値を越えた場合はセッションを切断します。どちらも有効化する設定はそれほど難しくありません。

設定を有効化するには「/etc/postfix/master.cf」というファイルを開いて smtp 設定の 14 ~ 18 行目辺りのコメントをはずして以下のように設定します。AUTH 機能をオフにする設定「smtpd_sasl_auth_enable=no」も smtpd の下に追加します。元々あった smtp 設定 (12 と 13 行目) はコメントアウトします。

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (no)    (never) (100)
# ==========================================================================
#smtp      inet  n       -       y       -       -       smtpd
#  -o smtpd_sasl_auth_enable=no
smtp      inet  n       -       y       -       1       postscreen
smtpd     pass  -       -       y       -       -       smtpd
  -o smtpd_sasl_auth_enable=no
dnsblog   unix  -       -       y       -       0       dnsblog
tlsproxy  unix  -       -       y       -       0       tlsproxy

次に「/etc/postfix/main.cf」ファイルを開いて、ファイルの最後に以下の設定を追加します。

# --- postscreen hardening (safe baseline) ---
postscreen_greet_action = enforce
postscreen_greet_wait = 6s

postscreen_pipelining_action = enforce
postscreen_non_smtp_command_action = enforce
postscreen_bare_newline_action = enforce

postscreen_cache_map = btree:/var/lib/postfix/postscreen_cache
postscreen_cache_retention_time = 7d

# --- DNSBL (optional but very effective) ---
postscreen_dnsbl_action = enforce
postscreen_dnsbl_sites =
    zen.spamhaus.org=127.0.0.[2..11]*3
    bl.spamcop.net=127.0.0.2*2
postscreen_dnsbl_threshold = 3

設定が完了したらメールサーバーの設定を更新または再起動します。

# 更新
sudo postfix reload

# 再起動
sudo systemctl restart postfix

実際のログを見てみると設定通りに動作しているようです。

2026-02-23T16:55:31.708828+09:00 mimura-soft postfix/postscreen[68523]: CONNECT from [xxx.xxx.xxx.xxx]:36690 to [153.126.131.65]:25
2026-02-23T16:55:31.999334+09:00 mimura-soft postfix/postscreen[68523]: PREGREET 11 after 0.29 from [xxx.xxx.xxx.xxx]:36690: EHLO User\r\n
2026-02-23T16:55:32.041153+09:00 mimura-soft postfix/smtpd[68524]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T16:55:32.335457+09:00 mimura-soft postfix/smtpd[68524]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 quit=1 commands=2
2026-02-23T16:55:58.639234+09:00 mimura-soft postfix/postscreen[68523]: CONNECT from [xxx.xxx.xxx.xxx]:44170 to [153.126.131.65]:25
2026-02-23T16:55:58.928043+09:00 mimura-soft postfix/postscreen[68523]: PREGREET 11 after 0.29 from [xxx.xxx.xxx.xxx]:44170: EHLO User\r\n
2026-02-23T16:55:58.929398+09:00 mimura-soft postfix/smtpd[68524]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T16:55:59.326516+09:00 mimura-soft postfix/smtpd[68524]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 quit=1 commands=2
2026-02-23T16:56:25.059487+09:00 mimura-soft postfix/postscreen[68523]: CONNECT from [xxx.xxx.xxx.xxx]:51454 to [153.126.131.65]:25
2026-02-23T16:56:25.349104+09:00 mimura-soft postfix/postscreen[68523]: PREGREET 11 after 0.29 from [xxx.xxx.xxx.xxx]:51454: EHLO User\r\n
2026-02-23T16:56:25.350193+09:00 mimura-soft postfix/smtpd[68524]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T16:56:25.789270+09:00 mimura-soft postfix/smtpd[68524]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 quit=1 commands=2
2026-02-23T16:56:52.042468+09:00 mimura-soft postfix/postscreen[68523]: CONNECT from [xxx.xxx.xxx.xxx]:58582 to [153.126.131.65]:25
2026-02-23T16:56:52.332820+09:00 mimura-soft postfix/postscreen[68523]: PREGREET 11 after 0.29 from [xxx.xxx.xxx.xxx]:58582: EHLO User\r\n
2026-02-23T16:56:52.333892+09:00 mimura-soft postfix/smtpd[68524]: connect from unknown[xxx.xxx.xxx.xxx]
2026-02-23T16:56:52.750680+09:00 mimura-soft postfix/smtpd[68524]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 quit=1 commands=2
2026-02-23T16:57:19.065611+09:00 mimura-soft postfix/postscreen[68523]: CONNECT from [xxx.xxx.xxx.xxx]:37512 to [153.126.131.65]:25
2026-02-23T16:57:19.757749+09:00 mimura-soft postfix/postscreen[68523]: PREGREET 11 after 0.69 from [xxx.xxx.xxx.xxx]:37512: EHLO User\r\n
...

ただこの機能はセッションを切断してくれますが、IP 接続そのものを遮断する訳ではないのでログファイルには延々と切断されたログが記録されます。

そこで Fail2ban と連携して、メールサーバーの通信ログに切断情報が記録されたら IP アドレスを Ban するように設定します。

まず「/etc/fail2ban/filter.d/」フォルダ内に「postfix-postscreen.conf」というファイルを作成して以下の内容を書き込みます。

[Definition]
failregex =
            ^.*\spostfix/postscreen\[\d+\]:\s+PREGREET\s+\d+\s+after\s+[0-9.]+\s+from\s+\[<HOST>\](?::\d+)?:
            ^.*\spostfix/postscreen\[\d+\]:\s+HANGUP\s+after\s+\d+\s+from\s+\[<HOST>\](?::\d+)?
            ^.*\spostfix/postscreen\[\d+\]:\s+DNSBL\s+rank\s+\d+\s+for\s+\[<HOST>\]:
            ^.*\spostfix/postscreen\[\d+\]:\s+(?:NON-SMTP command|BARE NEWLINE|COMMAND PIPELINING)\s+from\s+\[<HOST>\](?::\d+)?
ignoreregex =

「jail.local」ファイルに以下のセクションを追加します。セクション名は任意の名称が使用できます。

[postfix-postscreen]
enabled  = true
port     = smtp
filter   = postfix-postscreen
logpath  = /var/log/mail.log
maxretry = 2
findtime = 2h
bantime  = 24h

設定が完了したら Fail2ban をリロード (または再起動) します。

sudo fail2ban-client reload

以上で設定は完了です。実際にそれぞれの遮断を開始すると攻撃パターンもどんどん変化していきますので、ログなどを見ながら新しいフィルターを追加する必要が出てくるかもしれません。

まとめ

今回は Fail2ban というセキュリティツールの設定方法についてご説明しました。

それ以外にも

  • Web サーバー設定
  • メールサーバー側でのブロック
  • ファイアウォール

などとの連携方法についても考察しました。

Fail2ban は単体でも非常に強力なツールですが、他の機能を組み合わせることによりさらに強固なセキュリティ機能を構築することが可能です。

以上です。