OCIに設定したLet's Encrypt SSL/TLS証明書を自動更新

# 概要
[Oracle Cloud Infrastructure(OCI)](https://www.oracle.com/jp/cloud/)に設定したLet's Encrypt SSL/TLS証明書サービスを自動更新します

# 背景
AWSの[ACM](https://aws.amazon.com/jp/certificate-manager/)は証明書有効期限を自動更新してくれますので更新忘れを防ぐことができます。一方OCIはパブリック認証局がないためLet's Encryptの証明書をインポートして利用しますが、3か月毎に更新作業が必要になります。この負荷を軽減するために自動更新の仕組みを作成しました。
VMを立てる手間はありますが、証明書をExportできるためVMに直接インポートしたり他のクラウドサービスに使用することも可能になります。

# 構成
以下の流れで自動更新を行います。
(1)証明書期限確認して30日以下であれば発行を開始
(2)Route53のドメインにTXTレコード作成
(3)TXTレコードを確認
(4)証明書発行
(5)Route53のドメインからTXTレコード削除
(6)証明書をOCIにアップロード

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/454e4767-b588-9616-665f-f32d6e431e1a.png">

# Certbotインストールと初回発行作業
## Certbotインストール
OCIのCompute VMにcertbotをインストールします
```
$ cat /etc/oracle-release
Oracle Linux Server release 8.10
$ sudo python3 -m pip install --upgrade requests
$ env LANG=C sudo dnf repolist all
$ sudo yum-config-manager --enable ol8_developer_EPEL
$ sudo dnf install snapd
$ sudo systemctl start snapd.service
$ sudo snap install core
$ sudo snap refresh core
$ sudo ln -s /var/lib/snapd/snap /snap
$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
$ certbot --version
certbot 2.11.0
```
:::note info
情報
インストールに失敗する場合、SELinuxが有効になっていると影響を受ける可能性があります。
この場合はSELinuxをpermissiveに変更して試してください。
:::

## 証明書発行
初回の証明書発行を手動で実施します
今回の例はワイルドカードでの証明書です
```
$ sudo certbot certonly --manual -d *.example.com --preferred-challenges dns --key-type rsa 
```
成功すると/etc/letsencrypt/live/example.com/に証明書が作成されます

## OCI証明書サービスに登録
OCIにログインして以下を実施します
- 作成された証明書をOCI証明書サービスに登録
- FLBやAPI GW等に紐づけして稼働を確認

# 更新作業準備
## AWS Route53へのアクセスするユーザー作成
AWSにログインして以下を実施します
- Route53のホストゾーンIDを確認
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/d56caa76-3ce1-4051-bb2b-82022bab0db4.png">

- ユーザーIDを作成して上記DNSホストゾーンのレコード更新許可付与
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "route53:ChangeResourceRecordSets",
            "Resource": "arn:aws:route53:::hostedzone/【HOST ZONE ID】"
        },
        {
            "Effect": "Allow",
            "Action": "route53:GetChange",
            "Resource": "arn:aws:route53:::change/*"
        }
    ]
}
```
## AWS CLI実行環境をVM内に準備
Compute VMにて以下を実施します
- OCI VMにAWS CLIをインストール
- aws configure等でAWSユーザーIDを構成

## OCI Certificateへのアクセスポリシー付与
OCIにログインして以下を実施します
- 証明書サービスをインスタンスから利用するためのPolicy追加
*Allow dynamic-group 【動的グループ】 to use leaf-certificates in compartment 【コンパートメント名】*
- 証明書OCIDを確認
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730992/f5e1d633-d525-4140-b808-4a6468e49189.png">

## Shell作成
Compute VMにてShellを作成します。
certbotは証明書更新作業前後で実行するScriptを指定できます。
今回はRoute53に_acme-challenge.example.comのTXTレコードを作成/削除するShellを作成しました。

### TXTレコード作成およびレコード有効時間を考慮して2分待機
```Route53CreateRecord.sh
#!/bin/bash
aws route53 change-resource-record-sets --hosted-zone-id 【ホストゾーンID】 --change-batch file:///dev/stdin << EOF
{
    "Changes": [{"Action": "CREATE",
        "ResourceRecordSet": {
        "Name": "_acme-challenge.${CERTBOT_DOMAIN}",
        "Type": "TXT",
        "TTL": 300,
        "ResourceRecords": [{"Value":"\"${CERTBOT_VALIDATION}\""}]
    }}]
}
EOF
for (( i=1; i<13; i++ )); do sleep 10; echo $((i*10))s ; done
```
作成後、chmodコマンド(chmod 755)で実行可能に変更します。

### TXTレコード削除
```Route53DeleteRecord.sh
#!/bin/bash
aws route53 change-resource-record-sets --hosted-zone-id 【ホストゾーンID】 --change-batch file:///dev/stdin << EOF
{
    "Changes": [{"Action": "DELETE",
        "ResourceRecordSet": {
        "Name": "_acme-challenge.${CERTBOT_DOMAIN}",
        "Type": "TXT",
        "TTL": 300,
        "ResourceRecords": [{"Value":"\"${CERTBOT_VALIDATION}\""}]
    }}]
}
EOF
```
作成後、chmodコマンド(chmod 755)で実行可能に変更します。

### 証明書更新
証明書更新のShellは以下のとおり作成しました
- Certbotの実行
    - 有効期限が1ヶ月以上ある場合は更新されない
    - 有効期限が1ヶ月未満の場合以下を実行
        -  _acme-challenge.example.comのTXTレコードをRoute53に作成
        -  証明書更新
        -  作成したTXTレコードを削除
- 証明書の日付をチェックして更新(証明書が1日以内に作成された)があった場合はOCI 証明書を更新

```cert-chk-update.sh
#!/bin/bash

