hachiNote

勉強したことをメモします。

Let's EncryptでPostfix+Dovecotにちゃんとした証明書を設定する(Ubuntu 18.04)

目的

さくらのVPSUbuntu 18.04 LTS(Ubuntu 16.04 からアップグレードした)上でPostfix+Dovecotのメールサーバを運用しています(運用といっても、使っているのは自分だけ)。SSL/TLS対応はすでにしてありますが、証明書はオレオレ証明書を使っていました。

オレオレ証明書でも長いこと大丈夫でしたが、昨今のセキュリティ事情を踏まえて、ちゃんとした証明書にしたいところです。Let's Encryptを使うと無料で証明書を作成できるようですので、さっそく憧れのちゃんとした証明書を手に入れてメールサーバに設置したいと思います。

ということで、やってみた内容をメモしておきます。

注意点として、私のサーバはWebは運用していない(動かしてもいない)ので、あくまでPostfix+Dovecotに証明書を設置することしか考えていない内容になっています。Webサービスを運用している人の場合はまた違ったやり方があるんじゃないかと思います。

また、ここでは Postfix+Dovecotのインストール、DNSの設定、およびSSL/TLS対応はすでにできていることを前提としています。オレオレ証明書をLet's Encryptで取得した証明書に差し替えることが目的です。

はじめに:大まかな流れ

やり始めたら色々気になったことを調べまくってしまって、その過程も書いてあるのでやたら記事が長いです。最初に謝っておきます。

だけど結果的にはこんな感じだけで済みます。

  • certbotの公式サイトのUbuntu 18.04 LTS用の手順に従い、certbotをインストールする。
  • certbotで証明書を作成する。
  • 使用するサービスの設定ファイルの証明書と鍵のパスを作ったものに合わせる。
  • サービスをreloadして動作確認。
  • 証明書更新の仕組みはcertbotインストール時にできてしまっている。あとはcertbotのhook機能を使って証明書更新後にサービスをreloadする処理を入れるだけでOK。

Let's Encryptすごい。わかってしまえばすごく簡単です。

Let's Encryptの公式を確認する

まず、Let's Encryptの公式はここみたいです。

letsencrypt.org

「はじめに」とかいろいろドキュメントを読み込んでいく。証明書の自動更新をするには普通ACMEクライアントを使うようだが、ここの記述によると、公式としては Certbot というクライアントをおすすめしている。

ACME クライアント実装 - Let's Encrypt - フリーな SSL/TLS 証明書

おすすめしているので、Certbotを使うことにする。

certbot をインストールする

Ubuntuの場合、Certbotはaptで入れられるのかしらと思ってググって調べてみると、どうも人によって結構やり方が違う。普通にapt installしている人もいれば、Ubuntuの標準リポジトリに入っているcertbotは若干古いからという理由でリポジトリを追加してからやっている人もいるし、はたまたGitHubからcloneしている人もいる。

うーん、どうするか。ひとまず公式に近い情報を知りたい。Let's Encrypt公式にはACMEクライアントの紹介をしているが具体的なやり方はある意味ほとんど書いていない。ので、上記のサイトの Certbot というリンクから、Certbotの公式の情報を調べてみる。

すると certbot の公式と思われるサイトに飛ぶわけだが、なんだろう、なんかすごく詐欺サイトっぽい雰囲気なんだが……。いやまあでもこれが公式らしいです。

certbot.eff.org

このサイトがまたすごく見づらいというか調べづらくて、必要な情報がどこにあるのかわかんない。

My HTTP website is running [Software] on [System]

となっていて、プルダウンで自分の状況に合わせた選択肢を選ぶと説明が出てくる作りになっているっぽいが、いや私はWebサイト用意したいんじゃない、Nginxとかそんなんじゃないねん……とひとしきり悩んだ挙句、Softwareのところを「None of the above」、Systemのところを「Ubuntu 18.04 LTS (bionic)」を選べば良いということに気づいた。

