CentOS Stream 9 の Apache で海外からのアクセスを 403 にする

CentOS Stream 9 の Apache で海外からのアクセスを 403 にする

アフィリエイト広告を利用しています

このページの内容が役に立ったら X (旧twitter) でフォローして頂けると励みになります
挨拶や報告は無しで大丈夫です

さくらのVPSのCentOS Stream 9に、Apache の設定を足した覚書です。

今回は、Apache で、日本以外の IP アドレスからのアクセスを、同居するすべてのサイトでまとめて 403 にします。検索エンジンなど来てほしい bot は、海外からでも通します。歓迎しない bot を名乗りで止める設定は、別の記事にしています。
記事の中の IP アドレス・ドメインなどは、実際のものから書き換えています。

なお本番環境で利用される場合はここにある内容だけを鵜呑みにせずセキュリティ専門家に相談されることをお勧めします。

環境

実施日2026-10-12
サーバさくらのVPS 2G
OSCentOS Stream 9
cat /etc/redhat-release
CentOS Stream release 9

経緯と解決策

経緯について

名乗り(User-Agent)で bot を止めても、ブラウザを名乗って来るものは残ります。
たとえば、ある 2 時間のアクセスを数えた例では、ブラウザを名乗るアクセスの 86% が海外からでした。しかも 2,839 個の IP アドレスにばらけていて、1 つの IP アドレスからは 2 回ほどしか来ません。多数の IP アドレスを使い回して回るクローラーの動きで、名乗りでも、IP アドレスごとの回数の制限でも捕まえられません。

日本語のサイトで、読み手がほとんど日本にいるなら、日本以外の IP アドレスからのアクセスを止めると、こうしたクローラーをまとめて減らせます。

解決案を考える

アクセスを次のどれかに当てはまれば通し、どれにも当てはまらなければ 403 にします。

  1. サーバー自身からのアクセス:cron などで自分のサイトを呼ぶことがあるため
  2. 来てほしい bot:検索エンジン、広告、SNS のリンクの表示、AI の検索など。海外から来るので、名乗りで通す
  3. 日本の IP アドレス:APNIC が公開している割り当ての一覧から作る

例外として、Let's Encrypt の証明書の確認(/.well-known/acme-challenge/)は、国を問わず通します。証明書の発行と更新が止まらないようにするためです。

日本の IP アドレスの一覧は、追加のモジュール(国を判定する mod_maxminddb など)を使わずに、Apache の Require ip の行として書き出します。割り当ては変わっていくので、月に 1 回、自動で作り直します。

止まってしまうものも、先に知っておきます。

  • 海外に住んでいる人や、海外の VPN を使っている人は、読めなくなる
  • 外部のサービスからの通知(webhook)を受けるサイトでは、海外から来る通知が止まる。受けるサイトがあれば、その URL を例外にする
  • 日本にあるクラウドのサーバーでも、APNIC の一覧では日本に入らないことがある(AWS や Google Cloud の東京の範囲など)。そこからのアクセスも止まる

今回行うこと

はじめに、使うモジュールと、ほかの設定との関係を確かめます。
そのあと、日本の IP アドレスの一覧を作るスクリプトと、設定ファイル geo_block.conf を作り、Apache に読み込ませます。
最後に、日本の IP アドレスからの確認とアクセスログで効き目を確かめ、一覧を毎月作り直す仕組みを入れます。

事前確認

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 ip と 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)

ほかの設定との関係

自分で <Location "/"> に認可を書いているバーチャルホストや、外部のサービスからの通知を受けるサイトが無いかも、先に確かめておきます。確かめ方は、歓迎しない bot を止める記事の事前確認と同じです。

海外からのアクセスの遮断を導入する

日本の IP アドレスの一覧を作るスクリプト

APNIC の割り当ての一覧を取ってきて、日本(JP)の IP アドレスの範囲を、Apache の Require ip の行にします。初めに作るときと、月に 1 回作り直すときの両方に使います。

