SSL証明書更新のトラブルはたまに隙を生じぬ二段構え

## 端書
無料で発行できるSSL証明書といえば、Let'sEncryptが有名でしょう。
2019年の春、Let'sEncryptの更新にて嵌ったポストモーテムを見つけたので、当時の出来事を記していきます。


## 第一のトラブル
### ～ 事の起こり ～
2019年3月、自分は初めてのSSL証明書更新作業に臨みました。
Let'sEncryptは自動更新も設定できるのに、なぜわざわざ手動で設定しているのか？
已むに已まれぬ事情があるのです。お察しください。

さてさて、Let'sEncryptの更新はたとえ手動でも実に簡単。
下記コマンドを叩くだけです。

```bash
/usr/local/src/certbot/certbot-auto renew --apache
```

このコマンドを叩けば、更新期限が30日以内に迫った証明書を更新してくれます。

ここで異常に気付きます。

```bash
ls -lrt etc/letsencrypt/live/
drwxr-xr-x 2 root root 4096 12月 dd HH:mm 2018 hogehoge.jp
```
更新日を確認したら、更新できていないではありませんか。

おかしいなぁと首を傾げながらログを確認すると、（抜粋）

```
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/hogehoge.com-0001.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Traceback (most recent call last):
  File "/opt/eff.org/certbot/venv/lib64/python3.4/site-packages/certbot/renewal.py", line 67, in _reconstitute
    renewal_candidate = storage.RenewableCert(full_path, config)
  File "/opt/eff.org/certbot/venv/lib64/python3.4/site-packages/certbot/storage.py", line 463, in __init__
    self._check_symlinks()
  File "/opt/eff.org/certbot/venv/lib64/python3.4/site-packages/certbot/storage.py", line 522, in _check_symlinks
    "expected {0} to be a symlink".format(link))
certbot.errors.CertStorageError: expected /etc/letsencrypt/live/hogehoge.com-0001/cert.pem to be a symlink
Renewal configuration file /etc/letsencrypt/renewal/hogehoge.com-0001.conf is broken. Skipping.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/hogehoge.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert is due for renewal, auto-renewing...
Plugins selected: Authenticator apache, Installer apache
Renewing an existing certificate
Attempting to renew cert (hogehoge.com) from /etc/letsencrypt/renewal/hogehoge.com.conf produced an unexpected error: [Errno 17] File exists: '/etc/letsencrypt/archive/hogehoge.com/privkey3.pem'. Skipping.
The following certs could not be renewed:
  /etc/letsencrypt/live/hogehoge.com/fullchain.pem (failure)

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


The following certs could not be renewed:
  /etc/letsencrypt/live/hogehoge.com/fullchain.pem (failure)

Additionally, the following renewal configurations were invalid:
  /etc/letsencrypt/renewal/hogehoge.com-0001.conf (parsefail)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

```

とりあえず、なんかうまく行っていないようです。

巡り合わせが悪かったのでしょう。
気を取り直してもう一度更新を試みました。しかし、またしても上手くいきません。

仕方がないので、改めて上記のログを見てみます。
すると、`hogehoge.com.conf`の更新とは別に`hogehoge.com-0001.conf`の更新を試みて失敗しているではありませんか。

ちなみに/etc/letsencrypt/のディレクトリ構造はこんな感じになっています。
（大部分省略）

```
/etc/letsencrypt/
|--archive
|  |--hogehoge.com
|     |--cert1.pem
|     |--cert2.pem
|     |--cert3.pem
|     |--cert4.pem
|     |--chain1.pem
|     |--chain2.pem
|     |--chain3.pem
|     |--chain4.pem
|     |--fullchain1.pem
|     |--fullchain2.pem
|     |--fullchain3.pem
|     |--fullchain4.pem
|     |--privkey1.pem
|     |--privkey2.pem
|     |--privkey3.pem
|     |--privkey4.pem
|
|--live
|  |--hogehoge.com
|     |--README
|     |--cert.pem -> ../../archive/hogehoge.com/cert2.pem
|     |--chain.pem -> ../../archive/hogehoge.com/chain2.pem
|     |--fullchain.pem -> ../../archive/hogehoge.com/fullchain2.pem
|     |--privkey.pem -> ../../archive/hogehoge.com/privkey2.pem
|
|--renewal
   |--hogehoge.com.conf
   |--hogehoge.com-0001.conf
```