# Certbot renew
sudo certbot renew \
  --key-type rsa \
  --manual \
  --manual-auth-hook /path/to/Route53CreateRecord.sh \
  --manual-cleanup-hook /path/to/work/cert/Route53DeleteRecord.sh

# Check when the key file is updated
cert_date=`sudo date -r /etc/letsencrypt/live/example.com/cert.pem`
cert_date_sec=`date +%s -d "$cert_date"`
today_sec=`date +%s`
cert_diff_hr=$(((today_sec-cert_date_sec)/3600))
cert_diff_sec=$((today_sec-cert_date_sec))
echo $cert_diff_hr " hour ago key file created"
echo $cert_diff_sec " sec ago key file created"
# If the key file created within 1 day, send it to OCI
if [ "$cert_diff_sec" -le 86100 ]; then
    # Send new key file to OCI certificate
    echo "Send new key file"
    oci certs-mgmt certificate update-certificate-by-importing-config-details \
        --auth instance_principal \
        --certificate-id 【証明書OCID】 \
        --certificate-pem "$(sudo cat /etc/letsencrypt/live/example.com/cert.pem)" \
        --cert-chain-pem "$(sudo cat /etc/letsencrypt/live/example.com/chain.pem)" \
        --private-key-pem "$(sudo cat /etc/letsencrypt/live/example.com/privkey.pem)"
fi
```
certbot renewの後に--dry-runを加えることによってコマンド検証が可能です。

作成したShellを定期的に実行します。以下の例では毎日18:30に実行する例です。
```
30 18 * * * /bin/bash /path/to/cert-chk-update.sh
```

# 実行
ログを確認するとCronによる実行以外にもrenew実行ログがあります。
こちらはsnap.certbot.renew.timerによる実行となります。
```
#snap.certbot.renew.timerのrenew実行タイミング
[root@linux]# cat /etc/systemd/system/snap.certbot.renew.timer
:
[Timer]
Unit=snap.certbot.renew.service
OnCalendar=*-*-* 03:20
OnCalendar=*-*-* 22:11