vi /usr/local/sbin/update_jp_ip.sh
#!/bin/bash
# APNIC の割り当て一覧から日本の IP 帯を取り出し、Apache の Require ip の一覧を作り直す
# 失敗したら前の一覧に戻す。一覧が急に小さくなったときも差し替えない
set -eu

OUT=/etc/httpd/geo/jp_ip.conf
SRC=$(mktemp)
NEW=$(mktemp)
trap 'rm -f "$SRC" "$NEW"' EXIT

mkdir -p /etc/httpd/geo
curl -fsS -o "$SRC" https://ftp.apnic.net/stats/apnic/delegated-apnic-latest

awk -f - "$SRC" > "$NEW" <<'AWK'
# APNIC の割り当て一覧から、日本(JP)の IP 帯を Apache の Require ip の行にする
function n2ip(n) { return int(n / 16777216) "." int(n / 65536) % 256 "." int(n / 256) % 256 "." n % 256 }
function ip2n(ip,  a) { split(ip, a, "."); return ((a[1] * 256 + a[2]) * 256 + a[3]) * 256 + a[4] }
BEGIN { FS = "|" }
$2 == "JP" && $3 == "ipv4" {
    s = ip2n($4); c = $5
    # 帯の大きさが 2 のべき乗でない・境界がそろっていない場合も、CIDR に分けて書く
    while (c > 0) {
        b = 1; k = 0
        while (b * 2 <= c && s % (b * 2) == 0) { b *= 2; k++ }
        print "Require ip " n2ip(s) "/" (32 - k)
        s += b; c -= b
    }
}
$2 == "JP" && $3 == "ipv6" { print "Require ip " $4 "/" $5 }
AWK

# 2026-10-12 時点で 5,503 行。極端に少ないときは取得の失敗とみなす
if [ "$(wc -l < "$NEW")" -lt 4000 ]; then
    echo "jp_ip: 行数が少なすぎるので差し替えない" >&2
    exit 1
fi

[ -f "$OUT" ] && cp -p "$OUT" "$OUT.bak"
install -m 644 "$NEW" "$OUT"

if httpd -t 2>/dev/null; then
    systemctl reload httpd
else
    echo "jp_ip: httpd -t が通らないので前の一覧に戻す" >&2
    [ -f "$OUT.bak" ] && cp -p "$OUT.bak" "$OUT"
    exit 1
fi

root だけが実行できるようにします。

chmod 700 /usr/local/sbin/update_jp_ip.sh

一覧を作る

設定ファイルを置く前に、一度実行して一覧を作ります。この時点では、一覧はまだどの設定からも読まれていないので、Apache を読み直してもサイトの動きは変わりません。

/usr/local/sbin/update_jp_ip.sh && echo OK
wc -l /etc/httpd/geo/jp_ip.conf
head -3 /etc/httpd/geo/jp_ip.conf
OK
5503 /etc/httpd/geo/jp_ip.conf
Require ip 1.0.16.0/20
Require ip 1.0.64.0/18
Require ip 1.1.64.0/18

行の数は、APNIC の一覧が更新されると少しずつ変わります。

geo_block.conf を作る

<Location "/"> の直下の AuthMerging And を間違えると、サイトごとで設定している Basic 認証などが効かなくなる恐れがあります。設定後は効いているか確認するようにしてください。

vi /etc/httpd/conf.d/geo_block.conf
# 海外からのアクセスを、全サイトで 403 にする(来てほしい bot は海外からでも通す)

