2026年5月24日
2026年5月24日
mTLS(クライアント証明書認証)の設定方法(独自CAの作成からNginx・Apacheでの適用まで)
mTLS(相互 TLS)は、サーバーだけでなく接続してくるクライアントにも証明書を提示させ、手元の認証局(CA)が発行した証明書を持つ端末やシステムだけを通す仕組みです。この記事では、クライアント証明書用の独自 CA の作り方、証明書の発行と配布、Nginx と Apache での設定、WordPress の管理画面だけに適用するときの注意点、失効と更新の運用までを順に説明します。読み終えると、管理画面やサーバー間 API に「パスワードの前のもう一枚の鍵」を付けられるようになります。
mTLS が向いている場面
通常の HTTPS では、クライアントがサーバーの証明書を確認するだけです。mTLS ではこれに加えて、サーバーがクライアントに証明書を要求し、信頼する CA が署名したものかを検証します。証明書を持たない接続は、アプリケーションに届く前に TLS の段階で拒否されます。
向いているのは、利用者や接続元が限られている次のような用途です。
- WordPress の管理画面など、少人数の管理者だけが使うページ
- サーバー同士の API 連携や Webhook の受け口
- 社内向けのツールや、ステージング環境
逆に、不特定多数の訪問者が使う公開ページには向きません。証明書の配布と管理が必要になるためです。また mTLS は「その端末に証明書が入っている」ことを確かめる仕組みで、WordPress のログインや二要素認証の代わりにはなりません。重ねて使う追加の防御と考えてください。
作業前の準備と締め出し対策
設定を誤ると管理画面に入れなくなります。始める前に次を確認してください。
- Web サーバーの設定ファイルとサイトのバックアップを取る
- SSH など、Web 以外の経路で設定を戻せることを確認する(問題が起きたら mTLS の行をコメントアウトして再読み込みすれば元に戻せます)
- CDN を経由している場合、閲覧者の証明書は CDN で止まりオリジンには届かない。CDN 側の mTLS 機能を使うか、オリジンへ直接つながる経路で構成する
- 可能なら検証環境で一通り試してから本番に適用する
1. クライアント証明書用の CA を作る
クライアント証明書の発行には、公開の認証局ではなく自分専用の CA を使います。公開の CA を信頼させると、その CA が発行した他人の証明書でも通ってしまうためです。
mkdir -p ~/mtls-ca && cd ~/mtls-ca
chmod 700 ~/mtls-ca
# CA の秘密鍵(ECDSA P-256)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ca.key
chmod 600 ca.key
# CA 証明書(有効期間 5 年の例)
openssl req -x509 -new -key ca.key -sha256 -days 1825 \
-subj "/O=Example/CN=Example Admin Client CA" -out ca.crt
# CA として使える証明書になっているか確認(CA:TRUE が表示されればよい)
openssl x509 -in ca.crt -noout -text | grep -A1 "Basic Constraints"
ca.key はすべてのクライアント証明書の信頼の元になる鍵です。Web サーバー上には置かず、発行作業をする別の端末で保管してください。Web サーバーに置くのは公開情報である ca.crt だけです。
2. クライアント証明書を発行する
利用者または接続元ごとに1枚ずつ発行します。共有すると、1人分だけを無効にすることができなくなります。
# クライアント証明書に付ける拡張(クライアント認証用であることを明示)
cat > client.ext <<'EOF'
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
EOF
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out alice.key
openssl req -new -key alice.key -subj "/O=Example/CN=alice" -out alice.csr
openssl x509 -req -in alice.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 365 -sha256 -extfile client.ext -out alice.crt
# シリアル番号を控えておく(失効の管理に使う)
openssl x509 -in alice.crt -noout -serial -subject -enddate
ブラウザに読み込ませるには、証明書と秘密鍵をまとめた PKCS#12 形式(.p12)に変換します。
openssl pkcs12 -export -inkey alice.key -in alice.crt -certfile ca.crt \
-name "Example Admin - alice" -out alice.p12
書き出し時にパスワードを求められます。.p12 ファイルとパスワードは別々の経路で渡してください(ファイルはメール、パスワードは電話など)。OpenSSL 3 系で作った .p12 は、古い OS やアプリで読み込めないことがあり、その場合は -legacy オプションで作り直すと読み込めることがあります(2026年10月現在)。
3. Nginx に設定する
サーバー全体に適用する(API や専用サブドメイン向け)
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_client_certificate /etc/nginx/mtls/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
}
ssl_certificate はサーバー自身の証明書(公開 CA のもの)、ssl_client_certificate はクライアント証明書を検証するための独自 CA の証明書で、役割が違います。証明書を持たずに接続すると、Nginx は 400 エラー(「No required SSL certificate was sent」)を返します。
WordPress の管理画面だけに適用する
ssl_verify_client は server 単位の設定で、location ごとには書けません。管理画面だけを守るには、サーバー全体を optional(証明書があれば検証する)にし、パスに応じて検証結果を判定します。
# http ブロック:保護するパスかどうか
map $uri $mtls_required {
default 0;
~^/wp-admin/admin-ajax\.php$ 0;
~^/wp-admin/ 1;
~^/wp-login\.php$ 1;
}
# 検証に成功したか
map $ssl_client_verify $mtls_ok {
default 0;
SUCCESS 1;
}
# 保護対象なのに検証に成功していなければ拒否
map "$mtls_required$mtls_ok" $mtls_deny {
default 0;
"10" 1;
}
# server ブロック
ssl_client_certificate /etc/nginx/mtls/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
if ($mtls_deny) {
return 403;
}
admin-ajax.php を除外しているのは、多くのテーマやプラグインが公開ページからこのファイルを呼び出すためです。除外しないと、訪問者側の機能(フォーム送信や「もっと見る」など)が動かなくなることがあります。/wp-json/(REST API)も公開側で使われることが多いため、ここでは保護対象に含めていません。正規表現の map は上から順に評価され、最初に一致したものが使われるので、除外の行を先に書きます。
optional にすると、ブラウザによっては公開ページを開いたときにも証明書の選択画面が出ることがあります。気になる場合は、管理専用のホスト名を用意してそちらを on にする構成も検討してください。
4. Apache に設定する
Apache では <Location> の中に SSLVerifyClient require を書く方法も紹介されていますが、これは接続後に再ネゴシエーションを行う方式で、TLS 1.3 では主要ブラウザが対応していない仕組みに依存するため、うまく動かないことがあります。VirtualHost 全体を optional にし、パスごとに検証結果で判定する書き方が安全です。
<VirtualHost *:443>
ServerName example.com
# ...サーバー証明書の設定
SSLCACertificateFile /etc/apache2/mtls/ca.crt
SSLVerifyClient optional
SSLVerifyDepth 2
<LocationMatch "^/wp-admin/(?!admin-ajax\.php)">
Require expr "%{SSL_CLIENT_VERIFY} == 'SUCCESS'"
</LocationMatch>
<Location "/wp-login.php">
Require expr "%{SSL_CLIENT_VERIFY} == 'SUCCESS'"
</Location>
</VirtualHost>
設定後は構文チェックをしてから再読み込みします。
sudo apachectl configtest && sudo systemctl reload apache2 # RHEL系は httpd
sudo nginx -t && sudo systemctl reload nginx # Nginx の場合
5. 動作を確認する
# 証明書なし:403(全体適用なら 400)になればよい
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php
# 証明書あり:200 が返ればよい
curl -s -o /dev/null -w "%{http_code}\n" \
--cert alice.crt --key alice.key https://example.com/wp-login.php
# 公開ページと admin-ajax.php が証明書なしで使えるか
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
ブラウザでは .p12 を OS またはブラウザの証明書ストアに読み込み、管理画面を開いたときに証明書の選択画面が出て、選択後にログイン画面が表示されれば成功です。
失効と更新の運用
証明書を入れた端末をなくした、担当者が退職した、という場合に備えて、特定の証明書を無効にする手段を用意しておきます。本格的には CRL(失効リスト)を作って Nginx の ssl_crl や Apache の SSLCARevocationFile で読み込みますが、CRL の発行には openssl ca の管理データベースなどの準備が必要です。少人数の運用なら、Nginx でシリアル番号を拒否リストにする方法が手軽です。
map $ssl_client_serial $mtls_revoked {
default 0;
1A2B3C4D5E6F 1; # 失効させた証明書のシリアル(例)
}
server ブロックに if ($mtls_revoked) { return 403; } を加えれば、該当する証明書を拒否できます。シリアル番号は、発行時に控えた openssl x509 -serial の出力(serial= の後ろ)と同じ16進数の表記で書きます。あわせて、クライアント証明書の有効期間を1年程度に抑え、期限が来る前に再発行する手順を決めておくと、漏えい時の影響を小さくできます。利用者が多くなってきたら、Smallstep の step-ca のような CA 運用ツールの導入も選択肢になります。
ログには $ssl_client_s_dn や $ssl_client_serial を出しておくと、どの証明書で誰がアクセスしたかを後から追えます。
まとめ
- mTLS はクライアントにも証明書を求める仕組みで、管理画面やサーバー間 API など利用者が限られる場所に向く。ログインや二要素認証の代わりではない
- クライアント証明書は独自 CA で発行し、CA の秘密鍵は Web サーバーに置かない。証明書は1人1枚にする
- Nginx の
ssl_verify_clientは server 単位。管理画面だけを守るならoptionalとmapで判定し、admin-ajax.phpは除外する - Apache は
<Location>内の再ネゴシエーションに頼らず、SSLVerifyClient optionalとRequire exprで判定する - 締め出しに備えて SSH で戻せる経路とバックアップを用意し、失効の手段と更新の期限を決めておく