見出し画像

【Let's Encrypt】SSL証明書をcertbotで自動更新 (ポート80使ってない人向け) ~Pythonでポート80開放を自動化して更新~

こんにちはRcatです。
前回の記事でcertbotを使用したSSL証明書の取得を行いました。
なんとか取得に持ち込めたものの、ポート80を使用して認証を行うため、自動更新はできませんでした。
ということで、今回は更新を自動化するためのプログラムを書いていこうと思います。

前回の記事はこちら
読んでいない場合よくわからない個所が出てくるので先に読んでおくことをお勧めします。



はじめに

利用規約

情報や作品の活用時は事前に利用規約をご確認ください。

https://note.com/rcat999/n/nb6a601a36ef5

コメントについて

利用規約のガイドラインを確認の上コメントしてください


概要

一部の方は「え?何で自動更新できないの?普通ウェブで使ってるよね??」と思ったかもしれませんね。
前提として、私はポート80番でWebサーバーを立てていません。というか個人でこういうことしてる人って、なかなかいないんじゃないでしょうか?リスクも高いですし。

なのでcertbotを使用して証明書を更新するには、その時々で80番ポートを開放する必要があります。

というわけで、今回はPythonを使って80番ポートの開放を自動で行い証明書の取得までを自動化しようと思います。

certbotとは?

先ほどから何回か出てきているcertbotですが、こちらはLet's EncryptからSSL証明書を簡単に取得するためのツールです。

詳しくは以下で紹介しています。

自動更新サービスを確認する

サービスの内容確認

前回は最後に無効化しましたが、そもそもそのサービスでどんなことをやっているのか参考にするため確認します。

こちらのコマンドで確認します。
下の方で入ったんですね。

$ sudo ls /etc/systemd/system/ | grep cert
$ sudo ls /lib/systemd/system/ | grep cert
certbot.service
certbot.timer

systemctl cat certbot.service
# /lib/systemd/system/certbot.service
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://certbot.eff.org/docs
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true

$ systemctl cat certbot.timer
# /lib/systemd/system/certbot.timer
[Unit]
Description=Run certbot twice daily

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true

[Install]
WantedBy=timers.target

普通のサービスとタイマーというのが入ってますね。タイマーというのは初めて見ました。
いやー勉強になりますね。今度使う機会があれば使いましょう。

で確認したいのがこちら
ExecStart=/usr/bin/certbot -q renew
これが実行コマンドですね。で、これで何をやっているのか、まず確認しましょう。

詳細を確認する

全てのヘルプを表示します。

certbot -h all

これを実行すると、膨大な量のヘルプが表示されます。
上で使われてる引数は以下のように確認できました。
-qは余計な表示をさせないような意味があるみたいです。
renewは、有効期限の近い証明書を更新するという意味みたいです。

-q, --quiet           Silence all output except errors. Useful for automation via cron. Implies --non-interactive. (default: False)

manage:
  Various subcommands and flags are available for managing your certificates:

  certificates          List certificates managed by Certbot
  delete                Clean up all files related to a certificate
  renew                 Renew all certificates (or one specified with --cert-name)
  revoke                Revoke a certificate specified with --cert-path or --cert-name
  reconfigure           Update renewal configuration for a certificate specified by --cert-name

前回作成時の引数を調べてみると、以下のようなことが分かりました。

certonly
証明書の取得と更新はしますが、インストールはしないという設定です。
取得した後、Pythonのフォルダに持ってくのでこれで正解ですね。

standalone
認証用にWebサーバーを立ち上げる設定みたいです。
なるほど。これでWebサーバーを立ち上げるから、前回80番を横取りされたんですね。
まぁどっちにしろ80番を使うっていうのは良くないので、これ用にあけておきますが

-d ドメイン
ドメインの指定です。

ドメインの部分はまあ更新だから自動で読み取りとかしそうですが、残りの2つはまた同じ動作をしてくれるのでしょうか?
と思ったら書いてありました。どうやらオプションは再利用されるみたいです。