# ---- 海外からでも通す bot ----
SetEnvIfNoCase User-Agent "googlebot|bingbot|duckduckbot|yandexbot|applebot|baiduspider|seznambot" allowed_bot
SetEnvIfNoCase User-Agent "mediapartners-google|adsbot-google" allowed_bot
SetEnvIfNoCase User-Agent "facebookexternalhit|twitterbot|slackbot|discordbot" allowed_bot
SetEnvIfNoCase User-Agent "oai-searchbot|chatgpt-user|amzn-searchbot|amzn-user|meta-externalfetcher|meta-webindexer|claude-searchbot|claude-user|perplexitybot|perplexity-user" allowed_bot
SetEnvIfNoCase User-Agent "serankingbacklinksbot|ahrefsbot|semrushbot|dotbot|mj12bot|dataforseobot|barkrowler|linkupbot" allowed_bot
SetEnvIfNoCase User-Agent "gptbot|claudebot|ccbot|googleother|amazonbot|meta-externalagent|reflectionbot|exasearchbot|cotoyogi" allowed_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

# ---- サイトの中で、海外からのアクセスも歓迎するディレクトリ(例。使わなければ消す) ----
SetEnvIfExpr "req('Host') =~ /^(www\.)?site-d\.example$/ && %{REQUEST_URI} =~ m#^/en/#" allow_site
SetEnvIfExpr "req('Host') =~ /^(www\.)?site-e\.example$/ && %{REQUEST_URI} =~ m#^/zh/#" allow_site
SetEnvIfExpr "req('Host') =~ /^(www\.)?site-f\.example$/ && %{REQUEST_URI} =~ m#^/ko/#" allow_site

<Location "/">
    # 各サイトの Basic 認証などと AND で合わせる。無いと、それらがこの設定に置き換わって効かなくなる
    AuthMerging And

    <RequireAny>
        # このサーバ自身からのアクセス(cron など)は止めない
        Require local
        # 上で決めたサイト・ディレクトリは、国を問わず通す
        Require env allow_site
        Require env allowed_bot
        # 日本の IP(APNIC の一覧から作る)
        Include /etc/httpd/geo/jp_ip.conf
    </RequireAny>
</Location>

# Let's Encrypt の確認は国を問わず通す("/" の Location より後に書く。後に書いたほうが優先される)
<Location "/.well-known/acme-challenge/">
    Require all granted
</Location>

このファイルは、海外からのアクセスを止める部分だけを取り出した形です。構文は httpd -t で確かめています。実際のサーバーでは、歓迎しない bot を止める設定と 1 つのファイルにまとめて動かしています(「すでに別の設定がある場合」)。

反映する

構文を確認してから、Apache に読み込ませます。ここで海外からのアクセスの遮断が効き始めます。

# 構文の確認
httpd -t

# 反映
systemctl reload httpd

確認

日本の IP アドレスのパソコンから、ふつうのブラウザの名乗りで試します。

curl -s -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36' https://www.example.com/
200

Let's Encrypt の確認のパスは、通ったうえで、ファイルが無いので 404 になります。403 でなければ通っています。

curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/.well-known/acme-challenge/test
404

海外からのアクセスは、日本のパソコンからは試せません。サーバーのアクセスログで、反映したあとの状態を数えます。
次のコマンドは、全サイトのアクセスを名乗りつきで記録している場合のものです(記録の設定は、歓迎しない bot を止める記事の事前確認)。S に数え始める時刻を入れます。

G=/var/log/httpd/all_vhosts_ua_log
S='12/Oct/2026:00:32'
awk -F'"' -v s="$S" '{split($1,a," "); split($3,b," "); t=substr(a[5],2,17); if (t >= s) print b[1], (tolower($6) ~ /bot|spider|crawl/ ? "named" : "browser")}' $G | sort | uniq -c | sort -rn

状態(200・403 など)と、名乗る bot(named)かブラウザを名乗るもの(browser)かで数えます。
うまく効いていれば、403 browser が多くなり、200 named が残ります。ある例では、反映して 10 分で、ブラウザを名乗るもの約 760 件のうち 483 件が 403 になり、名乗る bot は 200 が 415 件残っていました。

一覧を毎月作り直す

/etc/cron.monthly/ に置いたファイルは、anacron が月に 1 回まとめて実行します。その設定があるかを先に見ます。

