Nginx設定の肝

まともなサイトを公開したいということであれば指定(設定)しておきたい、そんな6項目。
6項目ともadd_headerで指定するものなのでサーバー側はHTTPヘッダにそれを表示するだけで処理はブラウザが行うもの。だけど重要なセキュリティ指定。

HTTP Strict Transport Security (HSTS)

HTTPでアクセスしてきたブラウザに次からHTTPSで通信するよう指示する指定。HTTPで接続してきたらHTTPSにリダイレクトさせる設定はよく行うが、そこに改竄があると不正なリダイレクトが行われる可能性がある。HSTSが指定してあるとブラウザが通信を開始しようとしたときにHTTPヘッダを見てHSTS有効のサイトだと判断次第HTTPSの接続に切り換える。(詳しくはRFC6797)

1
2
# add_header Strict-Transport-Security "max-age=31536000;includeSubdomains;preload";
add_header Strict-Transport-Security "max-age=15552000";  #たぶん実際は上の行ではなくこの行みたいな感じで指定

max-age=nn; は「HTTPSで来てね」を覚えておく時間(秒)を指定で必須。アクセスの度に指定の時間にリセットされる。長めに指定することで次回のアクセスが数日後、数週間後、数カ月後であっても有効にできる。
includeSubdomains; は対象にサブドメインも含める場合に指定。(任意)
preload; はRFC6797には無いけど、GoogleにHSTSのドメインを登録して且つサーバー側のヘッダにこの指定があれば有効になるみたい。(任意)
ただし、TLDだけしか登録できないよう(或るサブドメインだけというのは不可)なのでincludeSubdomains;も併記になることが多いと思われる。
Googleのリストに登録されることでブラウザ側は最初からHTTPSだと知ってる状態になるのかな。っていうかまたGoogleか・・

X-XSS-Protection

クロスサイトスクリプティング(XSS)フィルタを機能させる指定を行う。モダンブラウザは基本的にはXSSフィルタが有効だが、もし無効にされていても有効に強制する。特別な理由がなければ指定して良い筈。

add_header X-XSS-Protection "1; mode=block";

基本的に上の指定以外を使うことはないと思われるが
0 は無効
1 は有効
mode=block は攻撃を検知した際に応答しない指定。

X-Frame-Options

Clickjacking防止に関わる指定。分割フレームやインラインフレームを使用して別ページを表示することの拒否/一部許可。

1
2
3
add_header X-Frame-Options DENY;     #表示不許可
add_header X-Frame-Options SAMEORIGIN;   #同オリジン(ドメイン)を許可
add_header X-Frame-Options "ALLOW-FROM https://foobar.example.com";  #指定URIを許可

X-Content-Type-Options

ブラウザは本来はContent-Typeに従ってすべき処理を行うが、もしコンテンツ提供者側が誤って作っていてもブラウザがContent-Typeタイプを無視して(融通を利かせて)なんとか適当に処理(sniff)してくれると便利ではある。でも、それって意図しない動作をしたら危険だよねってことでブラウザに厳密に処理させる指定。仕組み的に筋の悪いコンテンツを提供しているのでなければ指定して良い筈。

add_header X-Content-Type-Options nosniff;

Content-Security-Policy (CSP)

その名のとおりコンテンツのセキュリティポリシーの指定。もしかすると一番難しいかも。サイトの性質に適切に合わせてやらないと全体あるいは一部が表示されない使えないといったトラブルが大なり小なり起きるかも。でも、本来はサイトの中身をポリシーに合わせるのが筋な筈。重要なので指定しておきたい。

基本的には使って良いものを「何は どこから」で指定する。
「何は」に相当するものは以下。
default-src デフォルト。最低限指定する。

connect-src AJAXとかイベントソースとかWebSocketとか
font-src ウェブフォント
frame-src 分割フレームやインラインフレーム
img-src 画像
media-src 動画や音声
object-src アプレットとかプラグインデータなど、オブジェクト
sandbox サンドボックス sandbox-srcではないので注意
script-src スクリプト
style-src スタイルシート
report-uri ポリシー違反を報告する(意味不明)

「どこから」に相当するものは以下。(複数併記可)

‘self’ 同オリジン(同ドメイン)
‘none’ どこからも無し(要するに使用不許可)
‘*’ どこからでも
data: データであれば?(意味不明)
FQDN foobar.example.comのような形式で指定
* でワイルドカードも使えるど*.example.comみたいな書き方ではなくて .example.comみたいな指定
URI http://foobar.example.comのような形式で指定
https: HTTPSでなければならない
‘unsafe-inline’ インラインスタイルやインラインスクリプトを許可
‘unsafe-eval’ JavaScriptなど危険な動的コードを許可

指定方法
default-src ‘none’; デフォルトは許可無し、別途個別指定すると厳しいポリシーができる。 default-src ‘self’; デフォルトは同オリジンのみ(基本指定)
default-src ‘self’ ‘unsafe-inline’ ‘unsafe-eval’ cdn.example.com デフォルトは同オリジンとインラインと動的コードとexample.comのCDNを許可(実用的な緩い指定)
media-src video.example.com 動画音声メディアはvideo.example.comのものを利用可
script-src ‘self’ www.google.com 同オリジンに追加してjQqueryやPrototypeはGoogleのCDNから使わせたい場合