renew:
  The 'renew' subcommand will attempt to renew any certificates previously obtained if they are close to expiry, and print a summary of the results. By default,
  'renew' will reuse the plugins and options used to obtain or most recently renew each certificate. You can test whether future renewals will succeed with `--dry-
  run`. Individual certificates can be renewed with the `--cert-name` option. Hooks are available to run commands before and after renewal; see
  https://certbot.eff.org/docs/using.html#renewal for more information on these.

スクリプトを変更する

現在の更新用コマンドは以下のようになっていますが、こちらを次のように書き換えます

元の内容
ExecStart=/usr/bin/certbot -q renew

変更する内容
ExecStart=/SSD/certbot/venv/bin/certbot renew --pre-hook "/SSD/certbot/portopen.sh open" --post-hook "/SSD/certbot/portopen.
sh close"

変更のポイント

前提ですが、自動的に設定された更新タイマーを使います。
Pythonなのでインポートしても行けるかと思ったんですが、思ったより難しそうなのでタイマー流用にします。

まず、実行ファイルを変えます。
というのも普通ならそのままでいいのかもしれませんが、私が今回やった内容では仮想環境にインストールしているので、そこから起動しなければならないからです。

次に2つのオプションを追加します。

  • --pre-hook
    証明書更新作業前に実行するスクリプトを指定します。
    ここでポートを開放するスクリプトを呼び出します。

  • --post-hook
    証明書更新作業後に実行するスクリプトを指定します。
    ここでポートを閉鎖するスクリプトを呼び出します。

こうすることでポート開放作業を自動実行に組み込むことができます。

動作確認

オプションの中に実験のために使う"--dry-run"というのがあるので、これを指定しつつ、上のコマンドを実行してみます。

$ sudo ./venv/bin/certbot renew --dry-run --pre-hook "/SSD/certbot/portopen.sh open" --post-hook "/SSD/certbot/portopen.sh close"
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/DNS.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if response_ocsp.next_update and response_ocsp.next_update < now - timedelta(minutes=5):
Hook 'pre-hook' ran with output:
 操作に成功しました
Simulating renewal of an existing certificate for DNS.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/DNS.com/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hook 'post-hook' ran with output:
 操作に成功しました

ポート操作のスクリプトは結果をプリントするので、操作が成功したことがわかります。
鍵の更新の方も正常にアクセスできているみたいです。

ソースコード

というわけで、上で使ったポート開放用のスクリプトを見てみましょう。

呼び出し用シェルスクリプト

Pythonを起動するためだけのシェルです。
コマンド自体がrootで実行するので、仮想環境で実行しないとルートのPythonで起動してしまうので、ライブラリが読み込めません。
このような形で起動するようにしましょう。

ポート開放用Python

ポートを開放する本体です。
このスクリプトには引数に"open"か"close"の文字列を渡すことで動作を変更できます。
このスクリプトでは、ポート開放時に30分という時間制限を設けています。万が一クローズがうまくいかなかった場合でも、30分後には勝手に閉じます。

ちなみにライブラリのインポートはこんな感じになっています。
ポートを開けるためのmsというのは自作のポート操作モジュールです。

M_search2モジュールに関しては以下の記事で紹介/配布しています。

systemdに反映させる

では、今までの内容をまとめて操作に反映させましょう。

ユニットファイルを編集する

まずはsystemdのユニットファイルを編集します。
一部だけ上書きということができるみたいなので、今回はそれで行きます。

sudo systemctl edit certbot.service

エディターが起動するので上の方にあるコメントとコメントの間に変更する行を追加します。
今回はServiceの行を追加しているのがわかります。
ExecStartだけを書き込むとうまくいかなかったので、ちょっと調べたら以下みたいな書き方がいいっていうのがあったので試したところ、これでうまくいきました。

### Editing /etc/systemd/system/certbot.service.d/override.conf
### Anything between here and the comment below will become the new contents of the file

[Service]
ExecStart=
ExecStart=/SSD/certbot/venv/bin/certbot renew --pre-hook "/SSD/certbot/portopen.sh open" --post-hook "/SSD/certbot/portopen.sh close"

### Lines below this comment will be discarded

