HSTSとは?仕組み・設定方法・解除手順をcurl出力で解説

実務レシピ

HSTS(HTTP Strict Transport Security)とは、Webサーバーがレスポンスヘッダで「このサイトには今後かならずHTTPSで接続しなさい」とブラウザに指示する仕組みです。RFC 6797で標準化されており、有効になっているサイトでは、利用者がhttp://と入力してもブラウザ側が接続前にhttps://へ書き換えます。この記事では、Ubuntu上のDocker検証環境で実際にnginxにHSTSを設定し、curlの実行結果でその挙動を1つずつ確認していきます。

curl -sSI https://github.com/ | grep -i strict-transport-security
strict-transport-security: max-age=31536000; includeSubdomains; preload

この1行が返ってくれば、そのサイトはHSTSが有効です。本記事の中心は「HSTSがあるときとないときで、curlの接続先ポートがどう変わるか」を実測するパートです。仕様の説明だけでなく、設定してもヘッダが出ない典型的な原因、そして一度有効にしたHSTSを解除する手順まで扱います。

  1. HSTSとは|ブラウザにHTTPS接続を強制させる仕組み
    1. HSTSが塞ぐのは「リダイレクトされる直前の一瞬」
  2. Strict-Transport-Securityヘッダの構文|3つのディレクティブ
    1. max-ageは小さい値から段階的に上げる
    2. includeSubDomainsはサブドメイン全体を巻き込む
    3. preloadは「書くだけ」では何も起きない
  3. HSTSが有効か確認する|curl -Iで1行
    1. プリロードリストに載っているかを調べる
  4. HSTSの動きをcurlで再現する|検証環境と実行結果
    1. 検証環境をDockerで作る
    2. HSTS未記録の状態:ポート80に接続して301が返る
    3. HTTPSに1回アクセスしてポリシーを記録させる
    4. HSTS記録後:ポート443へ直接つながる
    5. 平文HTTPで返したSTSヘッダは無視される
  5. nginxでHSTSを設定する
    1. 基本の設定
    2. 落とし穴1:locationのadd_headerがHSTSを消す
    3. 落とし穴2:alwaysを付けないとエラー応答に付かない
  6. ApacheでHSTSを設定する
    1. Header set と Header always set の違い
    2. .htaccessでも設定できるが推奨しない
  7. HSTSを解除する|サーバー側とブラウザ側の両方が必要
    1. サーバー側:ヘッダを消すのではなくmax-age=0を返す
    2. Chrome:chrome://net-internals/#hsts で削除する
    3. Firefox:サイトデータの削除で消える
    4. curl:キャッシュファイルを消すだけ
  8. プリロードリストへの登録と、その代償
    1. 申請の条件
    2. 解除には数か月かかる
  9. HSTS導入前チェックリスト
  10. よくある質問
    1. Q. HSTSを設定するとSEOに影響しますか?
    2. Q. 301リダイレクトを設定していればHSTSは不要ですか?
    3. Q. serverブロックに書いたのにHSTSヘッダが出ません
    4. Q. curlでは301が返るのに、ブラウザではHTTPSになります
    5. Q. localhostや開発環境でHSTSがかかって開けなくなりました
    6. Q. max-ageは何秒に設定すべきですか?
    7. Q. 証明書が切れたらどうなりますか?
    8. Q. HTTPポート80は閉じてしまってよいですか?
  11. まとめ
  12. 関連記事
  13. 関連記事

HSTSとは|ブラウザにHTTPS接続を強制させる仕組み

HSTSは「エイチ・エス・ティー・エス」と読み、正式名称はHTTP Strict Transport Securityです。日本語では「HTTP厳格トランスポートセキュリティ」と訳されることもありますが、実務ではほぼHSTSと呼ばれます。

実体はきわめて単純で、HTTPSのレスポンスにStrict-Transport-Securityというヘッダを1行足すだけです。ブラウザはそのヘッダを受け取ると「このホストはHTTPS必須」というメモを指定期間だけ保存し、以後そのホストへのアクセスは、たとえhttp://で始まるURLであっても、サーバーに問い合わせる前にhttps://へ書き換えて接続します。

HSTSが塞ぐのは「リダイレクトされる直前の一瞬」

「HTTPからHTTPSへ301リダイレクトしているから安全では?」という疑問がよく出ます。ここがHSTSを理解する最大のポイントです。

