nginxでmTLSを設定しクライアント証明書を必須にする方法
nginxの管理画面をmTLSで保護します。opensslでプライベートCAを作成し、利用者ごとの証明書を発行して、証明書のない接続を拒否する設定を紹介します。
mTLS の役割
相互 TLS(通常は mTLS と表記します)を使用すると、nginx はすべてのクライアントに証明書を要求します。その証明書がない場合や、管理下の認証局(CA)が発行したものではない場合、リクエストを拒否します。この検証は TLS(トランスポート層セキュリティ)のハンドシェイク中に行われるため、有効なクライアント証明書を持たない接続元がアプリケーションに到達することはありません。ここに mTLS の利点があります。管理画面やメトリクスエンドポイントを、ログインページなしで、ボットが推測できる情報もない状態のまま、パブリックインターネット上に公開できます。
構成は小規模です。openssl で作成したプライベート CA を 1 つ、利用者ごとの証明書を 1 つずつ、nginx の server ブロック内のディレクティブを 3 つ用意します。これを 1 年間運用できるかどうかを左右するのは、運用面の作業です。そのため、このガイドでは有効期間、失効、利用者ごとの証明書、クライアントが拒否されたにもかかわらず原因を確認できない場合の対応を中心に説明します。
証明書チェーンは1つではなく2つです
mTLS の構成には2つの証明書チェーンがあり、互いに関係ありません。これらを1つにまとめることが、ほとんどの人が最初に犯す間違いです。
1つ目はサーバーの証明書チェーンです。VPS は、Let's Encrypt などのパブリック CA が発行した admin.example.com の証明書を提示します。ブラウザーは、オペレーティングシステムに同梱されたルートストアを使って、その証明書を検証します。mTLS によって、この部分が変わることはありません。certbot が今日その証明書を発行した場合も、設定はそのまま維持してください。certbot で nginx 用の Let's Encrypt 証明書を発行するを参照してください。
2つ目はクライアントの証明書チェーンです。独自の小規模な CA を作成し、接続を許可する利用者ごとに1つの証明書へ署名します。そのうえで、クライアントを検証するときに nginx が信頼する CA を、その CA だけに指定します。パブリックなルートストアがこの CA を知る必要はなく、知っている必要もありません。この CA を信頼する必要があるのは、ssl_client_certificate ファイルを通じて nginx だけです。
したがって、ssl_client_certificate が nginx の提示する証明書に影響することはありません。また、Let's Encrypt の証明書チェーンが、接続を許可するクライアントに影響することもありません。ssl_client_certificate を fullchain.pem に指定しても、見た目どおりの動作にはなりません。このディレクティブは、クライアント証明書の発行元として許可する CA を指定するものです。つまり、接続の反対側に関する設定です。サーバー自身の外向き通信で独自 CA を信頼させる作業は別に必要です。これはUbuntu の trust store に独自 CA を追加するで説明しています。また、nginx がクライアント証明書を検証するときに読み込むのは、システムの trust store ではありません。
openssl で独自のクライアント CA を構築する
CA は Web サーバーとは別の場所に構築してください。nginx に必要なのは CA の公開証明書だけです。CA の秘密鍵は新しいクライアント証明書への署名に使うため、インターネットに公開されたホストに置くと、1 回侵入されただけで、攻撃者が任意に有効なクライアント証明書を発行できるようになります。
mkdir -p ~/client-ca/certs ~/client-ca/newcerts ~/client-ca/private ~/client-ca/csr
cd ~/client-ca
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumberindex.txt、serial、crlnumber は CA データベースです。openssl ca は、これらがないと実行できません。後で失効を可能にする役割もあります。失効リストはシリアル番号を記載するため、CA はどのシリアル番号を誰に割り当てたかを記録しておく必要があります。
~/client-ca/openssl.cnf を作成します。dir には、そのディレクトリの実際のパスを設定してください。openssl ca は ~ を展開しないためです。
[ ca ]
default_ca = client_ca
[ client_ca ]
dir = /home/you/client-ca
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/ca.crt
private_key = $dir/private/ca.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 30
policy = policy_loose
rand_serial = no
unique_subject = no
email_in_dn = no
[ policy_loose ]
commonName = supplied
countryName = optional
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
emailAddress = optional
[ client_ext ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer次に、CA の鍵と自己署名証明書を作成します。
openssl genrsa -aes256 -out private/ca.key 4096
chmod 600 private/ca.key
openssl req -x509 -new -key private/ca.key -sha256 -days 3650 \
-subj "/O=Example Ops/CN=Example Ops Client CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-out ca.crt-aes256 は CA の鍵にパスフレーズを設定するため、署名を実行するたびに入力を求められます。これが -aes256 の目的です。作成した内容を確認します。
openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraintsSubject には CA が設定され、有効期間は 10 年になっているはずです。拡張行は CA:TRUE, pathlen:0 になっているはずです。pathlen:0 は、この CA がエンドエンティティ証明書には署名できる一方、別の CA には署名できないことを示します。これにより、証明書チェーンを正確に 1 階層に保ち、ssl_verify_depth をそのままにできます。
個人ごとにクライアント証明書を発行する
1 人につき 1 つの証明書を発行します。チームで 1 つの証明書を共有してはいけません。共有証明書は、全員を締め出さずに失効させることができず、誰が呼び出したのかも分からないためです。
openssl genrsa -out private/alice.key 2048
openssl req -new -key private/alice.key -out csr/alice.csr \
-subj "/O=Example Ops/CN=alice"
openssl ca -config openssl.cnf -extensions client_ext \
-days 365 -notext -in csr/alice.csr -out certs/alice.crtopenssl ca は署名する証明書を表示し、CA のパスフレーズを求め、確認を 2 回求めた後、index.txt に 1 行を追加します。スクリプトで実行する場合は -batch を追加します。client_ext セクションが重要なのは、その中に extendedKeyUsage = clientAuth という 1 行があるためです。拡張キー用途に serverAuth だけを指定した証明書は、クライアント認証に適さないとして拒否されます。推測に頼らず、用途を明示してください。
引き渡す前に、CA に対して証明書と鍵の組み合わせを検証します。
openssl verify -CAfile ca.crt certs/alice.crtcerts/alice.crt: OK が表示されます。それ以外の出力になった場合、証明書と CA は一致していません。nginx の設定で解決することはできません。
ブラウザーがインポートできるように、鍵と証明書を 1 つのファイルにまとめます。
openssl pkcs12 -export -inkey private/alice.key -in certs/alice.crt \
-name "alice at example ops" -out alice.p12エクスポート時にパスワードを指定します。これにより、転送中のファイルが保護されます。ファイルとパスワードは異なる経路で送信し、利用者には単独の .key ではなく .p12 を渡してください。-certfile ca.crt を追加して CA をバンドルに含めることもできますが、nginx には不要です。nginx はすでに ca.crt を保持しているため、その CA が直接署名した証明書は単独で検証できます。
Ubuntu 24.04 に同梱されている OpenSSL 3 は、現在の暗号化方式で PKCS#12 ファイルを書き出します。2026 年 8 月時点で使用されているブラウザーとオペレーティングシステムは、このファイルを読み取れます。古いインポーターがファイルを拒否する場合は、-legacy を追加して再エクスポートします。これにより、そのインポーターが想定する古いアルゴリズムに戻ります。このフラグを使う前に、インポーターが表示したメッセージを確認してください。
nginx を ssl_client_certificate と ssl_verify_client で設定する
CA 証明書だけをサーバーにコピーします。
scp ca.crt user@admin.example.com:/tmp/client-ca.crt
ssh user@admin.example.com \
'sudo install -o root -g root -m 644 /tmp/client-ca.crt /etc/nginx/client-ca.crt'ここではモード 644 が正しい設定です。CA 証明書は公開情報です。CA キーはワークステーション上に保管します。
次に、TLS 終端をすでに担っている server block に 3 つのディレクティブを追加します。
server {
listen 443 ssl;
server_name admin.example.com;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on;
ssl_verify_depth 1;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}ssl_verify_depth 1 は nginx のデフォルト値です。このファイルに記載された CA が直接署名したクライアント証明書だけを許可します。中間 CA を追加する場合にだけ、この値を引き上げます。nginx はハンドシェイク中に ssl_client_certificate の subject 名もクライアントへ送信します。これにより、ブラウザーは提示する証明書を判断できます。この動作が、同じ方法で検証するものの一覧を送信しない ssl_trusted_certificate ではなく、ssl_client_certificate を使用する理由です。
Ubuntu 24.04 に同梱される nginx 1.24 では、HTTP/2 を listen 443 ssl http2; として listen 行に指定します。nginx 1.25.1 以降ではこの形式は非推奨で、HTTP/2 は専用ディレクティブの http2 on; で指定します。どちらを選んでも証明書の検証には影響しません。
設定を reload して結果を確認します。
sudo nginx -t && sudo systemctl reload nginx
curl -i https://admin.example.com/nginx -t は syntax is ok と test is successful を出力します。curl の呼び出しには証明書がないため、本文 No required SSL certificate was sent とともに 400 Bad Request が返るはずです。これは nginx 自身の入口で拒否されたことを示します。つまり、設定は有効で、アプリケーションにはリクエストが届いていません。次に、正しい方法で試します。
curl --cert certs/alice.crt --key private/alice.key https://admin.example.com/アプリケーションが提供する内容がそのまま返るはずです。
サーバーブロックにゲートを配置する理由
証明書は TLS ハンドシェイク中に交換されます。この時点では nginx はリクエスト行を読み取っていないため、リクエストが到達する location を把握していません。location の内部に ssl_verify_client on; を配置すると、接続途中でクライアントに再ネゴシエーションを求めることになります。TLS 1.3 では再ネゴシエーションが廃止され、HTTP/2 では禁止されています。そのため、現在の構成ではこの方法は要求を促すのではなく失敗します。
スコープは自分で分けます。サーバーレベルで証明書を要求し、その後 location ごとに判断します。
ssl_verify_client optional;
location /metrics {
if ($ssl_client_verify != SUCCESS) { return 403; }
proxy_pass http://127.0.0.1:9090;
}
location /healthz {
proxy_pass http://127.0.0.1:8080;
}$ssl_client_verify には SUCCESS、クライアントが何も送信しなかった場合は NONE、または理由を付けた FAILED: が設定されます。optional を使用すると、nginx は証明書を要求し、証明書が届いた場合にだけ検証します。これにより、上記の公開 /healthz パスを利用しながら、/metrics は閉じたままにできます。送信された証明書の検証に失敗した場合も、その時点で nginx に拒否されます。失敗した証明書を自分で調べたい場合は optional_no_ca を使用します。この場合、独自のテストでは SUCCESS 以外のすべての値を拒否として扱う必要があります。
nginx にはこの用途の非標準ステータスコードがあります。error_page でこれらを捕捉すると、拒否された訪問者に単なる 400 ではなく説明を表示できます。
error_page 495 496 = @needcert;
location @needcert {
default_type text/plain;
return 200 "This host requires a client certificate. Ask ops for one.\n";
}495 はクライアント証明書の検証に失敗したことを示します。496 はクライアントが証明書を提示しなかったことを示します。このページはプレーンテキストにしてください。読む人にはセッションもアカウントもないためです。
ブラウザーにクライアント証明書をインストールする方法
Firefox は独自の証明書ストアを使用します。Settings、Privacy and Security、View Certificates、Your Certificates タブ、Import の順に開き、.p12 を選択してパスワードを入力します。
Windows と macOS では、Chrome と Edge はオペレーティングシステムの証明書ストアを使用します。そのため、.p12 ファイルを開くと、システムのインポートウィザードが起動します。Linux では、Chrome はホームディレクトリ内の独立した NSS (network security services) データベースを読み取ります。コマンドラインツールを使用する方法が確実です。
sudo apt install -y libnss3-tools
pk12util -d sql:$HOME/.pki/nssdb -i alice.p12その後、サイトを読み込むと、ブラウザーで送信する証明書を選択するよう求められます。Chrome はブラウザーセッション中、その選択を記憶します。再度選択を求められるようにするには、ブラウザーを再起動します。証明書は、1 台のマシン上の 1 つのブラウザープロファイルに保存されます。そのため、Firefox にインポートした証明書は Chrome からは見えません。どちらのブラウザーにインポートした証明書も、スマートフォンからは見えません。
curl --cert を使用したテスト
実行内容を報告するため、curl でデバッグします。
curl -v --cert certs/alice.crt --key private/alice.key https://admin.example.com/証明書と鍵を 1 つの PEM ファイルに連結し、--cert alice.pem として渡せます。鍵にパスフレーズが設定されている場合、curl はその入力を求めます。--cert alice.pem:passphrase も使用できますが、パスフレーズがシェルの履歴に残るため、プロンプトを使用してください。
nginx を疑う前に、2 つの確認を実行してください。まず、証明書と鍵が対になっている必要があります。
openssl x509 -noout -pubkey -in certs/alice.crt | openssl sha256
openssl pkey -pubout -in private/alice.key | openssl sha2562 つのハッシュが同一なら、ファイルは正しく対応しています。異なる場合は、別の 2 人分のファイルを取り違えています。クライアント側のエラーメッセージから、その原因を特定することはできません。
次に、サーバーが CA を要求していることを確認します。
openssl s_client -connect admin.example.com:443 -servername admin.example.com </dev/null出力内の Acceptable client certificate CA names ブロックと、その中にある CA の subject を確認します。このブロック自体がない場合、応答した server block の nginx は証明書を要求していません。指定したディレクティブが別の server block に入っており、通常は default server に入っている可能性があります。
アプリケーションへのクライアント CN の渡し方
証明書には接続元が示されていますが、プロキシの背後にあるアプリケーションからは TLS 層が見えません。そのため、nginx で名前を渡す必要があります。
map $ssl_client_s_dn $client_cn {
default "";
"~,?CN=(?<cn>[^,]+)" $cn;
}$ssl_client_s_dn には、RFC 2253 形式の subject distinguished name が格納されます。この形式では CN=alice,O=Example Ops のように表示されます。map により、CN フィールドが $client_cn に取り出されます。CN は単純なユーザー名にしてください。この形式では CN 内のコンマがエスケープされますが、上の小さな正規表現はそのエスケープに対応していません。
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Cert-CN $client_cn;
proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
}proxy_set_header は、呼び出し元が送信した同名のヘッダーをすべて置き換えます。そのため、この location を通じて誰かが X-Client-Cert-CN を偽装することはできません。この状態を維持するには、2 つの条件があります。nginx は、内側のレベルで独自の proxy_set_header が定義されていない場合にだけ、外側のレベルから proxy_set_header を継承します。そのため、別の location に proxy_set_header の行が 1 つでもあると、その上位レベルで設定したすべてのヘッダーが、このヘッダーも含めて暗黙的に失われます。また、アプリケーションには nginx 経由以外で到達できないようにする必要があります。そのため、0.0.0.0 ではなく 127.0.0.1 にバインドしてください。アプリケーションが公開ポートで待ち受けると、インターネットから偽装されたヘッダーを直接読み取ってしまいます。プロキシ側の設定については、nginx リバースプロキシ設定を行ごとに解説で説明しています。アプリケーションが名前ではなく証明書全体を必要とする場合は、$ssl_client_escaped_cert に URL エンコードされた安全な形式で格納されます。
クライアント証明書を1つだけ失効させる方法
ユーザーが離職した場合や、ノート PC を紛失した場合は、その証明書だけを失効させます。他のユーザーはそのまま利用できます。ユーザーごとに証明書を発行する理由はここにあります。
cd ~/client-ca
openssl ca -config openssl.cnf -revoke certs/alice.crt
openssl ca -config openssl.cnf -gencrl -out crl.pem最初のコマンドは、index.txt にあるそのシリアル番号の行を V から R に変更します。2つ目のコマンドは、失効したシリアル番号を記載した署名済みファイルである証明書失効リスト(CRL)を書き出します。CRL を配置し、他のディレクティブと同じ場所で ssl_crl /etc/nginx/client-ca.crl; を使って nginx に指定します。
scp crl.pem user@admin.example.com:/tmp/client-ca.crl
ssh user@admin.example.com 'sudo install -m 644 /tmp/client-ca.crl /etc/nginx/client-ca.crl && sudo nginx -t && sudo systemctl reload nginx'全員の接続を止める落とし穴があります。CRL には nextUpdate の日付が含まれます。この日付は、上記の設定では default_crl_days によって設定され、値は 30 です。この日付を過ぎると、OpenSSL は CRL を古いと判断します。その結果、失効させた証明書だけでなく、すべてのクライアント証明書の検証が CRL has expired で失敗します。nginx は設定を読み込む際にファイルを読み取るため、ディスク上の CRL を更新しただけでは反映されません。必ず reload も実行してください。30日間の有効期間に対して週1回など、余裕を持った間隔で再生成と reload を実行します。コピーする前に日付も確認してください。
openssl crl -in crl.pem -noout -lastupdate -nextupdate少数のユーザーだけを管理する場合は、より小規模な方法があります。CA は自分で管理しているため、nginx でシリアル番号を直接拒否し、CRL の仕組みを省略できます。
map $ssl_client_serial $revoked {
default 0;
"1002" 1;
}location に if ($revoked) { return 403; } も指定します。この方法には、忘れると問題になる有効期限がありません。ただし、この拒否設定は他の環境には共有されません。そのため、CA を信頼する別のシステムはこの設定を認識しません。1台の nginx で1つのアプリケーションを保護する場合は、これが率直で単純な方法です。保護する入口が1つより多くなったら、CRL に移行します。
クライアント証明書の有効期間はどの程度にすべきですか?
クライアント証明書の有効期間は 1 年にします。再発行の作業を許容できる場合は、さらに短くしても構いません。ここで気付きにくい障害原因になるのが有効期限切れです。事前に所有者へ通知されることがないためです。ある朝パネルを開くと、nginx が接続を拒否します。ブラウザーは独自の表現で拒否理由を示すため、そこに expired という語が含まれることはほとんどありません。CA の有効期間は 10 年にし、有効期限を実際に確認する場所に記録してください。CA 証明書の有効期限が切れると、その CA の下にあるすべての証明書が同じ日に検証できなくなるためです。
次の 2 つのコマンドで、期限切れを事前に把握できます。
openssl x509 -in certs/alice.crt -noout -subject -serial -enddate
awk -F'\t' '{print $1, $2, $4}' ~/client-ca/index.txtindex.txt の 1 列目はステータスです。V は有効、R は失効済み、E は期限切れを示します。2 列目は YYMMDDHHMMSSZ 形式の有効期限で、4 列目はシリアル番号です。このファイルだけが、誰がどの証明書を保持しているかを示す記録です。そのため、CA key と一緒にバックアップし、どちらも秘密情報として扱ってください。
更新では証明書を拡張せず、新しい証明書を発行します。新しい key と CSR (certificate signing request) を生成して署名し、利用者に渡します。その後、利用者が新しい証明書を使用できることを確認してから、古い証明書を失効させます。
mTLS が防ぐものと、防げないもの
mTLS により排除できるのは、認証されていない接続経路です。スキャナーがホスト名を見つけても、TLS ハンドシェイク中に拒否されるため、HTTP リクエストを送信できず、ログインフォームも表示できず、盗まれたパスワードを試すこともできません。クレデンシャルスタッフィングの対象もありません。証明書を持たない攻撃者は、アプリケーションのログイン処理にある脆弱性へ到達できません。また、秘密鍵は誤ってコピーしにくいファイルであるため、チャットに貼り付ける共有シークレットも不要になります。
一方、侵害されたクライアントには何も対処できません。ノート PC 上のマルウェアは秘密鍵ファイルを取得でき、所有者がパスフレーズを入力した瞬間に、それも取得できます。サーバーから見ると、その攻撃者は正規ユーザーとまったく同じに見えます。証明書が証明するのはファイルを所有していることだけで、人が操作していることではないためです。.p12 パスワードとフルディスク暗号化には、依然として重要な役割があります。
また、mTLS は認可の仕組みでもありません。$client_cn を確認してその値に応じた処理を行わない限り、その server block が提供するすべてのリソースに有効な証明書でアクセスできます。デフォルトでは、2 人の証明書所有者のアクセス権は同一です。
さらに、保護されるのは nginx を経由する経路だけです。アプリケーションがパブリックポートでも待ち受けている場合、その手前に mTLS を置いても意味がありません。アプリを 127.0.0.1 に bind し、そのポートをファイアウォールで閉じたままにします。同じホストへ入るもう 1 つの経路は SSH です。SSH にも同じ注意が必要です。詳しくは VPS の SSH アクセスを強化する で説明します。
最後に、導入した日に問題になる制限があります。証明書を提示できないものは動作しなくなります。稼働監視サービス、決済プロバイダーからの webhook、RSS リーダー、到達可能な証明書ストアを持たないモバイルアプリなどです。ssl_verify_client on を設定する前に、これらをどう扱うか決めてください。失敗は全面的に発生し、相手側では通知されない場合があります。
クライアントが拒否されたときは、クライアント側の報告を読む
拒否されたクライアントに表示されるメッセージは、ブラウザー、curl のバージョン、基盤となる TLS ライブラリによって異なります。そのため、どこかに記録されたメッセージと照合するのではなく、自分のクライアントが出力した内容を確認してください。重要な情報はサーバー側にあります。
sudo tail -n 50 /var/log/nginx/error.log証明書が拒否されると、client SSL certificate verify error と OpenSSL が示した理由を含む行が出力されます。対処の起点にするのは、その理由です。通常は、いくつかの原因に絞られます。証明書の発行元 CA が、ssl_client_certificate で指定したファイルの CA と異なっています。証明書が有効期間外です。サーバー上の CRL が nextUpdate を過ぎているため、1 台のクライアントではなく、すべてのクライアントで失敗します。
ブラウザーが証明書をまったく提示しない場合、問題は検証より前の段階にあります。nginx はハンドシェイク中に受け入れ可能な発行者名を送信します。ブラウザーは、証明書ストア内に一致する証明書を見つけられなかったため、提示する証明書がありませんでした。実際にブラウジングに使用しているプロファイルへ .p12 を再度インポートしてください。
もう 1 つ、明記しておくべきケースがあります。CA が署名した証明書ではなく、単独の自己署名クライアント証明書でテストした場合、検証は成功しません。nginx は CA ファイルを使って署名を確認しますが、自己署名証明書はそのファイルに含まれていないためです。証明書の作成手順は、Ubuntu で自己署名証明書を生成する場合と同じです。mTLS では、CA に署名させる手順が追加で必要になります。
FAQ
Let's Encrypt の証明書を使用する場合も、mTLS の証明書は必要ですか?
はい。2 つの証明書は無関係です。サーバーは、ブラウザーがホスト名を信頼できるように自身の証明書を提示します。この証明書は、ブラウザーが既知の CA から取得する必要があります。クライアント CA は別のプライベートチェーンであり、接続元の識別にのみ使用します。ssl_client_certificate を設定しても、nginx が提示する証明書は変わりません。また、ここで Let's Encrypt のチェーンを指定してはいけません。
ブラウザーで証明書の選択を求められないのはなぜですか?
nginx はハンドシェイク中に、ssl_client_certificate のファイルを基に、受け入れ可能な発行者の一覧を送信します。ブラウザーは、その一覧に発行者が含まれる証明書だけを提示します。したがって、選択画面が表示されない場合、ブラウザーには CA の証明書がありません。別のブラウザープロファイルにインポートしたか、サーバーにインストールした CA とは別の CA で証明書が署名されています。openssl s_client -connect admin.example.com:443 を実行し、出力にある受け入れ可能なクライアント証明書 CA 名を確認すると、サーバーが実際に要求している CA が分かります。
1 つの URL にだけクライアント証明書を要求できますか?
location 内で ssl_verify_client on を使用する方法ではできません。証明書はハンドシェイク中に交換されます。この時点では nginx はリクエストパスを認識していません。また、回避策となる再ネゴシエーションは TLS 1.3 では廃止され、HTTP/2 では禁止されています。server ブロックに ssl_verify_client optional; を設定し、保護する各 location で $ssl_client_verify を確認してください。値が SUCCESS でない場合は 403 を返します。
1 人だけのアクセスを取り消すにはどうすればよいですか?
openssl ca -revoke でその証明書を失効させ、openssl ca -gencrl で一覧を再生成します。その後、サーバーにコピーし、nginx を reload して新しいファイルを読み込ませます。他のユーザーには影響しません。ただし、各ユーザーが共有証明書ではなく、それぞれ固有の証明書を保持している場合に限ります。CRL の nextUpdate 日付を確認してください。CRL の期限が切れると、失効したクライアントだけでなく、すべてのクライアントで検証に失敗します。
mTLS はログインページの代わりになりますか?
到達制御については、はい。証明書がなければアプリケーションまで何も到達しないため、攻撃対象となるフォームも、推測されるパスワードもありません。ただし、アプリケーション内の識別については、いいえ。証明書が証明するのは、接続元が鍵ファイルを保持していることだけです。そのため、ノート PC が盗まれれば、その利用者は有効なユーザーとして扱われます。CN を upstream に渡し、アプリケーションが既に持つアカウントと権限を維持してください。証明書は、その前段に置くゲートとして扱います。