### /lib/systemd/system/certbot.service
# [Unit]
# Description=Certbot
# Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
# Documentation=https://certbot.eff.org/docs
# [Service]
# Type=oneshot
# ExecStart=/usr/bin/certbot -q renew
# PrivateTmp=true

保存する時に変な名前で保存されるのですが、ここで名前をきちんと指定しないと対応されません
ファイル名は以下の感じになるようにしてください。
/etc/systemd/system/certbot.service.d/override.conf
デフォルトでは、最後の名前のところにランダムな文字列が付いていたりすると思うので、そういったものを外します。

内容を表示してみます。
なんか増えてますね。
これうまくいった感じなんでしょうか?
まぁ、実際に動かせばわかるでしょう。

$ systemctl cat certbot.service
# /lib/systemd/system/certbot.service
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://certbot.eff.org/docs
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true

# /etc/systemd/system/certbot.service.d/override.conf
### Editing /etc/systemd/system/certbot.service.d/override.conf
### Anything between here and the comment below will become the new contents of the file

[Service]
ExecStart=
ExecStart=/SSD/certbot/venv/bin/certbot renew --pre-hook "/SSD/certbot/portopen.sh open" --post-hook "/SSD/certbot/portopen.sh close"

### Lines below this comment will be discarded

### /lib/systemd/system/certbot.service
# [Unit]
# Description=Certbot
# Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
# Documentation=https://certbot.eff.org/docs
# [Service]
# Type=oneshot
# ExecStart=/usr/bin/certbot -q renew
# PrivateTmp=true

ちなみに、初期に設定されていた自動更新は以下のようになっていて落ちていました
そりゃそうですよね。動かないファイルを指定しているんですから。
つまり、このサービスを再開した時に即落ちしなければうまくいったとも言えます。