301リダイレクトの場合、ブラウザは「リダイレクト先を教えてもらうために、まず暗号化されていないHTTPでサーバーに接続する」必要があります。この最初の1往復は平文です。同じWi-Fiにいる攻撃者や経路上の機器は、この往復に割り込んで、リダイレクト先を偽サイトに差し替えたり、HTTPS化そのものを剥がしたりできます(SSLストリッピング攻撃)。

方式最初の通信経路上での改ざん
301リダイレクトのみ平文HTTPで1往復してから、HTTPSへ移るその1往復を書き換えられる
HSTS有効(記録済み)いきなりHTTPSで接続する平文の往復自体が発生しない

つまりHSTSは301リダイレクトの代わりではなく、301リダイレクトが残している「最初の1往復」という穴を塞ぐ追加の仕組みです。両方を設定するのが正解で、どちらか一方ではありません。

Strict-Transport-Securityヘッダの構文|3つのディレクティブ

ヘッダの書式は次のとおりです。指定できる要素は3つだけで、必須なのはmax-ageのみです。

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
ディレクティブ必須意味
max-age=<秒>必須ブラウザがこのポリシーを覚えておく秒数。アクセスのたびに再カウントされる
includeSubDomains任意すべてのサブドメインにも同じポリシーを適用する
preload任意ブラウザ組み込みのプリロードリストへの登録を希望する意思表示

max-ageは小さい値から段階的に上げる

max-ageは秒数で指定します。よく使われる値は次のとおりです。

期間使う場面
3005分導入直後の動作確認。問題が起きてもすぐ元に戻せる
864001日数日間の様子見
259200030日本番運用の第一段階
315360001年安定運用時の推奨値。プリロード登録の最低要件でもある
0ポリシーの取り消し(解除手順で使う)

いきなり31536000を設定しないでください。設定を誤ったまま1年を指定すると、一度アクセスした利用者のブラウザは最大1年間HTTPSでしかつながらなくなり、サーバー側の設定を戻しても復旧しません。まず300で挙動を確かめ、問題がないことを確認してから段階的に伸ばすのが安全です。

includeSubDomainsはサブドメイン全体を巻き込む

includeSubDomainsを付けると、example.comで受け取ったポリシーがdev.example.comold.example.comにも適用されます。便利ですが、次のようなケースで事故になります。

  • HTTPSに対応していない社内向けサブドメインが存在する
  • 証明書を用意していない検証用サブドメインがある
  • サブドメインを別部署や外部ベンダーが管理していて、全体像を把握していない

付ける前に、そのドメイン配下のホスト名をすべて洗い出し、全部がHTTPSで応答するか確認してください。DNSレコードの棚卸しについてはDNSサーバーとは?仕組み・種類・確認方法をコマンドで解説、ホスト名の考え方はFQDNとは?ドメイン名・ホスト名との違いと確認コマンドを実例で解説が参考になります。

preloadは「書くだけ」では何も起きない

preloadはヘッダに書いただけでは機能しません。これは「プリロードリストに登録してほしい」という意思表示にすぎず、実際の登録はhstspreload.orgへの申請が必要です。詳細は後述の「プリロードリスト登録」の項で扱います。

HSTSが有効か確認する|curl -Iで1行

あるサイトでHSTSが有効かどうかは、レスポンスヘッダを見れば一発でわかります。

curl -sSI https://example.com/ | grep -i strict-transport-security

実際に主要サイトへ実行すると、運用方針の違いが見えます(2026年8月時点の実測)。

for u in github.com www.mozilla.org qiita.com; do
  printf "%-20s " "$u"
  h=$(curl -sSI "https://$u/" | grep -i "^strict-transport-security" | tr -d '\r')
  echo "${h:-(ヘッダなし)}"
done
github.com           strict-transport-security: max-age=31536000; includeSubdomains; preload
www.mozilla.org      strict-transport-security: max-age=31536000
qiita.com            strict-transport-security: max-age=2592000

GitHubは1年+サブドメイン込み+プリロードのフルセット、Mozillaは1年だがサブドメインは対象外、Qiitaは30日と、同じHSTSでも設定はさまざまです。-IはHEADリクエストを送るオプションで、-sは進捗表示の抑制、-Sはエラーだけは表示する指定です。curlの他のオプションはcurl オプション一覧|目的別の使い方と実行例まとめにまとめています。

