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を解除する手順まで扱います。
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は秒数で指定します。よく使われる値は次のとおりです。
| 値 | 期間 | 使う場面 |
|---|---|---|
300 | 5分 | 導入直後の動作確認。問題が起きてもすぐ元に戻せる |
86400 | 1日 | 数日間の様子見 |
2592000 | 30日 | 本番運用の第一段階 |
31536000 | 1年 | 安定運用時の推奨値。プリロード登録の最低要件でもある |
0 | — | ポリシーの取り消し(解除手順で使う) |
いきなり31536000を設定しないでください。設定を誤ったまま1年を指定すると、一度アクセスした利用者のブラウザは最大1年間HTTPSでしかつながらなくなり、サーバー側の設定を戻しても復旧しません。まず300で挙動を確かめ、問題がないことを確認してから段階的に伸ばすのが安全です。
includeSubDomainsはサブドメイン全体を巻き込む
includeSubDomainsを付けると、example.comで受け取ったポリシーがdev.example.comやold.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_effectiveはhttps://です。書き換えはサーバーではなくクライアント内部で起きているので、平文の通信は一度も発生していません。
サーバー側のアクセスログを見ると、この事実がさらにはっきりします。
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つです。
add_headerを書いているlocationすべてに、HSTSの行も書き足す- 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を強制してしまい困った場合は、ローカルのキャッシュを直接消します。
- アドレスバーに
chrome://net-internals/#hstsと入力して開く - 「Query HSTS/PKP domain」にドメイン名を入れ、現在の登録状況を確認する
- 「Delete domain security policies」にドメイン名を入れて「Delete」を押す
- 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年)以上であること - 同じヘッダに
includeSubDomainsとpreloadが含まれていること - リダイレクト応答にも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の値を確認するところから始めてみてください。
関連記事
- nginxとは?読み方・仕組み・Apacheとの違いをコマンドで解説 – HSTSを設定するWebサーバー側の基礎
- curl オプション一覧|目的別の使い方と実行例まとめ – ヘッダ確認に使う
-Iや-wの詳細 - openssl – 暗号処理ツールキット – 証明書の作成と有効期限の確認
- TCP/IPとは?4階層の役割と通信の流れをコマンド出力で解説 – HTTPSが乗っている通信の階層構造
- FQDNとは?ドメイン名・ホスト名との違いと確認コマンドを実例で解説 –
includeSubDomainsの影響範囲を判断する前提知識 - DNSサーバーとは?仕組み・種類・確認方法をコマンドで解説 – サブドメインの棚卸しに使う確認方法
- ミドルウェアとは?種類・具体例とサーバーでの確認コマンドを解説 – Webサーバーがシステム全体のどこに位置するか

コメント