grep -n monthly /etc/anacrontab
16:@monthly 45	cron.monthly		nice run-parts /etc/cron.monthly

上のスクリプトを呼ぶだけのファイルを置きます。

printf '#!/bin/sh\n/usr/local/sbin/update_jp_ip.sh\n' > /etc/cron.monthly/update_jp_ip
chmod 700 /etc/cron.monthly/update_jp_ip

中身と書き方を確かめます。

cat -A /etc/cron.monthly/update_jp_ip
sh -n /etc/cron.monthly/update_jp_ip && echo SYNTAX-OK
#!/bin/sh$
/usr/local/sbin/update_jp_ip.sh$
SYNTAX-OK

行の末尾の $ は改行の印です。^M が混ざっていなければ、改行は正しく LF です。

取り直しに失敗したときや、一覧が急に小さくなったときは、前の一覧のまま動き続けます。失敗のメッセージは、cron の決まりで root 宛てのメールに届きます。

元に戻す

設定ファイルを conf.d/ の外へ移して、読み直します。一覧(/etc/httpd/geo/)とスクリプトは残しておいて構いません。

mv /etc/httpd/conf.d/geo_block.conf /root/geo_block.conf.off
httpd -t && systemctl reload httpd

補足説明

判定の組み立て

<RequireAny> は「どれか 1 つでも満たせば通す」です。今回の設定を言葉にすると、次のようになります。

サーバー自身からのアクセス
または
海外も歓迎すると決めたサイト・ディレクトリへのアクセス
または
通す bot の名乗り
または
日本の IP アドレス

Include で読み込んだ一覧の Require ip の行は、それぞれが <RequireAny> の条件の 1 つになります。どれか 1 つの範囲に入っていれば通ります。

海外からのアクセスも歓迎するサイトは、サイト名(Host)で allow_site の目印を付けます。そのサイトへは、国を問わず通ります。そのサイトの Basic 認証は、AuthMerging And で今までどおり効きます。

英語版を /en/ のようなディレクトリで分けているサイトでは、SetEnvIfExpr でサイト名とパスの両方を見て、同じ allow_site の目印を付けます。そのディレクトリから下だけが国を問わず通り、ほかのパスは今までどおり日本の IP アドレスだけです。
使わないときは、SetEnvIfNoCase Host と SetEnvIfExpr の行、Require env allow_site を消します。消し忘れても、目印が立たないだけで、エラーにはなりません。

日本の IP アドレスの一覧

APNIC の一覧(delegated-apnic-latest)は、1 行が 1 つの割り当てで、| で区切られています。国が JP の行だけを取り出します。

apnic|JP|ipv6|2001:200::|35|19990813|allocated
  • IPv4 の行は、始まりのアドレスと個数で書かれています。個数が 2 のべき乗でない場合や、境界がそろっていない場合も、CIDR に分けて書き出すようにしています
  • IPv6 の行は、始まりのアドレスとプレフィックス長なので、そのまま書き出します
  • 取ってきた一覧の例では、IPv4 が 4,737 件、IPv6 が 766 件で、合わせて 5,503 行になりました。元の範囲と書き出した CIDR とで、IPv4 のアドレスの数(188,583,168)が一致することも確かめています

スクリプトは、次のときに一覧を差し替えません。

  • APNIC から取ってこられなかったとき(curl -f と set -e で止まる)
  • 書き出した行が 4,000 行より少ないとき(取得の失敗とみなす)
  • 差し替えたあとの httpd -t が通らないとき(控えておいた前の一覧に戻す)

一覧を conf.d の外に置く理由

一覧は /etc/httpd/geo/ に置き、<RequireAny> の中から Include で読み込みます。
conf.d/ に *.conf の名前で置くと、Apache が設定ファイルとして直接読み込みます。そうすると、Require ip の行が <Location> の外に出て、エラーになります。

海外からでも通す bot