プリロードリストに載っているかを調べる

ヘッダとは別に、プリロードリストへの登録状況はAPIで確認できます。

curl -sS "https://hstspreload.org/api/v2/status?domain=github.com"
{
  "name": "github.com",
  "status": "preloaded",
  "bulk": true,
  "preloadedDomain": "github.com"
}

"status": "preloaded"なら登録済み、"unknown"なら未登録です。

HSTSの動きをcurlで再現する|検証環境と実行結果

ここからが本題です。HSTSは「ブラウザが覚える」仕組みなので、動作を目で確かめにくいのが厄介なところですが、curlには--hsts <ファイル>というオプションがあり、HSTSキャッシュをテキストファイルとして扱えます。これを使えば、ブラウザが内部で何を記録しているかをそのまま観察できます。

検証環境をDockerで作る

HSTSは一度有効にすると解除に手間がかかるため、本番はもちろん、普段使いのブラウザや自分のPCで気軽に試してはいけません。Dockerで隔離したネットワークを作り、その中だけで検証します。

# 検証用の自己署名証明書を作る
openssl req -x509 -newkey rsa:2048 -nodes -days 30 \
  -keyout server.key -out server.crt \
  -subj "/CN=hsts-demo.example" \
  -addext "subjectAltName=DNS:hsts-demo.example"

# 隔離ネットワークを作る
docker network create hstsnet

証明書の作り方の詳細はopenssl – 暗号処理ツールキットを参照してください。次にnginxの設定ファイルdefault.confを用意します。

server {
    listen 80;
    server_name hsts-demo.example;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name hsts-demo.example;

    ssl_certificate     /etc/nginx/certs/server.crt;
    ssl_certificate_key /etc/nginx/certs/server.key;
    root /usr/share/nginx/html;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
    }
}

サーバー側とクライアント側の2つのコンテナを、同じネットワークで起動します。--network-aliasを使うと、コンテナ間でそのホスト名が名前解決できるようになります。

docker run -d --name hsts-web --network hstsnet \
  --network-alias hsts-demo.example \
  -v "$PWD/default.conf:/etc/nginx/conf.d/default.conf:ro" \
  -v "$PWD/server.crt:/etc/nginx/certs/server.crt:ro" \
  -v "$PWD/server.key:/etc/nginx/certs/server.key:ro" \
  nginx:alpine

docker run -d --name hsts-client --network hstsnet \
  -v "$PWD/server.crt:/certs/server.crt:ro" \
  alpine:3.20 sleep 3600
docker exec hsts-client apk add --no-cache curl

自己署名証明書なので、クライアントからは--cacert /certs/server.crtを付けて信頼させます。nginxそのものの役割や設定の考え方はnginxとは?読み方・仕組み・Apacheとの違いをコマンドで解説で解説しています。

HSTS未記録の状態:ポート80に接続して301が返る

まず、HSTSキャッシュが空の状態でhttp://にアクセスします。-wで実際につながったポート番号を表示させるのがポイントです。

docker exec hsts-client sh -c '
rm -f /tmp/hsts.txt
curl -sS --hsts /tmp/hsts.txt -o /dev/null \
  -w "http_code=%{http_code}  connected_port=%{remote_port}  redirect_url=%{redirect_url}\n" \
  http://hsts-demo.example/'
http_code=301  connected_port=80  redirect_url=https://hsts-demo.example/

ポート80、つまり平文のHTTPで接続し、301を受け取っています。これが前述の「攻撃者に割り込まれる1往復」です。この時点ではまだHSTSを何も知らないので、避けようがありません。

HTTPSに1回アクセスしてポリシーを記録させる

docker exec hsts-client sh -c '
curl -sSI --hsts /tmp/hsts.txt --cacert /certs/server.crt \
  https://hsts-demo.example/ | grep -iE "^(HTTP/|strict)"'
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains

ヘッダを受け取りました。このときcurlが書き出したキャッシュファイルを覗くと、ブラウザが内部で持っている情報の正体がわかります。

docker exec hsts-client cat /tmp/hsts.txt
# Your HSTS cache. https://curl.se/docs/hsts.html
# This file was generated by libcurl! Edit at your own risk.
.hsts-demo.example "20270802 11:28:44"

