CloudflareでサイトをHTTPS専用にした(リダイレクトルールとHSTSの設定)

こんにちは、zaihoです。

今回は、個人開発している「デキレーレット」のサイトをHTTPS専用にした作業をまとめました。 きっかけは友人が起票したissueで、「http:// でアクセスするとHTTPSにリダイレクトされず、そのまま200で本文が返ってくる」と指摘されたことです。確認した時点では、CloudflareのAlways Use HTTPSがオンになっていて、この症状は出なくなっていました。ただ、調べていくと、Always Use HTTPSをオンにするだけでは足りない部分があったので、なぜ必要だったのかも含めて残しておこうと思います。 この記事は2026/10/11時点であるため、Cloudflareの画面や設定項目が変わる可能性はあるのでご注意ください

目次 見出しへのリンク

なぜHTTPS専用にする必要があったのか 見出しへのリンク

HTTPSでしか配信していなくても、ユーザーが http:// でアクセスしてくることはあります。リンクを http:// で共有されたり、アドレスバーに手打ちされたりするパターンですね。

このときの最初のリクエストは暗号化されていません。

  • HTTPのまま本文を返していると、通信内容が丸見えで、公衆Wi-Fiなど経路上の人にページやJSを差し替えられても、利用者は気づけない
  • 301でHTTPSに転送していても、最初のリクエストはHTTPなので、経路上の人は301ではなく偽のページを返すこともできる。転送が守ってくれるのは、経路上に攻撃者がいないときだけ

この最初の平文のやり取りを、2回目以降は起こさせない仕組みがHSTSです。

HSTSとは 見出しへのリンク

Strict-Transport-Security というレスポンスヘッダで、「このドメインは、指定した期間はHTTPSでしか接続しないでね」とブラウザに覚えさせる仕組みです。

覚えたあとは、ブラウザが http:// を自分で https:// に書き換えてからリクエストを出すので、平文のリクエストが出ていかなくなります。

ただ、ここに注意点があります。

  • HSTSが効くのは、HTTPSの応答で受け取ったときだけ(HTTPで受け取ったものは無視される仕様)
  • つまり、HSTSを受け取る前の最初の1回は守れない

あとで出てくる「preload」はこの1回を埋めるための仕組みです。

今回のサイトの構成 見出しへのリンク

今回の話に関係するホストは、こんな構成でした。

ホスト配信の中身
roulette.basekeita.comWorkersの静的アセット配信。public/_headers でHSTSを付けていた
basekeita.com(apex)アプリは無く、Cloudflareのリダイレクトルールで roulette へ転送しているだけ

roulette のリポジトリにある public/_headers が効くのは、roulette のWorkersの静的アセットだけです。apexの転送の応答には付きません。ここが抜けていました。

現状を確認する 見出しへのリンク

まず、本番の応答を curl で見てみました。

curl -sI http://roulette.basekeita.com/ | grep -i -E '^(HTTP|location)'
curl -sI http://basekeita.com/ | grep -i -E '^(HTTP|location)'
curl -sI https://basekeita.com/ | grep -i -E '^(HTTP|location|strict-transport)'

roulette のHTTPは301でHTTPSに飛んでいました(Cloudflareの「Always Use HTTPS」がオンでした)。ここは問題なしです。

でも、apexはこうなっていました。

$ curl -sI http://basekeita.com/ | grep -i -E '^(HTTP|location)'
HTTP/1.1 301 Moved Permanently
Location: https://roulette.basekeita.com/

HTTPで来たリクエストが、そのまま https://roulette.basekeita.com/ に飛んでいます。しかも https://basekeita.com/ にはHSTSのヘッダがありません。

さらに、preloadの条件を満たしているかを確かめてくれる hstspreload.org のAPIでチェックしてみました(出力は一部省略しています)。

curl -s 'https://hstspreload.org/api/v2/preloadable?domain=basekeita.com'
{
  "errors": [
    {
      "code": "response.no_header",
      "summary": "No HSTS header"
    },
    {
      "code": "redirects.http.first_redirect.insecure",
      "summary": "HTTP does not redirect to HTTPS"
    }
  ]
}

指摘は2つです。

  • apexのHTTPS応答にHSTSヘッダがない
  • http://basekeita.com の最初の転送先が、同じホストのHTTPS(https://basekeita.com)になっていない

