取引先から届いたセキュリティの指摘事項を見ながら、自分が保守しているサーバーの設定をどこから直そうか考えている保守運用担当者

HTTPセキュリティヘッダの基本設定|まず入れる3つと確認手順

「セキュリティヘッダが設定されていません」

取引先のチェックシートや、無料の診断サイトの結果に、そう書かれていたことはないでしょうか。指摘そのものは短い一行なのに、調べ始めると出てくる名前が多い。CSP、HSTS、X-Frame-Options……。しかも本番で動いているサイトです。うっかり入れて表示が壊れたら、指摘を直すどころの話ではなくなる。

その慎重さは、正しい感覚だと思います。セキュリティヘッダの中には、入れた瞬間にサイトの一部が動かなくなるものが実際にあります。だから「全部いっぺんに入れましょう」という記事は、保守運用の現場ではあまり役に立ちません。

この記事では、副作用がほとんどないものから順に、3つだけ入れるところまでを一緒にやります。残りは、入れなくても責められない理由も含めて整理します。

結論:まず入れるのは、①X-Content-Type-Options: nosniff(ほぼ無害)②X-Frame-Options: SAMEORIGIN(iframe利用の有無だけ確認)③Referrer-Policy: strict-origin-when-cross-origin(実質、いまのブラウザの既定と同じ)の3つです。この3つは、既存サイトを壊す可能性がかなり低いわりに、指摘の大半に答えられます。HSTS は「HTTPSへの移行が完全に終わっているか」を確かめてから、短い期間の指定で。CSP は Report-Only から。そして X-XSS-Protection は、いまはもう入れなくて大丈夫です。作業前に、必ず設定ファイルの退避と curl での現状確認をしておきます。

なお、設定を書く場所(Webサーバーかアプリか)や記法は、環境によって変わります。ここに載せる例はそのまま貼れることを狙ったものではなく、自分の環境の書き方に置き換える前提の見本です。本番反映の前に、検証環境と公式ドキュメントで確かめてください。

何が起きているか:ヘッダは「サーバーからブラウザへの、守り方のお願い」

そもそもセキュリティヘッダとは何かを、一度だけ整理させてください。ここが腑に落ちると、あとの判断がぐっと楽になります。

サーバーが返す応答にヘッダという指示が付き、それを受け取ったブラウザ側が守りを働かせるという流れを左から右へ示した図
ヘッダを出すのはサーバー。実際に守るのはブラウザ側。だから「古いブラウザには効かない」も起きる

Webサーバーがページを返すとき、本文(HTML)の前にヘッダという短い指示の並びが付いています。セキュリティヘッダは、その中の一部です。中身をざっくり言うと、こういうお願いです。

ここで大事なのは、守る仕事をするのはサーバーではなくブラウザ側だということです。サーバーは「こう扱ってほしい」と伝えるだけ。だから、

つまりセキュリティヘッダは、万能ではないけれど、費用対効果がとても良い一手です。「これさえ入れれば安全」ではないと分かったうえで、入れられるものを入れておく。その温度感がちょうどいいと思います。

作業の前に:現状を見て、戻せるようにしておく

設定を足す前に、5分だけ準備をします。ここを飛ばすと、あとで「元がどうだったか分からない」状態になりがちです。

いま何が返っているかを見ます。

# 応答ヘッダだけを表示する(-o /dev/null で本文は捨てる)
curl -s -o /dev/null -D - https://example.com/

curl -I でも見られますが、あれは HEAD というリクエストで、環境によっては通常の表示(GET)と挙動が違うことがあります。実際に人が見るときと同じ GET で確かめるほうが確実です。上の書き方なら GET のままヘッダだけ取れます。

セキュリティ関連だけ絞りたいときは、こうします。

curl -s -o /dev/null -D - https://example.com/ \
  | grep -i -E 'content-security-policy|strict-transport-security|x-frame-options|x-content-type-options|referrer-policy|permissions-policy'

結果は、作業前のひかえとしてファイルに残しておきます。 何かおかしくなったとき、「入れる前はこうだった」がすぐ出せます。