記録されているのはホスト名と有効期限だけです。実行日は2026年8月2日で、期限は2027年8月2日。max-age=31536000(365日)がそのまま日付に変換されていることが確認できます。

そしてホスト名の先頭にある.(ドット)がincludeSubDomainsの印です。実際、includeSubDomainsを外した設定のホストにアクセスすると、ドットが付きません。

.hsts-demo.example "20270802 11:25:39"        <- includeSubDomains あり
nosub.hsts-demo.example "20270802 11:25:39"  <- includeSubDomains なし

HSTS記録後:ポート443へ直接つながる

同じキャッシュファイルを使って、もう一度http://にアクセスします。URLは1回目とまったく同じです。

docker exec hsts-client sh -c '
curl -sS --hsts /tmp/hsts.txt --cacert /certs/server.crt -o /dev/null \
  -w "http_code=%{http_code}  connected_port=%{remote_port}  url_effective=%{url_effective}\n" \
  http://hsts-demo.example/'
http_code=200  connected_port=443  url_effective=https://hsts-demo.example/

結果が変わりました。301が消え、最初からポート443(HTTPS)に接続し、200が返っていますhttp://と指定したのにurl_effectivehttps://です。書き換えはサーバーではなくクライアント内部で起きているので、平文の通信は一度も発生していません。

サーバー側のアクセスログを見ると、この事実がさらにはっきりします。

