Webサーバを運用していて、HTTPレスポンスヘッダをデフォルトの値から変更したいと思ったことはないだろうか? Front Doorなど有料のクラウドサービスを追加することなく、Azure App Service on Linuxでレスポンスヘッダの改変を実現する方法を紹介する。
対象:Azure App Service on Linux
AzureのWebサーバ/アプリサービス「App Service」が返すHTTPレスポンスヘッダをカスタマイズする方法については、Tech TIPS「【Azure】WebサーバをいじらずにHTTPレスポンスヘッダを追加/変更する(Front Door編)」で説明した。
ただ、この方法だとFront Doorのサービス利用料がかかってしまう。小規模あるいは無償公開のサイトだと適用しにくい方法だろう。
そこで本Tech TIPSでは、App Service内蔵のNGINXの設定を変更することで、レスポンスヘッダを追加する方法を紹介する。App Serviceの機能なので追加コストは不要だ。また、PHPなどでは面倒な画像などの静的ファイルに対するレスポンスヘッダの追加もNGINXなら可能だ。
App Service内蔵のNGINXの設定変更については、「【Azure】App ServiceのWebサーバ『NGINX』をカスタマイズする方法」を前提としているので、以下を読み進める前に参照していただきたい。また、「【Azure】App Service on LinuxでディレクトリごとにクライアントIP制限を実装する方法と注意点」も参考になるだろう。
NGINXでレスポンスヘッダを追加するには、「add_header」ディレクティブを用いる。基本的な構文は以下の通りだ。
add_header <レスポンスヘッダ名> <値>;
App Serviceの場合、これを[nginx/sites-available/default]ファイルの「server」または「location」コンテキスト内に記述する。例えば「X-Content-Type-Options」という名前のレスポンスヘッダに「nosniff」という値を指定するには、以下のように記述すればよい。
add_header X-Content-Type-Options "nosniff";
するとHTTPレスポンスには、以下のヘッダが追加される。
X-Content-Type-Options: nosniff
レスポンスヘッダを追加する目的の一つとして、キャッシュ(クライアントデバイスまたはキャッシュサーバがコンテンツを保存して再利用すること)の有効期間(期限)を変更することが挙げられる。具体的には「Cache-Control」「Expires」というレスポンスヘッダにキャッシュ可能な期間や期限などを指定する、というものだ。
これには「expires」というキャッシュ専用のディレクティブを利用するのが一般的だ。その使い方や注意点については別の機会に紹介したい。
ここからは、もっと具体的な目的を想定して、実装方法を例示していく。
あるディレクトリ以下がアクセスされた時に特定のレスポンスヘッダを付けたいといった場合、単純に実装するなら、そのディレクトリ以下をlocationコンテキストで特定しつつ、その中にadd_headerディレクティブを記述するとよい。
server {
<前略>
# 以下のレスポンスヘッダは、このserverコンテキスト全体で追加(継承)される
add_header X-Content-Type-Options "nosniff";
# [/himitsu]ディレクトリ以下に限定
location ~ ^/himitsu/ {
# 検索エンジンボットにインデックス登録とリンク追跡の禁止を伝達
add_header X-Robots-Tag "noindex, nofollow";
# 上記のadd_headerにより、上位(serverコンテキスト)でのヘッダ追加がリセットされるため、改めてここに同じ記述が必要
add_header X-Content-Type-Options "nosniff";
}
<後略>
}
上記リストでは、まずserverコンテキスト全体で追加したい、いわばデフォルトのレスポンスヘッダ「X-Content-Type-Options」を追加している。このヘッダはデフォルトで下位のlocationコンテキストに継承される(レスポンスに追加される)。
一方、上記リストの「/himitsu」ディレクトリのように、下位のlocationコンテキスト内でadd_headerディレクティブを記述した場合、上位コンテキストでのadd_headerは継承されず、そのヘッダはレスポンスに追加されない。もしそのヘッダが対象のlocationコンテキストでも必要なら、明示的にそれを記述し直す必要があるので気を付けよう。
また、もっと複雑なパターンを実装する場合は、locationではなく後述の「map」ディレクティブを多段で設定する方が、より少ない行数で記述できることもある。
レスポンスヘッダによっては、TLS(SSL)で暗号化されていない場合には追加しない、つまりHTTPSで通信している場合のみレスポンスに追加するように規定されていることがある。一例として、ここでは「Strict-Transport-Security」というヘッダを取り上げる。
まず、「map」ディレクティブで通信中のプロトコルがHTTPSか否かを判定する。
<前略>
map $http_x_forwarded_proto $strict_transport_security_header {
default "";
"~*https" "max-age=31536000; includeSubDomains; preload";
# 「~*」は大文字小文字を区別せず、正規表現でマッチングすることを表している
}
<後略>
mapディレクティブはhttpコンテキストにしか記述できない。そのため、ここでは[nginx.conf]のhttpコンテキスト内でインクルードされる[conf.d]ディレクトリの[mapping.conf]という設定ファイルに、上記リストを記している。
上記リストにある変数「$http_x_forwarded_proto」には、「X-Forwarded-Proto」リクエストヘッダの値が格納されている。その値は、クライアントデバイスと通信路の途中にあるリバースプロキシやロードバランサーとの間のプロトコル名を指す。HTTPSで通信中なら、「https」という文字列が入るはずだ。
同様の変数としては「$scheme」も挙げられる。しかしApp Serviceの場合、これにはロードバランサーとNGINX間のプロトコルを表す「http」が固定的に代入される。そのため、クライアントデバイスとのプロトコルの判別には使えないので注意してほしい。
さて、クライアントデバイスとHTTPSで通信している場合、変数「$http_x_forwarded_proto」の内容は上記リストの「"~*https"」に一致する。そのため、変数「$strict_transport_security_header」には「max-age=31536000; includeSubDomains; preload」という文字列が代入される。それ以外の状況では空文字列が代入される。
実際にレスポンスヘッダを追加する際には、前述の変数をadd_headerディレクティブの第2パラメーターに指定すればよい。
<前略>
add_header Strict-Transport-Security $strict_transport_security_header;
<後略>
HTTPSで通信していない場合、変数の値は空文字列となり、対象のレスポンスヘッダは追加されない。
HTTPSで通信中なら、変数の値は「max-age=31536000; includeSubDomains; preload」となり、それがそのままStrict-Transport-Securityレスポンスヘッダの値としてクライアントに返される。
例えばセキュリティ上の理由から、特定の種類のクライアントだけに特定のレスポンスヘッダを返したいことがある。
そのような場合は、クライアントの種別を表す「User-Agent」リクエストヘッダの値を前述のmapディレクティブで判別すればよい。
<前略>
map $http_user_agent $x_xss_protection_header {
default "";
"AlwaysOn" ""; # Always On(常時接続)のプローブ
"Azure Traffic Manager Endpoint Monitor" ""; # Traffic Managerのプローブ
"Edge Health Probe" ""; # Front Doorのプローブ
"~*Firefox/" ""; # Mozilla Firefox
"~*OPR/([1-6]\d\.)" "1; mode=block"; # Opera Ver.10-69
"~*Edge/(1\d)\." "1; mode=block"; # Microsoft Edge Legacy Ver.10-19
"~*Version/([1-9]|1[0-5])\..* Safari/" "1; mode=block"; # Apple Safari Ver.1-15
"~*Chrome/([1-9]|[1-7]\d)\." "1; mode=block"; # Google Chrome Ver.1-79
"~*Trident/\d+\.\d+;" "1; mode=block"; # Internet Explorer
"~*MSIE /\d+\.\d+;" "1; mode=block"; # Internet Explorer
}
<後略>
上記リストでは、User-Agentリクエストヘッダに記載されている、Webブラウザの種類やバージョンを表す文字列に応じて、変数「$x_xss_protection_header」に空文字列か「1; mode=block」という文字列のどちらかが代入される。
マッチングする文字列(一部)の先頭に入っている「~*」は、大文字小文字の区別をせずに正規表現で判定する、という指示である。上記の場合、「^」「/」のような行頭/行末を指定していないため、部分一致での判定となる。
後は、レスポンスヘッダを追加したいコンテキスト内で、add_headerディレクティブの第2パラメーターにこの変数を指定すればよい。
<前略>
add_header X-XSS-Protection $x_xss_protection_header;
<後略>
これにより、特定バージョンの特定Webブラウザだけに「1; mode=block」という値の「X-XSS-Protection」レスポンスヘッダが返される。その他のクライアントへのレスポンスには、X-XSS-Protectionヘッダは付加されない。
App ServiceのWebサーバ(NGINX)で400番台のHTTPステータスコードを返すようなエラーが発生したとき、add_headerディレクティブはデフォルトで指定のレスポンスヘッダを返してくれない。返すのはHTTPステータスコードが「200/201/204/ 206/301/302/303/304/307/308」のいずれかの場合だけだ。
しかし、デバッグ情報を独自のレスポンスヘッダに記載してクライアントに返す、といった用途の場合、エラーが発生したときこそレスポンスヘッダを返してほしいはずだ。
そのような場合、add_headerディレクティブの第3パラメーターに「always」を指定すればよい。
add_header X-FWIN2K-Debug "$val1, $val2, $val3, $val4" always;
この場合、たとえ「404 Not Found」のエラーが発生しても、「X-FWIN2K-Debug」レスポンスヘッダに変数「$val1」~「$val4」の内容が列挙された値がクライアントに返される。
なお、App Serviceで500番台のエラーが発生した場合、その時のレスポンスはWebサーバではなくその手前のロードバランサーが返していることがある。その場合、Webサーバによるレスポンスは無視されるため、いくらalwaysパラメーター付きのadd_headerディレクティブを実行させても、意図したレスポンスヘッダはクライアントに返らないので注意が必要だ。
■関連リンク
Copyright© Digital Advantage Corp. All Rights Reserved.
Windows Server Insider 髫ェ蛟�スコ荵斟帷ケ晢スウ郢ァ�ュ郢晢スウ郢ァ�ー