nginxのTLSをCloudflareに任せる
最近ドメインを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を経由するようになります。
リダイレクトループした
この項は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_headerでCF-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_module
とngx_http_realip_module
どっちが先に処理されるか調べてみます。
ソースコードを見に行くと、ngx_http_access_moduleはNGX_HTTP_ACCESS_PHASE、ngx_http_realip_moduleはNGX_HTTP_POST_READ_PHASEとNGX_HTTP_PREACCESS_PHASEで呼ばれるようです。
PhaseについてDevelopment guideのPhasesやら値の定義とハンドラ配列の構築と実行あたりをざっと見て、Nginx Admin's HandbookのRequest processing stagesを読んでみました。
これらの資料を読む限り、NGX_HTTP_ACCESS_PHASE
はNGX_HTTP_PREACCESS_PHASE
およびNGX_HTTP_POST_READ_PHASE
よりあとに実行されます。ゆえに、ngx_http_access_module
に処理が渡る時点でアクセス元IPはCloudflareに接続してきたUAのIPアドレスになっていて素朴にallow
/deny
を使って制限ができない。
インターネットで知見を探ると、ngx_http_realip_module
が設定する$realip_remote_addrをngx_http_geo_moduleで参照し、ifで分岐する方法[3]を見つけたけど、未検証。
ちなみにngx_http_realip_module
がNGX_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のみ設定する場合
ホスト起動時に実行するぐらいでも良いような気はしますが…。
[Unit]
Description=Update Cloudflare's IP Addresses list
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/cf_updater.pl /etc/nginx/cloudflare/filters.conf
[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のみ設定する場合は引数不要。
Discussion