$ systemctl status certbot.service
× certbot.service - Certbot
     Loaded: loaded (/lib/systemd/system/certbot.service; static)
     Active: failed (Result: exit-code) since Thu 2024-09-05 13:15:02 JST; 2 days ago
       Docs: file:///usr/share/doc/python-certbot-doc/html/index.html
             https://certbot.eff.org/docs
   Main PID: 1484963 (code=exited, status=1/FAILURE)
        CPU: 101ms

 905 13:15:02 rcat-Green-G5 certbot[1484963]:   File "/usr/lib/python3/dist-packages/OpenSSL/crypto.py", line 17, in <module>
 905 13:15:02 rcat-Green-G5 certbot[1484963]:     from OpenSSL._util import (
 905 13:15:02 rcat-Green-G5 certbot[1484963]:   File "/usr/lib/python3/dist-packages/OpenSSL/_util.py", line 6, in <module>
 905 13:15:02 rcat-Green-G5 certbot[1484963]:     from cryptography.hazmat.bindings.openssl.binding import Binding
 905 13:15:02 rcat-Green-G5 certbot[1484963]:   File "/usr/lib/python3/dist-packages/cryptography/hazmat/bindings/openssl/binding.py", line 14, in <module>
 905 13:15:02 rcat-Green-G5 certbot[1484963]:     from cryptography.hazmat.bindings._openssl import ffi, lib
 905 13:15:02 rcat-Green-G5 certbot[1484963]: ModuleNotFoundError: No module named '_cffi_backend'
 905 13:15:02 rcat-Green-G5 systemd[1]: certbot.service: Main process exited, code=exited, status=1/FAILURE
 905 13:15:02 rcat-Green-G5 systemd[1]: certbot.service: Failed with result 'exit-code'.
 905 13:15:02 rcat-Green-G5 systemd[1]: Failed to start Certbot.

サービスを再起動する

というわけでリスタートしてみます。
ログを確認すると即落ちはしていないみたいですね。

$ sudo systemctl restart certbot.service
$ sudo systemctl status certbot.service
○ certbot.service - Certbot
     Loaded: loaded (/lib/systemd/system/certbot.service; static)
    Drop-In: /etc/systemd/system/certbot.service.d
             mqoverride.conf
     Active: inactive (dead)
       Docs: file:///usr/share/doc/python-certbot-doc/html/index.html
             https://certbot.eff.org/docs

 908 13:27:35 rcat-Green-G5 certbot[2356085]:   if response_ocsp.next_update and response_ocsp.next_update < now - timedelta(minutes=5):
 908 13:27:35 rcat-Green-G5 certbot[2356085]: Certificate not yet due for renewal
 908 13:27:35 rcat-Green-G5 certbot[2356085]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 908 13:27:35 rcat-Green-G5 certbot[2356085]: The following certificates are not due for renewal yet:
 908 13:27:35 rcat-Green-G5 certbot[2356085]:   /etc/letsencrypt/live/DNS.com/fullchain.pem expires on 2024-11-26 (skipped)
 908 13:27:35 rcat-Green-G5 certbot[2356085]: No renewals were attempted.
 908 13:27:35 rcat-Green-G5 certbot[2356085]: No hooks were run.
 908 13:27:35 rcat-Green-G5 certbot[2356085]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 908 13:27:35 rcat-Green-G5 systemd[1]: certbot.service: Deactivated successfully.
 908 13:27:35 rcat-Green-G5 systemd[1]: Finished Certbot.

この状態だとそもそも何もしてくれなかったので、ドライランを入れ直して再度実行してみました。
ログの中にポート開放を操作した記述がありますね。どうやらうまくいってるみたいです。

$ sudo systemctl status certbot.service
○ certbot.service - Certbot
     Loaded: loaded (/lib/systemd/system/certbot.service; static)
    Drop-In: /etc/systemd/system/certbot.service.d
             mqoverride.conf
     Active: inactive (dead)
       Docs: file:///usr/share/doc/python-certbot-doc/html/index.html
             https://certbot.eff.org/docs

 908 13:45:22 rcat-Green-G5 certbot[2375987]:  操作に成功しました
 908 13:45:22 rcat-Green-G5 certbot[2375987]: Simulating renewal of an existing certificate for rcatnekokawaii.dynu.net
 908 13:45:31 rcat-Green-G5 certbot[2375987]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 908 13:45:31 rcat-Green-G5 certbot[2375987]: Congratulations, all simulated renewals succeeded:
 908 13:45:31 rcat-Green-G5 certbot[2375987]:   /etc/letsencrypt/live/rcatnekokawaii.dynu.net/fullchain.pem (success)
 908 13:45:31 rcat-Green-G5 certbot[2375987]: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 908 13:45:32 rcat-Green-G5 certbot[2375987]: Hook 'post-hook' ran with output:
 908 13:45:32 rcat-Green-G5 certbot[2375987]:  操作に成功しました
 908 13:45:32 rcat-Green-G5 systemd[1]: certbot.service: Deactivated successfully.
 908 13:45:32 rcat-Green-G5 systemd[1]: Finished Certbot.

タイマーを再起動する

前回止めてしまったタイマーを再起動します。
自動起動を有効にしてリスタートをかけます。

$ sudo systemctl enable certbot.timer
Created symlink /etc/systemd/system/timers.target.wants/certbot.timer → /lib/systemd/system/certbot.timer.
$ sudo systemctl restart certbot.timer

ステータスを確認すると以下のようになっていました。
正常に動き出したみたいです。

systemctl status certbot.timer
● certbot.timer - Run certbot twice daily
     Loaded: loaded (/lib/systemd/system/certbot.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Sun 2024-09-08 13:53:25 JST; 36s ago
    Trigger: Mon 2024-09-09 10:30:09 JST; 20h left
   Triggers: ● certbot.service

 9月 08 13:53:25 rcat-Green-G5 systemd[1]: Started Run certbot twice daily.

説明を見たところ、期限が1ヶ月に迫ると更新するみたいなので、ひとまずその辺まで待ってみましょう。
うまくいき次第記事に追記します。

まとめ

今回は無料のSSL証明書の自動更新について考えてみました。
普通の環境であれば、既存のWebサーバーを使って簡単に更新できるように設計されているみたいですが、私のようにちょっと違う使い方をする人にとっては少々工夫が必要ですね。
とはいえ、簡単であることには間違いないので非常にありがたいですね。
それではまたお会いしましょう。

情報が役に立ったと思えば、僅かでも投げ銭していただけるとありがたいです。