curl -s -o /dev/null -D - https://example.com/ > /var/backups/headers-before.txt

設定ファイルも、触る前に日付つきでコピーしておきます。 一行足すだけの作業でも、これは省かないほうがいいです。詳しい考え方は本番反映前のチェックリストと作業前バックアップにまとめています。

そして、どこでヘッダを出すかを1か所に決めます。nginx でも Apache でもアプリのフレームワークでも出せるため、二重に出て同じヘッダが2行返ることがあります。ブラウザの解釈が読みにくくなるので、原則どこか1か所にそろえます。現状確認で同じヘッダが2行出ていたら、まずそこの整理からです。

まず入れる3つ(副作用が小さい順)

① X-Content-Type-Options: nosniff

いちばん最初に入れていいのはこれです。 副作用がほぼありません。

ブラウザには、サーバーが申告したファイルの種類が怪しいとき、中身を見て種類を推測する動き(MIMEスニッフィング)があります。親切な機能なのですが、利用者がアップロードしたファイルが、意図せずスクリプトとして解釈されるきっかけになることがあります。nosniff は「推測しないで、こちらの申告どおりに扱って」という指示です。

副作用が出るとしたら、もともとファイルの種類(Content-Type)を間違って返していた場合です。たとえば CSS を text/plain で返していたようなケースでは、推測に助けられていたものが助けられなくなります。ただこれは、そもそも直すべき設定が表に出ただけとも言えます。心配なら、CSS と JavaScript が正しい種類で返っているかだけ、先に見ておきましょう。

② X-Frame-Options: SAMEORIGIN

よそのサイトの枠(iframe)に、自分のページを埋め込ませないという指示です。ボタンの位置に透明な枠を重ねて、意図しない操作をさせる手口への対策になります。

SAMEORIGIN は「同じサイト内の埋め込みなら許す」という設定です。入れる前に、ひとつだけ確認しておきます。

心当たりがあるなら、その埋め込みは壊れます。使っている場所を洗い出してから決めましょう。洗い出しの進め方は改修の影響範囲を見落とさない調査の順番が近い考え方です。

なお、いまのブラウザでは、後述の CSP に frame-ancestors を書くほうが正式な方法とされています。ただ X-Frame-Options もまだ広く解釈されているため、当面は両方書いておくのが無難です。CSP をまだ入れない段階なら、X-Frame-Options だけで十分に意味があります。

③ Referrer-Policy: strict-origin-when-cross-origin

リンクをたどってよそのサイトへ移動するとき、「どのページから来たか」をどこまで渡すかの指定です。社内システムや会員向けページでは、URL のパスやパラメータ自体が知られたくない情報を含むことがあります。

strict-origin-when-cross-origin は、同じサイト内では詳しく、よそへ出るときはドメイン名までしか渡さないという設定です。実はこれ、いまの主要ブラウザの既定値とほぼ同じです。つまり、明示的に書いても挙動はほとんど変わりません。

「変わらないなら書く意味がないのでは」と思うかもしれません。実務上の意味はふたつあります。ひとつは、古いブラウザや、既定が違う環境でも同じ動きにそろえられること。もうひとつは正直に言えば、診断ツールやチェックシートで「未設定」と書かれなくなることです。後者は本質ではありませんが、説明の手間が減るのは現場では地味に助かります。

3つをまとめて書くと

nginx の場合:

# server ブロックの中に書く。always を付けると 4xx・5xx の応答にも付く
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;

Apache の場合:

# mod_headers を有効にしたうえで、VirtualHost などに書く
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"

nginx で、ここだけは知っておきたい落とし穴があります。

nginx の add_header は、下位のブロック(location など)に add_header を1つでも書くと、上位のブロックで書いた add_header が、そのブロックでは効かなくなります。足し算ではなく、上書きに近い挙動です。

server {
    add_header X-Content-Type-Options "nosniff" always;   # ← 下の location では効かない

    location /api/ {
        add_header X-Cache-Status "MISS" always;          # ← ここに1つでも書くと、上のは消える
    }
}

