Let's encrypt のサーバー証明書を更新する
Let's Encrypt さんで取得したサーバー証明書を更新しようというおはなし。基本的に Let's Encrypt さんから発行される証明書の有効期限は 3か月なので、細かいサイクルで更新していく必要があります。
更新のやり方を考える
前回は初回だったので nginx とか普通に停止して実行しましたが、今回は既に稼働している https なり smtps, imaps なりが対象なので、ダウンタイムを極力短くしたいです。
また、そもそも 443 で稼働している https 用の証明書と、通常は 443 なんぞ開けていない smtps や imaps 用の証明書を一括で renew
するのはちょっとムリがある1)な……と。
standalone と webroot
https 用の証明書を更新する時は、稼働中の nginx に 443 を掴ませたまま、nginx を停止せずに更新できるように、--webroot
オプションを使用します。
他方、443 を掴んでいない smtps と imaps 用の証明書を更新する時は、--standalone
オプションで certbot に 443 を掴んで貰えばうまくいきそうです。
https 用証明書の更新
こんなコマンドを投げてあげればうまくいきそうです。
certbot certonly --non-interactive --force-renewal --webroot --webroot-path /path/to/htdocs -d manimani.cc -d www.manimani.cc
--standalone
を指定するとドメイン所有者確認2)用に certbot が 443 を掴みにくるため、既存の nginx なりは停止しておく必要がありますが、--webroot
を指定することで certbot ではなく既存の nginx なりを使用してドメイン所有者確認を実行できるようです。
--webroot-path
は、ドメイン所有者確認のためのファイル一式を展開する場所を指定します。let's encrypt 側から https://manimani.cc/.well-known/acme-challenge/
配下のファイルにアクセスが来るため、対応するパスを設定しておきます。要はドキュメントルートですね。
--non-interactive
は、一切の確認を表示せずに全自動で流れるようにします。そうすると、全ての選択肢が “より安全” な方向に振れてしまうので、“証明書の更新が必要になるまでは常に既存の証明書を保つ”……要はある程度有効期限が迫ってこないと、更新してくれなくなってしまいます。どの程度をもって “必要” と判断してくれるのかは調べてないのですが、これだとスケジュール処理で余裕をもって更新したい場合に困ります。なので、ここは --force-renewal
オプションを付けて強制更新することで回避します。
試しにやってみるには --dry-run
オプションを付ければ良いです。
Saving debug log to /var/log/letsencrypt/letsencrypt.log Starting new HTTPS connection (1): acme-staging.api.letsencrypt.org Cert not due for renewal, but simulating renewal for dry run Renewing an existing certificate Performing the following challenges: http-01 challenge for manimani.cc http-01 challenge for www.manimani.cc Using the webroot path /path/to/htdocs for all unmatched domains. Waiting for verification... Cleaning up challenges Generating key (2048 bits): /etc/letsencrypt/keys/0006_key-certbot.pem Creating CSR: /etc/letsencrypt/csr/0006_csr-certbot.pem IMPORTANT NOTES: - The dry run was successful.
こんな感じですね……なんですが、どうもこの --dry-run
オプションって、--dry-run
を指定しなかった時と結構挙動が異なるようです。テスト実行でコケても、本番の時にはすんなり通るとかあるので、あまり --dry-run
で成功させることに固執しないほうが良さそうですね。
smtps, imaps用証明書の更新
「メールサーバーに紐付けている ip アドレスの port:443 は使っていないから certbot の standalone
モードで掴めるだろう」とか思っていた時期も(ry
たぶん certbot は 0.0.0.0:443
を掴みにくるんだと思います。なので、たとえ ip が違っていてもそのサーバー上で 1つでも 443 ポートにバインドしているサーバーがあると、certbot は異常終了します。
Saving debug log to /var/log/letsencrypt/letsencrypt.log Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org Renewing an existing certificate Performing the following challenges: tls-sni-01 challenge for mail.manimani.cc Cleaning up challenges Could not bind TCP port 443 because it is already in use by another process on this system (such as a web server). Please stop the program in question and then try again.
nginx で 443 を listen する
できるだけダウンタイムを短くしたいので、稼働している nginx を落とすよりは、稼働している nginx に一時的にメールサーバーの ip で 443 を listen してもらった方がよさそうです。
完成
というわけで、こんな shell スクリプトを作って systemd の timer に登録しました。
- certrenew.sh
#!/bin/bash # # Update certificates. # . /path/to/tools/utils.sh CERTBOT_CMD='/usr/bin/certbot' SYSTEMCTL_CMD='/usr/bin/systemctl' CERTRENEW_DIR='/path/to/tools/certrenew.d' NGINX_CONF_DIR='/etc/nginx/conf.d' OPTS_LIST='certrenew.list' NGINX_CONF='certrenew.conf' if [ ! -f "${CERTBOT_CMD}" ] || [ ! -x "${CERTBOT_CMD}" ]; then error "Command not found or not executable: ${CERTBOT_CMD}" exit 1 fi if [ ! -f "${SYSTEMCTL_CMD}" ] || [ ! -x "${SYSTEMCTL_CMD}" ]; then error "Command not found or not executable: ${SYSTEMCTL_CMD}" exit 1 fi if [ ! -f "${CERTRENEW_DIR}/${OPTS_LIST}" ] || [ ! -r "${CERTRENEW_DIR}/${OPTS_LIST}" ]; then error "List not found or not readable: ${CERTRENEW_DIR}/${OPTS_LIST}" exit 2 fi if [ ! -f "${CERTRENEW_DIR}/${NGINX_CONF}" ] || [ ! -r "${CERTRENEW_DIR}/${NGINX_CONF}" ]; then error "File not found or not readable: ${CERTRENEW_DIR}/${NGINX_CONF}" exit 2 fi if [ ! -d "${NGINX_CONF_DIR}" ] || [ ! -w "${NGINX_CONF_DIR}" ]; then error "Directory not found or not writable: ${NGINX_CONF_DIR}" exit 2 fi # copy special nginx conf. /bin/cp ${CERTRENEW_DIR}/${NGINX_CONF} ${NGINX_CONF_DIR}/${NGINX_CONF} if [ $? -ne 0 ]; then error "Can't copy nginx config file." exit 3 fi # restart nginx ${SYSTEMCTL_CMD} restart nginx if [ $? -ne 0 ]; then error "Failed to restart nginx: $!" exit 4 fi # remove special nginx conf. /bin/rm -f ${NGINX_CONF_DIR}/${NGINX_CONF} if [ $? -ne 0 ]; then warn "Failed to remove nginx config: $!" fi info "Start update certificates." while read OPTS; do # skip blank line. if [[ "${OPTS}" =~ ^\s*$ ]]; then continue fi # skip comment line. if [[ "${OPTS}" =~ ^\s*# ]]; then continue fi # run certbot ${CERTBOT_CMD} certonly ${OPTS} if [ $? -ne 0 ]; then error "Failed to update certificate: ${OPTS}: $!" exit 3 fi info "Succeed to update certificate: ${OPTS}" done < ${CERTRENEW_DIR}/${OPTS_LIST} ${SYSTEMCTL_CMD} restart nginx if [ $? -ne 0 ]; then error "Failed to restart nginx: $!" exit 4 fi info "Succeed to update all certificates." exit 0
この shell スクリプトが読み込む certrenew.list はこんな感じに、certbot のコマンドラインオプションを列記してあります。
--non-interactive --force-renewal --webroot --webroot-path /path/to/htdocs -d manimani.cc -d www.manimani.cc --non-interactive --force-renewal --webroot --webroot-path /path/to/htdocs -d tekkadon.manimani.cc --non-interactive --force-renewal --webroot --webroot-path /path/to/htdocs -d mail.manimani.cc
いまんところ証明書を 3つ持っているので、こんな感じですね。
そして nginx の conf.d に放り込まれる certrenew.conf には、一時的に 443 を開けるバーチャルサーバーの設定が書いてあります。
server { listen 203.143.127.236:443; server_name mail.manimani.cc; root /path/to/htdocs; ssl on; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_certificate /path/to/mail.manimani.cc/fullchain.pem; ssl_certificate_key /path/to/ssl/mail.manimani.cc/privkey.pem; ssl_dhparam /path/to/dhparam.pem; ssl_session_timeout 2h; ssl_session_cache shared:SSL443:50m; ssl_session_tickets on; ssl_prefer_server_ciphers on; keepalive_timeout 70; sendfile on; client_max_body_size 0; access_log /path/to/access_log main; error_log /path/to/error_log info; }
こんな感じで、2か月に1回ペースで叩いておけば良いかなと。