Let’s EncryptのSSL証明書をDocker使って自動更新

Docker

ほぼ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 0

RELOAD_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

コメント

タイトルとURLをコピーしました