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

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

## このエントリを読むとできること
* CentOS7のapacheで使うための
* Let's Encryptの証明書を
* MyDNSを使って
* ワイルドカード かつ マルチドメインで発行して
* 自動的に更新する

ことが、できる。はず。

[![Screenshot from Gyazo](https://gyazo.com/50cf6ad1ed320e18dea0ac945de92fee/raw)](https://gyazo.com/50cf6ad1ed320e18dea0ac945de92fee)


## 環境など

### 対象機器および環境
* CentOS7(7.7.1908)
* certbot(1.0.0)
* php(5.4.16)

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

|項目|値|
|:---:|:---|
|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](https://github.com/bashaway/le_mydns_hook)


### 準備

```
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 youraddress@example.com \
 -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 youraddress@example.com \
 -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