するとちゃんとcertbotを使った証明書更新のやり方とか全部出てきた。これですよ、探していたのは。それによると、Ubuntu 18.04 LTSの場合のcertbotのインストール手順は以下のような感じ。

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository universe
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update

$ sudo apt-get install certbot

Ubuntu 18.04とUbuntu 20.04ではどう手順が違うか

話が脱線してしまいますが、Ubuntu 20.04の手順とはどう違うのか気になってしまったので、前述のサイトで表示された内容のdiffをとってみた。

すると違うのは certbot のインストール方法、正確にはcertbotをaptでインストールする前の、リポジトリの登録の仕方が違うだけだった。

Ubuntu 20.04の場合の手順はこんな感じだった。

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository universe
$ sudo apt-get update

$ sudo apt-get install certbot

そもそもこのインストール手順は何をしているのか

Ubuntu 18.04の場合との差分を見ながら、そもそもここで行っていることはなんなのかを勉強を兼ねて調べてみる。

まず add-apt-repository universe とあるが、これは apt でインストールするときに使われるリポジトリとして universe を追加することを意味する。

universeというのは、Ubuntuで使われるリポジトリの一つで、 main/restricted/universe/multiverse の4つがあるらしい。今どれを使う設定になっているかは、 /etc/apt/sources.list というファイルの中身を見るとわかるらしい。

Ubuntuのパッケージ管理のメモ - 蒼の王座・裏口

そして apt-get install software-properties-common でいれている software-properties-commonだが、これは add-apt-repositoryコマンドを使うのに必要なパッケージらしい。

Ubuntu Linuxで、add-apt-repositoryしようとして「コマンドがない」って言われたら - CLOVER🍀

そして Ubuntu 18.04の場合のみにある add-apt-repository ppa:certbot/certbot だが、これはPPA(Personal Package Archive)をリポジトリとして追加するということらしい。これは全然知らなかった。こちらの記事が大変参考になりました。

kazuhira-r.hatenablog.com

まとめると、Ubuntu 18.04向けとしてはcertbotをaptでインストールするためにはcertbot/certbotというPPAをリポジトリに追加する必要があったが、Ubuntu 20.04向けとしては必要なものはすべてuniverseリポジトリに入っているのでPPAの追加は必要なくなった、という感じか(推測入っているので正確ではない可能性あり)。

で結局自分はどうcertbotをインストールしたか

なるほど、少し理解が進んだところで自分の環境を確認してみる。 /etc/apt/sources.list を見てみると、universeリポジトリはすでに登録されている。add-apt-repositoryコマンドはなく、software-properties-commonは入っていない。ということで、最終的に以下の手順で certbot をインストールした。

$ sudo apt update
$ sudo apt install software-properties-common
$ sudo apt-add-repository ppa:certbot/certbot
$ sudo apt update

$ sudo apt install certbot

なお、 sudo apt-add-repository ppa:certbot/certbot を実行した時点で、 /etc/apt/sources.list.d/ の下に、 certbot-ubuntu-certbot-bionic.list というファイルが追加されていた。

certbotで証明書を作成する

ファイアウォールの80番ポートを開ける

チャレンジのタイプ - Let's Encrypt - フリーな SSL/TLS 証明書

上記のLet's Encryptの公式にもあるように、

Let’s Encrypt から証明書を取得するときには、ACME 標準で定義されている「チャレンジ」を使用して、証明書が証明しようとしているドメイン名があなたの制御下にあることを検証します。

となっていて、チャレンジは複数のやり方から選択できる。おすすめは「HTTP-01 チャレンジ」らしい。最初は「DNS-01 チャレンジ」を使おうかとなんとなく考えていたがリスクもあるらしく、「HTTP-01 チャレンジ」が最も多く使われているとのこと。おすすめに従っておこう。

