Step 03 — mTLS (相互認証)
ゴール
- クライアントにも証明書を提示させ、サーバー側で検証する mTLS を成立させる。
tls.Config.ClientAuthの各モードの違いを説明できるようにする。- ハンドシェイク完了後、
*tls.Conn.ConnectionState().PeerCertificatesから「誰が繋いできたか」を取り出して、認可ロジックの足がかりにする。
前提
make certstutorial/step03-mtls/certs/ 直下に ca.crt, server.crt, server.key, client.crt, client.key の5つが揃っていれば OK です。
ファイル
| ファイル | 役割 |
|---|---|
server/main.go | :9444 で mTLS listen。クライアント証明書を ca.crt で検証 |
client/main.go | localhost:9444 に接続。client.crt を提示 |
動かす
ターミナル A
go run ./tutorial/step03-mtls/server
# => step03 mTLS server listening on :9444 (mutual auth)ターミナル B
go run ./tutorial/step03-mtls/client
# => connected to localhost:9444 over mTLS (server CN=localhost)
hi
hello alice@example.com, you said: hiサーバー側のログにも peer CN=alice@example.com が出ているはずです。サーバーが相手の名前を知っていることが、Step02 との大きな違いです。
ハンドシェイクの追加分
Step02 のハンドシェイクに、サーバーからの CertificateRequest と、クライアントからの Certificate + CertificateVerify (秘密鍵で署名) が増えます。
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello
S->>C: ServerHello, Certificate
S->>C: CertificateRequest ※ Step03 から追加
C->>S: Certificate, CertificateVerify ※ Step03 から追加
C->>S: (鍵交換)
Note over C,S: 暗号化セッション確立
サーバーは:
- クライアントが提示した証明書チェーンが
ClientCAsに辿り着くか - 署名 (
CertificateVerify) が、提示された公開鍵に対応する秘密鍵で行われたか
を検証します。両方通って初めて接続が成立します。
ClientAuth の種類
server/main.go のコメントにもありますが、よく使うのは下の2つです。
| 値 | 意味 | 使いどころ |
|---|---|---|
tls.NoClientCert | クライアント証明書を要求しない | Step02 と同じ。Web の HTTPS など |
tls.RequireAndVerifyClientCert | 必須+検証。これが本当の mTLS | サービス間通信、専用クライアント、社内ツール |
中間の RequestClientCert / RequireAnyClientCert / VerifyClientCertIfGiven は 「出てくれば見るが信頼はしない」「出てくるが署名を見ない」 など中途半端な動きをするので、認証目的では使わないこと。
観察ポイント (実験してみよう)
1. 証明書なしのクライアントは弾かれる
Step02 のクライアントを Step03 のサーバーに繋いでみます。
# ターミナル A: Step03 サーバーが起動中
# ターミナル B:
go run ./tutorial/step02-tls/clientハンドシェイクが失敗します。サーバー側ログで:
handshake failed from 127.0.0.1:xxxxx: tls: client didn't provide a certificateクライアント側ログで:
remote error: tls: certificate requiredこれが ClientAuth = RequireAndVerifyClientCert の効果です。
実験するときは、Step02 クライアントの接続先を
localhost:9444に書き換える必要があります。
2. 別 CA の証明書を渡すと拒否される
別 CA を作って、その CA で署名した client cert を Step03 に置き換えてみます。
# 退避
mv tutorial/step03-mtls/certs/client.crt tutorial/step03-mtls/certs/client.crt.good
mv tutorial/step03-mtls/certs/client.key tutorial/step03-mtls/certs/client.key.good
# 別 CA + 別 client を一時ディレクトリで作成
TMP=$(mktemp -d)
openssl genrsa -out "$TMP/other-ca.key" 4096 2>/dev/null
openssl req -x509 -new -nodes -key "$TMP/other-ca.key" -sha256 -days 30 \
-subj "/CN=Other CA" -out "$TMP/other-ca.crt"
openssl genrsa -out "$TMP/client.key" 2048 2>/dev/null
openssl req -new -key "$TMP/client.key" -subj "/CN=mallory" -out "$TMP/client.csr"
openssl x509 -req -in "$TMP/client.csr" \
-CA "$TMP/other-ca.crt" -CAkey "$TMP/other-ca.key" -CAcreateserial \
-days 30 -out "$TMP/client.crt"
cp "$TMP/client.crt" tutorial/step03-mtls/certs/client.crt
cp "$TMP/client.key" tutorial/step03-mtls/certs/client.key
go run ./tutorial/step03-mtls/client
# => 拒否される: tls: unknown certificate authority
# 後始末
rm -rf "$TMP"
mv tutorial/step03-mtls/certs/client.crt.good tutorial/step03-mtls/certs/client.crt
mv tutorial/step03-mtls/certs/client.key.good tutorial/step03-mtls/certs/client.keyサーバー側の ClientCAs に Other CA が入っていないため、署名を検証できず拒否されます。
3. peer の身元で認可してみる
server/main.go の handle で peerCN を取り出しているので、ここで認可ロジックが書けます。例:
if peerCN != "alice@example.com" {
log.Printf("unauthorized CN=%s", peerCN)
return
}これだけで「特定のクライアントだけ受け付ける」サーバーになります。実運用では CN ではなく SAN (URI SAN や DNS SAN) を見るのが一般的です — CN は人間向けの表示名で、運用で書き換えられがちなため。
パケットダンプ
tcpdump で実際に観測した本ステップのハンドシェイクを mtls-dump.txt
に置いてあります。Step02 との差分として、サーバーから CertificateRequest が飛び、クライアントが自分の証明書 + CertificateVerify を返している様子を確認できます。
用語ミニまとめ
- mTLS (mutual TLS): クライアント/サーバー双方が証明書で身元を証明する TLS。
Certificates(サーバー側 / クライアント側): 自分が提示する証明書/鍵。同じフィールド名でも、サーバーでは「サーバー証明書」、クライアントでは「クライアント証明書」を意味する。ClientCAs: サーバーが、繋いでくるクライアントの証明書を検証するための CA 集合。PeerCertificates: ハンドシェイクで相手が提示してきた証明書チェーン。[0]が葉。
ここまでで身についたもの
tls.Configの主要フィールドが何のためにあるか、片側ずつではなく 両側のペアで 説明できる。- 「平文 TCP → サーバー認証 TLS → mTLS」と段階を踏んだことで、各ステップで 何が解決され、何が残るか を語れる。
発展トピックは docs/00-overview.md
の末尾に列挙してあります。