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 で戻せる経路とバックアップを用意し、失効の手段と更新の期限を決めておく

お気軽にご相談ください

お見積りへ お問い合わせへ
▲