「HTTP-01 チャレンジ」に対応するため、ファイアウォールのHTTPのポートを開ける。iptablesを使っているので、iptablesで80番を開ける。 ここでのやり方はさくらのVPSでUbuntu16.04をインストールした環境でのやり方なので、他の環境ではまたちょっと違うはずなのでご注意を。ちなみに7とは7行目に入れるの意味。

$ sudo iptables -L --line-numbers
$ sudo iptables -I INPUT 7 -p tcp -m tcp --dport 80 -j ACCEPT
$ sudo iptables -L --line-numbers
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination         
1    DROP       tcp  --  anywhere             anywhere             tcp flags:FIN,SYN,RST,PSH,ACK,URG/FIN,SYN,RST,PSH,ACK,URG
2    DROP       tcp  --  anywhere             anywhere             tcp flags:!FIN,SYN,RST,ACK/SYN state NEW
3    DROP       tcp  --  anywhere             anywhere             tcp flags:FIN,SYN,RST,PSH,ACK,URG/NONE
4    ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED
5    ACCEPT     icmp --  anywhere             anywhere            
6    ACCEPT     all  --  anywhere             anywhere            
7    ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:http
8    ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:smtp
9    ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:urd
10   ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:imaps
11   ACCEPT     tcp  --  anywhere             anywhere             state NEW tcp dpt:xxxx
12   REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT)
num  target     prot opt source               destination         
1    REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

Chain OUTPUT (policy ACCEPT)
num  target     prot opt source               destination   

$ sudo iptables-save | sudo tee /etc/iptables/iptables.rules

それにしてもほんと、iptablesって難しいな。ufwに移行したい。

certbotでコマンドを打って証明書作成

--help allで使い方表示してくれるらしい。

$ sudo certbot --help all

では certonly --standalonecertbotを実行する。対話形式で進むので、-d オプションとかなくても大丈夫。

$ sudo certbot certonly --standalone
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): xxx@yyy.zzz

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Please enter in your domain name(s) (comma and/or space separated)  (Enter 'c'
to cancel): mail.xxx.zzz
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for mail.xxx.zzz
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/mail.xxx.zzz/privkey.pem
   Your cert will expire on 2020-08-02. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

なんかあっさりできあがった。途中で入力したのは、Yes/No系のやつを除けば、更新時期が近づいたらメールを送ってくれるらしいのでその連絡用メールアドレスと、メールサーバのFQDN(mail.xxx.zzzみたいなやつ)だけ。できたものは /etc/letsencrypt/live/mail.xxx.zzz/ に置かれている。

これまでオレオレ証明書を散々作ってきたので、あっさり作成できすぎてむしろ気味が悪い。えっ、CN以外の情報、Country NameとかOrganization Nameとかはどうなったんだ? すごく気になるので、opensslコマンドを使って出来上がった fullchain.pem の内容を確認してみる。

$ sudo openssl x509 -text -noout -in /etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem

すると、Subjectの情報としてはCNしか書かれていない。CもOもない。えっ、他の情報は別になくてもよかったのか……? なんということだ……(衝撃)。

気を取り直して X509v3 extentionsの様子も見てみる。するとSAN(Subject Alternative Name)も含めて、いい感じに情報がセットされている。X509v3 extentionsは時代とともに必要な情報が変わっていくから、この辺を全部いい感じにしてくれるのはすごくありがたい。

PostfixDovecotの証明書のパスを書き換える

/etc/postfix/main.cf の中に書いてある証明書と鍵のパスを変更する。

smtpd_tls_cert_file=/etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/mail.xxx.zzz/privkey.pem

/etc/dovecot/conf.d/10-ssl.conf の中に書いてある証明書と鍵のパスを変更する。

ssl_cert = </etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.xxx.zzz/privkey.pem

できたらサービス再起動。

$ sudo systemctl restart postfix.service
$ sudo systemctl restart dovecot.service

