CentOS Stream 9 の Apache で歓迎しない bot を 403 にする
篠原 隆司
アフィリエイト広告を利用しています
このページの内容が役に立ったら X (旧twitter) でフォローして頂けると励みになります
挨拶や報告は無しで大丈夫です
さくらのVPSのCentOS Stream 9に、Apache の設定を足した覚書です。
今回は、Apache の設定で、歓迎しない bot を同居するすべてのサイトでまとめて 403 にします。海外からのアクセスを止める設定は、別の記事にしています。
記事の中の IP アドレス・ドメインなどは、実際のものから書き換えています。
なお本番環境で利用される場合はここにある内容だけを鵜呑みにせずセキュリティ専門家に相談されることをお勧めします。
環境
| 実施日 | 2026-10-11 |
| サーバ | さくらのVPS 2G
|
| OS | CentOS Stream 9 |
cat /etc/redhat-release
CentOS Stream release 9
経緯と解決策
経緯について
サイトによっては、アクセスのほとんどが bot ということがあります。たとえば 1 日 約 10 万件のアクセスの大半が bot、という例もあります。
サイトのプログラム(PHP など)の中で bot を見分けて、待たせたり別のページへ移したりするだけでは、bot は来続けます。そのたびにプログラムが動くので、サーバーの負担は減りません。
そこで、プログラムまで届く前に、Apache で 403 にします。
1 ユーザー 1 ドメインの形で、いくつものサイトが同居しているサーバーでは、サイトごとの .htaccess ではなく、Apache の全体の設定に書けば、すべてのサイトに同じように効きます。
解決案を考える
この記事で言う歓迎しない bot とは、robots.txt に従うと運営元が公式に約束していない bot、運営元が分からない bot、名乗らない bot のことです。robots.txt では止められないので、Apache で 403 にします。
robots.txt に従うと約束している bot は、来てほしくないものでも、歓迎しない bot には含めません。締め出さずに、robots.txt の Crawl-delay で頻度を落として扱います(この記事では扱いません)。
403 にするものは、次の 3 種類です。
- 名前で分かる、約束していない bot(Bytespider など)
- プログラムの既定の名乗りのまま来るもの(curl・python-requests など)
- ブラウザを装った名乗りや、名乗りが空のもの
設定は /etc/httpd/conf.d/ に 1 ファイルだけ置き、<Location "/"> で全サイトに効かせます。サーバー自身からのアクセスは止めません。
今回行うこと
はじめに、使うモジュールと、止めてはいけないアクセスが無いかを確かめます。
そのあと、歓迎しない bot を止める設定ファイル bot_block.conf を 1 つ作り、Apache に読み込ませます。
最後に、サーバーの外から名乗りを変えて試し、止まるもの・通るものを確かめます。
事前確認
Apache とモジュール
Apache のバージョンです。
httpd -v
Server version: Apache/2.4.62 (CentOS Stream)
Server built: Aug 18 2026 00:00:00
使うモジュールが読み込まれているかを確認します。名乗りで目印を付けるのが mod_setenvif、Require env を判定するのが mod_authz_core、Require local を判定するのが mod_authz_host です。
httpd -M 2>/dev/null | grep -E 'setenvif|authz_core|authz_host'
authz_core_module (shared)
authz_host_module (shared)
setenvif_module (shared)
正規の通知を止めないか
外部のサービスからの通知(webhook)は、プログラムの名乗りのまま来ることがあります。同居しているサイトに通知の受け口があると、それも 403 になってしまいます。
アクセスログに名乗り(User-Agent)が入っていない場合は、先に、全サイトのアクセスを名乗りとサイト名つきで記録する設定を入れておきます。何も止めない設定です。
vi /etc/httpd/conf.d/bot_measure.conf
# bot 対策の調査用。全バーチャルホストのアクセスを UA とサイト名つきで記録する
LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" vhost_ua
GlobalLog "logs/all_vhosts_ua_log" vhost_ua
出力先は /var/log/httpd/all_vhosts_ua_log です。既定の logrotate(/var/log/httpd/*log)で回ります。
しばらく記録してから、プログラムの名乗りが、どのサイトにどれだけ来ているかを数えます。
G=/var/log/httpd/all_vhosts_ua_log
P='curl|wget|python-requests|python-urllib|python-httpx|aiohttp|go-http-client|libwww-perl|java/'
# サイトごとの件数
awk -F'"' '{print $1"\t"$6}' $G | grep -iE "$P" | awk '{print $1}' | sort | uniq -c | sort -rn
# 名乗りの種類ごとの件数
awk -F'"' '{print $6}' $G | grep -ioE "$P" | tr A-Z a-z | sort | uniq -c | sort -rn
# メソッドごとの件数
awk -F'"' '{print $6"\t"$2}' $G | grep -iE "$P" | awk -F'\t' '{split($2,a," "); print a[1]}' | sort | uniq -c
プログラムの名乗りで来るものには、外部リンクをたどる bot や、不審な POST などもあります。正規の通知とみられるものが無ければ、そのまま止めて構いません。
通知の受け口があるサイトでは、そのサイト(または URL)を例外にしてから入れてください。
自分で <Location "/"> を持つサイト
バーチャルホストが自分で <Location "/"> に認可を書いていると、そのサイトでは、今回の設定がその書き方に置き換えられて効きません。
バーチャルホストの設定ファイルがある場所(ここでは /etc/httpd/conf.d/vhosts/。置き場はサーバーによって違います)を探します。
grep -rnE '<Location[[:space:]]+"?/"?[[:space:]]*>' /etc/httpd/conf.d/vhosts/
<Location / > のように空白が入った書き方にも当たるようにしています。
見つかった場所が、Basic 認証で守った管理用の場所などであれば、そのままでも困りません。公開しているページなら、その <Location> の中にも同じ判定を足します。
bot 対策を導入する
bot_block.conf を作る
<Location "/"> の直下の AuthMerging And を間違えると、サイトごとで設定している Basic 認証などが効かなくなる恐れがあります。設定後は効いているか確認するようにしてください。
vi /etc/httpd/conf.d/bot_block.conf
# 歓迎しない bot を全サイトで 403 にする
# 対象: robots.txt に従うと約束していない・運営元が分からない・名乗らないもの
# 運営元の公式な約束が無い、または User-agent: * に従わないと明言しているもの
SetEnvIfNoCase User-Agent "bytespider|tiktokspider|thinkbot|aionbot|petalbot|wellknownbot|sirdatabot" block_bot
# プログラムが使う UA
SetEnvIfNoCase User-Agent "curl|wget|python-requests|python-urllib|python-httpx|aiohttp|go-http-client|libwww-perl|java/" block_bot
# 偽装した UA
SetEnvIf User-Agent "Chrome/\d+\.0\.\d{4}\.\d{4}" block_bot
SetEnvIf User-Agent "Chrome/69\..*(?<!Safari/537\.36)$" block_bot
SetEnvIf User-Agent "G735-L23.*Chrome/81\." block_bot
# UA が空
SetEnvIf User-Agent "^$" block_bot
# 名乗りに関係なく通すサイト(例。使わなければ消す)
SetEnvIfNoCase Host "^(www\.)?site-a\.example$" allow_site
SetEnvIfNoCase Host "^(www\.)?site-b\.example$" allow_site
SetEnvIfNoCase Host "^(www\.)?site-c\.example$" allow_site
<Location "/">
# 各サイトの Basic 認証などと AND で合わせる。無いと、それらがこの設定に置き換わって効かなくなる
AuthMerging And
<RequireAny>
# このサーバ自身からのアクセス(cron など)は止めない
Require local
# 上で決めたサイトは、名乗りに関係なく通す
Require env allow_site
<RequireAll>
Require all granted
Require not env block_bot
</RequireAll>
</RequireAny>
</Location>
<Location "/"> の直下の AuthMerging And を忘れないでください。これが無いと、各サイトの Basic 認証や Require all denied が、この設定で置き換えられて効かなくなります。止める bot 以外は、パスワードを聞かれずに見えてしまいます。しかもエラーにはならないので、気づきにくいです(「トラブル対応」の「Basic 認証が効かなくなる」)。
反映する
構文を確認してから、Apache に読み込ませます。
# 構文の確認
httpd -t
# 反映
systemctl reload httpd
conf.d/ にファイルを置いた時点で、次に Apache が設定を読み直したときに効き始めます。logrotate も、ログを回すたびに Apache を読み直させます(/etc/logrotate.d/httpd の postrotate)。置いたら、事前確認を済ませてすぐに反映と確認まで進めるか、確認が終わるまで別の場所に置いておくのが安全です。
確認
サーバーの外(手元のパソコンなど)から確かめます。サーバーの上で打つと、Require local で通ってしまいます。
止める bot の名乗りです。
curl -s -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 (compatible; Bytespider)' https://www.example.com/
403
通す bot の名乗りです。
curl -s -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 (compatible; Googlebot/2.1)' https://www.example.com/
200
名乗りのヘッダーを付けないアクセスです。
curl -s -o /dev/null -w '%{http_code}\n' -H 'User-Agent:' https://www.example.com/
403
ヘッダーそのものが無くても、^$ の決まりに当たって 403 になります。
curl の既定の名乗り(curl/8.x.x)のままでも 403、ふつうの Chrome の名乗りなら 200 です。止めた bot は /robots.txt も 403 になります。
Basic 認証をかけた場所があれば、そこが今までどおりパスワードを聞いてくる(401 になる)ことも確かめます。200 なら AuthMerging And が効いていません。/basic-auth-dir/ は、Basic 認証をかけた場所に置き換えてください。
curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/basic-auth-dir/
補足説明
判定の組み立て
<RequireAny> は「どれか 1 つでも満たせば通す」、<RequireAll> は「すべて満たせば通す」です。今回の設定を言葉にすると、次のようになります。
サーバー自身からのアクセス
または
通すと決めたサイトへのアクセス
または
(だれでも かつ 403 にする bot ではない)
Require not は「当てはまったら通さない」だけで、それ自体は通す条件になりません。そのため、Require all granted と組にして <RequireAll> に入れています。
通すと決めたサイトは、サイト名(Host)で allow_site の目印を付けます。<RequireAll> の外にあるので、止める bot の名乗りでも通ります。そのサイトの Basic 認証は、AuthMerging And で今までどおり効きます。
使わないときは、SetEnvIfNoCase Host の行と Require env allow_site を消します。
名乗りで目印を付ける
SetEnvIf / SetEnvIfNoCase は、User-Agent が正規表現に当てはまったときに、環境変数(ここでは block_bot)を立てます。NoCase の付いたほうは、大文字と小文字を区別しません。
立てた目印を、Require not env block_bot で判定します。
決まりと、当てはまるものです。
| 決まり | 当てはまるもの |
|---|---|
bytespider|tiktokspider|… | robots.txt に従うと公式に約束していない、または User-agent: * には従わないと明言している bot |
curl|wget|python-requests|… | プログラムや HTTP のライブラリの、既定の名乗りのままのアクセス |
Chrome/\d+\.0\.\d{4}\.\d{4} | Chrome を装った名乗り。いまの Chrome の名乗りは、版の後ろが 0 になっている(例:Chrome/131.0.0.0)。後ろが 4 桁・4 桁になっているもの(例:Chrome/59.0.6868.1090)を、装ったものとみなす |
Chrome/69\..*(?<!Safari/537\.36)$ | Chrome 69 を名乗り、末尾が Safari/537.36 で終わらない不完全な名乗り。決まった間隔でトップページを見に来るものに、この形が見られる |
G735-L23.*Chrome/81\. | 古い Android 端末(Huawei G735-L23)と Chrome 81 の組み合わせ。いくつもの IP アドレスから、同じ名乗りで巡回するものがある |
^$ | 名乗りが空のもの |
ブラウザを装った名乗りの決まりは、自分たちで使っている監視などのプログラムにも当たることがあります(「トラブル対応」の「死活監視が 403 になる」)。
サーバー自身は止めない
Require local は、このサーバー自身からのアクセス(127.0.0.1 など)を通します。cron から自分のサイトを呼んだときに、名乗りが curl でも止まらないようにするためです。
AuthMerging And が要る理由
Apache 2.4 では、<Location> の設定は、<Directory> や .htaccess より後に合わさります。
そして、認可(Require)の合わさり方は、何も書かなければ AuthMerging Off です。これは「後から来た認可で、先の認可を置き換える」という意味です。
そのため、全サイトに効く <Location "/"> に認可を書くと、各サイトの <Directory> や .htaccess に書いた Require valid-user(Basic 認証)や Require all denied が、すべて置き換えられてしまいます。
AuthMerging And を書くと、先の認可と、この <Location> の認可の両方を満たしたときだけ通すようになります。
| アクセス | AuthMerging なし | AuthMerging And |
|---|---|---|
| ふつうのページ | 通る | 通る |
| Basic 認証のページ(パスワードなし) | 通ってしまう | パスワードを聞かれる |
Require all denied のページ | 通ってしまう | 403 |
| サーバー自身から、Basic 認証のページ | 通ってしまう | パスワードを聞かれる |
| 止める bot | 403 | 403 |
トラブル対応
死活監視が 403 になる
概要
bot 対策を入れたあとに、死活監視から「403 になった」という知らせが届くことがあります。
監視プログラムの名乗りが、偽装した bot を見分ける決まりに当たり、監視のアクセスが 403 で止められている状況です。サイトそのものは、ふつうに見えています。
知らせが毎回ではなく、届く時刻もばらばらだと、ときどき起きているように見えます。実際には、監視のアクセスは常に 403 になっています(「知らせがときどきしか届かない理由」)。
原因を探す
アクセスログを見る
監視サーバーの IP アドレス(ここでは 203.0.113.10)で、アクセスログを絞り込みます。
grep '^203\.0\.113\.10 ' /home/example_user/logs/ssl_access_log \
| awk '{print $4, $7, $9}' \
| tail -n 100
[30/Oct/2026:20:30:02 / 403
[30/Oct/2026:20:30:20 / 403
[30/Oct/2026:20:35:02 / 403
[30/Oct/2026:20:35:15 / 403
(中略)
[31/Oct/2026:00:30:03 / 403
[31/Oct/2026:00:30:15 / 403
[31/Oct/2026:00:35:03 / 403
[31/Oct/2026:00:35:15 / 403
この例では、5 分おきに 2 回ずつ、ずっと 403 になっています。
エラーログには、Apache の設定で拒否したという記録が残ります。
[Fri Oct 30 18:40:15.047449 2026] (中略) [client 203.0.113.10:33930] AH01630: client denied by server configuration: /home/example_user/public_html/
監視サーバーから curl で試す
名乗りを変えて結果が変わるかを見ます。
curl --http1.0 -sS -o /dev/null -w '%{http_code}\n' https://www.example.com/
403
curl --http1.0 -A 'Mozilla/5.0 (compatible; InternalMonitor/1.0)' -sS -o /dev/null -w '%{http_code}\n' https://www.example.com/
200
名乗りを変えると 200 になれば、名乗りの判定で止まっています。
原因
監視プログラムの名乗りが、古いブラウザの名乗りのままになっていることがあります。たとえば、次のような設定です。
'url_check_user_agent' => 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100',
Chrome 69 を名乗っていて、末尾が Safari/537.36 で終わっていません。bot_block.conf の次の決まりに、そのまま当てはまります。
SetEnvIf User-Agent "Chrome/69\..*(?<!Safari/537\.36)$" block_bot
「決まった間隔でトップページを見に来る、不完全な名乗り」という、偽装した bot を見分けるための特徴が、そのまま死活監視にも当てはまってしまいます。
直す
直し方
サーバー側の bot 対策はそのままにして、監視プログラムの名乗りを、監視だと分かるものに変えます。
'url_check_user_agent' => 'User-Agent: Mozilla/5.0 (compatible; InternalMonitor/1.0)',
bot_block.conf の Require local の並びに、監視サーバーの IP アドレスを足して通す方法もあります。サーバー側の設定を変えたくなければ、監視の側を直します。
知らせがときどきしか届かない理由
監視プログラムによっては、異常が続くと、知らせる間隔を段階的に延ばすものがあります(30 分、1 時間、2 時間……のように)。
その場合、ずっと 403 でも、知らせはときどきしか届かず、時刻もばらばらに見えます。知らせの回数ではなく、アクセスログで状態を確かめてください。
Basic 認証が効かなくなる
概要
AuthMerging And を入れずに bot_block.conf を反映すると、Basic 認証をかけた場所で、パスワードを聞かれずにページが見えてしまいます。Require all denied にした場所も見えてしまいます。
全サイトに効く <Location "/"> の認可が、各サイトの Basic 認証などを置き換えている状況です。エラーにはならないので、気づきにくいです。
原因を探す
モジュールを確認する
まず、Basic 認証に使うモジュールが読み込まれているかを見ます。
httpd -M 2>/dev/null | grep -E 'auth_basic|authn_core|authn_file|authz_core|authz_user'
auth_basic_module (shared)
authn_core_module (shared)
authn_file_module (shared)
authz_core_module (shared)
authz_user_module (shared)
すべて読み込まれていれば、モジュールの問題ではありません。
全体の設定を探す
いくつもの場所で同時に効かないなら、個別の設定ではなく、全体に効く設定を疑います。<Location> を探します。
grep -RniE \
'<Location|<LocationMatch|AuthMerging|Require all granted' \
/etc/httpd/conf /etc/httpd/conf.d
(中略)
/etc/httpd/conf.d/bot_block.conf:23:<Location "/">
(中略)
bot_block.conf の <Location "/"> が、全サイトのすべてのパスに効いています。
出力に AuthMerging の行が無ければ、入っていません。
原因
AuthMerging And が無いと、<Location "/"> の認可が、各ディレクトリの Basic 認証や Require all denied を置き換えます(「AuthMerging And が要る理由」)。
止める bot 以外は <Location "/"> の条件を満たすので、パスワードなしで見えてしまいます。
直す
<Location "/"> の直下に AuthMerging And を足し、構文を確かめてから反映します。本文の bot_block.conf には、最初から入れてあります。
httpd -t
systemctl reload httpd
まとめ
- robots.txt に従うと約束していない・運営元が分からない・名乗らない bot を、Apache の全体の設定でまとめて 403 にしました
- 設定は
/etc/httpd/conf.d/の 1 ファイルだけで、同居するすべてのサイトに効きます - 入れたあとは、自分たちの監視などのプログラムが止まっていないかも確かめます
<Location "/"> に認可を書くときは、AuthMerging And を必ず入れてください。無いと、各サイトの Basic 認証が効かなくなり、しかもエラーにはなりません。関連する記事です。
このページの内容が役に立ったら X (旧twitter) でフォローして頂けると励みになります
本ページの内容は可能な限り正確な情報を提供するよう努めていますが、内容の正確性・最新性・安全性を保証するものではありません。本情報を利用して生じたいかなる損害についても、当方は一切の責任を負いません。実施にあたっては必ずご自身の判断と自己責任にてお願いいたします。