はじめに

平素は大変お世話になっております。
クイックガードのパー子です。

誤って HSTS (HTTP Strict Transport Security) をすべてのサブドメインに対して有効化してしまったサイトについて、サーバ側から強制的にサブドメインのみ無効化する方法を調べてみました。

ちょっとググると「max-age=0 でレスポンス・ヘッダを送り直す」という解決策が出てきますが、今回はすべてのドメインを無効化するのではなく、 親ドメインは HSTS を維持しつつサブドメインだけ無効化する ための手順を検証しました。

背景

とある案件で SendGrid の Link branding という機能を使うことになりました。

Link branding とは、メール本文中のクリック・トラッキング用リンクのドメインを、SendGrid のものではなくカスタム・ドメイン (= ユーザが運営するサイトのサブドメイン) にする機能のことです。

HSTS が邪魔をする

Link branding の設定自体に難しいところはなくて、手順 どおりに進めればサッと設定できます。

ところが、我々もサッと設定を済ませて動作確認してみたところ、メール本文中のトラッキングURL をクリックすると SSL周りでエラーになってしまいました。

あらどこか手順を間違えたかしら… と思いながら挙動をよく確認してみると、自サイトの親ドメインのほうで送出している Strict-Transport-Securityレスポンス・ヘッダ (STSヘッダ) に includeSubDomainsディレクティブが含まれていることが原因だとわかりました。
(includeSubDomainsディレクティブが含まれていると、サブドメインに対しても HSTS が効く ようになります。)