エラーは出ていない。大丈夫のようだ。

と、最初は上記の通り restart を使ったが、後から動作確認してみたところ、reloadで証明書パスの変更は認識して動いていた。なので、以下のように reload でよくて、かつ複数サービス名を並べても動いてくれるみたい。

$ sudo systemctl reload postfix.service dovecot.service

証明書が本当に変わっているか接続確認

WebサーバならばHTTPSで接続してブラウザから証明書の確認すればすぐだけど、メールサーバだとどうやって接続確認すればいいのか。Thunderbirdでメール送受信できているので問題はなさそうだが、証明書がどうなっているのかを表示する方法がわからなかった。

調べてみると、openssl s_client を使う方法があるのですね。なるほど。

Postfixに入れたSSL証明書の更新手順 - Qiita

$ openssl s_client -connect mail.xxx.zzz:465 -showcerts
$ openssl s_client -connect mail.xxx.zzz:993 -showcerts

おおっ、ちゃんと変わっている。VerifyもOKになっている。オレオレじゃない。やったー!

certbotが自動的に更新処理を行うようにする

自動更新のコマンドを確認

certbotによる証明書作成時にも出力されていたように、更新処理は certbot renew を実行すればよいだけ。ちゃんと実行できるか確認したい場合は $ sudo certbot renew --dry-run とすれば試し実行してくれる。

この certbot renew の処理が大変賢くて、文字通り打つだけで必要なことを全部自動的にやってくれる。 certbot certonly で証明書作成したときの情報を /etc/letsencrypt/ 下にちゃんと残しておいてあるらしく、

  • 作成したすべての証明書をチェックし、有効期限が30日未満になっているものだけを更新処理してくれる。
  • 更新された証明書は、 最初に置かれた /etc/letsencrypt/live/mail.xxx.zzz/ に同じファイル名で置かれる(ので設定ファイルから見たときにパスが変わらない)。古くなった証明書は自動的に /etc/letsencrypt/archive/mail.xxx.zzz/ に格納されていく(実体はむしろarchive側にあって、liveのほうはシンボリックリンクになっている様子)。

といったことを自動で行ってくれる。すごい。ほんとすごい。

あとは certbot renew の実行を定期的かつ自動的に行うようにすればよい。

certbot renew の自動実行の登録……はすでに行われていた

実はこの後、自動実行のやり方をさんざん時間かけて調べて、cronで行う方法を確立できるところまで調べた。

が、結論から言うと、certbot renewの自動実行はすでに systemdのタイマーを使ったやり方で設定されていた。これは certbot をインストールしたときに自動的にやってくれていたみたいで、certbot の公式サイトに書いてあったUbuntu 18.04用のやり方に従ってインストールしたおかげだろう。まさかそんなことまでやってくれているなんで思ってもみなかった。

確かに、前述したcertbotUbuntu 18.04用の手順にはこう書いてあった。

The command to renew certbot is installed in one of the following locations:

