ほぼ5年ぶりですが、昔の書きかけの記事を完成させておきますw
さてさて、今回はLet’s EncryptでのSSL証明書の自動更新を紹介します。
Let’s Encryptの証明書有効期限は90日なので、最初の頃は手動で更新して運用していたのですが、さすがに面倒になって自動更新するようにしました。(2028年までには45日に短縮されるようです)
Let’s Encryptはドメインの所有者であれば無料で証明書を発行できるのですが、サブドメイン方式のDynamicDNSサービス等ではドメイン所有者とならず非対応という状況でした。(当時)
なので、海外無料ドメイン+Cloud FlareのAPIで自動更新なんて手法を使っていたのですが、MyDNSさんがLet’s Encryptに対応+自動化スクリプトを公開してくれてるので、それを使った証明書の自動更新をDocker化してみました。
Let’s Encryptで証明書を取得するには、ドメインの所有者を証明する必要があるのですが、方法として主にHTTPでアクセスできる場所に取得した特定のファイルを置く方式(HTTPチャレンジ)とDNSのテキストレコードに特定の値を記述する方式(DNSチャレンジ)があります。
前者は、事前にWEBサーバーでHTTPポート(80)を開けとく必要があるのと、ワイルドカード証明書の発行ができないのでほぼほぼDNSチャレンジでやることになるかと思います。
自動化するにはまずLet’s Encryptの証明書の発行・更新処理を自動化する必要があるのですが、これは公式自動化ツールとしてCertbotというのがあるので、それを利用します。
これを普通に使うとコマンド叩いて、コンソールに表示されたトークンをDNSのTXTレコードに設定してって流れで証明書の発行・更新が簡単にできます。Certbotにはこの部分も自動化する仕組みが用意されていて、各種DNSごとにプラグインが用意されてたりします。
しかし、MyDNSのプラグインは提供されていないので、自前でフックするオプションを利用して実行する必要があります。幸いMyDNSさん提供の自動化スクリプトのREADMEにやり方は書いてあるので、それを参考にスクリプトを組んでいきます。
#!/bin/bash
#############################
# script/cert
#############################
# Domain,MasterID,Password,Mailaddress
CREDENTIAL_PATH="/usr/local/etc/mydns-credential.csv"
# callback by renew
RENEW_HOOK="reload on"
MODE=$1
TARGET=$2
DOMAIN=""
EMAIL_ADDRESS=""
usage() {
echo "Usage: cert [ new [domain] | renew | reload ]" 1>&2
exit 1
}
_init() {
if [ -z $TARGET ]; then
usage
return 1
fi
# check credential
if [ ! -f $CREDENTIAL_PATH ]; then
echo "${CREDENTIAL_PATH} Not Found."
return 1
fi
local found=false
while IFS=, read domain master_id password email_address
do
if [ $domain = $TARGET ]; then
DOMAIN=$domain
EMAIL_ADDRESS=$email_address
found=true
break
fi
done < $CREDENTIAL_PATH
if [ ! "${found}" ]; then
echo "${TARGET} Not Found."
return 1
fi
return 0
}
_new_cert() {
local auth_hook=/opt/${DOMAIN}/txtregist.php
local cleanup_hook=/opt/${DOMAIN}/txtdelete.php
certbot certonly --manual \
--preferred-challenges=dns \
--manual-auth-hook ${auth_hook} \
--manual-cleanup-hook ${cleanup_hook} \
-d ${DOMAIN} -d *.${DOMAIN} \
--server https://acme-v02.api.letsencrypt.org/directory \
--agree-tos -m ${EMAIL_ADDRESS} \
if [ $? -ne 0 ]; then
return 1
fi
return 0
}
case $MODE in
"new")
_init || exit 1
_new_cert || exit 1
;;
"renew")
certbot renew --renew-hook "${RENEW_HOOK}"
reload
;;
"reload")
reload --force
;;
*)
usage
esac
exit 0複数ドメインを利用しているので、その対応とか証明書更新時にサービスをリロードする処理を入れてるので、ちょっと長くなってますが、、
あとリロード処理はちょっとひと工夫いれてます。複数ドメインが更新された場合に毎回リロード処理が走らないようにフラグ的な処理にしてます。リロード処理のスクリプトはこんな感じで。
#!/bin/bash
#############################
# script/reload
#############################
RELOAD_FILE=/tmp/reload
RELOAD_COMMAND=/usr/local/sbin/reload-command
_reload_on() {
if [ ! -f ${RELOAD_FILE} ]; then
touch ${RELOAD_FILE}
fi
}
_reload_off() {
if [ -f ${RELOAD_FILE} ]; then
rm ${RELOAD_FILE}
fi
}
_reload() {
if [ -f ${RELOAD_FILE} ]; then
sh -x ${RELOAD_COMMAND}
if [ $? -eq 0 ]; then
_reload_off
fi
fi
}
case $1 in
"on")
_reload_on
;;
"off")
_reload_off
;;
"--force")
_reload_on
_reload
;;
*)
_reload
esac
exit 0RELOAD_COMMANDには各種Daemonのリロード処理(systemctl reload nginxとか)か今回はDockerコンテナで実行するので呼び出し元にそれとわかる文字列を出力するとかすれば良いかと思います。Jenkinsならコンソール出力を文字列検索してリロード処理を後処理でやるとか。
さて、スクリプトの用意はできたので、あとはDockerfileの作成です。作成したのがだいぶ昔なので、PHP7.4のイメージ使ってますが、多分8系にしても動くとは思いますがMyDNSさんの自動化スクリプト自体も更新が古いので、自己責任でお願いします。
FROM php:7.4-cli
# Install some needed packages
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
certbot \
wget \
unzip
RUN wget 'https://github.com/disco-v8/DirectEdit/archive/master.zip' -O /tmp/master.zip
COPY ./entrypoint.sh /
COPY ./script /usr/local/sbin
COPY ./conf /usr/local/etc
RUN chmod +x /usr/local/sbin/* \
&& chmod 600 /usr/local/etc/* \
&& ln -s /usr/local/bin/php /usr/bin/php
ENTRYPOINT ["sh", "/entrypoint.sh"]
CMD ["cert"]
VOLUME ["/etc/letsencrypt"]MyDNSさんの自動化スクリプトはPHP製なので、php-cliのイメージを利用します。rootユーザで実行されるので、/usr/binにphpのシンボリックリンクを張ってます。先ほど作成したスクリプトは/script、DNS情報のクレデンシャルファイル(mydns-credential.csv)は/confに用意してるので、それぞれ所定のパスに置きます。証明書とかは/etc/letsencryptに作成されるので、volumeとして定義しておきます。
エントリポイントのスクリプトは複数ドメイン対応するためにドメインごとにMyDNSさんの自動化スクリプトが必要(正確には設定ファイル)なので、その処理を入れます。CMDは先ほど作成したスクリプトを呼ぶだけのをデフォで定義しておきます。entrypoint.shはこんな感じです。
#!/bin/bash
#############################
# entrypoint.sh
#############################
TEMPLATE_ARCHIVE=/tmp/master.zip
CREDENTIAL_PATH="/usr/local/etc/mydns-credential.csv"
# check credential && archive
if [ -f ${CREDENTIAL_PATH} ] && [ -f ${TEMPLATE_ARCHIVE} ]; then
while IFS=, read domain master_id password email_address
do
domain_dir=/opt/${domain}
config_file=${domain_dir}/txtedit.conf
if [ ! -d ${domain_dir} ]; then
mkdir -p ${domain_dir}
unzip -qq -j -d ${domain_dir} ${TEMPLATE_ARCHIVE}
chmod +x ${domain_dir}/*.php
# create txtedit.conf
sed -i -e "s/yourmasterid/${master_id}/" \
-e "s/yourpasswd/${password}/" \
-e "s/yourdomain/${domain}/" \
${config_file}
chmod 600 ${config_file}
fi
done < $CREDENTIAL_PATH
fi
exec "$@"ドメイン情報を定義したクレデンシャルファイル(CREDENTIAL_PATH)を見てドメインごとにフォルダ分けと設定ファイルの更新処理を入れてます。
あとはDocker buildして下記のコマンドをJenkinsなどで定期実行すれば完全自動化が完成です。
docker run –rm -v <ホストのパス>:/etc/letsencrypt <イメージ名> cert renew
年々、証明書の更新間隔は短くなっていくので、証明書の自動更新は必須になっていくと思います。自動化しておけば実行スケジュールの調整だけで済むようになるので、ぜひ自動化に取り組んでみてください。
あと、くれぐれもDNSのクレデンシャル情報を含んだ設定ファイルは漏洩しないようにしてください。漏洩したらドメイン乗っ取られますw

コメント