Lightsail で Let’s Encrypt の ワイルドカードSSL 証明書を自動更新するようにしてみた

Amazon Lightsail で Let’s Encrypt のワイルドカード SSL 証明書を自動で更新出来るようにしてみました。

いきなし結論

結論から言うと以下のコマンドで実現することが出来ました。Amazon Lightsail のドキュメント ではワイルドカード証明書を取得しており、ワイルドカード証明書の更新には一手間必要でした。

$ cat /etc/cron.d/root
00 03 01 * *  root certbot renew \
--manual --preferred-challenges dns \
--manual-public-ip-logging-ok \
--manual-auth-hook certbot-dns-auth.sh \
--manual-cleanup-hook certbot-dns-clean.sh \
--post-hook '/opt/bitnami/ctlscript.sh restart' \
>> /var/log/certbot 2>&1

manual-auth-hook と manual-cleanup-hook で指定しているシェルスクリプトは以下の通りです。

$ cat /usr/bin/certbot-dns-auth.sh
#!/bin/bash

export AWS_ACCESS_KEY_ID=*******************
export AWS_SECRET_ACCESS_KEY=*******************

echo '{"name": "_acme-challenge.'${CERTBOT_DOMAIN}'","target": "\"'${CERTBOT_VALIDATION}'\"","type": "TXT"}' | jq . > domain-entry.json 

/usr/local/bin/aws lightsail create-domain-entry \
  --domain-name ${CERTBOT_DOMAIN} \
  --domain-entry file://domain-entry.json \
  --region us-east-1
$ cat /usr/bin/certbot-dns-clean.sh
#!/bin/bash

export AWS_ACCESS_KEY_ID=*******************
export AWS_SECRET_ACCESS_KEY=*******************

echo '{"name": "_acme-challenge.'${CERTBOT_DOMAIN}'","target": "\"'${CERTBOT_VALIDATION}'\"","type": "TXT"}' | jq . > domain-entry.json

/usr/local/bin/aws lightsail delete-domain-entry \
  --domain-name ${CERTBOT_DOMAIN} \
  --domain-entry file://domain-entry.json \
  --region us-east-1

やってみたこと

Let’s Encrypt のワイルドカード SSL 証明書を更新するには DNS 認証が必要です。

なお、ワイルドカード証明書の取得の際には、必ず DNS-01 Challenge を使用した認証を行う必要があります。これは、ドメイン名の管理権を有していることの証明のために、DNS における TXT レコードを変更する必要があることを意味しています。

https://free-ssl.jp/blog/2018-03-14.html

通常であれば以下のようなコマンドを cron などで実行することで自動で更新することが出来るが、ワイルドカード証明書の場合は、初回取得時と同じように DNS レコードの登録の一手間が必要になる。

certbot renew –pre-hook “service nginx stop" –post-hook “service nginx start"

https://free-ssl.jp/docs/using.html#renew-subcommand

初回取得時は対話モードでレコードの値を指定され登録したが、それをどうやって自動にするかと言うと、以下のような更新時にレコードの値を受け取って処理を走らせる超便利なオプションが用意されているのでそちらを使ってみます。

Certbot allows for the specification of pre and post validation hooks when run in manual mode. The flags to specify these scripts are --manual-auth-hook and --manual-cleanup-hook respectively and can be used as follows:

https://certbot.eff.org/docs/using.html#pre-and-post-validation-hooks

当ドメイン doc-sin.life は Lightsail の DNS で管理しているので aws cli を使って DNS レコードを作成することにしてみます。

aws cli のインストール

Lightsail の wordpress のインスタンスに aws cli がインストールされていなかったのでまずは aws cli をインストールします。インストールは AWS のドキュメントを参考に進めます。

$ curl -O https://bootstrap.pypa.io/get-pip.py
$ sudo python3 get-pip.py
$ pip3 --version
pip 19.1.1 from /usr/local/lib/python3.5/dist-packages/pip (python 3.5)
$ sudo pip3 install awscli --upgrade
$ aws --version
aws-cli/1.16.153 Python/3.5.2 Linux/4.4.0-1074-aws botocore/1.12.143

IAMUser の作成

Lightsail のインスタンスには IAMRole をアタッチ出来ないので、DNS 認証に必要な Lightsail の DNS レコードを作成出来る IAMUser を作成します。ポリシーは以下の通り最小限に留めておきます。対象リソースのアカウント ID とドメインの ID は置き換えるか、" * " でも良いと思います。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lightsail:DeleteDomainEntry",
                "lightsail:CreateDomainEntry"
            ],
            "Resource": "arn:aws:lightsail:us-east-1:${ACCOUNT_ID}:Domain/${DOMAIN_ID}"
        }
    ]
}

シェルスクリプトの作成

上で紹介した manual-auth-hook と manual-cleanup-hook で実行するシェルスクリプトを作成します。シェルスクリプト内で、aws lightsail delete-domain-entry のオプションの domain-entry の値を一度 JSON ファイルに出力していますが、これは何故かと言うと Shorthand 形式では TXT レコードがどうにもこうにも出来なかったからです。A レコードの場合は場合は問題ないのですが、TXT レコードの場合以下のようなエラーが出て作れませんでした。

$ /usr/local/bin/aws lightsail delete-domain-entry --domain-name doc-sin.life --domain-entry name=txt.doc-sin.life,target=abc123,type=TXT --region us-east-1

An error occurred (InvalidInputException) when calling the DeleteDomainEntry operation: Input error: Target should be enclosed in quotation marks: abc123.

クォーテーションで囲めって言っているのは分かるのですが、いろいろ試行錯誤してのですが駄目だったので今の形になりました。

手動実行してみる

材料は揃ったので手動実行をしてみる。通常 Let’sEncrypt は証明書の期限が1ヶ月以上残っていると更新してくれないので –force-renewal をつけて強制的に更新しています。

$ sudo certbot renew --manual --preferred-challenges dns --manual-public-ip-logging-ok --manual-auth-hook certbot-dns-auth.sh --manual-cleanup-hook certbot-dns-clean.sh --post-hook '/opt/bitnami/ctlscript.sh restart' --force-renewal
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/doc-sin.life.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Plugins selected: Authenticator manual, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/doc-sin.life/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/doc-sin.life/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Running post-hook command: /opt/bitnami/ctlscript.sh restart
Output from ctlscript.sh:
/opt/bitnami/apache2/scripts/ctl.sh : httpd stopped
/opt/bitnami/php/scripts/ctl.sh : php-fpm stopped
/opt/bitnami/mysql/scripts/ctl.sh : mysql stopped
/opt/bitnami/mysql/scripts/ctl.sh : mysql  started at port 3306
/opt/bitnami/php/scripts/ctl.sh : php-fpm started
/opt/bitnami/apache2/scripts/ctl.sh : httpd started at port 80

Error output from ctlscript.sh:
Syntax OK
Syntax OK

問題なく更新出来たので、今実行したもを cron に登録し月イチで実行させて完了です。