/etc/crontab/
/etc/cron.*/*
systemctl list-timers

これ、最初読み違えていて、「このうちのどれかでやるといいよ」って言っていると思ってた。よく見たら「インストールされるよ」って書いてあるじゃん……。2日間くらいかけていろいろ調べてたわ、私。アホすぎる。

なので、certbot renewの自動実行はすでに行われるようになっている。なっているのだが、一応どうなっているのかを確認しておく。

systemdに登録された certbot.timer の内容を確認してみる

systemdのタイマーは初めて扱うが、cronの代わりとして使えるものらしい。 systemctl list-timers で起動しているタイマーを表示できるらしいのでまずは状況確認(これはsudoいらなかった)。

$ systemctl list-timers
NEXT                         LEFT          LAST                         PASSED       UNIT                 
Wed 2020-05-06 14:00:28 JST  32min left    Wed 2020-05-06 13:03:23 JST  24min ago    anacron.timer        
Wed 2020-05-06 19:26:58 JST  5h 58min left Wed 2020-05-06 02:16:23 JST  11h ago      certbot.timer        
Wed 2020-05-06 20:28:33 JST  7h left       Tue 2020-05-05 20:28:33 JST  16h ago      systemd-tmpfiles-clea
Wed 2020-05-06 23:54:30 JST  10h left      Wed 2020-05-06 08:51:33 JST  4h 36min ago apt-daily.timer      
Thu 2020-05-07 06:56:11 JST  17h left      Wed 2020-05-06 06:14:23 JST  7h ago       apt-daily-upgrade.tim
Thu 2020-05-07 10:59:51 JST  21h left      Wed 2020-05-06 12:39:23 JST  48min ago    motd-news.timer      
Mon 2020-05-11 00:00:00 JST  4 days left   Mon 2020-05-04 00:00:33 JST  2 days ago   fstrim.timer         

7 timers listed.
Pass --all to see loaded but inactive timers, too.

ややっ、確かに certbot.timer なるものがいる。しかも稼働している。いつのまに。 ユニット設定ファイル(?)の実体を探してみる。

$ ls -al /lib/systemd/system | grep certbot
-rw-r--r--  1 root root   233 Feb 10  2019 certbot.service
-rw-r--r--  1 root root   156 Feb 10  2019 certbot.timer

おお、確かにある。certbotをインストールしたときにできたようだ。サービスユニットとタイマーユニットのセットだ。

/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

/lib/systemd/system/certbot.serviceの中身。

[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://letsencrypt.readthedocs.io/en/latest/
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true

certbot.timerのほうで、毎日0時と12時に実行される設定になっている。実行されるのは名前が対応している certbot.service で、実際に実行されるのは /usr/bin/certbot -q renew であることがわかる。

ほうほう。なるほど、確かにもう定期的にcertbot renewする仕組みが出来上がってますね。1日2回もやらなくても週に1回くらいでいい気がしますが。

これで完了! ……と思いきやあと一つだけ。

certbot renewで証明書の更新に成功したあと、サービスをreloadするようにする

これもさんざん調べて、長くなるので詳細は後述するが、結論としてはcertbot renewで証明書の更新に成功した後に postfixdovecot を reload しないと新しい証明書が反映されない。

certbotにはhook機構が用意されているのでこれを使う。certbotの公式ドキュメント、それと前述したUbuntu 18.04用のcertbot手順が参考になる。

https://certbot.eff.org/docs/using.html?highlight=hooks#renewing-certificates

/etc/letsencrypt/renewal-hooks/deploy/ に、以下の内容をスクリプトを作成して置く。名前は仮に reloadmail.sh とした。

#!/bin/sh
systemctl reload postfix.service dovecot.service

そして作成したファイルに実行権限をつける。

$ sudo chmod 755 reloadmail.sh

これで certbot renew で更新されたときにはサービスをreloadしてくれるはず。が、実際に証明書が更新されるのは約60日後なので、それまで動作検証ができない。もし間違ってたらごめんなさい。

なお、Hookスクリプトを用意するやり方ではなく、 certbot renew --deploy-hook "systemctl reload postfix.service dovecot.service" のように certbot renew に --deply-hookオプションをつけるやり方もある。

さらに蛇足だが、certbot renew && systemctl reload postfix.service dovecot.service のような && を使った記述でcronなどに登録するやり方も考えられる。しかしこれはcertbot renew実行して証明書の更新が不要だった場合でもサービスをreloadしてしまうので、あまり効率的なやり方ではない。前述のcertbot公式ドキュメントにも、更新が必要なかった場合でもcertbot renewの終了ステータスは0(正常)で返ると書かれている。

certbot renew exit status will only be 1 if a renewal attempt failed. This means certbot renew exit status will be 0 if no certificate needs to be updated. If you write a custom script and expect to run a command only after a certificate was actually renewed you will need to use the --deploy-hook since the exit status will be 0 both on successful renewal and when renewal is not necessary.

これで本当に完了だよ。お疲れ様でした!

補足:他にいろいろ調べたことを自分用にメモ

もう「Let's EncryptでPostfix+Dovecotにちゃんとした証明書を設定する」ことは終わったので、これ以下は特に読む必要はありません。

いろいろ試行錯誤している時に調べた内容を残しておきます。

certbot renewで証明書の更新に成功したあと、サービスをreloadするのは本当に必要か

certbot renewによって証明書の更新が成功した場合、証明書ファイルは新しくなるが、postfixdovecotの設定ファイルから見たパスは変わらない。設定ファイルとして変更がない場合、reloadが必要なのかどうかはpostfixdovecotの実装次第な気がする。

certbotの公式サイトは本当に丁寧に書いてあり(探しづらかったが)、ここまでcertbotの説明や機能は本当に至れり尽くせりだった。であれば、証明書更新したあとにサービスのreload的なものが必要ならばそうやって説明してくれていてもよさそうだ。それが書いてないってことは、必ずしも必要とは限らないのでは?

postfixdovecotのマニュアルにも目を通してみたがはっきりとした答えは見つけられなかった。ならば試してみるしかない。

実際に、オレオレ証明書を用意して確認してみた。いったんオレオレ証明書の状態で465番と993番ポート経由で接続してその証明書が使われいることを確認したあと、サーバの鍵は変えずにサーバ証明書だけを新しく作り(これもオレオレ)、差し替えた。サービスをreloadしないまま、また465番と993番ポートでアクセス。

すると、465番(つまりpostfixのほう)では証明書は新しいものに変わっており、993番(つまりdovecotのほう)では証明書は古いままだった。

推測だが、これはおそらく両者のサービスとしての状態の違いによるものだと思う。systemctl で status を見ると、以下のように postfixは active (exited) で、dovecotは active (running) になっているので、この差によるものではないか。

$ sudo systemctl status postfix.service dovecot.service 
● postfix.service - Postfix Mail Transport Agent
   Loaded: loaded (/lib/systemd/system/postfix.service; enabled; vendor preset: enabled)
   Active: active (exited) since Mon 2020-05-04 19:17:27 JST; 1 day 11h ago
 (中略)
● dovecot.service - Dovecot IMAP/POP3 email server
   Loaded: loaded (/lib/systemd/system/dovecot.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2020-05-04 19:17:55 JST; 1 day 11h ago
 (略)

まあ理屈はどうあれ、少なくともdovecotのほうはreloadしないと新しくなった証明書を認識してくれないことがわかったので、おとなしくcertbot renewのあとにサービスをreloadすることにした。

cronを使った証明書更新のやり方はどうか

certbotの公式を確認してたとき、こういう記述があった。

https://certbot.eff.org/lets-encrypt/ubuntubionic-other

The command to renew certbot is installed in one of the following locations:

/etc/crontab/
/etc/cron.*/*
systemctl list-timers

