自分のサイトの動作を確かめようと、Chrome の開発者ツール(デベロッパーツール)でネットワークの一覧を開いたところ、http の URL に対する応答として 「307 Internal Redirect」 と表示されていた。サーバーのログには、そんな要求の記録が見当たらない。「サーバーが勝手に 307 を返しているのではないか」と、首をかしげた方もいるでしょう。

この 307 は、サーバーが返したものではありません。ブラウザーが自分の内部で作り出した、転送の記録です。原因は多くの場合、HSTS(HTTP Strict Transport Security)という安全のための仕組みです。つまり、サーバー側が壊れているのではなく、むしろ正しく守られている状態を示しています。

正体

HSTS により、ブラウザーが http を https に書き換えた記録です。

サーバーの関与

通信を始める前の処理なので、http の要求はサーバーに届きません。

対処の要否

通常は不要です。仕組みが働いている証拠です。

注意点

サーバー側の設定と食い違うと、転送ループの原因になります。

「サーバーのログに出ない 307」が引き起こす混乱

この表示が混乱を呼ぶのには、理由があります。

まず、サーバー側のログと食い違うことです。開発者ツールには 307 が出ているのに、アクセスログには http の要求が一つも見当たりません。サーバーの管理者からすれば、「存在しない通信」が報告されているように見えます。

次に、再現性が環境によってばらつくことです。同じサイトでも、一度 https でアクセスしたことのあるブラウザーでは 307 が出て、初めて使うブラウザーでは、サーバーの返す 301 が出ます。「自分のパソコンでは起きるが、同僚のパソコンでは起きない」と言われて、調査が迷走します。

さらに、設定を直したのに症状が消えないことがあります。サーバー側で HSTS を無効にしても、ブラウザーが記憶している間は、同じ 307 が出続けます。「修正が反映されていない」と誤解して、不要な再設定を繰り返してしまう事例は少なくありません。

また、HSTS とサーバー側の転送設定が噛み合っていないと、実際にリダイレクトループの原因になります。仕組みを知らないまま設定を触ると、状況を悪化させかねません。だからこそ、この 307 の正体を知っておく価値があります。

HSTS とは何か

HSTS は、RFC 6797 で定められた仕組みです。サーバーが https の応答に次のようなヘッダーを付けます。

Strict-Transport-Security: max-age=31536000; includeSubDomains

ブラウザーはこれを受け取ると、そのドメインを「今後は https でしかアクセスしない相手」として、max-age で指定された秒数のあいだ記憶します。上の例の 31536000 秒は 1 年です。includeSubDomains を付けると、サブドメインにも同じ扱いが適用されます。

仕様上のポイントは、次の 3 つです。

  • ヘッダーは https の応答でだけ有効。 安全でない通信(http)で受け取った Strict-Transport-Security は、ユーザーエージェントが無視しなければならない(MUST)と、RFC 6797 に書かれています。
  • max-age は必須。 記憶の期間を示す指示で、省略はできません。
  • includeSubDomains は任意。 値を持たない指示で、付けた場合のみサブドメインが対象になります。

ブラウザー内部で何が起きているか

HSTS を記憶したブラウザーが、そのドメインの http の URL を開こうとすると、次のような流れになります。

clientserver

(利用者) http://example.com/login を開く
ブラウザーは、通信を始める前に HSTS の記憶を確認します
307 Internal Redirect (ブラウザーが内部で生成)
Non-Authoritative-Reason: HSTS
URL を https://example.com/login に書き換えます
GET https://example.com/login (サーバーへ送信)
200 OK

RFC 6797 は、記憶済みのドメインに対する http の URI を読み込む前に、ユーザーエージェントが URI のスキームを https に置き換えるよう規定しています。明示的にポート 80 が指定されていた場合は 443 に変換され、それ以外のポートはそのまま保たれます。

この「置き換え」は、ブラウザーの内部で完結します。サーバーには、書き換え後の https の要求だけが届きます。そのため、http の要求がサーバーのログに残らないのは、当然の結果です。

Chromium が 307 を選んだ理由

Chromium(Chrome や Edge の土台となるプロジェクト)のソースコードを見ると、この書き換えを表現するために、状態コード 307 の転送応答を内部で組み立てています。コードのコメントには、307 を使うのはメソッドを保ち、POST 要求も正しく動くようにするためと書かれています。応答の状態行には「Internal Redirect」という文言が、ヘッダーには Non-Authoritative-Reason: HSTS が付きます。

開発者ツールで見かける「307 Internal Redirect」の正体は、これです。つまり、「サーバーの権威ある応答ではないが、HSTS の理由で転送した」ことを示す印が、自動で付いています。なお、この表示は Chromium 系のブラウザーでの話で、ほかのブラウザーでは見え方が異なる場合があります。

仕組みを理解すると見えてくる利点

「サーバーの不具合に見える仕組み」と書いてきましたが、HSTS と内部転送は、利用者とサイト運営者の両方に大きな利点をもたらします。

利点 1: 最初の http 通信が発生しない

通常の 301 や 308 による転送では、最初の 1 回は必ず http で通信します。この無防備な 1 回が、通信を盗み見や改ざんの危険にさらす隙になります。HSTS が働いていれば、その 1 回すら発生しません。

利点 2: 速い

サーバーへの往復が 1 回省けるため、表示が速くなります。ネットワークの遅い環境ほど、効果が大きくなります。

利点 3: POST の内容も失われない

