こんにちは、hachi8833です。RailsのHSTS周りでハマったので、自分のためにメモします。
Rails 5.0以降ではforce_ssl
でHSTSも有効になる
Rails 5.0以降では、アプリケーション設定でconfig.force_ssl = true
を指定すると、HSTS設定「も」同時に有効になります。
参考: Rails 5 adds more control to fine tuning SSL usage | BigBinary Blog
参考: HTTP Strict Transport Security - Wikipedia
HTTP Strict Transport Security (エイチティーティーピー・ストリクト・トランスポート・セキュリティ、略称 HSTS)とは、WebサーバーがWebブラウザに対して、現在接続しているドメイン(サブドメインを含む場合もある)に対するアクセスにおいて、次回以降HTTPの代わりにHTTPSを使うように伝達するセキュリティ機構である。RFC 6797 で規定されている。
Wikipediaより
Chromeでローカル環境のHSTSを試してみる
整理のため、ローカル環境でRailsとChromeのHSTSの挙動を試してみました。
使ったもの
- 業務で使わない「捨て」のRailsアプリ環境(ここではRails 6.0.3.1を使いました)
- Chrome(現時点では83.0.4103.97)
- ngrok
自分はMac環境でやりました。
手順
作業内容はgitに登録する必要はありません。
1. 準備
- Railsアプリのconfig/environments/development.rbの
Rails.application.configure do
ブロックに以下を追加しておきます。
config.hosts << 'usousouso.lvh.me'
config.hosts << ".ngrok.io"
念のため、config.hostsのローカルホスト名にはusousouso.lvh.me
のような絶対使いそうにないものを使いました。
- Chromeを起動し、念のためシークレットモードのウィンドウを開いておきます(コマンド+Shift+Nキー)。以後は常にここでブラウザ画面を表示します。
2. lvh.meでの通常起動を確認
- ローカルでRailsアプリを
bin/rails s
で起動します。 - ブラウザで
http://usousouso.lvh.me:3000
を開いてアプリ画面が表示されることを確認し、Railsをいったん終了します。
3. HSTSを有効にし、lvh.meにHTTP接続する
- Railsアプリのconfig/environments/development.rbの
Rails.application.configure do
ブロックに以下を一時的に追加します。
config.force_ssl = true
- ブラウザで再度
http://usousouso.lvh.me:3000
を開きます。以下の画面が表示されます。
このサイトは安全に接続できません
localhostから無効な応答が送信されました。
ERR_SSL_PROTOCOL_ERROR
Railsには以下のようなログが表示されます。
2020-04-30 12:19:55 +0000: HTTP parse error, malformed request (): #<Puma::HttpParserError: Invalid HTTP format, parsing fails.>
- ここでRailsをいったん止め、
config.force_ssl = true
をコメントアウトして再び起動しても、同じエラー画面が出ます。
ハマった点
話が横にそれますが、実はこのときの挙動がなかなかわからなくてebiさんとbabaさんに助けていただきました。このときのERR_SSL_PROTOCOL_ERRORは、HSTSそのもので設定されるのではなく、それより前の段階でHTTPSへの恒久的な301リダイレクトが発生して↓それがブラウザに記憶され、以後のHTTPアクセスでも同じリダイレクトが繰り返されます。
babaさんのご指摘のように、ブラウザ内のHSTSリストへの登録は、HTTPS接続を確立できてからでないと行えません。そしてここまでのセットアップではHTTPS通信を用意していません。
このとき表示されたERR_SSL_PROTOCOL_ERRORそのものは、単に「ブラウザから送ったプロトコルとサーバーが期待するプロトコルの不一致」を表すエラーであり、これ自体はHSTSともリダイレクトとも無関係であるとbabaさんから教わりました。
ebiさんに以下のリンクも教えていただきました。
参考: Strict-Transport-Security - HTTP | MDN
メモ: サイトに HTTP を使用してアクセスしたとき、ブラウザーは Strict-Transport-Security ヘッダーを無視します。これは攻撃者が HTTP 接続に介入して、ヘッダーを挿入したり削除したりするかもしれないからです。ウェブサイトに HTTPS でアクセスして、証明書のエラーがない場合、ブラウザーはサイトが HTTPS でアクセスできることを知り、 Strict-Transport-Security ヘッダーを信用します。
developer.mozilla.orgより
この記事ではすべてChromeのシークレットモードウィンドウで作業しているので、ウィンドウを閉じればいつでも元通りになりますが、これを通常ウィンドウでやってしまうと(以前localhostでこれをやってしまいました😅)、記事末尾に書いたようにブラウザのキャッシュを削除しないとそのURLではHTTPSへの301リダイレクトを取り消せません。
localhost:3000
にアクセスしてしまったとき、記事末尾のリンクを参考にChromeのchrome://net-internals/#hsts
でHSTS設定からこのドメイン名を削除しようとしても、ドメイン名がそもそも登録されていなくて首を傾げたのですが、これが理由だったんですね...4. HSTSを有効にし、ngrokでhttp接続する
ここからが通常のHSTSらしい動作になります。なおngrokでは同じURLでHTTP接続とHTTPS接続を両方使えます。
- Railsをいったん終了し、再び
config.force_ssl = true
を有効にして再度起動します。 - ターミナルで別ウィンドウを開き、
ngrok http 3000
でngrokを起動します。
- まずHTTPでアクセスしてみます。ngrok画面に表示されている
http://
で始まるURLをコピーして、シークレットモードウィンドウで開きます。
少々待たされますが、今度は以下のように307一時リダイレクトが発生し、正常にHTTPS接続できるようになります。
なおbabaさんによると、この307リダイレクトはChromeの内部実装によるもので、通信を伴わないそうです。まだ試していませんが、Firefoxでは起きないとも教わりました。
- HTTPとHTTPSでそれぞれアクセスを繰り返します。
以後はHTTPでアクセスすると常に307でリダイレクトし、HTTPSでアクセスすれば普通にHTTP 200で接続されます。
以上でお試しはおしまいです。Chromeのシークレットモードウィンドウを閉じ、ngrokとRailsも終了します。development.rbの変更も元に戻しておきましょう。
あ〜すっきりした。
おまけ: Chromeを復元する方法
実は元々この部分を記事にしていました😅。
たとえばHTTPS接続のないRailsのdevelopment環境でforce_ssl = true
をオンにしたまま、うっかりhttp://localhost:3000
をブラウザで開いてしまうと、ブラウザにlocalhost
ドメインが記憶されてしまい、先ほどと同じくHSTSが発動してしまいます(してしまいました)。こうなるとそのブラウザではローカルの開発サーバーをlocalhostで開けなくなってしまいます。
急いでいるときなら、ブラウザのシークレットモードウィンドウでhttp://localhost:3000
にアクセスするなりlvh.meを使うなりすれば取りあえず回避はできますが。
解決方法その1: ChromeのキャッシュとCookieを削除する
301リダイレクトが原因の場合は、この方法で復元します。
Chromeで⌘ + ,
キーを押して設定を表示し、「セキュリティとプライバシー」の「閲覧データの削除」を開きます。
「Cookieと他のサイトデータを削除」「キャッシュされた画像とファイル」チェックボックスをオンにします。発生してすぐなら期間を「1時間以内」にしておくとよいでしょう。
「データを削除」をクリックし、Chromeを再起動します。
解決方法その2: ChromeのHSTS設定を解除する
以下の記事では、Chromeのchrome://net-internals/#hsts
でHSTS設定からドメイン名を削除する方法が紹介されています。HTTPS接続が確立してブラウザのHSTSリストに登録された場合は、この方法で復元します。
参考: Google Chrome SSL Error Guide for “ERR_SSL_PROTOCOL_ERROR”