今度こそopensslコマンドを理解して使いたい (5) CRL（証明書失効リスト）を作成してOpenVPNに配布する

前回までの「今度こそopensslコマンドを理解して使いたい」:

* [第1回: ルートCAをスクリプトで作成する](https://qiita.com/3244/items/780469306a3c3051c9fe)
* [第2回: 設定ファイル（openssl.cnf）を理解する](https://qiita.com/3244/items/8c7e6892efdb4e500be9)
* [第3回: CA証明書の拡張設定を検証する](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9)
* [第4回: サーバー／クライアント証明書を一括生成する](https://qiita.com/3244/items/2618429ebe6dc16c074e)

前回までで、プライベートCAとサーバー／クライアント証明書の作成を自動化することができました。

今回は証明書の失効とCRL（証明書失効リスト）の発行をスクリプトで処理します。
また実際にOpenVPNサーバーにCRLを配布して、失効したクライアント証明書が認証されなくなることを確認します。

![crl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/83f64a68-ea3b-5d8b-467d-6434792e2e08.png)

# OpenVPNの試験環境を作る
その前に、OpenVPNの動作に必要なファイルの作成と設定を行います。

## 環境とバージョン
サーバー用OS: Amazon Linux 2
クライアント用OS: CentOS 7.6
CA用OS: CentOS 7.6

CentOS 7.6を使用するので、OpenSSLのバージョンは1.0.2を使用します。
参考: OpenSSL Manpages for 1.0.2: https://www.openssl.org/docs/man1.0.2/

OpenVPNは、CentOS 7.6とAmazon Linux 2にyumでインストール可能な2.4.7を使用します（2019年8月現在）。
参考: OpenVPN コミュニティ Wiki: https://community.openvpn.net/openvpn

## OpenVPN用の追加ファイルを作成する
これまでに作成した認証関係のファイルと、OpenVPNサーバー／クライアントに配置するファイルの関係は以下のとおりです。

ファイル|サーバー|クライアント|作成回
:--|:-:|:-:|:-:
CA秘密鍵| | |[第1回](https://qiita.com/3244/items/780469306a3c3051c9fe)、[第3回](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9)
CA証明書|X|X|[第1回](https://qiita.com/3244/items/780469306a3c3051c9fe)、[第3回](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9)
サーバー秘密鍵|X| |[第4回](https://qiita.com/3244/items/2618429ebe6dc16c074e)
サーバー証明書|X| |[第4回](https://qiita.com/3244/items/2618429ebe6dc16c074e)
クライアント秘密鍵| |X|[第4回](https://qiita.com/3244/items/2618429ebe6dc16c074e)
クライアント証明書| |X|[第4回](https://qiita.com/3244/items/2618429ebe6dc16c074e)
DHパラメータ|X| |今回|
TLS認証鍵|X|X|今回|

サーバーに5つ、クライアントに4つのファイルを配置します。まだ作成していないファイルは、DHパラメータとTLS認証鍵の2つです。

### DHパラメータ
セッションの共通鍵を安全に生成するための Diffie Hellman パラメータを、`openssl`の`dhparam`コマンドを使って作成します。
`-out`で任意のファイル名を指定して、最後に鍵長をビット数で指定します。2048はデフォルト値なので、省略しても同じ結果になります。

```Shell:
openssl dhparam -out ファイル名 2048
```
参考: `dhparam`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-dhparam.html

### TLS認証鍵
DoS攻撃への対策として認証の前段階で不正なパケットを破棄するための静的な共有鍵です。
これだけは`openssl`ではなく`openvpn`コマンドで作成します。以下のコマンドで、任意のファイル名を指定して作成します。

```Shell:
openvpn --genkey --secret ファイル名
```
参考: OpenVPN.JP How To:「OpenVPNのセキュリティを強化する」: https://www.openvpn.jp/document/how-to/#Security

## OpenVPNの設定ファイル
以上のファイルをOpenVPNサーバー／クライアントの任意の場所に配置して、設定ファイルでそれぞれのパスを指定します。

### サーバー
配置した5つのファイルのパスを以下のように指定します。`tls-auth`行の2番目のパラメータは、サーバーでは`0`を指定します。

```:/etc/openvpn/任意の名前.conf
ca        CA証明書のパス
cert      サーバー証明書のパス
key       サーバー秘密鍵のパス
dh        DHパラメータのパス
tls-auth  TLS認証鍵のパス 0
```

設定ファイルのパスは`/etc/openvpn/任意の名前.conf`です。
サービスのユニット名は設定ファイル名から動的に生成されて「`openvpn@任意の名前.service`」になります。

### クライアント
配置した4つのファイルのパスを以下のように指定します。CA証明書とTLS認証鍵は、サーバーに配置したファイルと同じです。`tls-auth`行の2番目のパラメータは、クライアントでは`1`を指定します。

```:/etc/openvpn/任意の名前.conf
ca        CA証明書のパス
cert      クライアント証明書のパス
key       クライアント秘密鍵のパス
tls-auth  TLS認証鍵のパス 1
```

サーバーと同様に、設定ファイルのパスが`/etc/openvpn/任意の名前.conf`なら、サービスのユニット名は「`openvpn@任意の名前.service`」になります。

# 証明書の失効処理
証明書の失効処理は、各証明書を発行した（署名した）CAで行います。
[前回](https://qiita.com/3244/items/2618429ebe6dc16c074e)、以下のように CA「ca1.mydomain」で「vpnserver_1」と「vpnclient_1」の証明書を発行して署名しました。

```Shell:
├── ca1.mydomain/
│   ├── certs/
│   │   ├── vpnclient_1.crt.pem  # 「vpnclient_1」のクライアント証明書
│   │   ├── vpnclient_1.key.pem  # 「vpnclient_1」の秘密鍵
│   │   ├── vpnserver_1.crt.pem  # 「vpnserver_1」のサーバー証明書
│   │   └── vpnserver_1.key.pem  # 「vpnserver_1」の秘密鍵
```

「ca1.mydomain」のデータベースインデックス（index.txt）には、これら2つの証明書の情報が正しく記録されています。

```Shell:
cat ca1.mydomain/index.txt

V   220821110732Z       1000    unknown /C=JP/ST=Chiba/O=myorg/CN=vpnserver_1
V   200821110732Z       1001    unknown /C=JP/ST=Chiba/O=myorg/CN=vpnclient_1
```

今回はこの中の「vpnclient_1」のクライアント証明書を失効します。

## 失効対象リスト
前回サーバー／クライアントの証明書を一括生成した時と同じように、複数の失効対象を指定できるようにします。サーバー／クライアントは混在可能です。また前回と同様に、処理済みの対象はスキップするようにします。

1列目: 失効対象のサーバー／クライアントのコモンネーム
2列目: 証明書に署名したCAのコモンネーム

```:revoke-list
vpnclient_1  ca1.mydomain
```

## 設定ファイル（openssl.cnf）
OpenSSLの設定ファイルは前回と同じ`openssl.cnf`を使用し、CAのディレクトリをスクリプトから指定できるようにします。
参考: [第2回 「環境変数」](https://qiita.com/3244/items/8c7e6892efdb4e500be9#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0)

```Bash:/etc/pki/tls/openssl.cnf
CA_DIR    = /etc/pki/CA     # 全体の保存場所（環境変数がない場合のデフォルト値）

[ CA_default ]
dir       = $ENV::CA_DIR    # 全体の保存場所（環境変数）
# 以下変更なし
certs     = $dir/certs      # Where the issued certs are kept
crl_dir   = $dir/crl        # Where the issued crl are kept
database  = $dir/index.txt  # database index file.
```
## CA秘密鍵のパスフレーズ
CRLに署名するために、CAの秘密鍵のパスフレーズを指定する必要があります。[CAを作成した時のファイル`.capass`](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E5%A4%89%E6%9B%B4)をそのまま使用して、先頭行で定義されたパスフレーズを入力します。

```:.capass
mycap@ss
```

## 証明書一括失効用スクリプト
以上の仕様でスクリプトを作成します。エラー処理などは省略しています。

```Bash:revoke.sh
#!/bin/bash

CATOP="$(dirname "${0}")"

# 失効対象のコモンネーム、署名するCA名を失効対象リストから1件ずつ読み込む
while read common_name ca_name
do
  [[ ${common_name} != "" && ${ca_name} != "" ]] || continue

  # CAのディレクトリ
  ca_dir="${CATOP}/${ca_name}"

  # データーベースインデックスのレコードを見て、現在のターゲットが失効済み
  # ならスキップする。
  grep "^R.*/CN=${common_name//./\\.}$" "${ca_dir}/index.txt" && continue

  # CAのディレクトリを環境変数に出力する。
  export CA_DIR="${ca_dir}"

  # caコマンドで証明書を失効する。
  #   -revoke     失効対象の証明書ファイルのパス
  #   -passin     CAの秘密鍵のパスフレーズ入力
  openssl ca \
      -revoke   "${ca_dir}/certs/${common_name}.crt.pem" \
      -passin   "file:${CATOP}/.capass"

done <"${CATOP}/revoke-list"
```
参考: `ca`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-ca.html

正常に終了すると以下のように表示されます。「1001」は失効した証明書のシリアル番号です。

```Shell:
Using configuration from /etc/pki/tls/openssl.cnf
Revoking Certificate 1001.
Data Base Updated
```

実行後、データベースインデックスは以下のように更新されました。

```Shell:
cat ${CA_DIR}/index.txt

V	220821110732Z		1000	unknown	/C=JP/ST=Chiba/O=myorg/CN=vpnserver_1
R	200821110732Z	190831162004Z	1001	unknown	/C=JP/ST=Chiba/O=myorg/CN=vpnclient_1
```

以下は実行前のデータベースインデックスです。実行後はシリアル番号1001の行の先頭が「V」から「R」（失効）になり、2列目に失効日時が追加されたことが分かります。

```Shell:
V   220821110732Z       1000    unknown /C=JP/ST=Chiba/O=myorg/CN=vpnserver_1
V   200821110732Z       1001    unknown /C=JP/ST=Chiba/O=myorg/CN=vpnclient_1
```
以上で失効対象リストで指定した証明書の失効が、CAのデータベースインデックスに正しく記録されたことが確認できました。

# CRL（証明書失効リスト）の作成
各CAのデータベースインデックス（index.txt）の情報を元に、新しいCRLを作成します。

## 対象CAリスト
[第3回でCAを作成した時](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E5%A4%89%E6%9B%B4)に使用したファイル`ca-list`を使用します。リスト内のすべてのCAでCRLを作成します。「ca2.mydomain」ではまだ1つも失効処理をしていないので、「失効数0件のCRL」が作成されます。

```:ca-list
ca1.mydomain
ca2.mydomain
```

## CAのサブディレクトリとファイル
スクリプトを実行すると、各CAのディレクトリに以下のサブディレクトリとファイルが、デフォルトの名前で自動的に生成されます。

```Bash:
.
├── ca1.mydomain/
│   ├── certs/
│   ├── crl/           # CRLの出力用ディレクトリ
│   ├── newcerts/
│   ├── private/
│   │   └── cakey.pem
│   ├── cacert.pem
│   ├── crlnumber      # 次回のCRL番号ファイル。初期値が必要だが、インクリメントは自動
│   ├── index.txt
│   └── serial
```

## CRL一括生成用スクリプト
以上の仕様でスクリプトを作成します。

```Bash:update-crl.sh
#!/bin/bash

CATOP="$(dirname "${0}")"

# CRLを作成するCAのコモンネームをCAリストから1件ずつ読み込む
while read ca_name
do
  [[ ${ca_name} != "" ]] || continue

  # CAのディレクトリ
  ca_dir="${CATOP}/${ca_name}"

  # CRLのサブディレクトリとファイルがなければ作成する。
  #   - crlnumberにはCRL番号の初期値を出力する。
  mkdir -p "${ca_dir}/crl"
  [[ -e "${ca_dir}/crlnumber" ]] || echo "00" >"${ca_dir}/crlnumber"

  # CAのディレクトリを環境変数に出力する。
  export CA_DIR="${ca_dir}"

  # caコマンドでCRLを生成する。
  #   -gencrl     CRLを生成する
  #   -crldays    次回CRL更新までの日数
  #   -out        出力するCRLファイル
  #   -passin     CAの秘密鍵のパスフレーズ入力
  openssl ca \
      -gencrl \
      -crldays  365 \
      -out      "${ca_dir}/crl/crl.pem" \
      -passin   "file:${CATOP}/.capass"

done <"${CATOP}/ca-list"
```
注意: `-crldays`で次回CRL更新までの日数を指定します。**この期限を過ぎてCRLが更新されないとすべての証明書が認証されなくなってしまうので、CRLの運用が確立していない場合は多めの日数を指定します。**このオプションを指定しない場合は、設定ファイルの`default_crl_days`の値が使用されます。

参考: `ca`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-ca.html


コマンドが正常に修了すると、以下のようにファイルが生成／更新されます。

```Bash:
.
├── ca1.mydomain
│   ├── certs
│   │   ├── vpnclient_1.crt.pem
│   │   ├── vpnclient_1.key.pem
│   │   ├── vpnserver_1.crt.pem
│   │   └── vpnserver_1.key.pem
│   ├── crl
│   │   └── crl.pem     # ca1.mydomainのCRL
│   ├── newcerts
│   ├── private
│   │   └── cakey.pem
│   ├── cacert.pem
│   ├── crlnumber       # CRL番号ファイル
│   ├── crlnumber.old
│   ├── index.txt
│   ├── index.txt.attr
│   ├── index.txt.attr.old
│   ├── index.txt.old
│   ├── serial
│   └── serial.old
├── ca2.mydomain
│   ├── certs
│   │   ├── vpnserver_2.crt.pem
│   │   └── vpnserver_2.key.pem
│   ├── crl
│   │   └── crl.pem     # ca2.mydomainのCRL
│   ├── newcerts
│   ├── private
│   │   └── cakey.pem
│   ├── cacert.pem
│   ├── crlnumber       # CRL番号ファイル
│   ├── crlnumber.old
│   ├── index.txt
│   ├── index.txt.attr
│   ├── index.txt.old
│   ├── serial
│   └── serial.old
├── .capass
├── ca-list
├── make-crt.sh
├── newca.sh
├── revoke-list
├── revoke.sh
├── target-list
└── update-crl.sh
```

各CAで初めてのCRLを発行したので、`crlnumber`の初期値「00」が正しくインクリメントされて、次回のCRL番号「01」になりました。

```Shell:
find . -name crlnumber | xargs grep .

./ca1.mydomain/crlnumber:01
./ca2.mydomain/crlnumber:01
```
CA「ca1.mydomain」のCRLの内容を、以下のコマンドで確認します。
参考: `crl`コマンド manpage (1.0.2): https://www.openssl.org/docs/man1.0.2/man1/openssl-crl.html

```Shell:
openssl crl -in ca1.mydomain/crl/crl.pem -text -noout
```

```Shell:
Certificate Revocation List (CRL):
        Version 2 (0x1)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: /C=JP/ST=Chiba/O=myorg/CN=ca1.mydomain
        Last Update: Aug 31 16:39:28 2019 GMT
        Next Update: Aug 30 16:39:28 2020 GMT
        CRL extensions:
            X509v3 CRL Number: 
                0
Revoked Certificates:
    Serial Number: 1001
        Revocation Date: Aug 31 16:20:04 2019 GMT
    Signature Algorithm: sha256WithRSAEncryption
（略）
```
各項目の値が以下のようになっていることが確認できます。

* `Next Update:`次回CRL更新までの日数を365と指定したので、1年後の日付けになっています。この日までに次のCRLをサーバーに再配布しないと、すべての証明書が認証されなくなります。
* `X509v3 CRL Number:`crlnumberで「00」を指定したので、CRL番号が0になっています。
* `Revoked Certificates:`失効された証明書のシリアル番号「1001」が記載されています。複数の証明書を失効した後でCRLを発行すると、失効したすべての証明書のシリアル番号がここに列挙されます。

もう1つのCA「ca2.mydomain」には失効した証明書がないので、CRLの内容を見ると「No Revoked Certificates.」になっています。

```Shell:
openssl crl -in ca2.mydomain/crl/crl.pem -text -noout

Certificate Revocation List (CRL):
        Version 2 (0x1)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: /C=JP/ST=Chiba/O=myorg/CN=ca2.mydomain
        Last Update: Aug 31 16:39:28 2019 GMT
        Next Update: Aug 30 16:39:28 2020 GMT
        CRL extensions:
            X509v3 CRL Number: 
                0
No Revoked Certificates.
    Signature Algorithm: sha256WithRSAEncryption
（略）
```
以上で、証明書を1件だけ失効したCA、1件も失効していないCAともに、CRLが正しく作成されたことが確認できました。

# OpenVPNサーバーにCRLを配布する
発行したCRLファイルを OpenVPN サーバーの任意の場所に配置して、設定ファイルに以下の行を追加します。

```:/etc/openvpn/任意の名前.conf
crl-verify CRLのパス
```
OpenVPN サーバーを再起動してCRLを認識させます。クライアントは接続したままで構いません。

```Shell:
systemctl restart openvpn@任意の名前.service
```

正常に起動すれば証明書を失効したクライアントだけが再接続されず、その他のクライアントは自動的に再接続されます。
以後も新しいCRLを作成したら、ファイルを上書きして OpenVPN サーバーを再起動します。

# あとがき
以上で、今度こそopensslコマンドを理解して使いたいと考えていた作業と調査を終了します。
まだまだほんの一部を知ったに過ぎませんが、簡易ツールやコピペで対応していた時に比べれば、多少は自信を持って証明書の作成と運用ができるようになりました。

# 記事一覧
[今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する](https://qiita.com/3244/items/780469306a3c3051c9fe)
[今度こそopensslコマンドを理解して使いたい (2) 設定ファイル（openssl.cnf）を理解する](https://qiita.com/3244/items/8c7e6892efdb4e500be9)
[今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する](https://qiita.com/3244/items/2a2a2dc6cd1e2e35beb9)
[今度こそopensslコマンドを理解して使いたい (4) サーバー／クライアント証明書を一括生成する](https://qiita.com/3244/items/2618429ebe6dc16c074e)
今度こそopensslコマンドを理解して使いたい (5) CRL（証明書失効リスト）を作成してOpenVPNに配布する
[今度こそopensslコマンドを理解して使いたい (補足1) サンプルスクリプトのまとめ](https://qiita.com/3244/items/65dd3926fab76b9dd4aa)