30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Let's Encrypt と Route 53 でローカル開発環境を HTTPS 化する

ブラウザの機能のなかには、HTTPS でないと利用できなかったり、HTTPS か HTTP かで挙動が変わったりする機能がある。
そのため、ローカル開発環境を HTTPS で構築したいことがある。

一番簡単なのは自己署名証明書を作成し利用することだと思うが、その場合ブラウザが警告を出すため、利便性の点で難がある。

以下の記事では、Let's Encrypt で取得した正規の証明書を使って、ローカル開発環境を HTTPS 化している。
これなら、ブラウザが警告を出すことはない。

blog.jxck.io

勉強がてら、この内容を実践してみた。

この方法を試すためには、自由に使えるドメインを所有している必要がある。
既にnumb86.netというドメインを所有していたので、これを利用してlocalhost.numb86.netというドメインで開発環境を作っていく。

A レコードの値として 127.0.0.1 を設定する

127.0.0.1はループバックアドレスという特殊な IP アドレスで、自分自身を指す。

例えば、以下のコードを Deno で実行してhttp://127.0.0.1:8080/にアクセスすると、Hello Deno.と表示される。
なお、Deno のバージョンは1.2.2

import {
  listenAndServe,
} from "https://deno.land/std@0.63.0/http/mod.ts";

listenAndServe({ port: 8080 }, (req) => {
  req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "text/plain",
    }),
    body: "Hello Deno.\n",
  });
});

console.log("Server running on localhost:8080");

この127.0.0.1localhost.numb86.netと紐付けることで、localhost.numb86.netへのアクセスがローカル開発環境に対するアクセスとなるようにする。
元々ネームサーバとして Route 53 を利用していたので、その管理画面で設定を行う。

f:id:numb_86:20200804222729p:plain

digコマンドで、正しく設定されていることを確認する。

$ dig localhost.numb86.net a +short
127.0.0.1

これで、http://localhost.numb86.net:8080/にアクセスしてもHello Deno.が表示されるようになった。

次は、localhost.numb86.netの証明書を Let's Encrypt で取得する。

DNS 認証による証明書の取得

Let's Encrypt は ACME(Automatic Certificate Management Environment)というプロトコルを利用しており、そのプロトコルで定義されている「チャレンジ」によって、申請の正当性をチェックしている。
今回の例で言えば、証明書を取得しようとしている私が本当にlocalhost.numb86.netの管理者であるかを確認するために、チャレンジが行われる。

チャレンジにはいくつか種類があるが、今回のようなケースでは「DNS-01」というチャレンジを使う。

まず、certbotというツールをインストールする。

$ brew install certbot
$ certbot --version
certbot 1.6.0

certbotは ACME のクライアントのひとつ。

以下のコマンドで、チャレンジを行う。

$ sudo certbot certonly --manual -d localhost.numb86.net --preferred-challenges dns-01

いくつか質問に答えていくと、以下のように表示される。

Please deploy a DNS TXT record under the name
_acme-challenge.localhost.numb86.net with the following value:

XXX

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

XXXの部分にトークンが表示される。
それを、_acme-challenge.localhost.numb86.netの TXT レコードの値として設定する。
表示されている通り、TXT レコードの設定が終わるまではエンターキーは押さない。

これも Route 53 で設定する。

f:id:numb_86:20200804222743p:plain

設定が反映されているか、digコマンドで確認する。トークンが表示されれば、設定は完了している。

$ dig _acme-challenge.localhost.numb86.net txt +short
"XXX"

設定が完了した状態でエンターキーを押すと、チャレンジが完了し、証明書が手に入る。

/etc/letsencrypt/live/localhost.numb86.net/にインストールされている。
cert.pemが証明書で、privkey.pemが秘密鍵。
このディレクトリにアクセスするには権限が必要だったので、sudo surootに切り替えて操作した。

また、ファイルのパーミッションを変えておかないと取り扱いづらいので、以下のコマンドも実行した。

$ sudo chmod 664 privkey.pem

Deno で HTTPS サーバを立てる

証明書が手に入ったので、それを使って HTTPS サーバを立てる。

以下のコードを Deno で実行してhttps://localhost.numb86.net:8443/にアクセスすると、Hello Secure Deno!と表示される。

import {
  listenAndServeTLS,
} from "https://deno.land/std@0.63.0/http/mod.ts";

listenAndServeTLS(
  { port: 8443, certFile: "./cert.pem", keyFile: "./privkey.pem" },
  (req) => {
    req.respond({
      status: 200,
      headers: new Headers({
        "content-type": "text/plain",
      }),
      body: "Hello Secure Deno!\n",
    });
  },
);

console.log("Server running on localhost:8443");

自己署名証明書ではないので、警告も出ない。

f:id:numb_86:20200804222654p:plain

webpack-dev-server で HTTPS サーバを立てる

この環境を使って、webpack-dev-server で HTTPS サーバを立てることもできる。

以下のバージョンで動作確認をした。

  • webpack@4.44.1
  • webpack-cli@3.3.12
  • webpack-dev-server@3.11.0

以下のファイルと、上記の秘密鍵、証明書を用意した上で$ yarn run webpack-dev-server --mode=developmentを実行すると、https://localhost.numb86.net:8443/Hello webpack!と表示される。

<!-- ./dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>webpack</title>
</head>
<body>
<div id="app"></div>
<script src="./index.js"></script>
</body>
</html>
// ./src/index.js
const elem = document.querySelector('#app');
elem.textContent = 'Hello webpack!';
// ./webpack.config.js
const path = require('path');
const fs = require('fs');

module.exports = () => {
  return {
    entry: {
      index: './src/index.js',
    },
    output: {
      path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
      contentBase: path.resolve(__dirname, 'dist'),
      host: 'localhost.numb86.net',
      port: 8443,
      https: true,
      key: fs.readFileSync('./privkey.pem'),
      cert: fs.readFileSync('./cert.pem')
    },
  }
};