1
2
3
4
5
#個別で指定 デフォルト無し、スクリプトは同オリジンと動的コードとGoogle、スタイルシートは同オリジンのみ、画像は何処のでも全て許可
add_header Content-Security-Policy " default-src 'none'; script-src 'self' 'unsafe-eval' www.google.com; style-src 'self'; img-src '*';";

#全般で一括指定、基本同オリジンのみスクリプト可、外部ソースはGoogleのみ
add_header Content-Security-Policy " default-src 'self' 'unsafe-inline' 'unsafe-eval' www.google.com;";
#全般で一括指定、全部許可。これだと指定する意味がないけどここから設定を作っていくというのもあり
#行末は指定内容の ; とNginxの設定行末の ; があるので注意
add_header Content-Security-Policy "default-src * 'self' data: 'unsafe-inline' 'unsafe-eval' ;";
#もう少し実用的でGoogleのAdsense, Analytics, Mapくらいは使いたいという場合
add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' pagead2.googlesyndication.com googleads.g.doubleclick.net www.google.co.jp apis.google.com www.google-analytics.com adservice.google.co.jp adservice.google.com; font-src 'self' fonts.googleapis.com fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' fonts.googleapis.com ajax.googleapis.com;  img-src * data:; child-src www.google.com apis.google.com accounts.google.com googleads.g.doubleclick.net; object-src 'self' pagead2.googlesyndication.com; media-src 'self' pagead2.googlesyndication.com; connect-src 'self' pagead2.googlesyndication.com googleads.g.doubleclick.net;";

意識高い系の人には怒られそうだけど、これくらいの緩さで勘弁して貰いたい。
unsafe-inline, unsafe-evalは追々無くしていく方向ということで(←これ既存のウェブサイトでやろうとすると手直し多すぎで大抵どえらくツラい筈)。

あと、add_header Content-Security-Policyだけじゃなくてadd_header X-Content-Security-Policy(Firefox等,IE11用 ), add_header X-Webkit-CSP (Androidブラウザ,古いChrome,Safari用)あたりも足す。(内容は同じでいいと思う。)

HTTP Public Key Pinning (HPKP)

2018年頃からHPKPは非推奨になっています。代替としてのCertificate Transparency(CT 証明書の透明性)が普及するようになり、CAがSCT(Signed Certificate Timestamp)を埋め込んだ証明書を発行するのが当たり前になったのでウェブサーバの管理者は特にすることがありません。Expect-CTレスポンスヘッダを出力するようにすると異常時に接続を拒否したりポリシー違反の通知を受けることができます。

SSLのサーバ証明書はあちこちで発行できてしまう、例えばfoobar.example.comのサーバ証明書はcomodoとGeoTrustで別々に発行できる。だから勝手に別の証明書が発行されて悪用される可能性がある。でも、サーバー証明書をサーバにピン留めしてやれば偽造したのは使えないよねっていう仕組み。(何でそれでいいのか解ってないけど)

たぶん取っ付き難さは今回の設定6項目の中では一番。設定間違うと下手すると暫くウェブサイトが利用できなくなるという怖いもの。間違いさえしなければ難しくはない。SSL証明書関係の設定なので非HTTPSサイトには関係ない。

ピン留めする証明書は最低1つが証明書チェーンの中にあればどれでも良いようだが、サーバ証明書がもっとも厳しく中間認証局証明書、ルート証明書になると緩いということになる。基本はサーバ証明書で良いかと。
なお、ピン1つだと例えばそのサーバ証明書が無効になったら(無効にしたら)えらいことになるのでバックアップのピンも必須。これがこの設定の面倒なところ。

既にサイトをHTTPSで公開しているならプライベート鍵、サーバ鍵、CSRを作成しサーバ証明書を発行して貰ったはず。
それとは別にもう一つ予備(バックアップ用)のCSRを作る。既にあるサーバ証明書が無効になる(なった)ときはこの予備のCSRから新しいサーバ証明書を発行してもらうことになるのでこちらも後々まで残すこと。なお、新しいCSRを作る際は既存の証明書を作った時とは別の独自のパスワードでプライベート鍵から完全に新しく作ることを推奨。
その際は既存のプライベートキーやCSRを間違って上書きしないように別ディレクトリで作業しましょう。
CSR作成までの手順はApacheでSSL (その勘所) を参照。

利用中のサーバ証明書からハッシュを求める。

% openssl x509 -pubkey < server.crt | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64  ←これを実行
abcABC123defDEF456ghiGHI789=   ←ハッシュが表示される
%

サーバ証明書(server.crt)からabcABC123defDEF456ghiGHI789=というsha256のハッシュが求められた(とする)。

なお、面倒だったらHPKP Hash GeneratorにサイトのURLを入力すればサーバ証明書からルート証明書まで証明書チェーン内全てのハッシュが得られる。

新たに作成した予備のCSRからハッシュを求める。