docker logs hsts-web 2>/dev/null | tail -3
172.18.0.3 - - [02/Aug/2026:11:28:44 +0000] "GET / HTTP/1.1" 301 169 "-" "curl/8.14.1" "-"
172.18.0.3 - - [02/Aug/2026:11:28:44 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1" "-"
172.18.0.3 - - [02/Aug/2026:11:28:44 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.14.1" "-"

1行目がHSTS未記録時の301、3行目が記録後のアクセスです。3行目には301が記録されていません。サーバーはリダイレクトを返す機会すら与えられておらず、クライアントが自力でHTTPSに切り替えたことがログから裏づけられます。

平文HTTPで返したSTSヘッダは無視される

「ポート80のserverブロックにもヘッダを書けば、初回から効くのでは?」と考えたくなりますが、これは効きません。RFC 6797は、暗号化されていない接続で受け取ったSTSヘッダをクライアントは無視しなければならないと定めています。平文で受け取ったヘッダは、そもそも改ざんされている可能性があるからです。

実際にポート80でSTSヘッダを返す設定にして確かめます。

curl -sSI --hsts /tmp/p.txt http://plain.hsts-demo.example/ | grep -iE "^(HTTP/|strict)"
grep -v "^#" /tmp/p.txt || echo "(エントリなし)"
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains
(エントリなし)

ヘッダは確かに届いているのに、キャッシュファイルには1件も記録されていません。HTTPS応答に書かなければ意味がない、ということです。この「初回アクセスだけはどうしても守れない」問題を解決するのが、次に説明するプリロードリストです。

nginxでHSTSを設定する

設定自体は1行ですが、nginxにはadd_header特有の落とし穴が2つあります。むしろそちらが本題です。

基本の設定

server {
    listen 443 ssl;
    server_name example.com;

    # HSTSはHTTPS側のserverブロックに書く
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;   # HSTSと併用する
}

反映前に必ず構文チェックを行い、リロードします。

sudo nginx -t
sudo systemctl reload nginx

落とし穴1:locationのadd_headerがHSTSを消す

「serverブロックに書いたのにHSTSヘッダが出ない」という相談の大半がこれです。nginxのadd_headerは、下位のブロックに1つでもadd_headerがあると、上位から継承したヘッダをすべて破棄します。追加ではなく置換である、という挙動です。

次の設定で実測しました。/にはlocation側のadd_headerがなく、/broken/には別のヘッダを1つだけ足してあります。

server {
    listen 443 ssl;
    server_name hsts-demo.example;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
    }

    location /broken/ {
        add_header X-Demo "location-level" always;   # これがHSTSを消す
        alias /usr/share/nginx/html/;
    }
}
[/ (locationにadd_headerなし)]
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains

[/broken/ (locationにadd_headerあり)]
HTTP/1.1 200 OK
X-Demo: location-level

/broken/ではHSTSヘッダが完全に消えました。X-DemoはHSTSと無関係なヘッダですが、それを1つ足しただけで、serverブロックのHSTSが道連れになります。CORS用のAccess-Control-Allow-Originやキャッシュ制御をlocationに書いている構成では、特定のパスだけHSTSが抜ける状態になりがちです。

対処は次の2つです。

  1. add_headerを書いているlocationすべてに、HSTSの行も書き足す
  2. locationごとのadd_headerをやめ、serverブロックに集約する

いずれにせよ、設定後はトップページだけでなく、静的ファイル・API・管理画面など複数のパスでヘッダを確認してください。

for p in / /assets/style.css /api/health /wp-admin/; do
  printf "%-20s " "$p"
  curl -sSI "https://example.com$p" | grep -ic "^strict-transport-security"
done

すべて1が返れば正常、0があればそのパスでHSTSが抜けています。

落とし穴2:alwaysを付けないとエラー応答に付かない

add_headerは、末尾にalwaysを付けないと2xxや3xxなどの成功系応答にしか適用されません(alwaysはnginx 1.7.5以降で使えます)。alwaysの有無で404応答を比較すると、違いは明確です。

# add_header ... ;      (alwaysなし)の場合
200応答のSTSヘッダ数: 1
404応答のSTSヘッダ数: 0

404や500といったエラーページにHSTSが付かないと、最初にアクセスしたURLがたまたま404だった利用者にはポリシーが伝わりません。エラー画面から入ってくる導線は珍しくないので、alwaysは必ず付けてください。エラー応答の切り分けについては500エラーの原因切り分け|Nginx・PHP-FPM・アプリを5分で特定する手順も参考になります。

ApacheでHSTSを設定する

Apacheではmod_headersを使います。有効化されていない場合は先に読み込みます。

# Debian / Ubuntu
sudo a2enmod headers
sudo systemctl reload apache2
<VirtualHost *:443>
    ServerName example.com
    SSLEngine on
    SSLCertificateFile    /etc/ssl/certs/example.crt
    SSLCertificateKeyFile /etc/ssl/private/example.key

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>

httpd 2.4のコンテナで実際に確認した出力がこちらです。

HTTP/1.1 200 OK
Server: Apache/2.4.68 (Unix) OpenSSL/3.5.6
Strict-Transport-Security: max-age=31536000; includeSubDomains

Header set と Header always set の違い

nginxのalwaysと同じ問題がApacheにもあります。Header setだけだと成功応答にしか適用されず、alwaysを付けて初めてエラー応答にも付きます。同じ環境でalwaysを外して測ると次のとおりでした。

Apache: Header set (alwaysなし)
  200応答のSTSヘッダ数: 1
  404応答のSTSヘッダ数: 0

nginxと同じ結果です。どちらのWebサーバーでも「エラー応答にも付ける」指定を忘れない、と覚えておけば移行時に迷いません。nginxとApacheの設計思想の違いはnginxとは?読み方・仕組み・Apacheとの違いをコマンドで解説で比較しています。

.htaccessでも設定できるが推奨しない

レンタルサーバーなどでVirtualHostを触れない場合、.htaccessに同じHeader always setを書けば動きます。ただしリクエストのたびにファイルを読むため性能面で不利で、設定ミスにも気づきにくくなります。VirtualHostを編集できる環境ならそちらを使ってください。

HSTSを解除する|サーバー側とブラウザ側の両方が必要

HSTSでもっとも問い合わせが多いのが解除です。ここで重要なのは、ポリシーを保存しているのはブラウザ側なので、サーバーの設定を消すだけでは解除にならないという点です。

やりたいこと操作する場所手順
今後の利用者に適用させないサーバーmax-age=0を返す
すでに記録済みの利用者を解放するサーバーmax-age=0を返し、各利用者が1回アクセスするのを待つ
自分の環境だけ今すぐ直すブラウザHSTSキャッシュを削除する

サーバー側:ヘッダを消すのではなくmax-age=0を返す

やりがちな誤りが「add_headerの行を削除する」ことです。ヘッダを消しても、すでにポリシーを記録したブラウザは期限が切れるまで(最悪1年間)HTTPSを強制し続けます。正しい手順は、期限0のヘッダを返して明示的に忘れさせることです。

add_header Strict-Transport-Security "max-age=0" always;

この状態で、記録済みのクライアントがもう一度アクセスすると何が起きるかを実測しました。

ロールバック前のキャッシュ: .hsts-demo.example "20270802 11:25:57"
Strict-Transport-Security: max-age=0
ロールバック後のキャッシュ: (エントリ削除)

1年先まで有効だったエントリが、max-age=0を受け取っただけで消えました。この解除ヘッダは、すべての利用者が最低1回アクセスするまで(数週間〜数か月)残しておく必要があります。全員が忘れたと判断できてから、初めて行を削除してください。

Chrome:chrome://net-internals/#hsts で削除する

開発中に自分のブラウザがHTTPSを強制してしまい困った場合は、ローカルのキャッシュを直接消します。

  1. アドレスバーにchrome://net-internals/#hstsと入力して開く
  2. 「Query HSTS/PKP domain」にドメイン名を入れ、現在の登録状況を確認する
  3. 「Delete domain security policies」にドメイン名を入れて「Delete」を押す
  4. Chromeを再起動する

Edgeなど他のChromiumベースのブラウザでも、edge://net-internals/#hstsのように同じ画面が使えます。

ただし、サーバーがまだHSTSヘッダを返している場合、次にHTTPSでアクセスした瞬間に再び記録されます。自分が管理していないサイトのHSTSを消し続けることはできません。また、プリロードリストに載っているドメインは、この操作でも解除できません。

Firefox:サイトデータの削除で消える

Firefoxは専用の管理画面を持たず、対象サイトのサイトデータを削除するとHSTSの記録も消えます。履歴画面で対象サイトを右クリックし「このサイトを忘れる」を選ぶのが手軽です。

curl:キャッシュファイルを消すだけ

curlの場合は--hstsで指定したファイルが実体なので、削除するか、該当行だけ消せばリセットされます。テキストなのでgrep -vで特定ホストだけ除外することもできます。

# ファイルごと削除
rm -f ~/.curl-hsts

# 特定ホストのエントリだけ削除
grep -v 'example\.com' ~/.curl-hsts > /tmp/h && mv /tmp/h ~/.curl-hsts

なおcurlは--hstsを指定しない限りHSTSを一切使いません。「ブラウザではHTTPSに飛ぶのに、curlでは301が返る」という食い違いは、この仕様が原因です。切り分けのときに覚えておくと混乱せずに済みます。

プリロードリストへの登録と、その代償

ここまで見たとおり、HSTSには「一度もアクセスしたことがない利用者の初回接続は守れない」という構造的な穴があります。これを埋めるのがHSTSプリロードリストです。主要ブラウザにドメイン一覧が組み込まれており、載っているドメインは初回アクセスから問答無用でHTTPSになります。

申請の条件

hstspreload.orgが公開している要件は次のとおりです。

  • 有効なTLS証明書を提供していること
  • ポート80で待ち受けているなら、同一ホストでHTTPからHTTPSへリダイレクトすること
  • www を含むすべてのサブドメインをHTTPSで提供していること
  • ベースドメインのHTTPS応答で、max-ageが31536000秒(1年)以上であること
  • 同じヘッダにincludeSubDomainspreloadが含まれていること
  • リダイレクト応答にもHSTSヘッダが含まれていること

解除には数か月かかる

プリロードリストは気軽に試すものではありません。リストはブラウザ本体に埋め込まれているため、削除申請をしても、変更が利用者に届くのはブラウザのアップデートを通じてになり、数か月かかります。hstspreload.org自身も「登録は簡単には取り消せない」と明記しています。

特に、HTTPS化していないサブドメインが1つでも残っている状態で登録すると、そのサブドメインは数か月にわたって閲覧不能になります。登録前に、DNSに存在するすべてのホスト名を洗い出して確認してください。

HSTS導入前チェックリスト

事故の大半は準備不足が原因です。導入前に次を確認してください。

  • サイト全体がHTTPSで正常に表示される(混在コンテンツの警告が出ていない)
  • 証明書の自動更新が動いており、更新失敗の通知経路がある
  • includeSubDomainsを付けるなら、全サブドメインがHTTPS対応済み
  • 最初はmax-age=300で開始し、数日間問題がないことを確認する
  • 複数のパス(トップ・静的ファイル・API・404)でヘッダが返ることを確認した
  • always(ApacheはHeader always set)を付けた
  • 解除手順(max-age=0)をチームで共有している

とくに証明書の期限切れは深刻です。HSTSが有効だと、証明書エラー画面の「詳細設定」から先へ進むボタンが表示されなくなり、利用者は回避できません。HTTPへのフォールバックもできないため、証明書が切れた瞬間にサイトが完全に閲覧不能になります。証明書の有効期限は次のコマンドで確認できます。

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

よくある質問

Q. HSTSを設定するとSEOに影響しますか?

HSTS自体は検索順位を直接上げる要素ではありません。ただし、HTTPからHTTPSへの301リダイレクトが1回減るぶん、初回以降の表示は速くなります。HTTPS化そのものは以前からランキング要因とされているため、「HTTPS化+301リダイレクト+HSTS」をセットで正しく構成するのが基本方針です。

Q. 301リダイレクトを設定していればHSTSは不要ですか?

不要ではありません。301はリダイレクト先を知らせるために平文HTTPで1往復する必要があり、その往復が攻撃対象になります。HSTSはその往復自体をなくす仕組みなので、役割が違います。両方を設定してください。

Q. serverブロックに書いたのにHSTSヘッダが出ません

nginxなら、そのパスを処理するlocationブロックに別のadd_headerがないかを確認してください。1つでもあると、上位から継承したHSTSヘッダは破棄されます。あわせて、alwaysを付けているか、HTTPS側のserverブロックに書いているかも確認してください。

Q. curlでは301が返るのに、ブラウザではHTTPSになります

正常な挙動です。curlは--hsts <ファイル>を明示的に指定しない限りHSTSキャッシュを持たないため、毎回サーバーに問い合わせて301を受け取ります。ブラウザは記録済みのポリシーに従って自分で書き換えているので、結果が食い違います。

Q. localhostや開発環境でHSTSがかかって開けなくなりました

ブラウザ側のHSTSキャッシュを削除してください。Chromeならchrome://net-internals/#hstsの「Delete domain security policies」です。なお、.dev.appといったTLDはプリロードリストにTLDごと登録されているため、この操作でも解除できません。開発用ドメインには.test.exampleを使うのが無難です。

Q. max-ageは何秒に設定すべきですか?

最終的には31536000(1年)が推奨ですが、いきなり設定するのは危険です。300(5分)で開始し、問題がなければ2592000(30日)、最後に1年へ、と段階的に上げてください。値を大きくする前に戻すのは簡単ですが、大きくしたあとに戻すのは利用者のブラウザ次第になります。

Q. 証明書が切れたらどうなりますか?

HSTSが有効なサイトでは、証明書エラーを利用者が回避できません。通常なら警告画面から「危険を承知で進む」を選べますが、HSTS適用下ではそのボタン自体が表示されず、サイトは完全に開けなくなります。証明書の自動更新と、更新失敗を検知する監視をセットで用意してください。

Q. HTTPポート80は閉じてしまってよいですか?

閉じないでください。HSTSを記録していない利用者や検索エンジンのクローラーはhttp://でアクセスしてくる可能性があり、ポート80が閉じているとリダイレクト先を知らせることすらできず、単に接続失敗になります。ポート80は301リダイレクト専用として開けておくのが定石です。プリロードリストの要件でも、ポート80で待ち受ける場合はHTTPSへのリダイレクトが求められています。

まとめ

HSTSはStrict-Transport-Securityヘッダを1行足すだけの仕組みですが、挙動を理解せずに設定すると復旧が難しくなる、扱いに注意が必要な機能です。この記事で実測した内容をまとめます。

  • HSTSはブラウザに「以後HTTPSのみ」と記憶させ、301リダイレクトが残す平文1往復の穴を塞ぐ
  • 記録後はhttp://指定でもポート443へ直接接続し、サーバーのログに301が残らない
  • 平文HTTPで返したSTSヘッダはクライアントに無視されるため、HTTPS応答に書く必要がある
  • nginxではlocationのadd_headerがserverのHSTSを消すため、複数パスで検証する
  • always(ApacheはHeader always set)がないと404などのエラー応答にHSTSが付かない
  • 解除はヘッダ削除ではなくmax-age=0。手元のブラウザはchrome://net-internals/#hstsで消す
  • プリロードリストは初回アクセスも守れるが、解除に数か月かかるため準備を万全にしてから申請する

まずは自分が管理するサイトにcurl -sSIを実行し、ヘッダの有無とmax-ageの値を確認するところから始めてみてください。

関連記事

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする

コメント