対象読者: Cloudflare などの CDN/WAF の後ろで Web サイトを運用している Web 担当者・制作会社の方
解決すること: 「CDN を入れたのに、サーバー(オリジン)へ直接アクセスされると守りが効かない」状態を、設定 5 つで塞ぎます
前提環境: nginx 1.19.4 以降 / Apache 2.4 / Linux(iptables + ipset)、Cloudflare のプロキシ(オレンジ雲)を利用中
なぜ「オリジン直アクセス」が問題なのか
Cloudflare の WAF・レート制限・DDoS 吸収は、Cloudflare を経由したリクエストにだけ効きます。
オリジン(=実際にサイトを動かしているサーバー)の IP アドレスが知られ、そこへ直接 HTTP(S) で接続できる状態だと、これらの守りをすべて迂回されます。
オリジン IP が知られる主な経路は、Cloudflare 公式の「Protect your origin server」で次のように整理されています。
| 経路 | 内容 |
|---|---|
| プロキシしていない DNS レコード |
mail. ftp. dev. などのサブドメインや、SPF の TXT に同じ IP が書かれている |
| DNS の履歴 | Cloudflare 導入前の A レコードが第三者のデータベースに残っている |
| 同居しているメールサーバー | Web と同じサーバーから送ったメール(エラーの返送含む)のヘッダに IP が載る |
履歴は消せないため、公式ドキュメントは導入後にオリジン IP を変更(ローテーション)することを推奨しています。IP を変えられない場合でも、以下の 5 つで「知られても入れない」状態を作れます。
設定1: DNS レコードを棚卸しし、オリジン IP が出ていないか確認する
まず自社ドメインのレコードが Cloudflare の IP を返しているかを機械的に確認します。
Cloudflare の IP レンジは API(https://api.cloudflare.com/client/v4/ips)で公開されているので、それと照合します。
# 使い方: bash check_origin.sh example.com www mail ftp dev staging
D="$1"; shift
CF=$(curl -fsS https://api.cloudflare.com/client/v4/ips)
for h in @ "$@"; do
f=$([ "$h" = "@" ] && echo "$D" || echo "$h.$D")
for ip in $(dig +short A "$f"; dig +short AAAA "$f"); do
python3 -c '
import ipaddress as I, json, sys
f, ip, cf = sys.argv[1:4]
try: a = I.ip_address(ip)
except ValueError: sys.exit() # CNAME の途中結果は除外
r = json.loads(cf)["result"]
ok = any(a in I.ip_network(c) for c in r["ipv4_cidrs"] + r["ipv6_cidrs"])
print(f"{f:28} {ip:40}", "OK" if ok else "要確認(Cloudflare以外)")' "$f" "$ip" "$CF"
done
done
dig +short TXT "$D" | grep -i 'v=spf1' # SPF に Web サーバーの IP が無いか
「要確認」と出たレコードが Web サーバーと同じ IP なら、次のどちらかで対応します。
- Web 用途ならプロキシ(オレンジ雲)を有効にする
- メールや FTP など Cloudflare で中継できない用途は、Web サーバーとは別の IP・別のサーバーに分ける
設定2: ファイアウォールで 80/443 を Cloudflare からだけ受け付ける
最も確実なのは、アプリより手前のファイアウォール層で絞ることです(公式ドキュメントでも iptables での許可リストが例示されています)。
ipset を使うと、IP レンジの更新時にルールを作り直さず中身だけ入れ替えられます。
#!/usr/bin/env bash
# /usr/local/sbin/cf-firewall-sync.sh — 80/443 は Cloudflare の IP レンジからだけ許可
# 事前: apt install ipset (永続化は ipset-persistent / netfilter-persistent)
set -euo pipefail
for v in 4 6; do
L=$(curl -fsS "https://www.cloudflare.com/ips-v$v")
# 取得失敗や空リストで全遮断にならないよう件数を確認
[ "$(grep -c / <<<"$L")" -ge 5 ] || { echo "ips-v$v が異常なため中止"; exit 1; }
fam=$([ $v = 4 ] && echo inet || echo inet6)
ipt=$([ $v = 4 ] && echo iptables || echo ip6tables)
ipset create cf$v hash:net family $fam -exist
ipset create cf$v-new hash:net family $fam -exist
ipset flush cf$v-new
for c in $L; do ipset add cf$v-new "$c"; done
ipset swap cf$v-new cf$v && ipset destroy cf$v-new # 中身だけ入れ替え
R="-p tcp -m multiport --dports 80,443"
# DROP を先に入れ、その前に ACCEPT を差し込む(既にあれば何もしない)
$ipt -C INPUT $R -j DROP 2>/dev/null || $ipt -I INPUT 1 $R -j DROP
$ipt -C INPUT $R -m set --match-set cf$v src -j ACCEPT 2>/dev/null \
|| $ipt -I INPUT 1 $R -m set --match-set cf$v src -j ACCEPT
done
週 1 回 cron で回せば、レンジの変更にも追従できます(公式によると変更はまれで、本番投入前にリストへ追加されます)。
注意点:
- SSH のポートには触れていません。初回はサーバーのコンソール(クラウドの管理画面など)を開いた状態で実行してください
- 外形監視サービスや決済事業者の通知など、Cloudflare を通らずにオリジンへ来る正規の通信があれば、その送信元も許可に加えます
- Docker で公開したポートは INPUT チェーンを通らないため、この設定では絞れません(
DOCKER-USERチェーン側で同様の制御が必要です)
設定3: nginx でも Cloudflare 以外を拒否する(realip との罠に注意)
ファイアウォールを触れない共用環境や、多層で守りたい場合は nginx でも絞ります。
ここによくある罠があります。アクセスログに訪問者の IP を残すために real_ip_header CF-Connecting-IP; を設定していると、$remote_addr は訪問者の IP に書き換わるため、allow/deny で Cloudflare のレンジを許可すると正規の訪問者を全員弾いてしまいます。
そこで、書き換え前の接続元 IP を持つ $realip_remote_addr を geo で判定します。realip を使っていてもいなくても同じ設定で動きます。
#!/usr/bin/env bash
# /usr/local/sbin/cf-nginx-sync.sh — Cloudflare レンジの geo 定義を生成し、検証が通ったときだけ反映
set -euo pipefail
OUT=/etc/nginx/conf.d/cloudflare-geo.inc
NEW=$(for v in 4 6; do curl -fsS "https://www.cloudflare.com/ips-v$v"; echo; done \
| grep / | sed 's/^/ /; s/$/ 1;/')
[ "$(wc -l <<<"$NEW")" -ge 15 ] || { echo "リストが異常なため中止"; exit 1; }
[ -f "$OUT" ] && cp -p "$OUT" "$OUT.bak"
echo "$NEW" > "$OUT"
if ! nginx -t; then
[ -f "$OUT.bak" ] && cp -p "$OUT.bak" "$OUT"
echo "nginx -t 失敗のため元に戻しました"; exit 1
fi
systemctl reload nginx
# /etc/nginx/nginx.conf の http {} 内(conf.d/*.conf より前に読まれる位置)
geo $realip_remote_addr $from_cloudflare {
default 0;
include /etc/nginx/conf.d/cloudflare-geo.inc; # 拡張子 .inc なので conf.d/*.conf に二重読込されない
}
# サイトの server {} 内
server {
listen 443 ssl;
server_name www.example.com;
if ($from_cloudflare = 0) {
return 444; # 応答を返さず接続を閉じる
}
# ...既存の設定...
}
Apache で mod_remoteip を使っている場合も同様に、Require ip は書き換え後の訪問者 IP で評価されます。Apache 環境では設定2のファイアウォール層で絞るのが安全です。
設定4: IP 直打ち・知らないホスト名は受け付けない
IP アドレスだけで接続してきたリクエストに、既定のサイトやドメイン名入りの証明書を返すと、その IP がどのサイトのオリジンなのかを教えてしまいます。
どの server_name にも一致しない接続は、TLS のハンドシェイク段階で断ります。
# /etc/nginx/conf.d/00-default.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on; # nginx 1.19.4 以降。証明書を提示せずに拒否
}
Apache では最初に読み込まれる VirtualHost が既定になります。先頭に拒否専用の VirtualHost を置き、443 側には本番ドメインではなく自己署名のダミー証明書を載せます。
# /etc/apache2/sites-available/000-default-deny.conf (a2ensite 000-default-deny)
<VirtualHost *:443>
ServerName default.invalid
SSLEngine on
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
<Location />
Require all denied
</Location>
</VirtualHost>
設定5: Authenticated Origin Pulls で「本当に自分のゾーン経由か」を確かめる
設定2・3 は「Cloudflare のネットワークから来たか」までしか判定できません。Cloudflare の IP は多数の利用者で共有されているためです。
より強くするには、Cloudflare からオリジンへの接続時にクライアント証明書を提示させる Authenticated Origin Pulls(mTLS) を使います。SSL/TLS の暗号化モードは Full 以上が必要です。
公式手順の順番は「証明書を配置 → オリジンで検証を optional で有効化 → ダッシュボードで有効化 → 動作確認後に必須化」です。いきなり必須にすると全停止するので、この順序を守ります。
server {
listen 443 ssl;
server_name www.example.com;
# 手順1: まず optional で受け入れ、ダッシュボード側の有効化後に動作を確認
ssl_client_certificate /etc/nginx/certs/cloudflare-aop.crt;
ssl_verify_client optional;
# 手順2: 確認できたら on に変更して nginx -t && systemctl reload nginx
# ssl_verify_client on;
}
Apache では SSLCACertificateFile に証明書を指定し、SSLVerifyClient optional → 確認後 require の順で同様に進めます。
証明書の選び方:
- グローバル AOP: Cloudflare 提供の共通証明書。公式ドキュメントも「アカウント専用ではなく、Cloudflare のネットワークから来たことしか保証しない」と明記しています
- ゾーン単位 / ホスト名単位 AOP: 自分で発行した証明書をアップロード。自分のゾーン経由であることまで確認したいならこちらを選びます
新規構築や移設の際は、cloudflared がオリジンから外向きにだけ接続して受信ポートを全閉にできる Cloudflare Tunnel も選択肢です。
確認方法: 外からオリジンへ直接つないでみる
自社で管理しているサーバーに対してのみ、Cloudflare を経由しない回線から確認します。203.0.113.10 はオリジン IP に置き換えてください。
# 1) ドメイン経由(Cloudflare 経由)は従来どおり 200 / 301 などが返ること
curl -sI https://www.example.com/ | head -n 1
# 2) オリジン IP に直接つなぐと、接続できない・応答なしになること
curl -skI --max-time 10 --resolve www.example.com:443:203.0.113.10 https://www.example.com/ \
|| echo "OK: 直接接続は拒否されました"
- が正常で、2) が接続できなければ完了です。設定変更の前後で必ず 1) を確認し、正規の訪問者を止めていないことを確かめてください。
まとめ
- DNS を棚卸しし、可能ならオリジン IP を変更する
- ファイアウォールで 80/443 を Cloudflare からだけ許可する
- nginx では
geo $realip_remote_addrで判定する(realip の罠を回避) - default_server で IP 直打ちを拒否し、証明書を見せない
- Authenticated Origin Pulls で自分のゾーン経由かを確かめる
CDN/WAF は「経由させて初めて効く」仕組みです。導入した日に、オリジン側の入口も一緒に閉じておきましょう。
関連記事
- アクセスログから不正アクセスの兆候を見つける7つの集計(Cloudflare 配下の実 IP 記録も解説)
- nginxレート制限で総当たり・スクレイピング・軽量DoSを緩和する設定
- バージョン情報の露出を防ぐサーバー設定まとめ
本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp