🦆

nginxのTLSをCloudflareに任せる

2023/05/19に公開

最近ドメインをCloudflare Registrarへ移管したのをきっかけに、Cloudflareを使ってみることにしました。

CloudflareといえばCDNなので、手始めにVPS上のnginxが提供してるWebサイトをCloudflare経由にしてみました。

https?以外の通信を別ホスト名に分ける

ドキュメントにあるとおり、デフォルト(およびFree plan)ではhttp/https以外を通さないので、適当にホスト名を作って分離しました。

IoT機器の場合はファーム焼き直しが必要だったりするのでこれが地味にめんどい。

CDNを使う設定を投入する

ドメインのネームサーバをCloudflareに設定した上で、DNSレコードの設定からCDNの設定をアクティブにします。

Products > DNS > ... > Reference > Proxy statusにあるとおり、DNSの設定で「プロキシ済み」にすると、当該hostnameのAレコードがCloudflareのCDNを指すようになり、当該ホスト対する世界からのアクセスがCloudflareを経由するようになります。

dash.cloudflare.com

リダイレクトループした

この項はERR_TOO_MANY_REDIRECTS · Cloudflare SSL/TLS docsを読めば十分ですがもう少し背景を含めて書き足すとこんな感じ。

素朴に前項の設定したらSSL設定がFlexibleになり、この設定だとOriginとの通信は平文httpで行います。このときOriginに「httpアクセスをすべてhttpsへリダイレクトする」設定が入っているとリダイレクトループします[1]

終わってみれば当たり前の事象なんですけど「httpからhttpsへリダイレクト」ってありがちな割に存在忘れるので要注意。

OriginのTLS証明書をCloudflare発行のやつに変更

今まではLet's EncryptのTLS証明書をlegoで更新していましたが、アクセスしてくるのがCloudflareだけになるのでOrigin CAに入れ替えてみました。

ボタンをぽちって発行されたCert/PrivKeyを入れるだけなので瞬殺。

テスト

curl--resolveを使い直接叩いてみると、ちゃんと証明書検証エラーになりました。

curl -vvv https://www.example.com/ --resolve www.example.com:443:10.0.0.1

nginxのログでアクセス元を出す

このままではnginxのアクセスログに出てくるIPアドレスはCloudflareのものになります。なので、ngx_http_realip_moduleモジュールを使って接続元IPアドレスをCloudflareが渡してくる値に書き換えます。

具体的には、set_real_ip_fromでCloudflareのIPアドレスセットを設定し、real_ip_headerCF-Connecting-IPを指定[2]します。

IPアドレスレンジはIP Ranges | Cloudflareから取ってきますが、数年に一度変化があるっぽいので自動的に更新できるようにしておきます(後述)

設定するIPアドレスレンジについて

次節でCloudflare以外からの443/tcp接続をtcpレベルで蹴る設定をするならば、リクエストに書かれたCF-Connecting-IPを無邪気に信じても良くなります。

実装見るset_real_ip_fromは略せないので、0.0.0.0/0を投入。

real_ip_header CF-Connecting-IP;
set_real_ip_from 0.0.0.0/0;

443/tcpへの接続をCloudflareに限定する

nginxでアクセス制限をかけてもいいけど、前項で接続元IPアドレスを書き換えているので一筋縄ではいかない。

一筋縄ではいかない理由

nginxのIPアドレスによるアクセス制限はngx_http_access_moduleを使います。なので、ngx_http_access_modulengx_http_realip_moduleどっちが先に処理されるか調べてみます。

ソースコードを見に行くと、ngx_http_access_moduleNGX_HTTP_ACCESS_PHASEngx_http_realip_moduleNGX_HTTP_POST_READ_PHASENGX_HTTP_PREACCESS_PHASEで呼ばれるようです。

PhaseについてDevelopment guideのPhasesやら値の定義とハンドラ配列の構築実行あたりをざっと見て、Nginx Admin's HandbookRequest processing stagesを読んでみました。

これらの資料を読む限り、NGX_HTTP_ACCESS_PHASENGX_HTTP_PREACCESS_PHASEおよびNGX_HTTP_POST_READ_PHASEよりあとに実行されます。ゆえに、ngx_http_access_moduleに処理が渡る時点でアクセス元IPはCloudflareに接続してきたUAのIPアドレスになっていて素朴にallow/denyを使って制限ができない。

インターネットで知見を探ると、ngx_http_realip_moduleが設定する$realip_remote_addrngx_http_geo_moduleで参照し、ifで分岐する方法[3]を見つけたけど、未検証。

ちなみにngx_http_realip_moduleNGX_HTTP_POST_READ_PHASEだけでなくNGX_HTTP_PREACCESS_PHASEでも呼ばれる理由を考えてみたんですけど、rewrite phaseでclient ipが変わりうる場合があるってことなんですかね、よくわかりませんでした…。

今回はホストのiptablesを使ってtcpレベルで制限する方法を取りました。CloudflareのIPアドレスセットをipsetに設定して、iptablesで参照します。

iptables -I INPUT -i ens3 -p tcp -m state --state NEW -m tcp --dport 443 -m set --match-set cloudflare-origin-v4 src -m comment --comment "raw-nginx" -j ACCEPT

OriginはIPv4だけなのでiptablesもIPv4のみ。ipset(cloudflare-origin-v4)については後述。なお、iptablesで参照するipsetは存在しないとエラーが発生します。もしipset-persistentなどのipset永続化が入っていなければよしなに設定してください。

CloudflareのIPアドレスセットを設定する

(nginxおよび)iptablesでCloudflareのIPアドレスを認識する必要があります。前出の通りIP Ranges | Cloudflareにあるんですが、数年に一度変わるっぽいので自動的によしなにするスクリプトを書きました。

スクリプトとsystemdで定期実行する設定例

nginxのset_real_ip_fromとipsetを設定する場合

ipsetのみ設定する場合

ホスト起動時に実行するぐらいでも良いような気はしますが…。

cfupdater.service
[Unit]
Description=Update Cloudflare's IP Addresses list

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/cf_updater.pl /etc/nginx/cloudflare/filters.conf
cfupdater.timer
[Unit]
Description=Update Cloudflare's IP Addresses list

[Timer]
OnCalendar=04:36

[Install]
WantedBy=timers.target

引数で指定したpath(ここでは/etc/nginx/cloudflare/filters.conf)にnginx confを吐くので、nginxのconfigでincludeします。ipsetのみ設定する場合は引数不要。

脚注
  1. Flexible encryption modeのパターンです。 ↩︎

  2. この項は公式ドキュメントja/enも参照。 ↩︎

  3. proxy - nginx allow|deny $realip_remote_addr - Stack Overflow ↩︎

Discussion