TLS の記事では、クライアントがサーバの証明書を確かめるところまでを見ました。でも API の現場では、「つないできた相手は本当に取引先のシステムか」「社内のどのサービスか」を、サーバの側でも確かめたい場面があります。この記事では、クライアントも証明書を出す mTLS(相互 TLS)を、RFC と Microsoft のドキュメントで確かめながら、テスト用の環境で OpenSSL と curl を使って実際に動かした結果と一緒に、一歩ずつ図にしました。
- mTLS は、TLS のハンドシェイクの中で、クライアントも証明書と秘密鍵で自分を示すしくみ
- API キーやトークンとの違いと、組み合わせ方(証明書に結びついたトークン)
- サーバが確かめること(つながり・期限・用途・持ち主)と、「誰か」を決める仕事
- OpenSSL と curl で mTLS を動かし、失敗したときのエラーを読む方法
- Azure App Service と API Management で mTLS を使うときの注意点
まずはたとえで:入館証
取引先のビルを訪ねると、受付の窓口は会社の名札を下げています。ふつうの TLS は、この「窓口の身分証」だけを見せるしくみです。クライアント(来訪者)は窓口が本物かどうかを確かめますが、来訪者が誰かは TLS では確かめず、そのあとの ID やパスワード、トークンで確かめます。
セキュリティの厳しいビルでは、来訪者も入館証を見せます。入館証は会社が出したもので、窓口は「自分が信じる会社の入館証か」を見て通します。mTLS(mutual TLS、相互 TLS)はこれと同じで、サーバ証明書に加えて、クライアントもクライアント証明書を出します。入館証を出したのが CA(認証局)、入館証がクライアント証明書です。証明書と CA のしくみは、PKI の記事で扱いました。
使う場面
mTLS は、相手が「人」ではなく「プログラムや機器」で、あらかじめ証明書を配っておける場面で使われます。
OAuth 2.0 でも、クライアントの認証に mTLS を使う方法が RFC 8705 で決まっています。クライアントシークレットの代わりに証明書でトークンを取り、さらにそのトークンを証明書に結びつけて、盗まれても使えないようにできます。後半の「証明書に結びついたトークン」で図にします。
mTLS の基本の形
API キー・トークンとの違い
最初に、よく比べられる3つの形を並べます。API キーや OAuth のトークンは、HTTP のヘッダに入れて送る値です。値そのものを持っていれば誰でも使えるので、漏れると困ります。mTLS では、サーバに見せるのは証明書(公開鍵)ですが、証明書だけでは通りません。クライアントの秘密鍵で署名できることを、ハンドシェイクの中で示す必要があります。
| 比べること | API キー・トークンだけ | mTLS | mTLS + トークン |
|---|---|---|---|
| 何で確かめるか | ヘッダの値 | 証明書と、秘密鍵での署名 | 両方 |
| どこで確かめるか | API(アプリ) | TLS の段階(サーバや前段) | TLS の段階と API |
| 漏れたら | 値を持つ人なら使える | 証明書だけでは使えない。秘密鍵が漏れたら失効させる | トークンだけでは使えない |
| 権限(何をしてよいか) | トークンのスコープなど | 証明書からは分からない。サーバが対応表で決める | トークンのスコープなど |
| 向いている相手 | 人・アプリ全般 | 決まったプログラムや機器 | 決まったプログラム(厳しい API) |
mTLS が確かめるのは「相手が誰か」(認証)までです。その相手に何を許すか(認可)は、サーバやアプリが別に決めます。認証と認可の違いは、ここでも同じです。
サーバが確かめること
入館証のたとえで続けます。窓口は、来訪者が差し出した入館証を、上から順に5つ確かめます。1〜4 は「本物の入館証か」、5 は「今日ここに通してよい人か」の確認です。mTLS のサーバも、クライアント証明書を同じ順で確かめます。
| 確認 | たとえ(入館証) | 実際に見るもの | だめなとき (この記事の試験) |
|---|---|---|---|
| 1 つながり | 信じる会社のハンコが押してあるか | サーバが信じる CA まで、証明書の署名をたどれるか | unknown ca(サーバ側は error 20) |
| 2 期限・失効 | 期限内で、紛失届が出ていないか | 有効期間の中か。CRL などで失効していないか | PKI の記事の error 10・23 と同じ |
| 3 用途 | 来訪者用の入館証か | 用途(EKU)に clientAuth があるか | unsupported certificate(サーバ側は error 26) |
| 4 持ち主 | その場でサインして、本人と示せるか | CertificateVerify の署名を、証明書の公開鍵で確かめて合うか | 署名が合わなければ断る |
| 5 誰か | 来訪者の名簿に載っているか | SAN・サブジェクト・指紋などが、登録した値と合うか | 断り方は作りによる(接続を切る、403 を返すなど) |
1〜3 は、TLS の記事でクライアントがサーバ証明書に対して行ったのと同じ確認で、向きが逆になっただけです。2 の失効の確認は、製品や設定によっては行いません。たとえば OpenSSL では -crl_check などを付けたときだけ CRL を見ます。API Management の validate-client-certificate ポリシーは、既定で失効も確かめます。
持ち主の確認(CertificateVerify)が要る理由
証明書は、名前と公開鍵に CA がハンコを押したもので、相手に見せるためのものです。ログに残ったり、別の担当者に渡ったりすることもあるので、「証明書を持っている」だけでは本人とは言えません。そこでクライアントは、ハンドシェイクのその場のやり取りに、自分の秘密鍵で署名して送ります。これが CertificateVerify です。
サーバは、証明書に入っている公開鍵でこの署名を確かめます。公開鍵と合う署名を作れるのは、ペアの秘密鍵を持つ app01 だけです。署名するやり取りには毎回変わる乱数が入っているので、前に見た署名をコピーして使い回すこともできません。RFC 8446 も、CertificateVerify を「証明書に対応する秘密鍵を持っていることの明示的な証明」と説明しています。
「本物の証明書」と「通してよい相手」は別
1〜4 が通っても、分かるのは「信じる CA が出した、本物の証明書を、本人が使っている」ことまでです。社内の CA は、ほかのシステムや社員のパソコンにも証明書を出しているかもしれません。それらもすべて 1〜4 を通ります。
そこで 5 として、「この API に通してよい相手の名簿」を持ち、SAN や指紋などを照らし合わせます。名簿を持つのは、サーバの設定、アプリのコード、前段の API ゲートウェイなどです。STEP 2 で使う openssl s_server は名簿を持たないので、Demo Root CA が出した clientAuth の証明書なら誰でも通します。本番では、この 5 を必ずどこかで行います。
誰が何を持つか
持ち物は、「自分を示すもの」と「相手を確かめるもの」の2種類に分けると整理しやすくなります。ふつうの TLS では、自分を示すのはサーバだけでした。mTLS では、クライアントも自分を示すものを持ち、サーバも相手を確かめるものを持ちます。図の網掛けが、mTLS で増える持ち物です。
| 持ち物 | 持つ人 | 何に使うか | 見せてよいか | たとえ |
|---|---|---|---|---|
| クライアントの秘密鍵 | クライアント | CertificateVerify の署名を作る(確認 4) | 見せない | 本人しか書けないサイン |
| クライアント証明書 | クライアント | サーバに見せて、名前と公開鍵を伝える | 見せてよい | 入館証 |
| サーバの秘密鍵 | サーバ | サーバの CertificateVerify の署名を作る | 見せない | 窓口のサイン |
| サーバ証明書 | サーバ | クライアントに見せる(TLS と同じ) | 見せてよい | 窓口の名札 |
| 信じる CA 証明書 | 両方 | 相手の証明書の署名をたどる起点(確認 1) | 見せてよい | 「この会社のハンコなら信じる」という見本 |
| 名簿(対応表) | サーバ | 証明書から誰か・何を許すかを決める(確認 5) | 社内の設定 | 来訪者名簿 |
| CA の秘密鍵 | CA | 証明書に署名する | 見せない | 会社のハンコ |
押さえておきたいのは次の3つです。
- 秘密鍵と証明書はペアです。証明書の中の公開鍵と対になる秘密鍵があって、初めて「自分」を示せます。証明書は見せてよく、秘密鍵は持ち主の外に出しません。
- 「信じる CA 証明書」は向きが逆です。クライアントが持つのはサーバ証明書を確かめるため、サーバが持つのはクライアント証明書を確かめるためのものです。この記事の試験では1つの CA で両方を出していますが、本番ではサーバ用とクライアント用で CA が違うことも多く、その場合は持つ CA 証明書も別になります。
- クライアントの秘密鍵が漏れたら、証明書を失効させます。秘密鍵があれば、ほかの誰かが app01 として確認 1〜4 を通れてしまいます。失効させて、新しい鍵で証明書を出し直します(失効のしくみは PKI の記事で扱いました)。
全体の流れ
準備:クライアント証明書を発行して、サーバに信じる CA を設定する
サーバ証明書は、TLS の記事と PKI の記事で用意した形と同じです。ここでは、クライアント(app01)の証明書を発行し、サーバに設定するところまでを見ます。
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか・使う鍵 |
|---|---|---|---|
| ① | クライアントで操作 | 鍵ペアと CSR(証明書の申請書)を作る | クライアントの秘密鍵は、クライアントの中だけ |
| ② | クライアント → CA | CSR を渡す | CSR に入るのは名前と公開鍵。秘密鍵は送らない |
| ③ | CA | 申請を確かめて署名する | CA の秘密鍵で署名。用途に clientAuth、名前に SAN を付ける |
| ④ | CA → クライアント | クライアント証明書を渡す | 証明書は公開してよいもの |
| ⑤ | クライアントで操作 | 証明書と秘密鍵を、接続に使う場所に置く | 秘密鍵は自分だけが読めるようにする |
| ⑥ | CA → サーバ | CA 証明書を渡す | サーバが「この CA のクライアント証明書を信じる」ための起点 |
| ⑦ | サーバで操作 | クライアント証明書を必須にする | 証明書を出さない相手は、ハンドシェイクで断る |
| ⑧ | サーバで操作 | 証明書と、誰か・権限の対応を決める | SAN・サブジェクト・指紋などを、app01 に結びつける |
本番では、クライアント証明書は社内の CA(AD CS など)や、クラウドのプライベート CA で出すのが一般的です。⑧の対応は、サーバやアプリ、前段の API ゲートウェイの設定で持ちます。
Microsoft のドキュメントによると、Chrome のルートプログラムの方針の更新で、2026年6月15日以降に発行される公的なサーバ証明書には、クライアント認証の用途(clientAuth)を入れられなくなりました。Azure App Service の証明書(App Service マネージド証明書と App Service 証明書)も clientAuth を外していて、これらの証明書を mTLS のクライアント証明書として使っている場合は、別の方法に移す必要があります。それより前に発行された clientAuth 付きの証明書は、期限が切れるか失効するまで有効です。
接続:mTLS のハンドシェイク
準備ができたら、クライアントは API につなぎます。TLS 1.3 のハンドシェイクに、サーバが証明書を求めるメッセージと、クライアントが証明書と署名を返すメッセージが加わります。
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか・使う鍵 |
|---|---|---|---|
| ⑨ | クライアント → サーバ | ClientHello を送る | 鍵交換の材料(key_share)。TLS と同じ |
| ⑩ | サーバ → クライアント | ServerHello を返す | ここで共通の鍵ができ、この先は暗号化される |
| ⑪ | サーバ → クライアント | EncryptedExtensions を送る | TLS と同じ |
| ⑫ | サーバ → クライアント | CertificateRequest で証明書を求める | 受け付ける署名の方式や、受け付ける CA の一覧を付けられる |
| ⑬ | サーバ → クライアント | サーバの証明書・署名・Finished を送る | サーバの秘密鍵で署名(TLS と同じ) |
| ⑭ | クライアント | サーバを確かめる | クライアントが信じる CA でたどる(TLS と同じ) |
| ⑮ | クライアント → サーバ | Certificate でクライアント証明書を送る | 暗号化された中で送られる |
| ⑯ | クライアント → サーバ | CertificateVerify を送る | ここまでのやり取りに、クライアントの秘密鍵で署名する |
| ⑰ | クライアント → サーバ | Finished を送る | やり取りが改ざんされていないことの確認 |
| ⑱ | サーバ | クライアントを確かめる | サーバが信じる CA でたどり、⑯の署名を証明書の公開鍵で確かめる |
| ⑲ | サーバ(アプリや設定) | 誰かを決める | SAN や指紋から app01 と決める |
| ⑳ | クライアント → サーバ | HTTP リクエストを送る | トークンを一緒に送ることも多い |
| ㉑ | サーバ(アプリ) | app01 の権限で処理する | 認可はアプリが決める |
| ㉒ | サーバ → クライアント | HTTP レスポンスを返す | 同じ暗号化された通信で返す |
RFC 8446(TLS 1.3)では、サーバが CertificateRequest を送ったときだけ、クライアントは Certificate を送ります。証明書を送ったときは、続けて CertificateVerify を送ります。⑫に付ける「受け付ける CA の一覧」(certificate_authorities)は、クライアントが複数の証明書を持っているときに、どれを出すかを選ぶための手がかりです。
TLS 1.3 では、クライアント証明書も暗号化された中で送られるので、通信を見ている人には app01 の証明書は見えません。
証明書に結びついたトークン(RFC 8705)
mTLS とトークンを組み合わせると、トークンを盗まれたときの被害を小さくできます。RFC 8705 は、認可サーバが、トークンを取りに来たクライアントの証明書の「指紋」をトークンに入れる方法を決めています。API は、TLS でつないできた証明書の指紋と、トークンに入った指紋を比べます。
| 項目 | 最初の図(図9) | この図(証明書に結びついたトークン) |
|---|---|---|
| 登場するもの | クライアントとサーバ | クライアント・認可サーバ・API の3つ |
| トークン | 使っても使わなくてもよい(⑳) | 認可サーバが、証明書の指紋(cnf の x5t#S256)を入れて出す(❷❸) |
| API が確かめること | クライアント証明書(⑱) | クライアント証明書と、トークンの指紋が一致すること(❺) |
| トークンが漏れたら | トークンだけの API なら使えてしまう | 指紋が合わないので 401 invalid_token(❽❾) |
指紋は、証明書を DER(バイナリの形)にして SHA-256 でハッシュし、base64url で文字列にしたものです。JWT のアクセストークンなら、cnf(confirmation)というクレームの中の x5t#S256 に入ります。RFC 8705 では、API(リソースサーバ)は TLS で受け取った証明書とこの値が合うかを確かめ、合わなければ 401 と invalid_token のエラーで断ります。
RFC 8705 には、クライアントの認証のしかたも2つ決まっています。CA が出した証明書を使い、サブジェクトや SAN の値を登録しておく tls_client_auth と、自己署名の証明書をそのまま登録しておく self_signed_tls_client_auth です。tls_client_auth では、照合に使う値(サブジェクトの DN、SAN の DNS 名など)を、クライアントごとに1つだけ登録します。
試してみる(OpenSSL と curl)
ここからは、テスト用の環境で OpenSSL 3.0.13 と curl を使い、mTLS を動かした結果を見ていきます。名前は試験用のドメイン(example.test)にし、CA は1つで、サーバ証明書とクライアント証明書の両方を出しています。
CA とサーバ・クライアントの証明書を作る(図8の①〜⑤)
まず、テスト用の CA(Demo Root CA)を作り、その CA でサーバ証明書とクライアント証明書を発行します。違いは用途(extendedKeyUsage)と名前(subjectAltName)だけです。
CA を作る(CA で操作)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout ca.key -out ca.crt -days 30 -subj "/CN=Demo Root CA"
サーバ証明書を発行する(サーバ用:serverAuth)
printf "subjectAltName=DNS:api.example.test,DNS:localhost\nextendedKeyUsage=serverAuth\n" > server.ext openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout server.key -out server.csr -subj "/CN=api.example.test" openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 7 -extfile server.ext
クライアント証明書を発行する(クライアントで①、CA で③、図8の①〜④)
printf "subjectAltName=DNS:app01.example.test\nextendedKeyUsage=clientAuth\n" > client.ext openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout client.key -out client.csr -subj "/CN=app01" openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 7 -extfile client.ext
Certificate request self-signature ok
subject=CN = app01
実際の手順では、1行目と2行目(鍵ペアと CSR の作成)をクライアントで行い、CSR だけを CA に渡して、3行目を CA で行います。ここでは1台で続けて実行しています。
クライアント証明書の中身を見る
openssl x509 -in client.crt -noout -subject -issuer -ext subjectAltName,extendedKeyUsage
subject=CN = app01 issuer=CN = Demo Root CA X509v3 Subject Alternative Name: DNS:app01.example.test X509v3 Extended Key Usage: TLS Web Client Authentication
発行者は Demo Root CA、名前は app01.example.test、用途は「TLS Web Client Authentication」(clientAuth)です。サーバ証明書のほうは、用途が「TLS Web Server Authentication」になります。
クライアント証明書を求めるサーバを起動する(図8の⑥〜⑦)
テスト用のサーバとして openssl s_server を使います。クライアント証明書について決めるのは3つのオプションです。
クライアント証明書を必須にして起動する(サーバで操作、図8の⑥⑦)
openssl s_server -accept 8443 -cert server.crt -key server.key -CAfile ca.crt -Verify 1 -verify_return_error -www
-CAfile に、クライアント証明書を確かめる CA を指定します。-Verify は証明書を必須にするオプションで、数字はたどる証明書の段数の上限です(小文字の -verify にすると、求めはするが必須にはしません)。-verify_return_error を付けないと、確認に失敗してもメッセージを出すだけで接続を続けるので、試すときは付けておきます。なお、この s_server は「誰か」を決める⑧の対応表を持たないので、Demo Root CA が出した clientAuth の証明書なら通します。
s_client でつなぐ(図9の⑨〜⑲)
クライアントは、-cert と -key でクライアント証明書と秘密鍵を指定します。
クライアント証明書を付けてつなぐ(クライアントで操作)
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -cert client.crt -key client.key -brief
CONNECTION ESTABLISHED Protocol version: TLSv1.3 Ciphersuite: TLS_AES_256_GCM_SHA384 Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA224:RSA+SHA224 Peer certificate: CN = api.example.test Hash used: SHA256 Signature type: ECDSA Verification: OK Server Temp Key: X25519, 253 bits DONE
クライアント側の Verification: OK は、⑭でサーバ証明書の確認が通ったという意味です。クライアント証明書の確認の結果は、サーバ側に出ます。
サーバ側の表示(図9の⑱)
verify depth is 1, must return a certificate Using default temp DH parameters ACCEPT depth=1 CN = Demo Root CA verify return:1 depth=0 CN = app01 verify return:1
1行目の must return a certificate は、-Verify で証明書を必須にしたことを示しています。depth=1 が CA、depth=0 がクライアント証明書で、どちらも verify return:1(確認が通った)です。
ハンドシェイクのメッセージを見る(図9の⑨〜⑰)
-trace を付けると、やり取りしたメッセージが表示されます。メッセージの名前と長さの行だけを取り出しました。
メッセージの名前を見る(クライアントで操作)
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -cert client.crt -key client.key -trace 2>&1 | grep -E "^(Sent|Received) Record|^ [A-Za-z]+, Length"
Sent Record
ClientHello, Length=309
Received Record
ServerHello, Length=118
Received Record
Received Record
EncryptedExtensions, Length=2
Received Record
CertificateRequest, Length=74
Received Record
Certificate, Length=844
Received Record
CertificateVerify, Length=74
Received Record
Finished, Length=48
Sent Record
Sent Record
Certificate, Length=823
Sent Record
CertificateVerify, Length=76
Sent Record
Finished, Length=48
Sent Record
サーバから CertificateRequest が届き(⑫)、サーバの Certificate・CertificateVerify・Finished の後で、クライアントが自分の Certificate(⑮)・CertificateVerify(⑯)・Finished(⑰)を送っています。ServerHello の後と、クライアントが Certificate を送る前に、中身の表示がない Received Record と Sent Record があります。grep で絞らずに見ると、どちらも TLS の記事でも見た互換のためのダミーのメッセージ(ChangeCipherSpec)で、最後の Sent Record は接続を終えるときのアラートでした。
curl でクライアント証明書を付けて API を呼ぶ(図9の⑳〜㉒)
curl では --cert と --key を使います。s_server の -www は、接続の情報をページにして返すので、サーバが受け取ったクライアント証明書の欄だけを取り出しました。
クライアント証明書を付けて呼ぶ(クライアントで操作)
curl -sS --cacert ca.crt --cert client.crt --key client.key https://localhost:8443/ | grep -E "^New, |^Client certificate|Issuer: |Subject: |TLS Web Client"
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Client certificate Issuer: CN=Demo Root CA Subject: CN=app01 TLS Web Client Authentication
サーバ側には、つないできた証明書の発行者・サブジェクト・用途が渡っています。本物の API では、アプリやゲートウェイが、この情報から app01 だと決め(⑲)、app01 に許した処理だけを行います(㉑)。
証明書の指紋(x5t#S256)を計算する(図10の❷❺)
証明書に結びついたトークンで使う x5t#S256 の値を、client.crt から計算してみます。
x5t#S256 の値を計算する
openssl x509 -in client.crt -outform DER | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '='
0GAQkN7mTAx7bRqY1jaMgJr-DDoPhJ_2B3aHBwBXLMY
比べるために、SHA-256 の指紋を16進数で見る
openssl x509 -in client.crt -noout -fingerprint -sha256
sha256 Fingerprint=D0:60:10:90:DE:E6:4C:0C:7B:6D:1A:98:D6:36:8C:80:9A:FE:0C:3A:0F:84:9F:F6:07:76:87:07:00:57:2C:C6
2つは同じハッシュ値を、base64url と16進数で書いたものです(先頭の D0 60 10 90 が base64url の 0GAQkN にあたります)。RFC 8705 の例では、アクセストークンの中に次の形で入ります。
RFC 8705 の cnf の例(RFC の本文より)
{
"iss": "https://server.example.com",
"sub": "ty.webb@example.com",
"exp": 1493726400,
"nbf": 1493722800,
"cnf":{
"x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
}
}
ここからは Microsoft の場合です
Azure App Service
Azure App Service では、アプリの設定でクライアント証明書を有効にできます(Basic 以上のプラン)。大事なのは、TLS を終えるのがアプリではなく、App Service の前段(フロントエンドのロードバランサ)だという点です。
| 項目 | 最初の図(図9) | App Service(この図) |
|---|---|---|
| TLS を終えるところ | サーバ(API)そのもの | App Service のフロントエンド(❷) |
| 証明書の確認 | TLS の段階でサーバが確かめる(⑱) | App Service は確かめずに渡し、アプリのコードが確かめる(❹) |
| 証明書の渡し方 | TLS の中 | X-ARR-ClientCert ヘッダ(Base64)で転送する(❸) |
| だめなとき | ハンドシェイクで断る | アプリが 403 などを返す(❺) |
Microsoft のドキュメントは、App Service はクライアント証明書をアプリに転送する以外のことはしないので、アプリのコードで確かめる必要がある、と書いています。ドキュメントのサンプルでも、指紋・サブジェクト・発行者・有効期間を確かめ、だめなら 403 を返しています。ASP.NET Core には、転送された証明書を読み取るミドルウェアがあります。また、HTTP(暗号化なし)でアクセスされるとクライアント証明書は届かないので、証明書が必要なアプリでは HTTP を許さないようにします。
クライアント証明書を有効にする(Azure CLI)
az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>
Bicep で設定する場合(抜き出し)
clientCertEnabled: true
clientCertMode: 'Required'
clientCertExclusionPaths: '/sample1;/sample2'
| クライアント証明書のモード | 動き |
|---|---|
| Required | すべてのリクエストでクライアント証明書が必要 |
| Optional | 証明書は使っても使わなくてもよい。既定でクライアントに求める(ブラウザなら選ぶ画面が出る) |
| Optional Interactive User | 証明書は使っても使わなくてもよい。既定では求めない |
「Optional Interactive User」のモードか、証明書を求めないパス(除外パス、clientCertExclusionPaths)を使うと、App Service は TLS の再ネゴシエーション(つないだ後でもう一度ハンドシェイクする)で証明書を求めます。TLS 1.3 と HTTP/2 は再ネゴシエーションに対応していないので、この設定では使えません。また、100 KB を超える POST・PUT は 403 で失敗します。ハンドシェイクの中で証明書を求めたいときは、モードを Required か Optional にし、除外パスを外します。
Azure API Management
API Management(APIM)では、ゲートウェイがクライアント証明書を受け取り、validate-client-certificate ポリシーで確かめられます。信頼できる CA までたどれるか、期限、失効(既定はすべて確かめる)に加えて、指紋・サブジェクト・SAN の DNS 名・発行者などを「identity」として最大10個まで書いておき、どれかに合う証明書だけを通せます。指紋(thumbprint)は SHA-1 の値です。
- クラシックの Developer・Basic・Standard・Premium では、カスタムドメインの「Negotiate client certificate」を、Consumption と v2 のプランでは「Request client certificate」を有効にします。
- Application Gateway の後ろに APIM を置くと、Application Gateway が TLS を終えるので、クライアントの証明書は APIM に届きません。サーバ変数で証明書の情報を渡す方法が案内されています。
Microsoft Entra ID の「証明書」は mTLS ではない
Entra ID のアプリ登録で使う「証明書の資格情報」も、証明書の秘密鍵で署名する点は同じですが、mTLS とは別のしくみです。アプリは証明書の秘密鍵で JWT(クライアントアサーション)に署名し、client_assertion としてトークンのエンドポイントに送ります。TLS の段階ではクライアント証明書を出しません。
JWT のヘッダには、どの証明書かを示すために x5t#S256(証明書の SHA-256 の指紋)を入れます。STEP 6 で計算したものと同じ形の値です。このしくみはクライアントクレデンシャルの記事の続きとして見ると分かりやすいです。
よくある間違い
| 間違い | どうするか |
|---|---|
| 信じる CA の証明書なら、誰でも通している | SAN や指紋などを登録した値と比べて、通す相手を決める |
| 証明書を確かめるのを、前段の仕事だと思い込む | App Service のように、前段は渡すだけの場合がある。どこで確かめるかを確認する |
| サーバ証明書をクライアント証明書の代わりに使う | clientAuth の用途を持つ証明書を発行する。公的なサーバ証明書は clientAuth を含まなくなっている |
| mTLS が通ったので、何でも許す | mTLS は認証まで。権限は対応表やトークンのスコープで決める |
| クライアントの秘密鍵を、設定ファイルと一緒に配る | 秘密鍵は Key Vault や証明書ストアなど、守られた場所に置き、期限と失効を管理する |
うまくいかないとき
mTLS の失敗は、クライアントには短いアラートしか届かず、詳しい理由はサーバ側のログに出ます。テスト用の環境で、よくある3つの失敗をわざと起こしました。
証明書を出さなかった
クライアント証明書を付けずに、証明書が必須のサーバにつなぎます。
クライアント証明書なしでつなぐ
echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -brief
CONNECTION ESTABLISHED Protocol version: TLSv1.3 Ciphersuite: TLS_AES_256_GCM_SHA384 Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA224:RSA+SHA224 Peer certificate: CN = api.example.test Hash used: SHA256 Signature type: ECDSA Verification: OK Server Temp Key: X25519, 253 bits 4077D11BC17F0000:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:../ssl/record/rec_layer_s3.c:1599:SSL alert number 116
サーバ側の表示
40275245057F0000:error:0A0000C7:SSL routines:tls_process_client_certificate:peer did not return a certificate:../ssl/statem/statem_srvr.c:3509:
先に CONNECTION ESTABLISHED と出てから、certificate required のアラートで切られています。TLS 1.3 では、クライアントが Finished を送った時点でクライアント側のハンドシェイクは終わり、サーバの判断はその後に届くからです。RFC 8446 では、証明書を求められて出せる証明書がないクライアントは、空の Certificate を送ります。そのときサーバは、認証なしで続けるか、certificate_required のアラートで終えるかを選べます。
信じていない CA の証明書を出した
別の CA(Other CA)で、同じ CN=app01 の証明書を作って出します。
別の CA の証明書でつなぐ
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout other-ca.key -out other-ca.crt -days 30 -subj "/CN=Other CA" openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout rogue.key -out rogue.csr -subj "/CN=app01" openssl x509 -req -in rogue.csr -CA other-ca.crt -CAkey other-ca.key -CAcreateserial -out rogue.crt -days 7 -extfile client.ext curl -sS -o /dev/null --cacert ca.crt --cert rogue.crt --key rogue.key https://localhost:8443/
curl: (56) OpenSSL SSL_read: OpenSSL/3.0.13: error:0A000418:SSL routines::tlsv1 alert unknown ca, errno 0
サーバ側の表示
depth=0 CN = app01
verify error:num=20:unable to get local issuer certificate
40275245057F0000:error:0A000086:SSL routines:tls_process_client_certificate:certificate verify failed:../ssl/statem/statem_srvr.c:3522:
名前が同じ app01 でも、サーバが信じる CA(Demo Root CA)までたどれないので断られます。PKI の記事で見た error 20 と同じです。
サーバ用の証明書を出した
Demo Root CA が出した証明書でも、用途がサーバ用(serverAuth)の server.crt をクライアント証明書として出します。
サーバ用の証明書でつなぐ
curl -sS -o /dev/null --cacert ca.crt --cert server.crt --key server.key https://localhost:8443/
curl: (56) OpenSSL SSL_read: OpenSSL/3.0.13: error:0A000413:SSL routines::sslv3 alert unsupported certificate, errno 0
サーバ側の表示
depth=0 CN = api.example.test
verify error:num=26:unsuitable certificate purpose
40275245057F0000:error:0A000086:SSL routines:tls_process_client_certificate:certificate verify failed:../ssl/statem/statem_srvr.c:3522:
CA は信じられても、用途が clientAuth でないので断られます。クライアントに届くアラートの名前(unknown ca、unsupported certificate など)で、どの確認で止まったかの見当がつきます。期限切れや失効などのエラーの番号は、PKI の記事で見たものと同じです。
| クライアントの表示 | サーバ側の理由 | 確認すること |
|---|---|---|
| certificate required | peer did not return a certificate | クライアントに証明書と鍵を指定したか。CertificateRequest の CA の一覧に合う証明書を持っているか |
| unknown ca | error 20 | サーバの「信じる CA」に、発行した CA(と中間 CA)が入っているか |
| unsupported certificate | error 26 | 証明書の用途に clientAuth があるか |
まとめ
| 項目 | 内容 |
|---|---|
| mTLS とは | TLS のハンドシェイクで、クライアントも証明書を出し、秘密鍵で署名して自分を示す |
| 増えるメッセージ | CertificateRequest(サーバ)、Certificate と CertificateVerify(クライアント) |
| サーバが確かめること | 信じる CA までのつながり・期限と失効・clientAuth の用途・CertificateVerify の署名 |
| 別に決めること | どの証明書が誰か、その相手に何を許すか |
| トークンとの組み合わせ | cnf の x5t#S256 でトークンを証明書に結びつけ、盗まれても使えないようにする(RFC 8705) |
| Azure App Service | 前段で TLS を終え、X-ARR-ClientCert で転送する。確かめるのはアプリ |
| Azure API Management | validate-client-certificate ポリシーで確かめる |
参考にした資料
この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3(IETF):CertificateRequest・Certificate・CertificateVerify の流れ、空の Certificate と certificate_required、certificate_authorities
- RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens(IETF):tls_client_auth と self_signed_tls_client_auth、cnf の x5t#S256、合わないときの 401 invalid_token
- openssl-s_server.pod.in・openssl-verification-options.pod(OpenSSL 3.0):-verify と -Verify、-verify_return_error、-www、-crl_check
- Configure TLS mutual authentication in Azure App Service(Microsoft Learn):対応するプラン、3つのモード、X-ARR-ClientCert とアプリでの確認、再ネゴシエーションと TLS 1.3・HTTP/2・100 KB の制限、Azure CLI と Bicep
- Industry-wide certificate changes impacting Azure App Service(Microsoft Learn):公的なサーバ証明書から clientAuth の用途が外れること(2026年6月15日以降の発行分)
- How to secure APIs using client certificate authentication in API Management・Validate client certificate(Microsoft Learn):Negotiate client certificate と Request client certificate、Application Gateway の後ろでの注意、ポリシーの属性と identity
- Microsoft identity platform application authentication certificate credentials(Microsoft Learn):クライアントアサーションの形(PS256、x5t#S256)、client_assertion_type
あとがき
実際に試してみて意外だったのは、証明書を出さずにつないだとき、クライアントには先に「つながった」と表示されることでした。TLS 1.3 ではクライアントのほうが先にハンドシェイクを終えるので、断られたことは後から届きます。また、App Service のように前段で TLS が終わる構成では、証明書を確かめる責任がアプリに移ることも、図にして改めて整理できました。次の記事では、こうした証明書や API キーなどの秘密の値を、どこにどう置いて管理するかを扱います。
コメント