つまりすでに説明したsystemdのタイマー方式だけでなく、cronを使ったやり方もcerbotインストール時に実は入っている。 /etc/cron.d/ に、certbot というファイルができているのだ。

/etc/cro.d/certbotの中身。

# /etc/cron.d/certbot: crontab entries for the certbot package
# 
# Upstream recommends attempting renewal twice a day
#
# Eventually, this will be an opportunity to validate certificates
# haven't been revoked, etc.  Renewal will only occur if expiration
# is within 30 days.
#
# Important Note!  This cronjob will NOT be executed if you are
# running systemd as your init system.  If you are running systemd,
# the cronjob.timer function takes precedence over this cronjob.  For
# more details, see the systemd.timer manpage, or use systemctl show
# certbot.timer.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 */12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew

certbotインストール時に証明書更新の仕組みが勝手に入ってるなんてまだ思ってもいなかったときにたまたまこのファイルが置いてあるのを見つけたので、最初なんでこれがここにあるのか、これはなんなのが全くかわからなかった。ここからさんざんcronについて調べることになる。

すでにできていた /etc/cron.d/certbot の中身を少しずつ読み解いてみた

まず内容としてはcertbot renewを1日2回、0時と12時に行うやつだということはわかるが、コメントに気になる内容が書かれている。「あなたのシステムがsystemdを使っているなら、このcronjobは実行されません」とある。ん、これ実行されないの?

