期限切れとなってしまったLet’s EncryptのSSL証明書の新規発行と自動更新を行う

f:id:tanabebe:20191007143528p:plain

社内で使用しているknowledgeのWebアプリについてSSL証明書が切れてしまっており、自動更新の設定も当時はかけていなかったため、SSL証明書の新規発行と自動更新設定を行いました。

実行環境

SSL証明書の期限が切れているか確認する

rootユーザーへ切り替えます。

❯❯❯ sudo -i

期限が切れているのは明白なのですが、状況を確認します。 ドメイン名は伏せてあります

❯❯❯ certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Revocation status for /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem is unknown

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: xxxxx.xxxxxxx.com
    Domains: xxxxx.xxxxxxx.com
    Expiry Date: 2019-09-17 08:10:00+00:00 (INVALID: EXPIRED)
    Certificate Path: /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/xxxxx.xxxxxxx.com/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

INVALID: EXPIREDと出ているので期限が切れています。期限が切れている場合、SSL証明書の新規発行を行う必要があるため、再度発行していきます。

SSL証明書の新規発行を行う

ファイルは退避しておきます。

❯❯❯ mkdir /etc/letsencrypt/live/_bk
❯❯❯ mv /etc/letsencrypt/live/xxxxx.xxxxxxx.com /etc/letsencrypt/live/_bk

80, 443portが使われているとエラーとなるのでhttpdを停止しておきます。

❯❯❯ systemctl stop httpd

SSL証明書の新規発行を行います。

❯❯❯ certbot-auto certonly --standalone -d xxxxx.xxxxxxx.com

httpdを起動します。

❯❯❯ systemctl start httpd
Job for httpd.service failed because the control process exited with error code. See "systemctl status httpd.service" and "journalctl -xe" for details.

エラーとなってしまいました…ここは内容を見て探っていきます。

❯❯❯ systemctl status httpd 
Oct 06 13:03:31 xxxxx-instance httpd[10894]: AH00526: Syntax error on line 101 of /etc/httpd/conf.d/ssl.conf:
Oct 06 13:03:31 xxxxx-instance httpd[10894]: SSLCertificateFile: file '/etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem' does not exist or is empty
Oct 06 13:03:31 xxxxx-instance systemd[1]: httpd.service: main process exited, code=exited, status=1/FAILURE
Oct 06 13:03:31 xxxxx-instance kill[10895]: kill: cannot find process ""
Oct 06 13:03:31 xxxxx-instance systemd[1]: httpd.service: control process exited, code=exited status=1
Oct 06 13:03:31 xxxxx-instance systemd[1]: Failed to start The Apache HTTP Server.

エラーを見るとSyntax errorと言っており、更にfullchain.pemが存在しないと言っています。中身を見ていきます。

❯❯❯ vim /etc/httpd/conf.d/ssl.conf

該当箇所を見ると以下とありました。

SSLCertificateFile /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem

フォルダも移動したので問題ない、と思っていましたがどうやら違いました。確認してみます。

❯❯❯  ll /etc/letsencrypt/live/
total 4
drwxr-xr-x. 3 root root  38 Oct  6 12:47 _bk
drwxr-xr-x. 2 root root  93 Oct  6 19:59 xxxxx.xxxxxxx.com-0001
-rw-r--r--. 1 root root 740 Oct  6 12:56 README

フォルダ名がxxxxx.xxxxxxx.com-0001となっており、名前が拡張されていました。少し気持ち悪さは残りますが、ssl.conf内の以下で始まる定義のファイルパスに0001を付与し変更します。

SSLCertificateFile
SSLCertificateKeyFile
SSLCertificateChainFile

再度httpdの起動を行います。

❯❯❯ systemctl start httpd && systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2019-10-06 20:28:55 UTC; 13ms ago
     Docs: man:httpd(8)
           man:apachectl(8)
  Process: 11556 ExecStop=/bin/kill -WINCH ${MAINPID} (code=exited, status=0/SUCCESS)
  Process: 11515 ExecReload=/usr/sbin/httpd $OPTIONS -k graceful (code=exited, status=0/SUCCESS)
 Main PID: 11579 (httpd)
   Status: "Processing requests..."
   CGroup: /system.slice/httpd.service
           ├─11579 /usr/sbin/httpd -DFOREGROUND
           ├─11580 /usr/sbin/httpd -DFOREGROUND
           ├─11581 /usr/sbin/httpd -DFOREGROUND
           ├─11582 /usr/sbin/httpd -DFOREGROUND
           ├─11583 /usr/sbin/httpd -DFOREGROUND
           └─11584 /usr/sbin/httpd -DFOREGROUND

Oct 06 20:28:55 xxxxx-instance systemd[1]: Starting The Apache HTTP Server...
Oct 06 20:28:55 xxxxx-instance systemd[1]: Started The Apache HTTP Server.

修正箇所は正解ですね。これでhttpdが動作しました。

SSL証明書の自動更新を設定する

元を正せば更新を自動化していないのが問題ですね。期限が迫る度に手動で更新とか手間ですし、少し面倒な事を後回しにすると人間は大体忘れます。なので、このままSSL証明書の自動更新を行うように設定していきます。実際にSSL証明書の更新はかけずにチェックを行いたいので—dry-runをオプションにつけてrenewを実行します。

❯❯❯ cd /usr/local/certbot/
❯❯❯ ./certbot-auto renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Traceback (most recent call last):
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/renewal.py", line 64, in _reconstitute
    renewal_candidate = storage.RenewableCert(full_path, config)
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/storage.py", line 465, in __init__
    self._check_symlinks()
  File "/opt/eff.org/certbot/venv/lib/python2.7/site-packages/certbot/storage.py", line 523, in _check_symlinks
    "expected {0} to be a symlink".format(link))