どうやら SendGrid の計測用サーバはカスタム・ドメインの SSL証明書を持っていない (= 指定したカスタム・ドメイン用の証明書を即時発行してくれない) っぽくて、

  1. メール本文中のリンク (http:// <自サイトのサブドメイン>/…) をクリックする。
  2. サブドメインに対しても HSTS が効いているため、ブラウザは強制的に https:// でのアクセスに切り替える。
  3. SendGrid は <自サイトのサブドメイン> の SSL証明書を持っていないため、証明書の不一致でエラー!

という理屈でメール中のリンクが機能しなくなってしまうようです。

回避策は?

調べてみると GitHub に Issue が上がっていて、「CDN やリバース・プロキシを使って HSTS を受け止めろ。(= SSL に対応させろ。)」と書かれていました。

(ちなみに「SendGrid のほうで Let’s Encrypt とか使って証明書を勝手に取得してくれない?」という筋が良さげな要望も挙がっているのですが、残念ながら黙殺されてしまっています…。)

HSTS を何とかする

しかしながら、案件の都合により CDNなどの SSL終端コンポーネントの追加が難しかったため、今回は HSTS自体を何とかする方向で対処しました。

要件は次のとおりです。

  • HSTS自体は正当なセキュリティ要件なので、親ドメインでは引き続き HSTS を維持したい。
  • サブドメインだけ無効化する。
  • ブラウザ側で対応してもらう (= キャッシュをクリアしてもらう) わけにはいかないので、サーバ側から強制的に無効化したい。

無効化の仕組み

STSヘッダの取り扱い方は RFC 6797 で規定されています。

8.1. Strict-Transport-Security Response Header Field Processing

Update the UA’s cached information for the Known HSTS Host if
either or both of the max-age and includeSubDomains header field
value tokens are conveying information different than that already
maintained by the UA.

つまり、以前に受け取った STSヘッダから max-age または includeSubDomainsディレクティブに変更があれば、ブラウザの HSTSキャッシュが更新されて、新しいポリシーが適用されるとのことです。

検証

以上を踏まえて、本当に RFCどおりの振る舞いを示すのか、各OS、各ブラウザの実際の挙動を調べてみました。

方法

最初のアクセスでサブドメインも含めて HSTS を有効化したあと、続くアクセスで includeSubDomainsディレクティブを除去することで、親ドメインの HSTS を維持したままサブドメインだけ無効化できるかを検証しました。

具体的には、まず、次の URL を用意して、

スキームドメインパスSTSヘッダ意図
https/enablemax-age=31536000; includeSubDomains親 + サブドメインで HSTS を有効化する
https/sub-disablemax-age=31536000サブドメインのみ HSTS の対象外にする
http/<なし>HSTS の挙動チェック用ページ (親ドメイン)
https/<なし>
httpサブ/<なし>HSTS の挙動チェック用ページ (サブドメイン)
httpsサブ/<なし>

続いて、以下の手順でブラウザを動かして挙動を観察しました。

ステップ事前条件アクション期待する結果
1https://<親>/enable にアクセスhttp://<親>/ にアクセスhttps:// にリダイレクトされること
http://<サブ>/ にアクセスhttps:// にリダイレクトされること
2https://<親>/sub-disable にアクセスhttp://<親>/ にアクセスhttps:// にリダイレクトされること
http://<サブ>/ にアクセスhttps:// にリダイレクト されない こと

結果

以下の OS、ブラウザの組み合わせで動作確認を行なったところ、すべての組み合わせで期待する結果が得られました。

  • macOS - Catalina 10.15.5 (19F101)
    • Google Chrome - 84.0.4147.89 (Official Build) beta (64 ビット)
    • Safari - 13.1.1 (15609.2.9.1.2)
    • Firefox - Developer Edition 79.0b8 (64 ビット)
    • Vivaldi - 3.1.1929.45 (Stable channel) (64-bit)
    • Brave - 1.12.86, Chromium: 84.0.4147.85 (Official Build) beta (64 ビット)
  • Android 10 - Pixel 4, Build/QQ3A.200605.001
    • Google Chrome - 83.0.4103.106
    • Firefox Focus - 8.5.1 (Build #341921932 🦎 78.0.2-20200708170202)
    • Vivaldi - 3.1.1935.19
    • Brave - 1.10.99, Chromium: 83.0.4103.116
  • Windows 10 Home - 1909 OSビルド 18363.959
    • Microsoft Edge - 81.0.416.88 (公式ビルド) (64 ビット)
    • Google Chrome - 84.0.4147.89 (Official Build) beta (64 ビット)
    • Firefox - Developer Edition | 79.0b8 (32 ビット)
    • Vivaldi - 3.1.1929.45 (Stable channel) (64-bit)
    • Brave - 1.12.91, Chromium: 84.0.4147.89 (Official Build) beta (64 ビット)
  • iPadOS - 13.6
    • Safari - バージョン不明 (iPadOS のバージョンと対応?)
    • Google Chrome - 84.4147.71
    • Firefox Focus - 8.1.4
    • Brave - 1.18.1

あくまで印象ですが、モダンなブラウザであれば意図どおりに HSTSキャッシュが上書きされると思ってよさそうです。:)

Appendix

いくつかオマケを。

Preload

Preloadリスト に登録されている場合は、STSヘッダを変更するだけではダメで、併せてリストからの削除要求を申請しておく必要があります。

削除要求は 専用フォーム から申請できます。

HSTS発動時のブラウザの内部動作

HSTS が発動して https:// への切り替えが発生した際、各ブラウザはそれぞれ内部的に以下の振る舞いを示します。

Google Chrome

Google Chrome は 307 Internal Redirect を発動します。

Safari

Safari では 302 Found が実行されます。

Firefox

一方、Firefox は、開発ツール上ではリダイレクトのような処理は観測できず、いきなり https:// でアクセスしているように見えました。

他のブラウザ

Microsoft Edge、Vivaldi、Brave は Chromiumベースなので、Google Chrome と同じく 307 が発動します。

まとめ

この記事では、誤ってサブドメインまで含めて HSTS を利かせてしまったサイトについて、親ドメインの HSTS を維持したままサブドメインのみ無効化する手順を検証しました。

RFC を確認したところ、単純に STSヘッダから includeSubDomainsディレクティブを除去することで実現できることがわかりました。

実際にいくつかの OS、ブラウザで検証してみたところ、すべての組み合わせで期待どおりの結果が得られました。

この記事が、うっかりサブドメインまですべて HSTS の対象にしてしまった方のお役に立てば幸いです。
今後ともよろしくお願い申し上げます。