インフラもセキュリティも,まだまだ未熟な私ではありますが,これだけはお願いします.
オリジンサーバを暗号化なしの HTTP で運用しないでください.
TL;DR
ここで,オリジンサーバは,ファイアウォールやゲートウェイを通った先の最も奥にある,最終的にリクエストを処理するサーバをいいます.アプリケーションサーバが当てはまることが多いですが,静的ファイルサーバも例外ではありません.対して,間に入るサーバをエッジサーバと呼ぶことにします.
また,この記事では暗号化なしの HTTP を HTTP , TLS レイヤ上の HTTP を HTTPS として記述します.HTTPS における TLS 上での通信も HTTP ではあるため,差別化のために明記しておきます.
何がだめなのか
近年, Web サイトのほとんどが TLS を用いた HTTPS で運用されています.パブリックな静的コンテンツに対しては HTTPS 不要論を唱える人々もいます.
TLS を用いると End to End (E2E) の暗号化を実現することができますが,それだけでなく,署名によってレスポンスの発信元を検証することもできます.確かに,パブリックなコンテンツで E2E 暗号化を行う必要はないかもしれませんが,データの改竄が行われてはいけないはずです.
従って,私は常時 HTTPS に賛成派です.
さて,そんな中でご自身のサイト・サービスを HTTPS に対応させている方はもちろん多いでしょう.今では Let's Encrypt などのサービスによって個人でも用意に(メジャーなブラウザで信頼された)サーバ証明書を手に入れることができます.数年前と比べると HTTPS を導入する敷居は確実に下がっているはずです.
しかし,オリジンサーバはどうでしょうか?アプリケーションサーバやそのフレームワークには HTTPS 対応がなされていないものもまだ多くあります.そういった場合,また対応している場合でも,以下のような設定を書いている,書いたことのある方は多いのではないでしょうか:
http {
server {
listen [::]:443 ssl http2;
server_name: app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
}
}
}
<VirtualHost *:443>
ServerName app.example.com
SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/app.example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
</VirtualHost>
みたいな.
こうした構成をした場合,たしかにクライアントからは HTTPS 対応ができているように見えます.
でも, nginx や Apache といったエッジサーバからオリジンサーバまでは暗号化されず,署名の検証もされないのです.
なぜダメなのか
同じことを何度も言う形になってしまうのですがもう少し詳しく書きます.
データが暗号化されない
データはインターネットの間だけ暗号化されればいいわけではありません.確かに,インターネットはとても広いスコープであり,危険に晒される可能性は非常に高いです.しかし,あなたの LAN 内, VLAN 内,マシン内,コンテナ内は本当に安全でしょうか?
署名による検証がされない
あなたが通信している先にあるのは,本当に目的のオリジンサーバでしょうか.ドメイン名が正しいから, IP アドレスが正しいから,サーバ証明書が正常だから,そのオリジンサーバは正しいのでしょうか.何らかの原因によって,偽のオリジンサーバがスコープ内に発生したとしましょう.エッジサーバを通って返っていくレスポンスは, HTTP なので,データの検証はされません.クライアントは信頼されたサーバ証明書のまま,通信を行ってしまいます.
クライアントからはわからない
上記 2 つに共通することですが,これが一番重篤だと思っています.オリジンサーバとエッジサーバ間が何のプロトコルで通信していようと,クライアントからは知る由もありません.もしかしたら,インターネットの海を旅した後にオリジンサーバに到達しているかもしれません.クライアントは,エッジサーバのサーブするデータを信頼するしかないのです.
じゃあどうすればいいの
HTTPS でリレーする
簡単な話です.オリジンサーバとエッジサーバの間のすべての通信を HTTPS で行えばいいのです.ブラウザに信頼される証明書である必要はありません.独自 CA でもいいでしょう.はたまた自己署名証明書でもまあいいと思います.ただし,フィンガープリントを設定するなどして本物のオリジンサーバかを検証することは忘れないようにしてください.
追記 19/11/2020 22:38:
コメントで指摘いただいた通り,この方法は通信に要する計算リソースが大きくなります.TLS 通信のセッションの数だけ暗号化・復号処理が必要になるためです.
E2E 暗号化通信を行う
実は,すぐ上の方法は E2E 暗号化を実現できていません.リレーしているのですから.「大好きなあの子に,このチョコ渡してくれない?」と言って託した友人は信頼できますか?あなたが書いた秘密のメッセージを読んでいるかも,はたまた,勝手に食べてしまっているかもしれません.
すなわち,ペイロードを読まないままオリジンサーバまで届ければいいのです.HTTP では,複数ホストを共存する際に, ServerName で指定したものと Host ヘッダを照らし合わせてルーティングしていました.これでは,ペイロードを読む必要が出てしまいます.幸い, TLS には SNI (Server Name Indication) という,まるで Host ヘッダのような仕組みがあります.これは暗号化されていないため,ペイロードを読むことなく適切なオリジンサーバにルーティングできます.
SNI でどのサイトにアクセスしようとしたか分かるのではないかって?その通りです.まあ DNS で名前解決している時点でバレてますが. SNI も暗号化してしまう Encrypted SNI といったものも将来的に実現される可能性は高そうです.
TL;DR
- オリジンサーバとエッジサーバ間を HTTP で通信するな
- 自己署名証明書でもいいのでちゃんと設定してちゃんと検証しよう
- そもそもペイロードを読まずに TLS のままリレーしよう