CertStorageError: expected /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem to be a symlink
Renewal configuration file /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf is broken. Skipping.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)

Additionally, the following renewal configurations were invalid: 
  /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf (parsefail)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0 renew failure(s), 1 parse failure(s)

またまたエラーです。ここでは以下のエラーが問題となっています。

CertStorageError: expected /etc/letsencrypt/live/xxx.xxx.com/cert.pem to be a symlink
Renewal configuration file /etc/letsencrypt/renewal/xxx.xxx.com.conf is broken. Skipping.

シンボリックリンクについてファイルが壊れているという内容ですね。そもそもですが、前述したhttpdが起動しない問題と同じようにパスが違うだけと想定しますが、確認してみましょう。

どのファイル末尾に2prefixが振られてしまっているのは気になりますが、live内ではarchiveシンボリックリンクがはられているのでこちらについては問題なさそうです。

❯❯❯ ll /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/
total 4
lrwxrwxrwx. 1 root root  53 Oct  6 19:59 cert.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/cert2.pem
lrwxrwxrwx. 1 root root  54 Oct  6 19:59 chain.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/chain2.pem
lrwxrwxrwx. 1 root root  58 Oct  6 19:59 fullchain.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/fullchain2.pem
lrwxrwxrwx. 1 root root  56 Oct  6 19:59 privkey.pem -> ../../archive/xxxxx.xxxxxxx.com-0001/privkey2.pem
-rw-r--r--. 1 root root 692 Oct  6 12:56 README

renewal内のファイルを確認します。

❯❯❯  vim /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf

中身を見ると存在しないパスを指定しているのが問題でした。0001を付与していきます。

# 変更前
archive_dir = /etc/letsencrypt/archive/xxxxx.xxxxxxx.com
cert = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/cert.pem
privkey = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/privkey.pem
chain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/chain.pem
fullchain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com/fullchain.pem

# 変更後
archive_dir = /etc/letsencrypt/archive/xxxxx.xxxxxxx.com-0001
cert = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/cert.pem
privkey = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/privkey.pem
chain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/chain.pem
fullchain = /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem

再度、更新はかけずにチェックを行うコマンドを実行します。

❯❯❯  ./certbot-auto renew --dry-run

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

今度は成功しました。これでSSL証明書の更新についてテストが出来ました。

自動更新の設定を行う

renewの更新作業後はhttpdの起動を実行したいので、cronを使用します。念の為cronが動作しているかを確認します。

❯❯❯  systemctl status crond
● crond.service - Command Scheduler
   Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; vendor preset: enabled)
   Active: active (running) since Sun 2019-10-06 18:41:14 UTC; 2h 42min ago
 Main PID: 462 (crond)
   CGroup: /system.slice/crond.service
           └─462 /usr/sbin/crond -n

Oct 06 18:41:14 knowledge-instance systemd[1]: Started Command Scheduler.
Oct 06 18:41:14 knowledge-instance crond[462]: (CRON) INFO (RANDOM_DELAY will be scaled with factor 36% if used.)
Oct 06 18:41:14 knowledge-instance crond[462]: (CRON) INFO (running with inotify support)

cronは動作しているので以下のように記述していきます。crontab -eで作成しようと思いましたがここは別ファイルでの管理にします。理由として間違ってキーボード隣のrをタイプしcrontab -rを実行すると無慈悲に全て消え失せますので要注意です。

❯❯❯  vim /etc/cron.d/letsencrypt

# 毎週日曜日の23:50に実行。/var/log/letsencrypt/result-renewal.logへ書き込むようにしておく。
50 23 * * 0 root /usr/local/certbot/certbot-auto renew --post-hook "systemctl reload httpd" > /var/log/letsencrypt/result-renewal.log

cronへ作成したファイルを反映します。

❯❯❯ crontab /etc/cron.d/letsencrypt

一度、毎分のcronが実行されるように変更を行い、実際に動いているかを確認します。

❯❯❯ vim /etc/cron.d/letsencrypt

# 毎週日曜日の23:50に実行。/var/log/letsencrypt/result-renewal.logへ書き込むようにしておく。
* * * * * root /usr/local/certbot/certbot-auto renew --post-hook "systemctl httpd reload" > /var/log/letsencrypt/result-renewal.log

❯❯❯ less /var/log/letsencrypt/result-renewal.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/xxxxx.xxxxxxx.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

The following certs are not due for renewal yet:
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem expires on 2020-01-04 (skipped)
  /etc/letsencrypt/live/xxxxx.xxxxxxx.com-0001/fullchain.pem expires on 2020-01-04 (skipped)
No renewals were attempted.
No hooks were run.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -                                                                         

確認が出来たら実行したいスケジュールに戻しておきます。これでSSL証明書の期限が迫っていたら自動更新が行われるようになります。

まとめ

今回については既にSSL証明書を導入済みという前提で進めてはいますが、Let’s Encryptの期限は3か月で切れるため、自動更新をしておくと後々かなり手間が省けます。「忘れんなよ」という話しではあるのですが…そこまで使われていないシステムだとSSL証明書の更新は忘れがちです。自動化のためにある程度の手間をかけることで後で楽が出来ます。SSL証明書の自動更新に関わらず、こういった考えは他の業務を遂行する上でも有用ではないでしょうか。

参考

qiita.com

イケてるcrontabのいじり方 - Qiita

ozuma.hatenablog.jp