ローカル開発環境の https 化
Intro
Web の https 化が進み、それに伴って https を前提とする API も増えてきた。
そうした API を用いた開発をローカルで行う場合、localhost という特別なホストを用いることもできるが、それだけでは間に合わないケースも少なからずある。
localhost を https にするという方法もあるが、そのように紹介されている方法には、いくつか注意すべき点もある。
この辺りの話を、直近 1 ヶ月で 3 回くらいしたので、筆者が普段使っている方法や注意点についてまとめる。
特に推奨するつもりはない。
Update
- chrome の
--host-rules
について追記
localhost での開発の注意点
例として https://example.com
にデプロイする予定の ServiceWorker を用いたアプリがあったとする。
開発をローカルで行う場合、http://localhost:3000
などに立てたローカルサーバなどで開発を行うことが多いだろう。
筆者はそうした開発は極力しないようにしている。localhost が特殊すぎるからだ。
(この「localhost が特殊すぎる」と思ったことが無い読者や、自前でやらずに github pages や netlify などにデプロイすれば良いケースにとっては、読む必要のないことしか書いてない)
通信が外に出ないのを良いことに、ブラウザは開発向けに様々な例外を localhost に許している。例えば ServiceWorker や WebRTC(getUserMedia) といった https でしか使えない API の利用を、localhost の場合だけ http でも許すといったものだ。
localhost で開発し確認していても、実際にデプロイすると permission 周りの挙動の違いや、mixed contents の見落としなど、様々な差異に気づくという経験をした人も、少なく無いのではないだろうか?
本番との乖離があまりにも大きいため、サービスの特性や使う API によっては、 ちなみに、localhost の実態はただの hosts ファイルのエントリなので、そこを別の IP に書き換えれば、リモートのサーバを localhost で参照するといったこともできる。
(これは Windows IE 上で localhost でサーバを見たいが、サーバは Mac で動いてるなど、ブラウザを起動するマシンとコードを書くマシンを分けたい時などにやむを得ず使ったりする)
つまり localhost とは、単に多くの OS がデフォルトで hosts に書いているエントリというだけなので、sudo が必要とはいえ、厳密には localhost の解決先が 127.0.0.1 である保証は実はない。そしてやったことは無いが、hosts のエントリを消し DNS に localhost を解決させることも可能なはずだ。
そこで、様々な例外が認められた結果 Powerful Features が使い放題になった localhost が、必ず 127.0.0.1 を向くようにしようという提案もある。
とはいえ、開発上必要で /etc/hosts を書き換え、戻し忘れた経験が有る人も多いのではないだろうか。そもそも sudo が必要なところからも、hosts は頻繁に書き換えるような用途で使うものとはいえず、特に初心者がよくわからずにいじるようなファイルではない。
localhost も単なるホストなので、証明書を発行することは可能だ。つまり ただし、自前で発行した localhost の証明書はいわゆる自己署名証明書(平成ではオレオレ証明書と呼ばれていた)であるため、ブラウザはそれを信用しない。つまりエラーになる。エラーを無視して続けることもできるが、毎回エラーが出て煩わしいだけでなく、他のエラーがあっても URL バーが常に赤でわかりにくいといった問題も有る。
対策としては、その証明書を無理やり信用させる、具体的には証明書ストアに作った証明書を入れることになる。もう少し手をかけるなら、自前 CA(実態はサーバ証明書を署名するための証明書)を作り、その証明書を一回入れれば残りはその CA で署名できる。
これらが、ネットで散見される localhost の https 化の方法だろう。
localhost の特異性
http://localhost
が開発に適した環境ではない場面があり、そうした場面が増えているという実感が筆者にはある。
localhost の実態
127.0.0.1 localhost # default
203.0.113.0 localhost # localhost を別のアドレスにする
127.0.0.1 example.com # 別のドメインを loopback にする
localhost https
https://localhost
は実現でき、そうした環境を用意する方法はネット上でも散見される。
トラストアンカーとしての証明書ストア
証明書ストア(Mac では KeyChain)は OS ごとに用意されており、Firefox は OS のストアは見ずに自前で証明書ストアを持っている。(ここを忘れると Firefox で反映されずハマる)
そこには、予め審査を通った Root CA 等の証明書が並んでおり、これが PKI におけるトラストアンカーの実態だ。この証明書ストアが壊れれば、我々が https の緑を信用するための拠り所が壊れるということを意味する。したがって、hosts と同じく sudo などで変更できるとはいえ、本当は証明書ストアこそ素人がいじる場所ではない。
localhost を https にして URL バーを緑にするといった目的で、手軽に自前 CA を立てて軽い気持ちでいじるような運用が推奨されるのは、ちょっと危険ではないかと個人的には思っている。
なかには、こうした設定を自動で行ってくれる便利ツールもあるようだ。権限を与えたら証明書ストアを自動でいじるようなツールは、誰が作っていようと使用するのは怖いし、危険だろう。
個人の場合、普段の開発でカジュアルに hosts や証明書ストアをいじるよりも、実在するドメインを用意し、本物の証明書を取ってしまうのが一番安全かつ完全だ。何も変えず何も騙さず、何の危険もなく正しく動く。
例として、筆者が普段から個人的な開発や検証に使っている手元の https サーバの構成を紹介する。
まず、自分が所有する jxck.io のサブドメインとして localhost.jxck.io を登録している。これは「本物のドメイン」だ。
そして、このドメインの A レコードを 127.0.0.1 にし、変えない。(dig してもらえればわかる)
ここに対して Let's Encrypt で証明書を発行する。127.0.0.1 ではサーバ(.well-known url)での認証はできないので、DNS 認証で証明書を取る。すると、「本物の https の証明書」(すでに証明書ストアに入った証明書で検証できる証明書)が手に入る。
この証明書と鍵を、ローカルで立てるサーバが読むようにし、ブラウザに https://localhost.jxck.io:3000 と入れれば、ブラウザには本物の https 環境がローカルで手に入る。
パブリックな DNS に登録しているため、マシンが変わっても hosts を書き換える必要はない。鍵と証明書をコピーすればどのマシンでも同じ環境が再現でき、ローカルの証明書ストアをいじる必要もない。
本番と違ってこの秘密鍵と証明書はカジュアルに取り回す前提として用意するため、柔軟性があがり、例えば LocalProxy や WireShark などに鍵をそのまま指定できる。前回のブログ で使った QuicTransport サーバのようなものでも、証明書検証無視オプションを使わずにサーバが立てられたりもする。
管理もある程度気をつければ良く、仮に証明書が漏洩したところで、ドメインはループバック固定で、なにかがあっても再発行すればよく、最悪 3 ヶ月で revoke されるので、リスクも少ない。
ドメインさえ持っていれば誰にでもでき、ドメインはタダで手に入るものもあるので、この環境を作るコストはそこまで高くない。
筆者は、手元でちょっとした挙動を確認したりするときは常に使っている。
一方大きめの企業などでは、組織で管理する CA を持ち、社員に配る PC のキッティング時に、社内 CA の証明書を証明書ストアに入れて配布している場合があるだろう。その場合は組織内でイントラ向けのドメインを管理し、組織の CA で証明書を発行するといった運用になっているはずだ。こうした管理体制と運用がしっかりしており、すでに健全に回っているのであれば、その運用に従えば同じことができる。
例えば、example.com という会社ならば、社員やプロジェクトに対して 鍵は社員が作り、社内 CA で証明書を発行し、ドメインの A レコードは、組織の DNS でループバックにする。
社員は、付与された証明書を使ってローカルサーバを起動し、 注意点として、イントラでは任意のドメインを社内 DNS に載せ、証明書を出すこともできてしまうだろう。しかし、この用途で所有もしてない適当なドメインを使うべきではない(MUST)。今は取られてないドメインも将来誰かが所有する可能性があり、今はない TLD も将来登録されるかもしれない(.dev で懲りた人も多いだろう)。使うなら 予約済みドメイン (.example, .localhost, .test) について | blog.jxck.io
できれば、自社で取得しているドメインのサブドメインなどを使うのが良いだろう。
あと、こうした社内での運用は、申請の負荷や許可される範囲など様々な制限があり、大抵の企業で、大抵は面倒なんじゃないかと思われる。
組織で CA やイントラドメインを管理してない場合や、あっても申請が面倒で誰も活用してない場合でも、本物のドメインを用意すれば、前述の Let's Encrypt を用いた方法を適用することはできる。
例えば、example.com というドメインにデプロイする予定であれば、そのステージング環境用としてそのサブドメインや、似たちょっと違うドメインを用意することが多いだろう。ここでは仮に その場合は そして、開発で使う webpack-dev-server 的な何かにこの証明書を設定をしておく。最後に 新人が Wiki を見ながら手元で OpenSSL を叩いたり、KeyChain を雑にいじって証明書を入れたりするような運用よりは安全かと思い、筆者は実際にこうした運用を行ったこともある。
現場で使う上での注意点があるとすれば、Let's Encrypt は CT-Log に発行履歴が残るために、完全極秘なプロジェクトである場合は、ステージング用のドメインは関係ないものを選んだ方が良いだろう。
個人用開発環境
管理された証明書ストア
dev.intra.example.com
のようなドメインを付与するか、この目的のために localhost.intra.example.com
といったドメインを付与する。
https://{localhost|dev}.intra.example.com:3000
などにアクセスすれば、それを localhost の代わりに使うことができる。同じ社員同士なら、同じ環境がすぐ再現できるため環境構築の負荷も低い。
.test
などが自由に使える、詳細は以下を参照してほしい。
現場用開発環境
dev.example.com
とする。
dev.example.com
の証明書を Let's Encrypt で取得し、それをチームに安全に配布する方法を用意する。(Private Repo であれば、入れてしまってもいいのかもしれない)
dev.example.com
を 127.0.0.1 にしておけば、チームの誰もが本物の https://dev.example.com:3000
環境をすぐに再現できる。
開発ツールにはどこまでが許されるか?
今回は、ドメインも証明書もすべて本物を使うという振り切った運用だったが、それも Let's Encrypt のコストが低すぎるから成り立っているだけである。本来、開発用には開発用の環境がもっと手軽に用意できればそれにこしたことはない。
本来 hosts や証明書ストアなどは、管理者が重要な変更をたまに行う程度のもので、普段から頻繁にいじるようなものでは無かった。しかし、HTTPS が必須な世界となった今、そこをいじらないと成り立たない業務が出始めているという実態と、OS のモデルに乖離を感じたりもする。
そういう文脈を踏まえた上で、例えばブラウザの開発者ツールの中で、hosts に相当する名前解決や、許可リストベースでの証明書検証の無視などを、設定できたらどうかと思うこともある。
開発者ツールには、実質「閉じたら全てが無効になる」を権限の起点としており、かつ開発者が使い慣れているという点でも、sudo で永続的にいじる hosts や証明書よりよっぽど安全に運用できるだろう。
ただ、そうした文脈の外からは「そんな危ない機能が開発者ツールにあって良いのか」という反応しか期待されない。実際は、既に Local Override を始めとした様々な機能があり、残念ながら開発者ツールが掌握されれば、現状でもおおよそやりたい放題だとは思うが。
結局、開発者は正しい知識を身につけ各位工夫してやるか、http://localhost:3000
で足りるならそこで間に合わせるということになる。
追記
別件で Chrome のソースを漁っていたら host-rules
というフラグを見つけた。(コメントにあるように host-resolver-rules
でもいけるらしい)
ためしに以下のように開いてみると、確かにマッピングが指定できる。
$ google-chrome --host-rules="MAP example.com 127.0.0.1"
他にも色々な指定ができるようだ。
// For example:
// "MAP * 127.0.0.1" --> Forces all hostnames to be mapped to 127.0.0.1
// "MAP *.google.com proxy" --> Forces all google.com subdomains to be
// resolved to "proxy".
// "MAP test.com [::1]:77 --> Forces "test.com" to resolve to IPv6 loopback.
// Will also force the port of the resulting
// socket address to be 77.
// "MAP * baz, EXCLUDE www.google.com" --> Remaps everything to "baz",
// except for "www.google.com".
これなら権限もいらないが、既に開いている Chrome だと再起動が必要で面倒ではある。