2026年5月21日
2026年5月21日
Nginxのセキュリティヘッダー設定10選と設定時の注意点
Nginx でセキュリティヘッダーを付けるには add_header ディレクティブを使い、always を付けてエラーページにも出るようにし、設定を1つのファイルにまとめて各 server ブロックから読み込むのが扱いやすい方法です。この記事では、WordPress サイトで実際に使われる10項目を「何を防ぐのか」「どこで表示が崩れやすいか」とあわせて紹介し、Nginx 特有の継承のクセと、設定後の確認方法まで説明します。
ヘッダーはあくまで「ブラウザに安全側の動作を指示する」仕組みです。WordPress 本体・プラグインの更新や脆弱性対策の代わりにはならないことを前提に読んでください。
先に知っておきたい add_header の2つのクセ
always を付けないとエラー応答に出ない
add_header は、何も指定しないと 200・201・204・206・301・302・303・304・307・308 の応答にだけヘッダーを付けます。404 や 500 のページにも付けたい場合は、末尾に always を付けます。セキュリティヘッダーは応答コードに関係なく付けておきたいものなので、基本的に always 付きで書きます。
location に add_header を書くと上の階層の分が消える
Nginx の add_header は、その階層に1つも add_header が無い場合に限って上の階層(http や server)の設定を引き継ぎます。たとえば server にセキュリティヘッダーを書き、静的ファイル用の location に add_header Cache-Control ... を1行だけ足すと、その location の応答からはセキュリティヘッダーがすべて消えます。気付きにくいので、Nginx の設定で最もつまずきやすい点です。
対策は、ヘッダーを別ファイルにまとめ、add_header を書く場所すべてで include することです。比較的新しいメインライン版では継承の挙動を変えるディレクティブが追加されていますが、使えるかどうかは nginx -v で表示されるバージョンと公式ドキュメントで確認してください(2026年10月現在)。
# /etc/nginx/snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=300" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; frame-ancestors 'self'; object-src 'none'; base-uri 'self'" always;
server {
listen 443 ssl;
server_name example.com;
server_tokens off;
include /etc/nginx/snippets/security-headers.conf;
location ~* \.(css|js|png|jpe?g|gif|webp|svg|woff2?)$ {
add_header Cache-Control "public, max-age=2592000";
include /etc/nginx/snippets/security-headers.conf; # ここでも読み込み直す
try_files $uri =404;
}
}
設定したい10項目
1. Strict-Transport-Security(HSTS)
ブラウザに「このドメインには次回以降 HTTPS だけで接続する」と覚えさせ、HTTP への格下げや盗聴を防ぎます。強力な反面、HTTPS が使えなくなるとそのドメインに一定期間アクセスできなくなります。最初は上の例のように max-age=300(5分)程度で始め、問題が無いことを確認してから1年(31536000)へ伸ばします。includeSubDomains はすべてのサブドメインが HTTPS に対応している場合だけ付けます。HTTPS の server ブロックにだけ書き、HTTP 側には書きません。
2. X-Content-Type-Options: nosniff
ブラウザがファイルの中身から種類を推測する動作(MIME スニッフィング)を止めます。アップロードされたファイルがスクリプトとして解釈されるのを防ぐ効果があり、副作用もほとんどないので最初に入れてよい項目です。その代わり、Nginx の mime.types が読み込まれていないと CSS や JS が正しい Content-Type で配信されず、読み込みを拒否されます。
3. X-Frame-Options: SAMEORIGIN
自サイトを他サイトの iframe に埋め込ませないことで、クリックジャッキングを防ぎます。WordPress はログイン画面と管理画面で同じヘッダーを自分でも送っているため、Nginx 側でも付けると二重になります。値が同じなら実害は少ないものの、気になる場合は管理画面側で重複を確認してください。新しいブラウザでは後述の CSP frame-ancestors が優先されます。
4. Content-Security-Policy(CSP)
読み込んでよいスクリプト・画像・フォントなどの出どころを指定し、XSS の被害を抑えます。効果は大きい一方、WordPress は外部の広告・解析タグ・埋め込みやインラインスクリプトを多く使うため、いきなり適用すると表示や管理画面が壊れやすい項目です。まず Content-Security-Policy-Report-Only で違反を確認だけし、ブラウザの開発者ツールのコンソールに出る違反内容を見ながら許可リストを調整してから、本番のヘッダー名に切り替えます。ブロックエディターなど管理画面は厳しい CSP と相性が悪いため、まずはフロント側だけに適用する運用が現実的です。
5. Referrer-Policy
外部サイトへのリンクをたどったとき、遷移元 URL をどこまで渡すかを決めます。strict-origin-when-cross-origin にすると、他サイトにはドメイン名だけを渡し、URL のパスやクエリ(検索語や会員ページの ID など)を漏らしません。現在の主要ブラウザの既定値と同じ値ですが、明示しておくと意図がはっきりします。
6. Permissions-Policy
カメラ・マイク・位置情報などのブラウザ機能を、ページや埋め込みコンテンツが使えないように制限します。() は「どこにも許可しない」という意味です。地図や予約フォームなどで位置情報を使うサイトでは、その機能を外します。以前よく書かれていた interest-cohort は対象の仕組み自体が廃止されたため、今から入れる必要はありません。
7. Cross-Origin-Opener-Policy(COOP)
別オリジンのウィンドウから自分のページを操作されないようにします。same-origin が最も厳しい値ですが、ソーシャルログインや決済サービスのポップアップが動かなくなることがあるため、WordPress では same-origin-allow-popups から試すのが無難です。
8. Cross-Origin-Resource-Policy(CORP)
画像などのリソースを他サイトから読み込ませるかどうかを決めます。same-origin にすると、画像を CDN の別ドメインやメールマガジン、フィードリーダーから表示できなくなる場合があります。使うなら same-site から始め、メディア配信の経路を把握してから判断します。一律に付ける必要はない項目です。
9. バージョン情報を出さない(server_tokens と X-Powered-By)
これはヘッダーを「足す」のではなく「減らす」設定です。server_tokens off; で Server ヘッダーやエラーページから Nginx のバージョン番号を消し、PHP の X-Powered-By は php.ini の expose_php = Off、または fastcgi_hide_header X-Powered-By; で隠します。攻撃を防ぐ効果は限定的ですが、古いバージョンを狙った機械的な探索の手がかりを減らせます。隠すことよりも、バージョンを最新に保つことのほうが重要です。
10. X-XSS-Protection は付けないか 0 にする
古い記事では X-XSS-Protection: 1; mode=block を勧めていることがありますが、この機能は主要ブラウザから削除されており、古いブラウザでは逆に悪用される問題が知られています。現在は付けないか、明示的に 0 を指定するのが一般的な推奨です。古い設定を引き継いでいる場合は見直してください。XSS 対策は CSP と、WordPress 側の出力エスケープで行います。
反映と確認の手順
設定ファイルを変更する前に、元のファイルをコピーしておきます。
sudo cp -a /etc/nginx /etc/nginx.bak-$(date +%Y%m%d)
sudo nginx -t && sudo systemctl reload nginx
nginx -t で構文エラーが無いことを確かめてから reload します。restart ではなく reload にすると、設定に誤りがあっても稼働中のプロセスが止まりません。
反映後は、正常なページ・存在しないページ・静的ファイルの3種類でヘッダーを確認します。always と継承の問題は、この3つを比べると見つかります。
curl -sI https://example.com/ | grep -iE 'strict|x-content|x-frame|referrer|permissions|cross-origin|content-security'
curl -sI https://example.com/not-found-test/ | grep -iE 'strict|x-content|x-frame'
curl -sI https://example.com/wp-includes/css/dist/block-library/style.min.css | grep -iE 'strict|x-content|x-frame'
同じヘッダーが2行出る場合は、プラグイン(セキュリティ系やキャッシュ系)や PHP 側でも送っている可能性があります。どちらか一方に寄せたほうが管理しやすくなります。外部の診断サービスとしては MDN の HTTP Observatory などが使えますが、点数を上げること自体を目的にせず、サイトの表示や機能が壊れていないかを優先してください。
確認は本番の前にステージング環境で行い、ログイン・投稿の保存・メディアのアップロード・お問い合わせフォームの送信・外部決済やログイン連携まで一通り操作しておくと安心です。
まとめ
add_headerはalwaysを付け、ヘッダーはスニペットにまとめてincludeするlocationにadd_headerを1つでも書くと上の階層の分が消えるので、そこでも読み込み直す- HSTS は短い
max-ageから始め、includeSubDomainsは全サブドメインの HTTPS 対応を確かめてから - CSP は Report-Only で違反を確認してから本番化し、まずはフロント側に限定する
- COOP・CORP は外部連携や画像配信を壊しやすいので、緩い値から試す
- X-XSS-Protection は付けないか
0。バージョン番号は隠しつつ、更新を優先する - 変更前にバックアップを取り、
nginx -tと curl で正常・404・静的ファイルの応答を確認する