% openssl req -pubkey < server.csr | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64 ←これを実行
zyxZYX987wvuWVU654tsrTSR321=   ←ハッシュが表示される
%

CSR(server.csr)からzyxZYX987wvuWVU654tsrTSR321=というsha256ハッシュが求められた(とする)。
サーバー証明書からハッシュを求める場合とコマンド違うので注意。

add_header Public-Key-Pins 'pin-sha256="abcABC123defDEF456ghiGHI789="; pin-sha256="zyxZYX987wvuWVU654tsrTSR321="; max-age=5184000';

1つ目のピンはサーバ証明書から求めたものなので「有効」、2つ目のピンは全く公開されていないもののハッシュなので現時点では「無効」(無意味)なもの(だって発行さえされていない証明書用のCSRだから)。ただし、1つでも有効なピンがあれば大丈夫。ピンは2つに限定されるわけではないので中間CA証明書やルート証明書のピンを(複数)追加するということも可。でも、まぁ要らないかな。

max-age=nnは有効期間(秒)の指定。もしも何かしらの理由で有効ピンが1つも無い状態になると最大でこの値の期間アクセスできなくなる恐ろしい罠。ピンの正しさが確認できるまでは180(3分)以下程度の小さな値を指定するのが安全。

Qualys SSL Labsでテストを行い、「Certification Paths」の項目のPin(ハッシュ値)のどれかが緑色、または「Public Key Pinning (HPKP)」の項目が「Yes」になっていてPin(ハッシュ値)のどれかが緑色であれば有効ピンが正しく設定されているということです。

重要
  • 現在の証明書から新しいものに更新するときは予備CSRを使って新しい証明書を発行して貰うこと。
  • 新しい証明書を利用し始める際にはまた新しい予備CSRを作ってハッシュを求める。
  • 必ずNginx(ウェブサーバ)の設定ファイルのHPKPのハッシュ値を更新する。つまり無効になる証明書のpinを消し、新しく作成した(予備)CSRのpinを追加する。

ウェブサーバのHPKPの値を更新(新しい予備CSRのpinを追加)するのを忘れたまま更に次のSSL証明書更新時期を迎えてしまうと、その頃(場合によっては数ヶ月前から)にあなたのサイトを閲覧してHPKPのハッシュ値をキャッシュしたユーザーはHPKPの設定で指定した有効期間が過ぎるまであなたのサイトを見ることができなくなります。(全閲覧者がブラウザのキャッシュを消してくれたら解決ですが普通は無理)

クドくてゴメンナサイ。

このページの6項目の確認

% curl https://example.com --head
HTTP/2.0 200 OK
Server: nginx
Date: Sun, 27 Dec 2015 00:00:00 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 9999
Last-Modified: Mon, 7 Dec 2015 10:00:00 GMT
Connection: keep-alive
Vary: Accept-Encoding
ETag: "00000000-0000"
Strict-Transport-Security: max-age=31536000;
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Public-Key-Pins: pin-sha256="abcABC123defDEF456ghiGHI789="; pin-sha256="zyxZYX987wvuWVU654tsrTSR321="; max-age=5184000
Content-Security-Policy:  default-src 'self' 'unsafe-inline' 'unsafe-eval' apis.google.com www.google.com;
%

こんな感じでHTTPヘッダが表示される筈。

securityheaders.io

さらにsecurityheaders.ioで確認してみる。
このページの内容を何も指定していなければおそらく評価はFの筈。多くのサイトはFかE程度。
6項目全てを指定するとA+になる。ただし、指定内容(ポリシーの内容)は評価対象ではないようなので緩々なポリシーだとA+でも意味がないかも。

CSPポリシーの診断や書き方のアドバイスを得るにはCSP AnalyserCSP Builderが便利。

追記

CSPの面倒なところはサイト内で例えば広告を貼っていたりGoogleの各種サービスを利用していたりした場合許可しなければならないホスト(ドメイン)が増えること。

もしもadsenseを利用しているなら最低でも以下は許可しないと広告が表示されなくなる。
悪名高いadnxs.comや一見関係なさそうなyahoo.comも入る。場合によってはこれでも不足かもしれないので要注意。
*.google.com
*.googleadservices.com
*.googlesyndication.com
*.googleapis.com
*.gstatic.com
*.doubleclick.net
*.2mdn.net
*.adnxs.com
ads.yahoo.com

Google Analyticsを利用しているなら以下。
*.google-analytics.com

Amazon アソシエイトを利用しているなら最低でも以下は必要。(HTTPSサイトの場合)
*.amazon.co.jp
*.ssl-images-amazon.com

CSP設定を書くときは*.example.comじゃなく.example.comなので念の為。

2018年6月10日追記:
HPKPはもはや採用するべきではないでしょう。糞扱いされて主要ブラウザの非サポート化も進行中です。代替技術(仕様)としてはExpect-CTが筋だろうけど面倒なのでHPKPとは別の面で質が悪い気がします。HPKPの代替にはなりえませんがDNS CAAあたりでお茶を濁すのが簡単でよろしいかと。