2つ目は、preloadの条件というだけではなく、実際に困る話でもあります。HTTPで来たアクセスが apexのHTTPS応答を一度も通らず roulette へ直行してしまうので、そのブラウザは apexのHSTSを受け取るチャンスがありません(https://basekeita.com を直接開けば受け取れます)。

やったこと 見出しへのリンク

1. サブドメインを洗い出す 見出しへのリンク

HSTSに includeSubDomains を付けると、そのHSTSを受け取ったブラウザでは、配下のサブドメインすべてがHTTPS専用になります。HTTPSで開けないサブドメインがあると、見られなくなってしまうんですよね。

なので、先にCloudflareのDNSレコードを確認して、サブドメインを洗い出しました。どれもCloudflareのプロキシ経由で、curl で確かめたところ、HTTPは301でHTTPSへ、HTTPSは200で返ってきたので問題なしでした。

ちなみに、転送ルールの条件には www.basekeita.com も入っていますが、www のDNSレコードは無く名前解決できないので、対象外です。今後サブドメインを増やすときも、HTTPSで開けることが前提になります。

2. 転送ルールを「HTTPSのときだけ」にする 見出しへのリンク

apexからの転送は、リダイレクトルール(名前は root to roulette)で設定していました。条件はこうです。

(http.host in {"basekeita.com" "www.basekeita.com"})

HTTPかHTTPSかを問わないので、HTTPのリクエストにもこのルールが当たります。挙動を見る限り、Always Use HTTPSより先にこのルールが効いて、roulette へ直行していました。

そこで、条件に ssl を足しました。

(http.host in {"basekeita.com" "www.basekeita.com"} and ssl)

画面では、条件のビルダーで「AND」を押し、フィールドに「SSL/HTTPS」を選んで、値のトグルをオンにします。式のプレビューに and ssl が足されていればOKです。

これでHTTPのリクエストはルールに当たらず、Always Use HTTPSで https://basekeita.com/ へ飛び、そのあとにルールで roulette へ転送される、という2段階になります。

3. CloudflareでHSTSを有効にする 見出しへのリンク

apexにもHSTSを付けたいので、リポジトリの _headers ではなく、Cloudflareのゾーン全体の設定で有効にしました。

「SSL/TLS」→「エッジ証明書」→「HTTP Strict Transport Security (HSTS)」から設定します。

項目設定
HSTSを有効にするオン
Max Age12か月
サブドメインに適用する(includeSubDomains)オン
プリロードオフ
No-Sniffヘッダー今回は触っていない

プリロードは、いったんオンにして申請の条件を満たすか確かめたあと、オフにしました。理由は「preload申請は見送った」に書いています。

有効にするときには、いくつか警告が出ます。HSTSを有効にしたあとで、CloudflareのHTTPSを無効にして、オリジンにも有効な証明書が無いと、訪問者がサイトにアクセスできなくなる、という警告です。サブドメインの設定には「サブドメインのいずれかがHTTPSをサポートしていない場合、アクセスできなくなります」、プリロードには「HTTPSサポートのないWebサイトに完全にアクセスできなくなります」という警告が付いています。先にサブドメインを洗い出したのはこのためです。

また、本当は max-age を短い値から段階的に上げていくのが推奨されています。今回はサブドメインがすべてHTTPSで動くと確認できていたので、最初から12か月にしました。

4. _headers の値をそろえる 見出しへのリンク

roulette には、もともと _headers で同じ値のHSTSを付けていました。

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

ゾーンの設定を入れても、応答のヘッダは1本のままです。プリロードをオンにしていた間は、_headers にない preload が付いた1本のヘッダが返ってきたので、_headers の値はゾーンの値で置き換わっているようです。

ずれると後で混乱するので、ゾーンの値を変えたときは、_headers も同じ値にそろえるようにしています。

確認結果 見出しへのリンク

設定後に、もう一度 curl で確かめました。

URL結果
http://basekeita.com/301で https://basekeita.com/ へ
https://basekeita.com/301で https://roulette.basekeita.com/ へ。HSTSあり
http://roulette.basekeita.com/301で https:// へ
https://roulette.basekeita.com/200。HSTSは1本

HTTPSのホストには、こんなヘッダが付いています。

strict-transport-security: max-age=31536000; includeSubDomains

preload申請は見送った 見出しへのリンク

hstspreload.orgで申請すると、ブラウザに最初から「このドメインはHTTPS専用」と埋め込んでもらえます。HSTSを受け取る前の最初の1回も守れるようになる、というものです。

プリロードをオンにした状態でAPIを叩くと errors が空になり、申請ページでも「申請できる」と表示されました。そのうえで、結局見送りました。理由はこの3つです。

  • 申請ページ自体が、preloadは推奨しないと説明しています。ChromeやSafariはHTTPのアクセスを自動でHTTPSに上げてくれるので、preloadが役に立つのは、その自動の昇格が攻撃者のいる環境で失敗するときだけ、という内容でした
  • いったん載ると、外してもブラウザに反映されるまで数か月かかります
  • includeSubDomains は、a.b.basekeita.com のような2階層目以降のサブドメインも対象にします。今はHSTSを受け取った人だけですが、preloadすると、一度もサイトを開いたことがない人にも最初から効きます

申請しないので、ヘッダの preload も外して、Cloudflareのプリロードをオフに戻しました。preload を付けていると、申請の対象になってしまうからです。外したあとは、APIも header.preloadable.preload.missing(preload が無い)を返すようになりました。

まとめ 見出しへのリンク

  • HTTPをHTTPSへ転送するだけだと、最初の1リクエストは平文のまま。HSTSでブラウザに覚えさせて、次回以降を守る
  • HSTSはHTTPSの応答で受け取ったときだけ効く。apexからの転送が直接別ホストへ飛んでいると、apexのHSTSを受け取れない
  • Cloudflareでは、挙動を見る限りリダイレクトルールが先に効いていたので、条件に ssl を足して「HTTPSのときだけ」にした
  • _headers はWorkersのアセットにしか効かない。apexにも付けたいなら、ゾーンのHSTS設定を使う
  • includeSubDomains を付けるなら、先にサブドメインを洗い出してHTTPSで動くことを確かめる
  • preloadは、効果と外しにくさを比べて今回は見送った。ヘッダにも付けていない

「Always Use HTTPSをオンにして終わり」ではなく、apexやサブドメインまで見ないと穴が残るんだなと勉強になりました。 HSTSは一度出すと戻しにくいので、設定を変えるときは慎重にいこうと思います。