2026年9月20日
2026年9月20日
CSP と SNI の関係|別の層の仕組みが交わる場面とトラブルの見分け方
CSP(Content Security Policy)と SNI(Server Name Indication)は、名前が似た略語として並べられることがありますが、直接の関係はありません。CSP は HTTP の応答ヘッダーでブラウザに「どこからリソースを読み込んでよいか」を伝える仕組み、SNI は TLS の接続開始時に「どのホスト名に接続したいか」をサーバーに伝える仕組みで、働く層もタイミングも異なります。ただ、1台のサーバーで複数のドメインを運用したり CDN を使ったりすると、両者が絡んだように見えるトラブルが起きます。この記事では、それぞれの役割を整理し、交わる場面と見分け方を説明します。
それぞれの役割
SNI:TLS で接続先のホスト名を伝える
HTTPS の通信では、HTTP のやり取りより先に TLS のハンドシェイクが行われます。1つの IP アドレスで複数のドメインを運用していると、サーバーはどのドメインの証明書を返せばよいか分かりません。そこでクライアントは、最初のメッセージ(ClientHello)に SNI としてホスト名を含めて送ります。サーバーはそれを見て、対応する証明書と設定を選びます。
Nginx であれば、SNI の値と server_name が一致する server ブロックが選ばれ、そのブロックの ssl_certificate が使われます。一致するものが無い場合は既定のサーバー(default_server)が使われます。
なお、SNI は通常は暗号化されずに送られるため、通信経路から接続先のホスト名が見える状態です。これを暗号化する ECH(Encrypted Client Hello)という仕組みがあり、ブラウザや CDN での対応が進んでいます。対応状況は(2026年10月現在)も変化している途中なので、利用するサービスの情報を確認してください。
CSP:ブラウザに読み込みの許可範囲を伝える
CSP は TLS の接続が確立し、HTTP の応答が返ってきた後に働きます。Content-Security-Policy ヘッダーで、スクリプト、スタイル、画像、フォント、接続先などを、どのオリジンから読み込んでよいかをブラウザに指示します。主な目的は XSS などで差し込まれた不正なスクリプトの実行を抑えることです。
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' https://cdn.example.com data:
CSP はブラウザ側の判断材料であり、通信を暗号化したり、接続先のホスト名を隠したりする機能はありません。SNI で見えるホスト名を CSP で守ることはできず、逆に SNI の設定で CSP の役割を代替することもできません。
両者が交わる3つの場面
1. CSP で許可したホストに TLS で接続できない
CSP で https://cdn.example.com を許可しても、ブラウザがそのホストに接続する段階で証明書が合わなければ、リソースは読み込まれません。共有 IP のサーバーで cdn.example.com の server ブロックが無い、あるいは証明書に名前が含まれていないと、SNI で一致するブロックが見つからず、別のドメインの証明書が返って接続エラーになります。
画面上は「画像やスクリプトが読み込まれない」という同じ症状でも、原因は CSP ではなく TLS です。開発者ツールのコンソールで見分けられます。
- CSP が原因なら、コンソールにポリシー違反のメッセージ(Content Security Policy のディレクティブに違反したため読み込みを拒否した、という趣旨)が出る
- TLS が原因なら、ネットワークタブでそのリクエストが
net::ERR_CERT_COMMON_NAME_INVALIDなどの証明書エラーで失敗している
2. SNI の選択ミスで、別サイトの CSP が返る
同じサーバーで複数サイトを運用し、server ブロックごとに CSP を設定している場合、SNI と server_name が一致しないアクセスは既定のサーバーに届きます。その結果、別のサイト向けの証明書とともに、別のサイト向けの CSP が返ることがあります。サイト A の CSP がサイト B のドメインを許可していないため、表示が崩れる、という形で現れます。
証明書エラーが先に出ることが多いものの、検証ツールで証明書エラーを無視して取得した場合や、意図せず同じ証明書に複数の名前が入っている場合は、CSP の不一致だけが目立つことがあります。
3. upgrade-insecure-requests で HTTPS 化を強制したとき
CSP の upgrade-insecure-requests は、ページ内の http:// のリソースを https:// で読み込むようブラウザに指示します。混在コンテンツ対策として便利ですが、読み込み先のホストが HTTPS に対応していない、あるいは SNI に対応した正しい証明書を返さないと、そのリソースは読み込めなくなります。有効にする前に、外部の画像や埋め込みの読み込み先が HTTPS で正しく応答するかを確認してください。
切り分けの手順
読み込みのエラーが CSP と TLS のどちらによるものかは、次の順で確認すると判断できます。
まず、対象ホストが SNI に対して正しい証明書を返すかを確認します。-servername が SNI の値です。
openssl s_client -connect cdn.example.com:443 -servername cdn.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName -dates
表示された名前(subjectAltName)に対象ホストが含まれていなければ、TLS 側の問題です。証明書の追加や server ブロックの設定を見直します。
次に、ページが返している CSP ヘッダーを確認します。
curl -sI https://example.com/ | grep -i content-security-policy
DNS を切り替える前の新サーバーなど、特定の IP に対して確認したい場合は --resolve を使うと、SNI とホスト名を保ったまま接続先だけを変えられます。
curl -sI --resolve example.com:443:203.0.113.10 https://example.com/ | grep -i content-security-policy
Nginx での設定例と注意点
複数サイトを1台で運用するときは、サイトごとに証明書と CSP をそろえて書きます。
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' https://cdn.example.com data:" always;
}
http2 on; は Nginx 1.25.1 以降の書き方で、それより古い版では listen 443 ssl http2; を使います。また、add_header は location ブロックで1つでも書くと、server ブロックで書いたものが引き継がれなくなります。特定の location だけ CSP が消える場合は、この継承の仕様を疑ってください。
WordPress 側で CSP を出したい場合は、子テーマや mu-plugin で send_headers アクションにヘッダー送信の処理を登録する方法もあります。ただし、サーバーとプラグインの両方で CSP を出すとヘッダーが重複し、ブラウザは両方のポリシーを満たす読み込みだけを許可するため、意図より厳しくなります。どこで設定するかを1か所に決めてください。いきなり本番で適用せず、Content-Security-Policy-Report-Only で違反を観察してから切り替える手順が安全です。
まとめ
- SNI は TLS の接続時にホスト名を伝える仕組み、CSP は HTTP 応答でブラウザに読み込みの許可範囲を伝える仕組みで、直接の関係はない
- CSP で許可したホストでも、SNI に合った証明書が無ければ TLS の段階で読み込みに失敗する
- 共有 IP で SNI が一致しないと、既定のサーバーの証明書と CSP が返り、別のトラブルに見えることがある
- コンソールのメッセージ、
openssl s_client -servername、curl -sIで、CSP と TLS のどちらが原因かを切り分ける