不思議なことに、`live/`,`archive/`下には`hogehoge.com-0001`のディレクトリはありません。

一方`renewal/`を見てみれば、`hogehoge.com-0001.conf`がありました。


### ～ 推理推測 ～

- letsencryptの証明書発行コマンドを実行する際、証明書の名前（hogehoge.com）は発行時のFQDNとなる。
- 証明書名が重複すると後ろに番号を付けた証明書が発行される。
- 証明書の削除には、証明書の失効・削除のコマンドを実行しなければならない。
- `-0001`付のディレクトリが`live/`と`archive/`にはないのに`renewal/`にはある。

・・・ひょっとすれば、こんなことがあったのかもしれません。

- 昔、証明書を間違えて発行してしまった。（-0001爆誕）
→ ごみを残してはいけないと思い、`/etc/letsencrypt/live/`と`archive/`の`-0001`付のディレクトリをrmで削除した。
→ /etc/letsencrypt/renewal/はそのまま。
→ renewalを元に更新しようとして困る。`live/hogehoge.com-0001/cert.pem` がどこにもsymlinkしていない。（そもそも存在しない。）
→ エラー。

- いろいろあって、`archive/hogehoge.com/`下のナンバリングがおかしなことになった。
→ 本来privkey3.pemを新たに作成してsymlinkするはずが、なぜかすでにprivkey3.pemが存在する。
→ エラー

二つ目がとても雑なのですが、certbot-autoコマンドを真面目に紐解かなければならなそうなので、自分の哲学などには思いもよらぬでき事があったのだろう、と納得しました。
（`live/`と`archive/`下の`-0001`がなかったことが影響したのでは、と想像しています。）

いずれにせよ、更新失敗の要因は分かりました。

- symlink先であるarchiveにおかしなナンバリングのファイルが存在する。
- 存在しない証明書のファイルがrenewal/に存在する。

という事で…

### ～ 対応 ～

- `renewal/hogehoge.com-0001.conf`を無効化。

```bash
mv /etc/letsencrypt/renewal/hogehoge.com-0001.conf /root/hogehoge.com-0001.conf
```

- `archive/hogehoge.com/privkey3.pem`他を無効化。

```bash
mv /etc/letsencrypt/archive/hogehoge.com/cert3.pem{,.bk}
mv /etc/letsencrypt/archive/hogehoge.com/chain3.pem{,.bk} 
mv /etc/letsencrypt/archive/hogehoge.com/fullchain3.pem{,.bk} 
mv /etc/letsencrypt/archive/hogehoge.com/privkey3.pem{,.bk} 
```

- 証明書更新

```bash
/usr/local/src/certbot/certbot-auto renew --apache
```

`live/`下のsymlinkを張り直し。

```bash
cd /etc/letsencrypt/live/hogehoge.com
unlink  ./cert.pem
ln -s ../../archive/hogehoge.com/cert3.pem ./cert.pem
unlink  ./chain.pem
ln -s ../../archive/hogehoge.com/chain3.pem ./chain.pem
unlink  ./fullchain.pem
ln -s ../../archive/hogehoge.com/fullchain3.pem ./fullchain.pem
unlink  ./privkey.pem
ln -s ../../archive/hogehoge.com/privkey3.pem ./privkey.pem
```

これで無事、証明書の更新を行なう事が出来ました。

対応している最中、更新に失敗しすぎてLetsEncryptからペナルティを課せられて頭を抱えたのは、また別の話・・・。

―――めでたし、めでたし。



## 第二のトラブル
### ～ 事の起こり ～
2019年4月、別の証明書の更新のために私は下記コマンドを叩きました。

```bash
/usr/local/src/certbot/certbot-auto renew --dry-run
```

最期についている`--dry-run`オプション。
これはテスト用のオプションで、これを付けることによって更新可能かどうかのテストを行ない、実際には証明書に変更を加えないという優れたオプションなのです。（dry-run：予行演習）

これを使っていれば、いくら失敗してもペナルティを課されることはありません。

コマンドを実行し、ぼんやりと流れゆく出力を眺めていたら、営業の方からメッセージが届きました。
何やら急ぎ対応をしてほしい様子でしたので、私はSSL更新作業を中断し、営業の方の対応に移ります。

大過なく横槍の対応を終え、さてさて更新の続きをばとPCに向き直った時、別部署の方が神妙な顔でやってきて言いました。

