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 をそれぞれ確かめる

関連記事:

お気軽にご相談ください

お見積りへ お問い合わせへ
▲