0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare配下でもオリジンIP直アクセスで素通り──塞ぐ5つの設定(nginx/Apache・コピペOK)

0
Posted at

対象読者: 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: 直接接続は拒否されました"
  1. が正常で、2) が接続できなければ完了です。設定変更の前後で必ず 1) を確認し、正規の訪問者を止めていないことを確かめてください。

まとめ

  1. DNS を棚卸しし、可能ならオリジン IP を変更する
  2. ファイアウォールで 80/443 を Cloudflare からだけ許可する
  3. nginx では geo $realip_remote_addr で判定する(realip の罠を回避)
  4. default_server で IP 直打ちを拒否し、証明書を見せない
  5. Authenticated Origin Pulls で自分のゾーン経由かを確かめる

CDN/WAF は「経由させて初めて効く」仕組みです。導入した日に、オリジン側の入口も一緒に閉じておきましょう。

関連記事


本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?