「サーバー全体に入れたつもりが、一部のパスだけ抜けていた」という取りこぼしは、この仕様で起きます。設定を入れたあと、トップページだけでなく、location を分けているパスも curl で確かめてください。 抜けていたら、そのブロックにも同じ行を書き足します。

反映は、まず設定の構文チェック、それから再読み込みの順です。

# nginx
nginx -t && systemctl reload nginx

# Apache
apachectl configtest && systemctl reload apache2

reload は稼働中の接続を切らずに設定を読み直します。いきなり restart しなくて大丈夫です。反映後、もう一度 curl を叩いて、狙いどおり3行増えているかを見ます。ここまでで、指摘事項の多くには答えられる状態になります。

次の段階:確認してから入れるもの

3つ入れて落ち着いたら、次はここです。急がなくて大丈夫です。

HSTS(Strict-Transport-Security):戻しにくいので、短い期間から

「このサイトには、次からは必ず HTTPS で来てください」とブラウザに覚えさせる指示です。効果は高いのですが、このヘッダには他とは違う怖さがあります。

指定した期間、ブラウザ側が「HTTPで来るのを拒否する」状態を覚え込みます。 サーバー側でヘッダを消しても、すでに覚えたブラウザの記憶は消えません。 期限が切れるか、利用者が自分で消すまで残ります。つまり、設定ミスをサーバー側だけでは取り消せないわけです。

だから、入れる前に確認します。

進め方としては、最初はごく短い期間で試して、問題がなければ延ばすのが安全です。

# 最初は 5分(300秒)ほどで様子を見る
Strict-Transport-Security: max-age=300

# しばらく問題がなければ延ばす(1年)。サブドメインを含めるのは、全部HTTPS化を確認してから
Strict-Transport-Security: max-age=31536000; includeSubDomains

preload(ブラウザに最初から登録してもらう仕組み)もありますが、取り消しに時間がかかります。急いで付ける必要はありません。

CSP(Content-Security-Policy):Report-Only から育てる

いちばん効果が大きく、いちばん壊しやすいヘッダです。「読み込んでよいスクリプトや画像はここから」とブラウザに指定できます。

ただ、既存のサイトにいきなり入れると、かなりの確率で何かが止まります。 広告タグ、アクセス解析、チャット、外部フォント、HTMLに直接書かれたスクリプト。長く運用してきたサイトほど、外から読み込んでいるものが増えています。

そこで、まず「報告だけして、ブロックはしない」モードで様子を見ます。

# ブロックせず、違反だけをブラウザの開発者ツールに出す
Content-Security-Policy-Report-Only: default-src 'self'

この状態でしばらく運用し、自分のサイトが実際どこから何を読み込んでいるかの一覧を作ります。開発者ツールのコンソールに、違反の候補が並びます。その一覧をもとに許可先を足していき、止まるものが無くなってから -Report-Only を外す。これが唯一まっとうな進め方です。

急ぎで一部だけ効かせたいなら、範囲の狭い指定からという手もあります。

# 「よそのサイトの枠に埋め込ませない」だけを CSP で指定する
Content-Security-Policy: frame-ancestors 'self'

これは X-Frame-Options とほぼ同じ意味なので、既存サイトを壊すおそれが小さい部分です。CSP の第一歩としては現実的だと思います。

Permissions-Policy:使う機能だけ残す

カメラ・マイク・位置情報など、ブラウザの機能のうち使わないものを止めておく指定です。以前は Feature-Policy という名前でしたが、いまは Permissions-Policy が現行の名称です。古い記事を参考にすると旧名のまま書いてしまうことがあるので、そこだけ気をつけてください。

# カメラ・マイク・位置情報を、自サイト・埋め込み先ともに使わせない
Permissions-Policy: camera=(), microphone=(), geolocation=()

使っていない機能を止めるだけなので比較的安全ですが、Web会議や地図、写真アップロードのような機能がある場合は、そこを除外する必要があります。優先度はそれほど高くないので、余裕のあるときで大丈夫です。

もう入れなくていいもの:X-XSS-Protection

古い記事や、社内に残っている設定ファイルに X-XSS-Protection: 1; mode=block という行を見かけることがあります。これは、いまは入れる必要がありません。