#renew実行ログを確認
[root@linux]# ls -lart /var/log/letsencrypt/
-rw-r--r--   1 root root  3039 Mar  6 18:30 letsencrypt.log.2 <=cronによる実行
-rw-r--r--   1 root root  2522 Mar  6 22:11 letsencrypt.log.1 <=snap.certbot.renew.timerによる実行
-rw-r--r--   1 root root 30857 Mar  7 03:22 letsencrypt.log   <=snap.certbot.renew.timerによる実行

```
Mar  7 03:22の実行時に有効期限が1ヶ月未満と判定され証明書更新が実行されました。
以下は、有効期限まで30日以上の場合（更新しない）と有効期限まで30日未満の場合（更新する）の実行ログです。

- 証明書有効期限まで30日以上の場合（更新しない）のrenewログ
```
 :
2025-03-06 22:11:05,948:DEBUG:certbot._internal.display.obj:Notifying user: Certificate not yet due for renewal
 :
2025-03-06 22:11:05,949:DEBUG:certbot._internal.display.obj:Notifying user:- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2025-03-06 22:11:05,949:DEBUG:certbot._internal.display.obj:Notifying user: The following certificates are not due for renewal yet:
2025-03-06 22:11:05,949:DEBUG:certbot._internal.display.obj:Notifying user:   /etc/letsencrypt/live/example.com/fullchain.pem expires on 2025-04-05 (skipped)
2025-03-06 22:11:05,949:DEBUG:certbot._internal.display.obj:Notifying user: No renewals were attempted.
2025-03-06 22:11:05,950:DEBUG:certbot._internal.display.obj:Notifying user: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2025-03-06 22:11:05,950:DEBUG:certbot._internal.renewal:no renewal failures

```

- 証明書有効期限まで30日未満の場合（更新する）のrenewログ
```
 :
2025-03-07 03:20:04,213:INFO:certbot._internal.renewal:Certificate is due for renewal, auto-renewing...
2025-03-07 03:20:04,213:INFO:certbot._internal.renewal:Non-interactive renewal: random delay of 30.966559182213448 seconds
 :
2025-03-07 03:20:36,448:INFO:certbot._internal.auth_handler:Performing the following challenges:
2025-03-07 03:20:36,448:INFO:certbot._internal.auth_handler:dns-01 challenge for example.com
2025-03-07 03:20:36,448:INFO:certbot.compat.misc:Running manual-auth-hook command: /path/to/Route53CreateRecord.sh
 :
2025-03-07 03:22:38,476:INFO:certbot._internal.auth_handler:Waiting for verification...
 :
2025-03-07 03:22:39,649:INFO:certbot._internal.auth_handler:Cleaning up challenges
2025-03-07 03:22:39,649:INFO:certbot.compat.misc:Running manual-cleanup-hook command: /path/to/Route53DeleteRecord.sh
 :
2025-03-07 03:22:43,455:DEBUG:certbot._internal.display.obj:Notifying user:- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2025-03-07 03:22:43,455:DEBUG:certbot._internal.display.obj:Notifying user: Congratulations, all renewals succeeded:
2025-03-07 03:22:43,455:DEBUG:certbot._internal.display.obj:Notifying user:   /etc/letsencrypt/live/example.com/fullchain.pem (success)
2025-03-07 03:22:43,455:DEBUG:certbot._internal.display.obj:Notifying user: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2025-03-07 03:22:43,455:DEBUG:certbot._internal.renewal:no renewal failures
```


# 考慮点
実施するにあたり、何点か補足事項です。
- 強制更新を行うためにはcertbot renewの後に--force-renewalを追加します
- ただし強制更新を何度も行うとDNS確認はスキップされて更新する場合があるようです(有効なドキュメントは見つからなかったので詳細は不明）
- 自動更新が成功しない可能性を考慮して、以下のようにSSL証明書有効期限の定期的確認を併用すると更新漏れが防げるかと思います


https://qiita.com/tktk2712/items/babd1de87f50974940f3