前述のとおり、書き換えには 307 が使われます。フォームの内容を http に誤って送ろうとしても、メソッドと本文が保たれたまま、https へ向かいます。

利点 4: プリロードで、初回訪問からでも守られる

HSTS は通常、一度 https でアクセスして記憶させる必要があります。それより前の「初めての訪問」を守るために、ブラウザーに最初から組み込まれたプリロードリストがあります。リストを管理する hstspreload.org の説明では、Chrome がリストを管理し、Firefox・Safari・Edge なども、Chrome のリストをもとにしたリストを持っています。登録の条件には、有効な証明書、http から https への転送、全サブドメインの https 対応、そして max-age が 1 年(31536000 秒)以上、includeSubDomains、preload を含むヘッダーの設定があります。

307 Internal Redirect を確認する方法

「本当に HSTS による内部転送なのか」を確かめるには、次の手順が役立ちます。

  1. 開発者ツールで、応答ヘッダーを見る。 Non-Authoritative-Reason: HSTS があれば、HSTS による内部転送です。
  2. サーバーのアクセスログと照らす。 同じ時刻に、http の要求が記録されていなければ、ブラウザー内で完結しています。
  3. 記憶の状態を調べる。 Chromium 系のブラウザーでは、アドレスバーに chrome://net-internals/#hsts を入力して開くページで、ドメインの HSTS の状態を照会できます。

コマンドラインの curl には HSTS の記憶が(既定では)ないため、curl -I http://example.com/ は、サーバーが返す本来の転送(301 や 308 など)を見せてくれます。ブラウザーの挙動とサーバーの挙動を比べると、切り分けが進みます。

困るのはどんなときか

HSTS による 307 自体は問題ではありませんが、次の場面では対処が必要です。

場面 1: 転送ループが起きる

サーバー側に「https へのアクセスを http へ転送する」設定が残っている場合、ブラウザーは http を https に書き換え、サーバーは https を http に戻す、という往復が止まらなくなります。サーバー側の設定を、https のまま完結する形に直します。

場面 2: HSTS をやめたい

HSTS を解除するには、https の応答で max-age=0 を送ります。RFC 6797 では、max-age が 0 であることは、ユーザーエージェントがそのホストを HSTS の対象として扱うのをやめる合図になります。ただし、解除が届くのは、ブラウザーがそのサイトに https でアクセスした時です。解除の前に使っていたブラウザーが、サイトに一度も訪れなければ、記憶は残り続けます。

Chromium 系のブラウザーでは、手元の記憶を chrome://net-internals/#hsts の削除フォームで消せます。ただしこれは動的に保存された記憶のみが対象で、プリロードリストに載っているドメインは消せません。hstspreload.org は、プリロードリストへの登録が簡単には取り消せず、削除がユーザーに届くまでに数か月かかると警告しています。

場面 3: 開発環境で、http に戻せない

ローカル開発で、本番と同じドメイン名を使うと、HSTS の記憶が開発環境の邪魔になります。開発用には別のドメイン名を使うか、HSTS を付けない設定にしておくのが安全です。ステージング環境にも本番と同じヘッダーを付けたい場合は、期間を短くした max-age を使い、検証が終わったら期限切れで自然に解除されるようにしておくと、後片付けの手間が減ります。本番の設定を試すために一時的に長い期間を設定してしまった場合は、戻すのに時間がかかることを、前もって理解しておいてください。

2 種類の 307 を見分ける

開発者ツールに並ぶ 307 は、出どころによって 2 種類あります。見分けられるようになると、調査が速くなります。

見分けるポイント サーバーが返した 307 ブラウザーが作った 307
応答ヘッダー Location がサーバーの設定どおり Non-Authoritative-Reason: HSTS が付く
サーバーのログ 要求の記録が残る http の要求は残らない
転送の内容 設定した任意の URL http から https への書き換えのみ
直すべき場所 サーバーの転送設定 通常は直す必要がない

サーバー管理者が押さえておく HSTS の設定

HSTS ヘッダーは、https の応答にだけ付けます。nginx と Apache での書き方は、次のとおりです。いずれも、https を処理する設定ブロックの中に置きます。

# nginx: https の server ブロック内
add_header Strict-Transport-Security "max-age=300" always;
# Apache: https の VirtualHost 内(mod_headers が必要)
Header always set Strict-Transport-Security "max-age=300"

上の例の max-age=300 は 5 分間です。最初は短い期間で始め、問題がないことを確かめながら、1 日、1 週間、1 年と延ばしていくのが定石です。いきなり長い期間を設定すると、設定の誤りが見つかったときに、記憶したブラウザーが長期間にわたって影響を受けます。includeSubDomains を付ける場合は、すべてのサブドメインが https に対応していることを先に確認してください。

「サーバーの不具合ではない」と分かれば、半分は解決

要点を整理します。

  • 「307 Internal Redirect」は、ブラウザーが HSTS にしたがって、http を https に書き換えた内部の記録です。
  • サーバーには、書き換え後の https の要求だけが届きます。http の要求がログに残らないのは正常です。
  • 仕組みは、利用者の安全と表示の速さの両方に役立っています。
  • 問題になるのは、サーバー側の転送設定と食い違ってループになる場合と、HSTS をやめたい場合です。

この 307 を見かけたら、まず Non-Authoritative-Reason: HSTS の有無を確認してください。あれば、慌てる必要はありません。もしページが開かない場合は、リダイレクトが繰り返されるときの原因と直し方で、切り分けの手順を説明しています。