これは各ブラウザに搭載されていた簡易的な検査機能を制御するヘッダでしたが、その機能自体が主要ブラウザから削除済みです。もう働くものがないので、書いても効果がありません。さらに当時、この機能そのものが別の問題を生むことがあると指摘され、削除に至った経緯があります。

いま指定するとしたら、明示的に無効にする 0 です。

X-XSS-Protection: 0

とはいえ、すでに 1; mode=block が入っていても、慌てて夜中に直すほどの話ではありません。 次に設定ファイルを触るタイミングで、0 に直すか行ごと消せば十分です。

こういう「昔は正しかったが、いまは違う」設定は、保守運用ではよく出てきます。見つけたときに責める必要はありません。書いた当時は正しかったのですから。淡々と現在のかたちへ寄せていけば大丈夫です。

具体例:チェックシートの指摘から2日で返した現場

社内システムと会社サイトを、他の業務と兼務で守っている担当者の例で、順番をたどってみます。

この例で効いたのは、高度な知識ではありません。入れる前に iframe の利用を1本電話で確かめたことと、入れたあとに1か所ではなく3か所を確認したこと。この2つだけです。

影響:3行入れると、何が変わるか

正直に言うと、この3行でサイトが劇的に安全になるわけではありません。それでも、変わることがいくつかあります。

逆に、入れていない状態が続くと、困るのはたいていセキュリティ事故そのものより先に、説明を求められる場面です。取引先の審査、社内の点検、監査。そのたびに一から調べ直すことになります。一度入れて記録しておけば、その手間は次から消えます。

明日やること:まず「見るだけ」から

設定を変えなくていいので、明日は次の順で大丈夫です。

  1. 現状を1回取る。curl -s -o /dev/null -D - https://自分のサイト/ を叩いて、結果をファイルに保存する。3分で終わります。
  2. location や VirtualHost を分けているパスがあれば、そこも取る。あとの取りこぼしを防げます。
  3. iframe の利用があるかを、社内に1本だけ聞く。「うちのページを、よそのサイトに埋め込んでもらっている場所はありますか」。この一言で、②の事故はほぼ防げます。
  4. 検証環境で3行を入れて curl で確認する。本番はその次の日でも構いません。
  5. 入れたら、変更管理の記録に1行残す。「いつ・何を・どこに入れたか・戻し方」だけで十分です。

1と3だけでも、今日と明日で確実に前に進みます。設定を入れることより、現状を1回見ておくことのほうが価値が大きい場面は、保守運用ではよくあります。

セキュリティヘッダ チェックリスト

全部そろえる前提ではありません。上から優先順位で並べてあります。

まずここだけ(最低ライン・これだけで効きます)

余力があれば(落ち着いたら足す)

できる現場だけ(無理なら飛ばしてよい)

最低ラインの5つが埋まっていれば、チェックシートの多くの欄には自信を持って答えられます。残りに○が付かなくても大丈夫です。

よければ、こちらも

ヘッダの設定は、単体で完結する作業ではありません。周りを少し整えると、次の一歩が軽くなります。

設定を反映したあと、確認結果が想定どおりになっているのを見て静かに安心している保守運用担当者

セキュリティヘッダの話がしんどいのは、名前が多いわりに、どれが自分に必要か誰も教えてくれないからだと思います。調べるほど「入れるべき」ものが増えていって、でも本番は動いていて、結局手が止まる。その状態で立ち止まっていた時間は、慎重さの証拠であって、怠慢ではありません。

今日いきなり全部そろえなくて大丈夫です。まず curl を1回叩いて、いま何が返っているかを見る。 それだけで、「よく分からないもの」が「現在地の分かるもの」に変わります。3行入れるのは、そのあとでいい。

明日、その1行のコマンドを打つところから始めてみませんか。指摘の欄がひとつ埋まるだけでなく、次に同じことを聞かれたときの自分が、ずいぶん楽になります。

ほかの実務ヒントは記事一覧からどうぞ。保守運用の小さな備えを、メールでも少しずつお届けしています。

関連用語