Edited at

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

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

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

今回は証明書の失効とCRL(証明書失効リスト)の発行をスクリプトで処理します。

また実際にOpenVPNサーバーにCRLを配布して、失効したクライアント証明書が認証されなくなることを確認します。


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回第3回

CA証明書
X
X

第1回第3回

サーバー秘密鍵
X

第4回

サーバー証明書
X

第4回

クライアント秘密鍵

X
第4回

クライアント証明書

X
第4回

DHパラメータ
X

今回

TLS認証鍵
X
X
今回

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


DHパラメータ

セッションの共通鍵を安全に生成するための Diffie Hellman パラメータを、openssldhparamコマンドを使って作成します。

-outで任意のファイル名を指定して、最後に鍵長をビット数で指定します。2048はデフォルト値なので、省略しても同じ結果になります。

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コマンドで作成します。以下のコマンドで、任意のファイル名を指定して作成します。

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で行います。

前回、以下のように CA「ca1.mydomain」で「vpnserver_1」と「vpnclient_1」の証明書を発行して署名しました。

├── 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つの証明書の情報が正しく記録されています。

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回 「環境変数」


/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をそのまま使用して、先頭行で定義されたパスフレーズを入力します。


.capass

mycap@ss



証明書一括失効用スクリプト

以上の仕様でスクリプトを作成します。エラー処理などは省略しています。


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」は失効した証明書のシリアル番号です。

Using configuration from /etc/pki/tls/openssl.cnf

Revoking Certificate 1001.
Data Base Updated

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

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列目に失効日時が追加されたことが分かります。

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を作成した時に使用したファイルca-listを使用します。リスト内のすべてのCAでCRLを作成します。「ca2.mydomain」ではまだ1つも失効処理をしていないので、「失効数0件のCRL」が作成されます。


ca-list

ca1.mydomain

ca2.mydomain


CAのサブディレクトリとファイル

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

.

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


CRL一括生成用スクリプト

以上の仕様でスクリプトを作成します。


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

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

.

├── 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」になりました。

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

openssl crl -in ca1.mydomain/crl/crl.pem -text -noout

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.」になっています。

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を認識させます。クライアントは接続したままで構いません。

systemctl restart openvpn@任意の名前.service

正常に起動すれば証明書を失効したクライアントだけが再接続されず、その他のクライアントは自動的に再接続されます。

以後も新しいCRLを作成したら、ファイルを上書きして OpenVPN サーバーを再起動します。


あとがき

以上で、今度こそopensslコマンドを理解して使いたいと考えていた作業と調査を終了します。

まだまだほんの一部を知ったに過ぎませんが、簡易ツールやコピペで対応していた時に比べれば、多少は自信を持って証明書の作成と運用ができるようになりました。


記事一覧

今度こそopensslコマンドを理解して使いたい (1) ルートCAをスクリプトで作成する

今度こそopensslコマンドを理解して使いたい (2) 設定ファイル(openssl.cnf)を理解する

今度こそopensslコマンドを理解して使いたい (3) CA証明書の拡張設定を検証する

今度こそopensslコマンドを理解して使いたい (4) サーバー/クライアント証明書を一括生成する

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

今度こそopensslコマンドを理解して使いたい (補足1) サンプルスクリプトのまとめ