行中身
1 行目検索エンジン(Google・Bing・DuckDuckGo・Yandex・Apple・Baidu・Seznam)
2 行目Google の広告
3 行目SNS やチャットのリンクの表示(Facebook・X・Slack・Discord)
4 行目AI の検索と、利用者の依頼で取りに来るもの(OpenAI・Amazon・Meta・Anthropic・Perplexity)
5 行目SEO のツール(Ahrefs・Semrush など)
6 行目AI の学習などのクローラー(GPTBot・ClaudeBot・Common Crawl など)

5・6 行目は、来てほしくはないものの、robots.txt に従うと約束している bot です。締め出さずに、robots.txt の Crawl-delay で頻度を落とす扱いにしているので、ここでは通しています。

名乗りは自己申告なので、偽ることができます。ここで通しているのは「海外からでも、この名乗りなら通す」というだけで、本物かどうかまでは確かめていません。偽物が目立つようなら、各社が公開している IP アドレスの範囲で確かめる形に進めます。

反映したあとは、403 になった名乗る bot を数えて、来てほしいものが混ざっていないかを確かめます。一覧に無い名前で海外から来たものは、ここで止まります。

G=/var/log/httpd/all_vhosts_ua_log
S='12/Oct/2026:00:32'
awk -F'"' -v s="$S" '{split($1,a," "); split($3,b," "); t=substr(a[5],2,17); if (t >= s && b[1]=="403" && tolower($6) ~ /bot|spider|crawl/) print $6}' $G | sort | uniq -c | sort -rn | head -15

たとえば meta-webindexer(Meta AI の検索のための bot)のように、一覧に無い名前で来る、来てほしい bot もあります。見つけたら、運営元の公式ページで役割を確かめてから、通す bot の行に足します。上の設定には、足したあとの名前が入っています。

Let's Encrypt の確認は通す

Let's Encrypt は、証明書の発行と更新のときに、海外の複数の地点から /.well-known/acme-challenge/ を見に来ます。ここを止めると、証明書が更新できなくなり、全サイトの証明書が期限切れになります。
後に書いた <Location> のほうが優先されるので、"/" より後に書いて、このパスだけ誰でも通します。

Let's Encrypt の設定は、CentOS Stream 9に無料SSL「Let's Encrypt」を設定するで行っています。

すでに別の設定がある場合

歓迎しない bot を止める設定のように、ほかの設定でもすでに <Location "/"> に認可を書いている場合です。

同じ <Location "/"> の認可が 2 つのファイルにあると、後から読まれたほうが、先のほうを置き換えます(conf.d/ のファイルは名前の順に読まれる)。
両方の <Location "/"> に AuthMerging And があれば、置き換えずに、両方の条件を満たしたときだけ通すようになります。

このサーバーでは、2 つを 1 つのファイルの 1 つの判定にまとめています。

<Location "/">
    AuthMerging And

    <RequireAny>
        Require local
        <RequireAll>
            Require not env block_bot
            <RequireAny>
                Require env allowed_bot
                Include /etc/httpd/geo/jp_ip.conf
            </RequireAny>
        </RequireAll>
    </RequireAny>
</Location>

言葉にすると「サーバー自身 または (止める bot ではない かつ (通す bot または 日本の IP アドレス))」です。止める bot の判定が <RequireAll> の中にあるので、日本の IP アドレスからでも、通す bot の名乗りを含んでいても、止める bot は 403 になります。SetEnvIf の行は、両方の記事のものを同じファイルの先頭に並べます。

まとめ

  • 日本以外の IP アドレスからのアクセスを、Apache の全体の設定でまとめて 403 にしました
  • 日本の IP アドレスの一覧は APNIC の公開データから作り、月に 1 回、自動で作り直します
  • 検索エンジンなど来てほしい bot と、Let's Encrypt の確認は、海外からでも通します
  • 入れたあとは、403 になった bot に、来てほしいものが混ざっていないかを確かめます

関連する記事です。