Nodejs(Express) で Let’s Encrypt の証明書を使う(2)。Certbot をmanual オプションの実行
こんにちは!
週末プログラミングでNodejsのWebサービスを作っているのですが、なんだかんだとなかなか時間が取れないコロナ禍の週末プログラマー、しずかなかずしです。
NodejsでHTTPSサーバーを立てませんか?
独自にNodejsのサーバー構築をせずとも、herokuなどのJavascriptのホスティングサービスを使えば独自のサービスを開始できます。昔を知っているものとしてはいい時代になったものだと思います。
しかし、私のようにちょっと特殊な用途のサービスを立ち上げようとすると、こういう便利機能を使って達成することができないようです。私の週末プログラミングで立ち上げたのはこちらのサービスです。
そんなさなか、Let’s EncryptのSSL証明書の自動更新が失敗していることに気づきました。本日は、その奮闘記から得られた無料のSSL証明書のLet’s EncryptのHow-toを記事にしてみました。
Let’s Encryptとは?
Let’s Encryptは無料のHTTPSの証明書発行サービスです。サーバーのドメイン名はお金を払って取得する必要がありますが、サーバーをセキュアなHTTPSのサーバーにするの為のお金は節約できます。
以前のしずかなかずしブログの記事で、certbotというコマンドラインプログラムを使ってSSL証明書の取得をしました。certbotはLet’s Encrypt のSSL証明書を自動で取得しLinuxシステム上の適切な位置に保存してくれる、という便利なツールです。
以下のリンクの記事では、Nodejsサーバーで使う際のかんたんなサンプルプログラムもご紹介していますので、あわせてご覧ください。
certbotのstandaloneオプションの問題
上でご紹介した記事はcertbotのオプションでstandaloneを指定する方法でした。しかし、この方法では不便なケースがあります。というのも、standaloneはSSL証明書の取得のために自分自身でサーバーマシンのHTTPプロトコルのポート(80番)にサーバーを立てます。言い換えると、他のプログラムが80番を使っていない事を前提にしています。つまり、サービスが稼働中のマシンがHTTPプロトコルを使ってサービスをしている場合は、SSL証明書の取得の際に停止させる必要があります。Let’s EncryptのSSL証明書は3ヶ月で切れるので、そのたびに停止が必要ということになってしまいます。
certbotで一度運用を開始したサーバーは、
まずは、Certbotのstandaloneの仕組みからご説明します。
certbotはSSL証明書を設置したいマシンで実行します。cerbotを実行するとLet’s Encryptのサーバーから、certbotを実行したマシンの80番のポートに対してアクセスが来ます。standaloneオプションつきでcertbotを実行する場合、このリクエストを受けるためにcertbot自身がHTTPサーバーを起動して待ち受けます。このコミュニケーションがうまくいけば、certbotが正しく証明書をマシンに設定してくれる、という流れです。
このプロトコルはHTTP-01チャレンジと呼ばれています。HTTP-01チャレンジは80番ポートであることが必須になっています。セキュリティを担保するための仕様だそうです。プロトコルの詳細は以下の記事をみるとわかります。
さて、自分のサーバーでセキュアなHTTPSのサーバーを立てるとなるとプロトコルのデフォルトポートは443番です。ですので、443番のポートでは自分のサービスをhttps://example.comのように準備します。
このとき、80番ポート、つまり、http://example.comはどうしているでしょう?
セキュアではないHTTPは使わないので、サーバーは存在しない状態にしますか?
もし、そうしていれば、Let’s Encryptの定期的な証明書の更新のために80番ポートを使わない状態にしておけば良いでしょう。次回のcerbot renewでも新しい証明書の取得は可能です。
しかし、このマシンで運用されるサービスのユーザーにとってよりありがたいのは、HTTPでアクセスしても、HTTPSへredirectしてくれることではないでしょうか。そうすると、もはや80番ポートはcertbot standaloneのために空けておくことはできません。証明書更新の度に運営中のサービスを停止させる必要が出てきてしまいます。
私の週末プログラミングのサービスのSSL証明書の自動取得が失敗していた理由もまさにこれ。HTTPポート(80番)が空いていてイマイチだと思い、certbot standaloneの自動更新の仕組みをすっかり失念して、HTTP→HTTPSのリダイレクトを実装していたのでした。
幸いな事に、cerbotのオプションで、下記のように更新時のSSL証明書取得の前と後で実行するプログラムを指定することができます。(以下の例だと、最初に’service nginx stop’が実行され、SSL証明書取得後に’service nginx start’が実行される)。
certbot renew --pre-hook "service nginx stop" --post-hook "service nginx start"
ですので、更新のタイミングで必要なコマンドは上記のように実行できるので、更新プロセスをcronにより完全自動化できる訳です。
certbotのmanualオプションを試す
もう少し踏み込むと、standaloneというオプションではなく、manualというオプションがあるではないですか。
これは、certbotのオプションでmanualしていすると、チャレンジ・レスポンスの過程を横取りする方法です。これをすることで、certbotのサーバーではなく、自分が普段立ち上げているサーバー上でSSL証明書取得の過程を担うことができます。要は、HTTP-01チャレンジの一部を実装する、というイメージです。
ApacheやnginxのようなWebサーバー向けにはcertbotのプラグインが用意されているのですが、Nodejs + Expressだと自分で実装する必要があります。ただし、「実装する」といっても大した話ではありません。certbotプログラムに指定されたファイルをHTTPサーバー上で提供すればよいだけです。
サーバーのドメイン名が、example.comだとすると、以下のようなstaticファイルの置き場を作ります。
http://example.com/.well-known/acme-challenge/xxxxxx
例えば、Expressのgenerator (express-generator)でサーバープログラムを生成すると、以下のようなコードが生成され、’public’ディレクトリが、staticファイル置き場になります。
app.use(express.static(path.join(__dirname, 'public')));
ですので、publicディレクトリ以下に、.well-knownというディレクトリを作成し、その中にさらにacme-challengeというディレクトリを作っておきます。
さらに、express.staticがドットから始まるディレクトリもサーブするようにする為に、上記のコードをちょっと書き換えておきます。
app.use(express.static(path.join(__dirname, 'public'), {dotfiles: 'allow'}));
これで、サーバーのファイルシステム上でacme-challengeディレクトリにxxxxxxというファイルを作って、ブラウザからhttp://example.com/.well-known/acme-challenge/xxxxxxにアクセスすると、Expressのサーバーはxxxxxxファイルの中身を返す状態になりました。
certbotコマンドの実行と手順
それでは、manual オプション付きでcertbotを実行してみます。
$ sudo certbot certonly --manual
以下のようなメッセージが出ます。
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Please enter in your domain name(s) (comma and/or space separated) (Enter 'c'
to cancel):
ここでHTTPSで立ち上げるサーバーのドメイン名を入れます。上記の例だとexample.comのように入力してEnterキーを押します。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.
Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
IPアドレスがログに取られますけどいいですか?と言われ、仕方なくYを実行します。
すると、以下のように特定の文字列だけが入ったファイルを用意せよ、と言われます。
Create a file containing just this data:
AAA-BBB-CCC
And make it available on your web server at this URL:
http://example.com/.well-known/acme-challenge/XXX-YYY-ZZZ
AAA-BBB-CCCやXXX-YYY-ZZZはその都度異なりますので、指定の文字列を使う必要があります。
別のターミナルで先程作った、public/.well-known/acme-challengeディレクトリに移動します。
$ cat > XXX-YYY-ZZZ
のように実行し、次の行にAAA-BBB-CCCをペーストしてCtrl-Dを押しします。
すると、XXX-YYY-ZZZファイルの中にAAA-BBB-CCCが保存された状態でファイルができます。 中身を念の為確認してみましょう。’>’なしで以下を実行してAAA-BBB-CCCが表示されればOK。
$ cat XXX-YYY-ZZZ
AAA-BBB-CCC
ここまでできたら、certbotを実行したターミナルに戻り、Enterを押します。
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/example.com/privkey.pem
Your cert will expire on YYYY-MM-DD. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"
- If you like Certbot, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le
Congratulations!が表示されればうまく行きました。いつ期限切れになるか(YYYY-MM-DDの部分)に加えて、寄付してみてね、というメッセージがURLとともに表示されます。
ここまでできれば、以前の記事書いてあるサンプルプログラムにあるようにNodejs + Expressのプログラムで読み込めばOK。
と、ここまで書いて何なのですが…
サービスを止めずに証明書を更新できても、証明書をExpressのプログラムが再読込するには立ち上げ直しが必要ですね。もう少し自動化の仕組みを考えてみます。