「hogehoge.comってサイトがプライバシーエラー出してて、見てみたらSSL証明書の期限が切れてるっぽいんですけど…。」

忘れもしないhogehoge.comがまたしても問題を起こしたのです。

- プライバシーエラーという事はSSLの問題である可能性は濃厚
- 先月更新したばかりなので、更新期限切れはあり得ない。
- 発生タイミングは自分が作業をしていた時。
- 現段階では`dry-run`しかしていない。

自分がやってしまったという確信はあるものの、何が起こっているのかもわからないまま、とりあえず見てみました。

```bash
ls -l etc/letsencrypt/live/hogehoge.com/

-rw-r--r-- 1 root root 692 MM月 dd HH:mm dddd README
lrwxrwxrwx 1 root root  44 MM月 dd HH:mm dddd cert.pem -> ../../archive/hogehoge.com/cert4.pem
lrwxrwxrwx 1 root root  45 MM月 dd HH:mm dddd chain.pem -> ../../archive/hogehoge.com/chain4.pem
lrwxrwxrwx 1 root root  49 MM月 dd HH:mm dddd fullchain.pem -> ../../archive/hogehoge.com/fullchain4.pem
lrwxrwxrwx 1 root root  47 MM月 dd HH:mm dddd privkey.pem -> ../../archive/hogehoge.com/privkey4.pem
```

おや、1か月前に対応した時、3のナンバリングに張ったsymlinkが、なぜか4に張り直されています。

理解が追い付かないまま、dry-run時の標準出力をさかのぼります。
すると、hogehoge.comの更新テストの出力に、、、

```bash
Found a new cert /archive/ that was not linked to in /live/; fixing...
## 意訳
# 新しいのに紐づいてない証明書みつけたよ☆修正なう☆
```

### ～ 推理推測 ～

`archive/`のディレクトリは下記のようになっていました。

```bash
/etc/letsencrypt/archive/
|--hogehoge.com
|--cert1.pem
|--cert2.pem
|--cert3.pem（3月の対応時symlink）
|--cert3.pem.bk
|--cert4.pem
|--chain1.pem
|--chain2.pem
|--chain3.pem（3月の対応時symlink）
|--chain3.pem.bk
|--chain4.pem
|--fullchain1.pem
|--fullchain2.pem
|--fullchain3.pem（3月の対応時symlink）
|--fullchain3.pem.bk
|--fullchain4.pem
|--privkey1.pem
|--privkey2.pem
|--privkey3.pem（3月の対応時symlink）
|--privkey3.pem.bk
|--privkey4.pem
```

もはや推理推測の余地はありません。
`dry-run`実行時、「3」に張っていたsymlinkは、本来であれば最も新しいであろう「4」に自動で張り直されてしまったのです。

何という事をしてくれたのでしょう。
`dry-run`とはいったい何だったのでしょうか…。

### ～ 対応 ～
- `archive/`下の「3」を「4」にコピー

```bash
cd /etc/letsencrypt/archive/hogehoge.com
# 4を退避
mv cert4.pem cert4.pem.bk
mv chain4.pem chain4.pem.bk
mv fullchain4.pem fullchain4.pem.bk
mv privkey4.pem privkey4.pem.bk

# 3を4にコピー
cp cert3.pem cert4.pem
cp chain3.pem chain4.pem
cp fullchain3.pem fullchain4.pem
cp privkey3.pem privkey4.pem
```
これにて、最新の証明書に書き換えられ、無事サイトが映るようになりましたとさ。

―――めでたし、めでたし。

## まとめ
- SSL証明書を間違って作成してしまった場合、失効・削除コマンドを用いる。
- `dry-run`は変更を加えないのではなく、最小限にするオプション。
- 出力はしっかり見る。
- サーバは常にクリーンに。ゴミを残さない。
- 特殊な対応を行なった場合、次回対応時に必ず確認を行なう。
- ポストモーテムはとても大事。この記事も当時書いたポストモーテムがベースです。


## あとがき
今回、単純化のため説明を省いた点が多々ございます。

例えば、今回のトラブルはまるで自分が全て解決したかのように書いておりますが、実際はすぐに上司に泣きついて対応方法を教えてもらいました。
ホウレンソウは大事ですから。


中々遭遇することの無いトラブルとは思いますが、本記事によってポストモーテムの文化がわずかでも根付けば、投稿者冥利に尽きるというものです。