この時点では自分のシステムがsystemdベースになっていることに自信が持ててなかったので、まずこれが実行されるものなのかから確認し始めた。

cron実行のログは /var/log/syslogにも出力されていたけど、「systemdで動かされるプロセスのログは、ファイルではなくjournaldが保持している」という情報を見かけたので、journalctlコマンドを使って確認してみる。

cronの実行ログを見るにはjournalctlコマンドを使う - モヒカンメモ

$ sudo journalctl -u cron
 (中略)
May 05 12:00:01 hostname CRON[13184]: (root) CMD (test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew)

12時に実行されている。が、cronとしては実行されているという意味で、certbot renew自体は実行されていないかもしれない。

やはり test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew を解読する必要がある。特に前半の部分がまったくわからなかったので、シェルスクリプトに関してまた調べ始める。

このサイトがめちゃめちゃ詳しかった。大変助かりました。ありがとうございます。

if 文と test コマンド | UNIX & Linux コマンド・シェルスクリプト リファレンス

testが条件式を評価するコマンド、-x, -a, -dはtestコマンドのオプションで、

  • -x は「file が実行可能ならば真となる」
  • -d は「file がディレクトリならば真となる」
  • -a はAND条件

そして!はNOT条件、&&はAND条件、とのことだ。

!の前の\が何を意味するのかがわからなかったが、多分エスケープか何かなのかな。

これらをもとにtest -x /usr/bin/certbot -a \! -d /run/systemd/system を解釈すると、「certbot実行ファイルがあり、かつ、/run/systemd/systemがディレクトリとして存在しないなら」ということになる。

自分の環境を確認してみると、/run/systemd/systemディレクトリは存在するので(これはつまりシステムがsystemdで動いていることの判断基準なのだろう)この条件を満たさない。つまりこのcronではcertbot renewは実行されないということだ。

ならば実行されるように変えないといけないなーとさらにいろいろやっていた最中に、systemdのcertbotタイマーがすでに設定されていることを偶然見つけ、あとはすでに述べた通り。

それでもやはりcronでやりたいという場合は

私は結局やらなかったが、どうしてもcronでやりたい場合は先ほどの /etc/cron.d/certbotの中身を変えたバージョンを作ればうまくいくと思う。たぶんこんな感じかな(ためしてないけど)。

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 */12 * * * root perl -e 'sleep int(rand(43200))' && certbot -q renew

--deply-hookオプションをつけて certbot -q renew --deploy-hook "systemctl reload postfix.service dovecot.service" とするもよし、Hookクスリプトを置く方法でもよし。

なお、/etc/cron.d/の下に置くcronのファイルは拡張子をつけてはいけない(拡張子付いているとcron対象外と判断される)らしいです。

また、/etc/cron.d/下にcronのファイルを置いた後、 sudo service cron restart のような感じでcronサービスをリスタートするといった記事もググった感じだと見受けられましたが、少なくとも私の環境ではそれは不要で、ファイルを置いたり変更したりしただけで、勝手に認識されて実行されるようになりました。ログを見てみると、変更を検知したログが出力されていました。

$ sudo journalctl -u cron
 (中略)
May 05 21:34:01 hostname cron[518]: (*system*certbot) RELOAD (/etc/cron.d/certbot)

まあもしかしたらsystemdベースでcronが動いているかどうかでまた違うのかもしれない。