2026年8月27日
2026年8月27日
Nginxのセキュリティヘッダー設定方法|HSTS・CSP・X-Frame-Optionsとadd_headerの注意点
Nginx の add_header でセキュリティヘッダーを付けると、ブラウザに「HTTPS 以外で接続しない」「他のサイトの枠(iframe)の中に表示させない」などの指示を出せます。この記事では、WordPress を動かしている Nginx に主なセキュリティヘッダーを設定する手順と、設定したつもりで付いていない、という失敗を防ぐ方法を説明します。内容は2026年10月時点のもので、Ubuntu 24.04 の Nginx 1.24 で動作を確かめています。
設定するヘッダーと値の例
| ヘッダー | 役割 | WordPress で使いやすい値の例 |
|---|---|---|
Strict-Transport-Security(HSTS) |
指定した期間、そのドメインに HTTPS でしか接続させない | max-age=31536000(最初は短い値から) |
Content-Security-Policy(CSP) |
読み込んでよいスクリプトや画像の取得元を制限する | サイトごとに作る。最初は Report-Only で試す |
X-Frame-Options |
ほかのサイトの iframe の中に表示させない(クリックジャッキング対策) | SAMEORIGIN |
X-Content-Type-Options |
ブラウザがファイルの種類を推測して実行するのを止める | nosniff |
Referrer-Policy |
外部のサイトへ送る参照元 URL の範囲を決める | strict-origin-when-cross-origin |
Permissions-Policy |
カメラ・マイク・位置情報などの機能を使わせない | camera=(), microphone=(), geolocation=() |
X-XSS-Protection は、主なブラウザがもとになる機能を取り除いているため、新しく付ける必要はありません。
先に知っておきたいadd_headerの2つの仕様
1. 下の階層にadd_headerがあると、上の階層の指定は引き継がれない
Nginx の公式ドキュメントでは、add_header は「現在の階層に add_header が1つもない場合に限り」上の階層から引き継がれる、とされています。例えば server ブロックにセキュリティヘッダーを書いても、静的ファイル用の location に add_header Cache-Control ... が1行でもあると、その location のレスポンスにはセキュリティヘッダーが1つも付きません。
server {
add_header X-Frame-Options "SAMEORIGIN" always;
location ~* \.(css|js)$ {
# この location には X-Frame-Options が付かない
add_header Cache-Control "public, max-age=2592000";
}
}
このため、ヘッダーを1つのファイルにまとめ、add_header を持つ location にも同じファイルを読み込ませるのが確実な方法です(ステップ1)。
Nginx 1.29.3 以降では、add_header_inherit merge; で上の階層のヘッダーを足し合わせられるようになりました。2026年10月時点で Ubuntu 24.04 の標準パッケージは 1.24 のため、この記事では使える版を問わない書き方にしています。
2. alwaysを付けないとエラーのページには付かない
add_header は、初期状態では 200・301・302・304 などの決まったステータスコードのときだけヘッダーを付けます。404 や 500 のページにも付けるには、末尾に always を付けます。
ステップ1:ヘッダーを1つのファイルにまとめる
/etc/nginx/snippets/security-headers.conf を作り、次を書きます。
# /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;
HSTS の max-age=300(5分)は試すための値です。ステップ3で長くします。
ステップ2:HTTPSのserverブロックで読み込む
サイトの設定ファイル(例:/etc/nginx/sites-available/example.com)の、443番ポートの server ブロックで読み込みます。add_header を書いている location があれば、そこでも読み込みます。
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
# ssl_certificate などの設定は省略
include /etc/nginx/snippets/security-headers.conf;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~* \.(css|js|png|jpe?g|gif|webp|svg|woff2?)$ {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=2592000";
}
# location ~ \.php$ { ... } は既存の設定のまま
}
http2 on; は Nginx 1.25.1 以降の書き方です。1.24 では listen 443 ssl http2; と書きます。既存の設定がある場合は、その書き方のままで構いません。
HSTS を80番ポートの server ブロックに書いても意味はありません。HTTPS ではない接続で受け取った HSTS は、ブラウザが無視するよう仕様(RFC 6797)で決められています。80番ポートでは HTTPS へ転送するだけにします。
設定を確かめてから読み込み直します。
sudo nginx -t && sudo systemctl reload nginx
ステップ3:HSTSのmax-ageを長くする
HSTS を受け取ったブラウザは、max-age の秒数のあいだ、そのドメインに HTTPS でしか接続しなくなります。証明書が切れたり HTTPS の設定を誤ったりすると、その期間はサイトを開けなくなるため、短い値で問題がないことを確かめてから長くします。
| 段階 | 値 | 確かめること |
|---|---|---|
| 試す | max-age=300 |
サイト全体(管理画面・画像・外部連携)が HTTPS で動く |
| 延ばす | max-age=86400(1日)→ max-age=2592000(30日) |
数日から数週間、問題が出ない |
| 本番 | max-age=31536000(1年) |
証明書の自動更新が動いている |
includeSubDomains を付けると、すべてのサブドメインにも同じ制限がかかります。HTTPS に対応していないサブドメイン(社内用のシステムなど)が1つでもあると開けなくなるため、全部を確かめてから付けます。
preload を付けて hstspreload.org に登録すると、ブラウザに最初から HTTPS 専用のドメインとして組み込まれます。登録には max-age が31536000秒以上、includeSubDomains と preload の両方が必要です。取り消しはブラウザの更新を待つため数か月かかることがあり、簡単には戻せません。詳しくは WordPressのHTTPS強制とHSTSプリロードの設定手順とリスク を参考にしてください。
ステップ4:X-Frame-OptionsはSAMEORIGINにする
WordPress はログイン画面と管理画面に、自分で X-Frame-Options: SAMEORIGIN を付けています。Nginx でも同じ SAMEORIGIN にしておけば、値が食い違いません。
DENY にすると、同じサイトの iframe の中でも表示できなくなります。カスタマイザー(「外観」→「カスタマイズ」)はサイトを iframe でプレビューするため、プレビューが表示されなくなることがあります。自分のサイトでもまったく iframe を使わないとわかっている場合だけ DENY にします。
ステップ5:CSPはReport-Onlyから試す
CSP は効果が大きい一方で、許可していない読み込みはすべて止まります。WordPress の管理画面・ブロックエディター・多くのプラグインはインラインのスクリプトを使うため、いきなり Content-Security-Policy を付けると画面が壊れることがあります。
まず Content-Security-Policy-Report-Only で付けると、ブラウザは止めずに違反だけを報告します。違反はブラウザの開発者ツールのコンソールに表示されます。次は、管理画面とログイン画面を対象から外し、公開側のページだけに付ける例です。map は http ブロック(server ブロックの外。sites-available のファイルの先頭でも可)に書きます。
map $request_uri $csp_report_only {
default "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'";
~^/wp-admin/ "";
~^/wp-login\.php "";
}
security-headers.conf に次の1行を足します。値が空の場合、Nginx はそのヘッダーを付けないため、管理画面とログイン画面には付きません。
add_header Content-Security-Policy-Report-Only $csp_report_only always;
公開側のページをひととおり開き、コンソールに出た違反(Google アナリティクス、広告、埋め込み動画、Web フォントなど外部の取得元)を見て、script-src や img-src などに取得元を足していきます。違反が出なくなったら、ヘッダー名を Content-Security-Policy に変えて有効にします。
frame-ancestors 'self' は X-Frame-Options: SAMEORIGIN と同じ役割で、両方があるときは対応するブラウザでは CSP の側が優先されます。WordPress での CSP の作り方は WordPressのContent Security Policy(CSP)設定方法 で詳しく説明しています。
ステップ6:ヘッダーが付いているか確かめる
ページの種類ごとに確かめます。特に、静的ファイルとエラーのページは、上の2つの仕様で付かないことが多い場所です。
# トップページ
curl -sI https://example.com/ | grep -iE '^(strict-transport|content-security|x-frame|x-content-type|referrer|permissions)'
# 静的ファイル(location の add_header で消えていないか)
curl -sI https://example.com/wp-includes/css/dist/block-library/style.min.css | grep -iE '^(strict-transport|x-frame|cache-control)'
# 存在しない URL(404 でも付いているか)
curl -sI https://example.com/no-such-page-12345/ | grep -iE '^(HTTP|strict-transport|x-frame)'
同じヘッダーが2行出る場合は、Nginx と WordPress のプラグイン、または CDN の両方が付けています。どちらか一方で付けるようにします。Cloudflare などの CDN を使っている場合は、CDN の設定でもヘッダーを付けていないか確かめてください。
うまくいかないときの確認
| 症状 | 原因と対処 |
|---|---|
| 一部のファイルにだけヘッダーが付かない | その location に別の add_header がある。include で読み込む |
| 404 などのページに付かない | always が付いていない |
| HSTS が効かない | 80番ポートの server に書いている。443番に書く |
| 管理画面やエディターが動かなくなった | CSP が管理画面にもかかっている。Report-Only に戻し、管理画面を外す |
| カスタマイザーのプレビューが表示されない | X-Frame-Options: DENY になっている。SAMEORIGIN にする |
| 設定を変えても反映されない | nginx -t の後に systemctl reload nginx を実行したか。編集したファイルが sites-enabled から読み込まれているか |
まとめ
- ヘッダーは1つのファイルにまとめ、443番の
serverと、add_headerを持つすべてのlocationでincludeする alwaysを付けて、エラーのページにも付ける- HSTS は443番だけに書き、
max-age=300から段階的に1年まで延ばす。includeSubDomainsとpreloadはサブドメインまで確かめてから - WordPress では
X-Frame-Options: SAMEORIGINにする - CSP は
Content-Security-Policy-Report-Onlyで公開側だけに付け、違反を見ながら許可を足してから有効にする curl -sIでトップページ・静的ファイル・404 をそれぞれ確かめる
関連記事: