Let's EncryptとMyDNSでワイルドカードでマルチドメイン(含サブドメイン)な証明書を発行して自動更新する。apacheも。


はじめに

Let's Encryptを初めて使ってみました。
CNをホスト名にしてSANだけだと、とある場所で「CNが一致しません!」となってしまったので、その対策が必要でした。
1枚で使いまわし証明書が発行できたので、記録します。

このエントリを読むとできること

  • CentOS7のapacheで使うための
  • Let's Encryptの証明書を
  • MyDNSを使って
  • ワイルドカード かつ マルチドメインで発行して
  • 自動的に更新する

ことが、できる。はず。

環境など

対象機器および環境

  • CentOS7(7.7.1908)
  • certbot(1.0.0)
  • php(5.4.16)

こんな証明書が欲しいことがあったとしたら

私は example.com というドメインで検証環境を構築しています。
その配下に sub.example.com というサブドメインがあります。
その環境のサーバたちで、1枚の証明書を使いまわしたいと思いました。
なので、こんな証明書が欲しい、ということになります。

項目
CN *.example.com
SAN *.example.com
SAN *.sub.example.com
SAN example.com

MyDNSのアカウント情報

ドメイン
ドメイン名 example.com
マスターID mydns123456
パスワード mydnspassword
サブドメイン
サブドメイン名 sub.example.com
マスターID mydns654321
パスワード subdompassword

実装

フック用のスクリプト

MyDNSの https://github.com/disco-v8/DirectEdit があるのですが、サブドメインも含めては管理できないので、
これを参考にして、以下のものを作成しました。これを使って説明します。
Let's Encrypt MyDNS Hook Script

準備

yum -y install epel-release
yum -y update
yum -y install php php-mbstring certbot python2-certbot-apache mod_ssl
cd /root/
git clone https://github.com/bashaway/le_mydns_hook

./le_mydns_hook/accounts.conf にMyDNSのアカウント情報を記載します。

vi ./le_mydns_hook/accounts.conf
----------8<-----(snip)-----8<----------
$MYDNS_ID['ドメイン名']  = 'マスターID';
$MYDNS_PWD['ドメイン名'] = 'パスワード';
----------8<-----(snip)-----8<----------

例えば、上記の例の場合 accounts.confは以下のように修正します。

----------8<-----(snip)-----8<----------
$MYDNS_ID['example.com']  = 'mydns123456';
$MYDNS_PWD['example.com'] = 'mydnspassword';
$MYDNS_ID['sub.example.com']  = 'mydns654321';
$MYDNS_PWD['sub.example.com'] = 'subdompassword';
----------8<-----(snip)-----8<----------

証明書発行

一度作業すると、certbot renwe で引数が引き継がれるぽいので、自前フックはフルパスで指定しておく。

certbot certonly --manual \
 --server https://acme-v02.api.letsencrypt.org/directory \
 --preferred-challenges dns-01 \
 --agree-tos --no-eff-email \
 --manual-public-ip-logging-ok \
 --manual-auth-hook /root/le_mydns_hook/regist.php \
 --manual-cleanup-hook /root/le_mydns_hook/delete.php \
 -m [email protected] \
 -d *.example.com \
 -d *.sub.example.com \
 -d example.com 

確認してみます(下はステージングで発行したので、IssuerがFakeになっています)

# openssl x509 -in /etc/letsencrypt/archive/example.com/cert1.pem -text | egrep "CN|DNS"
        Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3
        Subject: CN=*.example.com
                DNS:*.sub.example.com, DNS:*.example.com, DNS:example.com

apacheの設定

firewallの設定

firewall-cmd --add-service https --zone=public --permanent
firewall-cmd --reload

既存で設定されている箇所は、Let's Encryptで発行された証明書におきかえます。

/etc/httpd/conf.d/ssl.conf
#SSLCertificateFile /etc/pki/tls/certs/localhost.crt
SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem

#SSLCertificateKeyFile /etc/pki/tls/private/localhost.key
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

#SSLCertificateChainFile /etc/pki/tls/certs/server-chain.crt
SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem

httpdに設定を読み込ませます。

systemctrl reload httpd

自動更新のチェック

チェックのために、--force-renewalをつけてみます。

certbot certonly --manual \
 --server https://acme-v02.api.letsencrypt.org/directory \
 --preferred-challenges dns-01 \
 --agree-tos --no-eff-email \
 --manual-public-ip-logging-ok \
 --manual-auth-hook /root//le_mydns_hook/regist.php \
 --manual-cleanup-hook /root/le_mydns_hook/delete.php \
 -m [email protected] \
 -d *.example.com \
 -d *.sub.example.com \
 -d example.com \
 --webroot-path /var/www/html/ \
 --post-hook "systemctl reload httpd" \
 --force-renewal

おそらく、以下のように更新後のものが発行されていると思います。

$ ls -1 /etc/letsencrypt/archive/example.com/cert*
/etc/letsencrypt/archive/example.com/cert1.pem <--- 最初に発行したもの
/etc/letsencrypt/archive/example.com/cert2.pem <--- force-renewalしたもの

cronによる自動更新

確認のため、短めで --force-renewal する。

/etc/cron.d/letsencrypt
0/10 * * * * root /bin/certbot renew --webroot-path /var/www/html/ --post-hook "systemctl reload httpd" --force-renewal
$ ls -1 /etc/letsencrypt/archive/example.com/cert*
/etc/letsencrypt/archive/example.com/cert1.pem <--- 最初に発行したもの
/etc/letsencrypt/archive/example.com/cert2.pem <--- 手動でforce-renewalしたもの
/etc/letsencrypt/archive/example.com/cert3.pem <--- cronでforce-renewalしたもの

問題なく更新されていたら、週に一度確認するように。
修正しないとレートリミットに引っかかっちゃうので。

/etc/cron.d/letsencrypt
0 1 * * 1 root /bin/certbot renew --webroot-path /var/www/html/ --post-hook "systemctl reload httpd"

さいごに

スクリプト

githubに置いているスクリプトです。
registでTXTレコードの追加、delete.phpでTXTレコードの削除をしています。
ふたつのスクリプトの違いは、 $CERTBOT_ENV['EDIT_CMD'] = 'REGIST';$CERTBOT_ENV['EDIT_CMD'] = 'DELETE';だけです。

regist.php
#!/usr/bin/php
<?php

// set environment
include(__DIR__.'/accounts.conf');
date_default_timezone_set(@date_default_timezone_get());
mb_internal_encoding('UTF-8');
mb_http_output('UTF-8');

// set certbot env
$CERTBOT_ENV['CERTBOT_DOMAIN']     = getenv('CERTBOT_DOMAIN');
$CERTBOT_ENV['CERTBOT_VALIDATION'] = getenv('CERTBOT_VALIDATION');

// txt record
$CERTBOT_ENV['EDIT_CMD'] = 'REGIST';

// mydns account
$MYDNS_ACCOUNT=$MYDNS_ID[$CERTBOT_ENV['CERTBOT_DOMAIN']].':'.$MYDNS_PWD[$CERTBOT_ENV['CERTBOT_DOMAIN']];

$MYDNS_HEADERS = array('Content-Type: application/x-www-form-urlencoded',
                       'Authorization: Basic '. base64_encode($MYDNS_ACCOUNT),);


// コンテクストリソースを設定
$POST_OPTIONS = array( 'http' => array('method' => 'POST',
                                       'header' => implode("\r\n", $MYDNS_HEADERS),
                                       'content' => http_build_query($CERTBOT_ENV)));

// get contents
$MYDNS_CONTENTS = file_get_contents($MYDNS_URL, false, stream_context_create($POST_OPTIONS));

sleep(2);

?>
delete.php
#!/usr/bin/php
<?php

// set environment
include(__DIR__.'/accounts.conf');
date_default_timezone_set(@date_default_timezone_get());
mb_internal_encoding('UTF-8');
mb_http_output('UTF-8');

// set certbot env
$CERTBOT_ENV['CERTBOT_DOMAIN']     = getenv('CERTBOT_DOMAIN');
$CERTBOT_ENV['CERTBOT_VALIDATION'] = getenv('CERTBOT_VALIDATION');

// txt record
$CERTBOT_ENV['EDIT_CMD'] = 'DELETE';

// mydns account
$MYDNS_ACCOUNT=$MYDNS_ID[$CERTBOT_ENV['CERTBOT_DOMAIN']].':'.$MYDNS_PWD[$CERTBOT_ENV['CERTBOT_DOMAIN']];

$MYDNS_HEADERS = array('Content-Type: application/x-www-form-urlencoded',
                       'Authorization: Basic '. base64_encode($MYDNS_ACCOUNT),);


// コンテクストリソースを設定
$POST_OPTIONS = array( 'http' => array('method' => 'POST',
                                       'header' => implode("\r\n", $MYDNS_HEADERS),
                                       'content' => http_build_query($CERTBOT_ENV)));

// get contents
$MYDNS_CONTENTS = file_get_contents($MYDNS_URL, false, stream_context_create($POST_OPTIONS));

?>

出典

https://letsencrypt.org/ja/
https://github.com/disco-v8/DirectEdit