以前、会社で作る認証局、と称してCAを作り、宅内でHTTPSの通信ができるようにした。
また、クライアント証明書編として、インターネット側に開いている口をクライアント証明書でアクセス制限するようにもした。
この運用を始めて8年が経過し、Webサーバーの証明書はLet's Encryptが発行するものに置き換わった。
しかし、クライアント証明書はまだまだ現役で頑張ってくれているし、今後も継続して利用していくことになるので、ここで運用を整理することにした。
当初はCA.shも提供されていたが、今はCA.plのみが提供されている。
CA.plは初心者向けのラッパーだ、とか、上手く動かない、といった話を以前から見かけてはいるものの、それはそれ、気にせず利用を継続する。
運用の構想
CAのディレクトリを丸ごとコピーしたら、別のサーバーでも稼働させられるようにしたい。
環境設定が少々面倒なので、CA.plのラッパーを作って、そのあたりの下処理をさせる。
現在運用中のCAを後もう少し継続利用し、新たなCAを作成する並行稼働を考慮。
サーバー証明書は自分でしか使わないし、少人数のグループで利用するなら使える鍵と証明書を渡すのが簡単。
クライアント証明書に至っては、「CSRを作ってくれ」なんて家族に伝えたところで通じない。
よって、ここは割り切り、こちらで鍵と証明書を作って渡す運用とする。
ツール収集
CA.pl
CA.plはこちらに入っている。
/usr/lib/ssl/misc/CA.pl
これをCAのディレクトリにコピーしてきて、ちょっとだけ更新する。
CA.pl
my $CATOP = $ENV{'MY_OPENSSL_CA_ROOT'};
my $DAYS = "-days 3650";
my $CADAYS = "-days 10950"; # 30 years
} elsif ($WHAT eq '-sign' ) {
$RET = run("$CA -policy policy_anything -out $NEWCERT $DAYS"
. " -infiles $NEWREQ $EXTRA{ca}");
print "Signed certificate is in $NEWCERT\n" if $RET == 0;
これは変更点の抜粋で、内容は以下の通り。
- 環境変数からCAのディレクトリが設定されるようにする。
- 発行する証明書は10年使えるものにする。
- CAは30年使えるようにする。
- 証明書を発行する際、有効期間を10年に設定するパラメーターがつくようにする。
CAも証明書も有効期間が長すぎるけれど、駄目かな、と思ったら失効させればいいや、と割り切り。
openssl.cnf
openssl.cnfはこちらに入っている。
/etc/ssl/openssl.cnf
これもCAのディレクトリにコピーして、ちょっとだけ更新する。
openssl.cnf
[ CA_default ]
dir = $ENV::MY_OPENSSL_CA_ROOT # Where everything is kept
copy_extensions = copy #コメント解除
[ req_distinguished_name ]
countryName_default = JP
stateOrProvinceName_default = Tokyo
0.organizationName_default = $ENV::MY_OPENSSL_CA_NAME
commonName_default = hogeserver.hogeddns.jp
emailAddress_default = webmaster@hogeserver.hogeddns.jp
これも変更点の抜粋で、内容は以下の通り。
- 環境変数からCAのディレクトリが設定されるようにする。
- 対話形式で情報を入力するときのため、デフォルト値をCAの情報にする。
CAのルートディレクトリは、CA.plで指定しただけでは上手く動かないので、こちらにも設定が必要。
ラッパーの作成
ca.configというのを作って、ラッパーから呼び出し、基本的な環境変数を設定できるようにする。
ラッパーは4つ作成する。
- CA.plを呼び出すだけのもの
- サーバー証明書を発行するもの
- クライアント証明書を発行するもの
- 証明書を失効させるもの
CA.plを呼び出すラッパー
ca.config
基本的な環境変数を設定する。
hogeCAのところを書き換えれば、新たにCAを作成する時にはその名前でディレクトリが作られる。
既存のCAを管理する時には、そのディレクトリ名を指定しておく。
# CAが置かれているディレクトリ名
export MY_OPENSSL_CA_NAME="hogeCA"
# スクリプトが置かれているディレクトリを取得
export MY_OPENSSL_BASE="$BASEDIR"
# CAのルートディレクトリ
export MY_OPENSSL_CA_ROOT="$MY_OPENSSL_BASE/$MY_OPENSSL_CA_NAME"
#
# 使用するcnfファイル
export OPENSSL_CONFIG="-config $MY_OPENSSL_BASE/$USECONF"
ca
環境変数を設定してCA.plを呼び出すだけのラッパー。
作成したら実行権限を付けておく。
#!/bin/bash
# 使用する設定ファイル
USECONF="openssl.cnf"
# スクリプトが置かれているディレクトリにあるca.configを読み込む
BASEDIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
source $BASEDIR/ca.config
# CA.plを実行
$MY_OPENSSL_BASE/CA.pl "$@"
サーバー証明書を発行するラッパー
server.cnf
openssl.cnfをインクルードした上で、サーバー証明書を作るために設定を上書きする。
# 基本情報を取り込む
.include $ENV::MY_OPENSSL_BASE/openssl.cnf
# 要求元サーバーの名前
[ req_distinguished_name ]
0.organizationName_default = Hoge
commonName_default = www.hogeserver.hogeddns.jp
# 要求元サーバーのSAN
[ req ]
req_extensions = v3_req
[ v3_req ]
subjectAltName=@v3_req_altname
[ v3_req_altname ]
DNS.1=hogeserver.hogeddns.jp
DNS.2=*.hogeserver.hogeddns.jp
#IP.1=192.168.nnn.nnn
# 署名時の追加情報(現時点で一般的なブラウザは利用しない模様)
# サーバー証明書を失効させたことを知らせるURLを指定する
# 複数CAを運営している場合でもまとめられないのでCA毎に指定
[ CA_default ]
x509_extensions = server_cert
[ server_cert ]
crlDistributionPoints = URI:http://www.hogeserver.hogeddns.jp/hoge.crl
server
サーバー証明書を発行するラッパー。
証明書署名要求から作成する、お節介仕様。
作成したら実行権限を付けておく。
#!/bin/bash
# 使用する設定ファイル
USECONF="server.cnf"
# スクリプトが置かれているディレクトリにあるca.configを読み込む
BASEDIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
source $BASEDIR/ca.config
# パラメーターが指定されていないときに使い方を表示
if [ -z "$1" ]; then
echo "Usage: $0 <サーバー名など何らかの名前>"
echo " 事前にserver.cnfに必要事項をセットしておくこと"
exit 1
fi
# 証明書用のディレクトリを作成
DATESTR=$(date +%Y%m%d)
WORKDIR="$MY_OPENSSL_CA_ROOT/certs/server/${DATESTR}_$1"
if [ ! -d "$WORKDIR" ]; then
mkdir -p "$WORKDIR"
fi
# 使用したパラメーターをシフトして残りをCA.plに渡す準備
shift
# CA.plを実行
cd "$WORKDIR"
echo "証明書署名要求を生成します"
$MY_OPENSSL_BASE/CA.pl -newreq-nodes "$@" -extra-req "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1" || {
echo "証明書署名要求の生成に失敗しました" >&2
exit 1
}
echo "証明書に署名します"
$MY_OPENSSL_BASE/CA.pl -sign "$@" || {
echo "証明書への署名に失敗しました" >&2
exit 1
}
if [ ! -f "newcert.pem" ];then
echo "証明書に署名しませんでした" >&2
exit 1
fi
echo "証明書を生成しました: $WORKDIR"
クライアント証明書を発行するラッパー
client.cnf
openssl.cnfをインクルードした上で、クライアント証明書を作るために設定を上書きする。
# 基本情報を取り込む
.include $ENV::MY_OPENSSL_BASE/openssl.cnf
# 発行先のCN
[ req_distinguished_name ]
commonName_default = $ENV::MY_OPENSSL_USER_NAME
emailAddress_default = $ENV::MY_OPENSSL_USER_EMAIL
# 発行する証明書のSAN
[ usr_cert ]
nsCertType = client
subjectAltName = email:copy
client
クライアント証明書を発行するラッパー。
メールアドレスを元に証明書署名要求から作成し、パスワードを適当に作ってPFX形式までにする、お節介仕様。
作成したら実行権限を付けておく。
#!/bin/bash
# 使用する設定ファイル
USECONF="client.cnf"
# スクリプトが置かれているディレクトリにあるca.configを読み込む
BASEDIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
source $BASEDIR/ca.config
# パラメーターが指定されていないときに使い方を表示
if [ -z "$2" ]; then
echo "Usage: $0 <ユーザーのメールアドレス>"
exit 1
fi
# メールアドレスから名前とメールアドレスをセット
export MY_OPENSSL_USER_NAME="${1%%@*}"
export MY_OPENSSL_USER_EMAIL="$1"
# 使用したパラメーターをシフトして残りをCA.plに渡す準備
shift
# 証明書用のディレクトリを作成
DATESTR=$(date +%Y%m%d)
WORKDIR="$MY_OPENSSL_CA_ROOT/certs/client/${DATESTR}_$MY_OPENSSL_USER_NAME"
if [ ! -d "$WORKDIR" ]; then
mkdir -p "$WORKDIR"
fi
# CA.plを実行
cd "$WORKDIR"
echo "証明書署名要求を生成します"
$MY_OPENSSL_BASE/CA.pl -newreq-nodes "$@" -extra-req "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1" || {
echo "証明書署名要求の生成に失敗しました" >&2
exit 1
}
echo "証明書に署名します"
$MY_OPENSSL_BASE/CA.pl -sign "$@" || {
echo "証明書への署名に失敗しました" >&2
exit 1
}
echo "証明書をPFX形式にします"
PFXNAME="$WORKDIR/$MY_OPENSSL_CA_NAME_$MY_OPENSSL_USER_NAME.pfx"
PFXPASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
openssl pkcs12 -export -in "$WORKDIR/newcert.pem" -inkey "$WORKDIR/newkey.pem" -out "$PFXNAME" \
-name "$MY_OPENSSL_CA_NAME($MY_OPENSSL_USER_NAME)" -passout "pass:$PFXPASS" || {
echo "PFX形式のファイル作成に失敗しました" >&2
exit 1
}
echo "$PFXPASS" > "$WORKDIR/password.txt"
# 結果の表示
echo "PFXファイル: $PFXNAME"
echo "PFXファイルのパスワード: $PFXPASS"
証明書を失効させるラッパー
revoke
CA.plを使って証明書を失効させ、証明書失効リストを作成する。
2回コマンドを実行するので、CAの秘密鍵のパスワードを2回聞かれてしまうが、そこには手を入れなかった。
作成したら実行権限を付けておく。
#!/bin/bash
# 使用する設定ファイル
USECONF="openssl.cnf"
# スクリプトが置かれているディレクトリにあるca.configを読み込む
BASEDIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
source $BASEDIR/ca.config
# パラメーターが指定されていないときに使い方を表示
if [ -z "$1" ]; then
echo "Usage: $0 <失効させる証明書のフルパスファイル名> <理由>"
echo "Reason: unspecified, keyCompromise, CACompromise,"
echo " affiliationChanged, superseded, cessationOfOperation"
echo " certificateHold, removeFromCRL"
exit 1
fi
# 引数を取り出す
PATHNAME=$1
if [ -z "$2" ]; then
REASON="unspecified"
else
REASON=$2
fi
# CA.plを実行
echo "証明書を失効させます"
$MY_OPENSSL_BASE/CA.pl -revoke "$PATHNAME" "$REASON" || {
echo "証明書を失効させられませんでした"
exit 1
}
echo "証明書失効リストを生成します"
openssl ca $OPENSSL_CONFIG -gencrl -out "$MY_OPENSSL_CA_ROOT/crl/$MY_OPENSSL_CA_NAME.crl" || {
echo "証明書失効リストを生成できませんでした"
exit 1
}
echo "証明書を失効させました"
今まで理由を設定したことがなかったが、理由が無指定の場合にはunspecifiedを設定するようにした。
opensslを呼び出すラッパー
openssl
環境変数を事前に設定しておかないと、opensslコマンドを上手く実行することができない。
細やかな作業をするにあたって、これはちょっと困る。
#!/bin/bash
# 使用する設定ファイル
USECONF="openssl.cnf"
# スクリプトが置かれているディレクトリにあるca.configを読み込む
BASEDIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
source $BASEDIR/ca.config
# 引数が指定されていない場合はそのままopensslを呼び出して終了
if [ $# -lt 1 ]; then
/usr/bin/openssl
exit 1
fi
# 第1引数(サブコマンド:ca, req, x509など)を取り出す
SUBCOMMAND="$1"
shift
# サブコマンドの直後に -config と設定ファイルパスを挟み込んで実行
/usr/bin/openssl "$SUBCOMMAND" -config "$MY_OPENSSL_BASE/$USECONF" "$@"
運用
CAの構築
準備ができたらCAを作る。
方法を2つ用意したが、今から作るなら、ECDSA(P-256)の方が良さそうだ。
コマンドを実行すると、opensslのコマンドが2回実行される。
# ECDSA(P-256) (鍵長が256bitと短い、署名時に乱数を使用しない、強度はRSA 2048bitと同等)
$ ./ca -newca -extra-req "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1"
CA certificate filename (or enter to create)
[enter]
Making CA certificate ...
====
openssl req -config /path/to/ca/openssl.cnf -new -keyout /path/to/ca/hogeCA/private/cakey.pem -out /path/to/ca/hogeCA/careq.pem -newkey ec -pkeyopt ec_paramgen_curve:prime256v1
Enter PEM pass phrase:[CAの秘密鍵のパスワード]
Verifying - Enter PEM pass phrase:[パスワードの確認入力]
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [JP]:[Enter]
State or Province Name (full name) [Tokyo]:[Enter]
Locality Name (eg, city) []:[Enter]
Organization Name (eg, company) [HOGE]:[Enter]
Organizational Unit Name (eg, section) []:[Enter]
Common Name (e.g. server FQDN or YOUR name) [hogeserver.hogeddns.jp]:[Enter]
Email Address [webmaster@hogeserver.hogeddns.jp]:[Enter]
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:[Enter]
An optional company name []:[Enter]
==> 0
====
====
openssl ca -config /path/to/ca/openssl.cnf -create_serial -out /path/to/ca/hogeCA/cacert.pem -days 10950 -batch -keyfile /path/to/ca/hogeCA/private/cakey.pem -selfsign -extensions v3_ca -infiles /path/to/ca/hogeCA/careq.pem
Using configuration from /path/to/ca/openssl.cnf
Enter pass phrase for /path/to/ca/hogeCA/private/cakey.pem:[CAの秘密鍵のパスワード]
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number:
21:21:17:d7:86:92:6d:ca:7a:14:84:af:6f:6e:6a:86:ed:e2:c9:74
Validity
Not Before: Oct 18 07:01:22 2026 GMT
Not After : Oct 10 07:01:22 2056 GMT
Subject:
countryName = JP
stateOrProvinceName = Tokyo
organizationName = HOGE
commonName = hogeserver.hogeddns.jp
emailAddress = webmaster@hogeserver.hogeddns.jp
X509v3 extensions:
X509v3 Subject Key Identifier:
4B:EC:87:61:EA:51:35:9B:D4:E5:38:88:65:68:28:09:BB:3E:6C:65
X509v3 Authority Key Identifier:
4B:EC:87:61:EA:51:35:9B:D4:E5:38:88:65:68:28:09:BB:3E:6C:65
X509v3 Basic Constraints: critical
CA:TRUE
Certificate is to be certified until Oct 10 07:01:22 2056 GMT (10950 days)
Write out database with 1 new entries
Database updated
==> 0
====
CA certificate is in /path/to/ca/hogeCA/cacert.pem
できあがるディレクトリの構成は以下。
hogeCA
├─ cacert.pem CAの自己署名ルート証明書
├─ careq.pem CAの証明書署名リクエスト(その後は使用しない)
├─ private/ CAの秘密鍵が生成されるディレクトリ
│ └─ cakey.pem CAの秘密鍵
├─ index.txt これまでに発行した証明書のステータス、シリアル番号、有効期限、識別子(DN)等
├─ index.txt.attr unique_subject = yes(or no)で識別子(Subject/CN)の重複の可否を決める
├─ index.txt.old index.txtのバックアップ
├─ newcerts/ CAが署名したすべての証明書の控え
│ └─ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.pem
├─ certs/ CAの管理者が発行した証明書を手動で管理しておくディレクトリ
├─ crl/ 証明書の失効リストが生成されるディレクトリ
├─ serial 次に発行する証明書のシリアル番号
└─ crlnumber 次に発行する証明書失効リストの番号
識別子の重複可否を設定
サーバー証明書を更新するタイミングでは、同じCNの証明書に署名することもあると思われる。
この場合、以下を書き換える。
hogeCA/index.txt.attr
unique_subject = no
クライアント証明書だけを管理するときには、識別子の重複は考えなくても良いかもしれない。
この辺りは運用の計画によって判断すればOK。
証明書失効リストの作成
最初は何も失効させていないのだけれど、失効リストがないとApacheが証明書の検証に失敗するので作っておく。
$ ./openssl ca -gencrl -out hogeCA/crl/hogeCA.crl
サーバー証明書の発行
サーバー証明書は、以下のラッパーで発行できる。
事前にserver.cnfで初期値を変えておくこともできるが、コマンド実行時に手で入力しても問題はない。
$ ./server www
ラッパーは、証明書署名要求を作成し、署名まで行う。
署名する際には、CAの秘密鍵のパスワードを要求されるので、正しく入力する。
証明書は、「日付_指定した名前」のディレクトリに作成される。
hogeCA/certs/server/20261018_www/
├─ newcert.pem
├─ newkey.pem
└─ newreq.pem
クライアント証明書の発行
クライアント証明書は、以下のラッパーで発行できる。
メールアドレスからCNを作成するようになっている。
$ ./client hoge@hogeserver.hogeddns.jp
ラッパーは、証明書署名要求を作成し、署名まで行う。
署名する際には、CAの秘密鍵のパスワードを要求されるので、正しく入力する。
証明書とPFXのパスワードは、「日付_アカウント名」のディレクトリに作成される。
PFXのパスワードは、ラッパーの中で8桁の文字列として自動生成されている。
hogeCA/certs/client/20261018_hoge/
├─ newcert.pem
├─ newcert.pfx
├─ newkey.pem
├─ newreq.pem
└─ password.txt
証明書を失効させる
証明書を指定して失効させる。
$ ./revoke hogeCA/certs/server/20261018_www/newcert.pem
$ ./revoke hogeCA/certs/client/20261018_hoge/newcert.pem unspecified
証明書の失効と、証明書失効リストの作成のため、2回CAの秘密鍵のパスワードを要求される。
ちょっと手間が掛かってしまって格好悪いが、ご愛敬。
証明書失効リストはこちらに生成される。
hogeCA/crl/
└─ hogeCA.crl
証明書を失効させる場合に、その理由を設定することができる。
以下の表はGeminiさんが生成してくれたもの。
| 指定可能な値 | 意味 | 備考 |
|---|---|---|
| unspecified | 理由未指定 | ラッパーでは理由未指定ならこれを設定 |
| keyCompromise | 秘密鍵の漏洩 | |
| CACompromise | CA秘密鍵の漏洩 | 理屈としてはCA無効化=署名した証明書全てが無効 とはいえ、一般のアプリはそこまで見に行かない |
| affiliationChanged | 組織名や所属の変更 | |
| superseded | 証明書の置き換え・更新 | |
| cessationOfOperation | 運用停止 | |
| certificateHold | 一時的な保留 | 復活はhogeCA/index.txtを手で書き換える |
| removeFromCRL | CRLからの削除 | この理由を付けても恐らくApacheは無効と判定する |
CRLの内容を表示させると、この理由が一緒に表示される。
今のところ人間的に意味があるが、一般的なアプリケーションは意味を気にすることはない、とみられる。
Apacheでの保護
CA証明書と証明書失効リストを作成し、それをApacheに設定していく。
CA証明書と証明書失効リストの準備
今まで運用しているCAは1つだった。
ルートCAとして認識させるために、/usr/local/share/ca-certificatesにCA証明書を置いていたので、それをそのままApacheでも利用していた。
今後、当面の間は2つのCAを運用しようとしているので、2つのCA証明書を連結する。
そうなると、いつもの場所に置くのも微妙なので、専用のディレクトリ/etc/apache2/sslを作ることにした。
せっかくディレクトリを作るので、ついでに証明書失効リストも入れておくことにした。
Apacheで利用するCA証明書と証明書失効リストは、連結して配置する。
Geminiにツールを作ってもらったけれど、手作業で実施しても全然問題ない。
/etc/apache2/ssl/ca-bundle.crt
-----BEGIN CERTIFICATE-----
MIINNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
<省略>
NNNNNNNNNNNNNNNNNN==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIINNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
<省略>
NN==
-----END CERTIFICATE-----
/etc/apache2/ssl/ca-bundle.crl
-----BEGIN X509 CRL-----
MIINNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
<省略>
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN==
-----END X509 CRL-----
-----BEGIN X509 CRL-----
MIINNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
<省略>
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
-----END X509 CRL-----
一方、クライアントに利用してもらう証明書は個別に取り扱う。
CA証明書はWindowsに配布するならDER形式が一般的とのこと。
生成したCA証明書をユーザーに配布し、ユーザーはルート認証局として登録する。
$ openssl x509 -in hogeCA/cacert.pem -outform DER -out hoge.crt
また、HTTPで公開する証明書失効リストもDERが一般的だそう。
こちらは、今回は/var/www/crlに配置した。
$ openssl crl -in hogeCA/crl/hogeCA.crl -outform DER -out hoge.crl
$ sudo mkdir /var/www/crl
$ sudo mv hoge.crl /var/www/crl/
$ sudo chown -R www-data:www-data /var/www/crl
証明書関連のファイルは準備できた。
クライアント証明書の提示を求める
いつもの設定に、特定のディレクトリにアクセスする条件として、
- 特定のIPアドレスからのアクセスであること。
- それ以外のIPアドレスからアクセスする場合には、CAが署名したクライアント証明書を提示すること。
という条件を付ける。
アプリケーションが証明書をどう取り扱うか、というのはまた別の話。
/etc/apache2/sites-available/service.conf
<VirtualHost *:443>
ServerName www.hogeserver.hogeddns.jp
ServerAdmin webmaster@hogeserver.hogeddns.jp
DocumentRoot /var/www/html
...
#
# Protection
#
SSLCACertificateFile /etc/apache2/ssl/ca-bundle.crt
SSLCARevocationFile /etc/apache2/ssl/ca-bundle.crl
SSLCARevocationCheck leaf
<LocationMatch ^/(secret|privacy|personal)>
<If "%{REMOTE_ADDR} =~ m/192\.168\.110\.[0-9]+/ || \
%{REMOTE_ADDR} =~ m/2001:db8:[0-9a-f:]+/">
</If>
<Else>
SSLVerifyClient require
SSLVerifyDepth 1
Require expr (%{SSL_CLIENT_VERIFY} == "SUCCESS")
</Else>
</LocationMatch>
</VirtualHost>
全てのディレクトリにアクセス制限を掛けるなら、
<LocationMatch ^/>
全てのIPアドレスからのアクセスに証明書の提示を求めるなら、
SSLVerifyClient require
SSLVerifyDepth 1
<Directory />
Require expr (%{SSL_CLIENT_VERIFY} == "SUCCESS")
</Directory>
という書き方ができる。
証明書失効リストの提示
Geminiさんによれば、証明書の中にCRLのURLを埋め込んだ場合、クライアントはその証明書が失効されていないか確認しに来てくれる、とのこと。
従来は、HTTPへのアクセスを無条件にHTTPSにリダイレクトしていたが、証明書失効リストは平文で提供する必要がある。
そこで、CRLへの要求のみリダイレクト対象から除外することにした。
/etc/apache2/sites-available/service.conf
<VirtualHost *:80>
ServerName www.hogeserver.hogeddns.jp
ServerAdmin webmaster@hogeserver.hogeddns.jp
DocumentRoot /var/www/crl
AddType application/pkix-crl .crl
ErrorLog ${APACHE_LOG_DIR}/my-error.log
CustomLog ${APACHE_LOG_DIR}/my-access.log combined
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{REQUEST_URI} !^/(hoge-old|hoge)\.crl$
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI}
</VirtualHost>
指定するCRLはDER形式で、CA毎に作成するのが一般的な模様。
よってApacheで利用するおまとめ形式は使わず、個別にアクセスできるように設定している。
設定の反映
準備ができたので、Apacheに設定を反映させる。
$ sudo systemctl reload apache2
テスト
クライアント証明書によるアクセス制限
Androidのスマートフォンを使って、外部から新旧のクライアント証明書を提示してみた。
証明書を提示しなければエラーとなり、新旧いずれかを提示すればアクセスができた。
スマートフォンで証明書を切り替えるときには、ブラウザを強制終了させるのが手っ取り早かった。
証明書失効リスト
証明書失効リストには、curlでアクセスしてみた。
$ curl -i http://www.hogeserver.hogeddns.jp/hoge.crl
HTTP/1.1 200 OK
Date: Sat, 24 Oct 2026 19:57:18 GMT
Server: Apache
Upgrade: h2,h2c
Connection: Upgrade
Last-Modified: Sat, 24 Oct 2026 19:55:05 GMT
ETag: "23e-65e9b792194d4"
Accept-Ranges: bytes
Content-Length: 574
Content-Type: application/pkix-crl
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
中身をみてみる場合には、こんなコマンドが使えた。
$ curl -s http://www.hogeserver.hogeddns.jp/hoge.crl | openssl crl -inform DER -noout -text
Certificate Revocation List (CRL):
Version 2 (0x1)
Signature Algorithm: sha256WithRSAEncryption
...
Revoked Certificates:
Serial Number: 5F71498A34704E51EEDE18EFB477D5E8917269C9
Revocation Date: Oct 24 22:12:13 2026 GMT
CRL entry extensions:
X509v3 CRL Reason Code:
Unspecified
...
問題なく公開できているように見える。
失効したサーバー証明書での通信
失効したサーバー証明書をApacheで使ってみた。
以下のコマンドで、サーバーが使っている証明書がどんなものなのかが分かる。
$ openssl s_client -connect www.hogeserver.hogeddns.jp:443 -servername www.hogeserver.hogeddns.jp </dev/null 2>/dev/null | openssl x509 -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
5f:71:49:8a:34:70:4e:51:ee:de:18:ef:b4:77:d5:e8:91:72:69:c9
...
X509v3 extensions:
X509v3 CRL Distribution Points:
Full Name:
URI:http://www.hogeserver.hogeddns.jp/hoge.crl
X509v3 Basic Constraints:
CA:FALSE
...
1つ前の項で調べた失効したシリアルナンバーと一致している。
しかしChromeは当該サーバーに普通にアクセスしている。
証明書の詳細を表示させると「CRL 配布ポイント」は表示されているが、失効されているという認識はしない。
将来的にはCRLをフェッチするようになるかもしれないが、少なくとも今は見ていない。
サーバーの秘密鍵が漏れたら、即座に利用を中止して、新しいものに差し替えるのが現状の最善。
サーバー証明書の期間がどんどん短くなるのも、こういった理由からなのかもしれない。
openssl.cnfの構造
以前CAを作成したときには、こうやればいい、という検索結果をそのまま設定していた。
どういう構造なのかをあまり理解していなかったので、何かをする度にチョイチョイとopenssl.cnfを書き換えていた。
サーバーの証明書とクライアントの証明書で、微妙に設定を変えるような運用で、面倒かつ危なっかしいものだった。
今回は、最初にGeminiにこの構造を分析してもらい、以下を出力してもらった。
その上で、server.cnfとclient.cnfに設定を分割したので、だいぶ安心して運用ができるようになっている。
[ Global Configuration ]
├── new_oids (OID追加用)
├── openssl_init
│ └── provider_sect
│ └── default = default_sect
│ └── [ default_sect ]
│ └── activate = 1 (コメントアウト)
│
├── [ ca ] (CA統括セクション)
│ └── default_ca = CA_default
│ └── [ CA_default ] (CA運用メイン設定)
│ ├── x509_extensions = usr_cert
│ │ └── [ usr_cert ] (通常ユーザー証明書用拡張)
│ └── policy = policy_match
│ ├── [ policy_match ] (厳格なポリシー)
│ └── [ policy_anything ] (柔軟なポリシー)
│
├── [ req ] (証明書署名リクエスト設定)
│ ├── distinguished_name = req_distinguished_name
│ │ └── [ req_distinguished_name ] (DNのデフォルト値設定)
│ ├── attributes = req_attributes
│ │ └── [ req_attributes ] (チャレンジパスワード等)
│ └── x509_extensions = v3_ca
│ └── [ v3_ca ] (自己署名CA証明書用拡張)
│
├── 独立した拡張セクション群
│ ├── [ v3_req ] (CSR用拡張)
│ ├── [ crl_ext ] (CRL用拡張)
│ └── [ proxy_cert_ext ] (プロキシ証明書用拡張)
│
├── [ tsa ] (タイムスタンプ局設定)
│ └── default_tsa = tsa_config1
│ └── [ tsa_config1 ]
│
└── Insta CMP 関連セクション群
├── [ insta ] (デモCA接続設定)
├── [ pbm ] (パスワード保護設定 -> $insta::ref, $insta::secret 参照)
├── [ signature ] (署名保護設定 -> $insta::out_trusted 等参照)
├── [ ir ] (初期登録)
├── [ cr ] (証明書発行)
├── [ kur ] (証明書更新 -> oldcert)
└── [ rr ] (証明書失効 -> oldcert)
さいごに
いくつかのラッパーを作ったことで、格段に運用は簡単になった。
CAの寿命をもう少し短くしたり、サーバー証明書やクライアント証明書の有効期間を短期間にすることを考えても良いかな、と考え始める程度に簡単。
改めて、認証局と証明書ってよくできた仕組みだなと思ったりして。
認証局が他の認証局の証明書に署名するとか、署名してもらうために申請書を作って提出するとか、鍵と証明書をパック詰めして持ち歩くとか、アナログの行動そのものをそのまま電子化したような感覚。
それを忠実に守ろうとすると、利用者側にもルールや手続きを覚えてもらう必要があるから、今回はそれを認証局側で全て実施しよう、という作りになっている。
確定申告を思い出すと、説明書きの通りに色々書いて役所に持っていく。
色々チェックしてもらい、時に修正したりなんかして、提出する。
ルールを説明しながら使ってもらうのは大変。
色々とサーバーは攻撃に晒されるけれど、昨今それが高度化してヤバいことになっているが、こういった保護がもしかしたら役に立つかもしれない。


コメントはこちらから お気軽にどうぞ ~ 投稿に関するご意見・感想・他