前提

mTLSで必要なファイル

CA関連

ファイル名役割
ca.csrCAの申請書(AWS Private CAが内部で生成)
ca-cert.pemCA証明書。「このCAは信頼できる」という証明

サーバー関連

ファイル名役割
server-key.pemサーバーの秘密鍵
server.csrサーバーの申請書
server-cert.pemサーバー証明書。「このサーバーは本物」とCAが署名

クライアント関連

ファイル名役割
client-key.pemクライアントの秘密鍵
client.csrクライアントの申請書
client-cert.pemクライアント証明書。「このクライアントは本物」とCAが署名

mTLS通信時に誰が何を使うか

サーバーに配置:

  • server-key.pem - 自分の秘密鍵
  • server-cert.pem - 自分の証明書(クライアントに提示)
  • ca-cert.pem - クライアント証明書を検証するため

クライアントがリクエスト時に使用:

  • client-key.pem - 自分の秘密鍵
  • client-cert.pem - 自分の証明書(サーバーに提示)
  • ca-cert.pem - サーバー証明書を検証するため

なぜ両方にca-cert.pemが必要か

  • mTLS-Server: 「このクライアント証明書は信頼できるCAが発行したか?」を確認
  • mTLS-Client: 「このサーバー証明書は信頼できるCAが発行したか?」を確認

同じCA(mTLS-Adminで作成したPrivate CA)が両方の証明書を発行しているので、同じ ca-cert.pem で検証できる。

今回はクライアント、サーバー証明書をそれぞれ同じCAで署名したが、異なるCAを用いてもよい

mTLSリクエスト時に必要な情報

例えば、curlを用いてクライアントから以下のようにリクエストする

curl --cacert ca-cert.pem \
     --cert client-cert.pem \
     --key client-key.pem \
     https://<サーバーのホス>
オプション送るもの目的
--cacert ca-cert.pem(送らない。検証に使う)サーバー証明書が信頼できるCAから発行されたか確認
--cert client-cert.pemクライアント証明書「私はこの証明書の持ち主です」とサーバーに提示
--key client-key.pem(送らない。署名に使う)TLSハンドシェイク中の署名に使用。秘密鍵を持っている証明

セットアップ

mtls-environment.yamlを用いて EC2インスタンスを3つ用意し、mTLSを試す。

mtls-environment.yaml でデプロイされる環境:

インスタンス役割
mTLS-AdminCA管理。証明書の発行・配布を行う
mTLS-ClientcurlでmTLSリクエストを送信
mTLS-ServerNginxでmtls接続を受け付ける

全インスタンスは同一セキュリティグループ内で相互通信可能

環境デプロイ

aws cloudformation create-stack \
  --stack-name mtls-env \
  --template-body file://mtls-environment.yaml \
  --parameters \
    ParameterKey=VpcId,ParameterValue=vpc-xxxxxxxx \
    ParameterKey=SubnetId,ParameterValue=subnet-xxxxxxxx

初期設定

【Admin】ホスト名変更

sudo hostnamectl set-hostname mtls-admin
exec bash

【Client】ホスト名変更

sudo hostnamectl set-hostname mtls-client
exec bash

【Server】ホスト名変更

sudo hostnamectl set-hostname mtls-server
exec bash

[Admin] aws cli 認証情報の設定

v 2.32.0 以降では、aws loginコマンドによりAWSコンソールの認証情報を設定可能。

Route53へのレコード追加、Private CAリソース作成に十分な権限を持ったIAMユーザー/ロールでAWSコンソールにログインしておく

参考: Sign in through the AWS Command Line Interface

// AWS CLI を最新にupdate
// http://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update

source ~/.bashrc
aws --version

aws login --remote
aws sts get-caller-identity

Private CA作成

【Admin】CA作成

CA_ARN=$(aws acm-pca create-certificate-authority \
  --certificate-authority-configuration '{
    "KeyAlgorithm": "RSA_2048",
    "SigningAlgorithm": "SHA256WITHRSA",
    "Subject": {
      "Country": "JP",
      "Organization": "TestOrg",
      "OrganizationalUnit": "IT",
      "CommonName": "Test-Private-CA"
    }
  }' \
  --certificate-authority-type ROOT \
  --usage-mode GENERAL_PURPOSE \
  --query 'CertificateAuthorityArn' --output text)
 
echo "Private CA ARN: $CA_ARN"

【Admin】CA CSR取得

aws acm-pca get-certificate-authority-csr \
  --certificate-authority-arn $CA_ARN \
  --output text > ca.csr

【Admin】Root CAとして自己署名

CSR をもとに証明書を発行する。

issue-certificate は Private CA 側に証明書を作成するコマンド。

証明書自体は別途手元に取得する必要がある

CERT_ARN=$(aws acm-pca issue-certificate \
  --certificate-authority-arn $CA_ARN \
  --csr fileb://ca.csr \
  --signing-algorithm SHA256WITHRSA \
  --template-arn arn:aws:acm-pca:::template/RootCACertificate/V1 \
  --validity Value=3650,Type=DAYS \
  --query 'CertificateArn' --output text)
echo $CERT_ARN

【Admin】CA証明書取得・インポート

前ステップでCA証明書を作成したが、まだ作成したCA(CA_ARN)で使用できる状態ではない。

Private CA では外部CAで署名した証明書を使うこともできるため、インポートという手順を踏みCAを有効化する必要がある

aws acm-pca get-certificate \
  --certificate-authority-arn $CA_ARN \
  --certificate-arn $CERT_ARN \
  --query 'Certificate' --output text > ca-cert.pem
 
aws acm-pca import-certificate-authority-certificate \
  --certificate-authority-arn $CA_ARN \
  --certificate fileb://ca-cert.pem

これにより、CAのステータスがアクティブとなる

クライアント証明書発行

【Client】秘密鍵・CSR作成

openssl genrsa -out client-key.pem 2048
 
openssl req -new -key client-key.pem -out client.csr \
  -subj "/C=JP/O=TestOrg/CN=test-client"
 
# CSRの内容を表示(これをコピー)
cat client.csr

【Admin】CSRを受け取り・クライアント証明書発行

# ClientでコピーしたCSRを貼り付け
cat > client.csr << 'EOF'
-----BEGIN CERTIFICATE REQUEST-----
(Clientでコピーした内容を貼り付け)
-----END CERTIFICATE REQUEST-----
EOF
 
CLIENT_CERT_ARN=$(aws acm-pca issue-certificate \
  --certificate-authority-arn $CA_ARN \
  --csr fileb://client.csr \
  --signing-algorithm SHA256WITHRSA \
  --template-arn arn:aws:acm-pca:::template/EndEntityCertificate/V1 \
  --validity Value=365,Type=DAYS \
  --query 'CertificateArn' --output text)
 
aws acm-pca get-certificate \
  --certificate-authority-arn $CA_ARN \
  --certificate-arn $CLIENT_CERT_ARN \
  --query 'Certificate' --output text > client-cert.pem
 
# クライアント証明書を表示(これをコピー)
cat client-cert.pem
 
# CA証明書も表示(これもコピー)
cat ca-cert.pem

【Client】証明書を受け取り・配置

# Adminでコピーしたクライアント証明書を貼り付け
cat > ~/client-cert.pem << 'EOF'
-----BEGIN CERTIFICATE-----
(Adminでコピーした内容を貼り付け)
-----END CERTIFICATE-----
EOF
 
# Adminでコピーしたca-cert.pemを貼り付け
cat > ~/ca-cert.pem << 'EOF'
-----BEGIN CERTIFICATE-----
(Adminでコピーした内容を貼り付け)
-----END CERTIFICATE-----
EOF
 
mv client-key.pem ~/

サーバー証明書発行

【Server】秘密鍵・CSR作成

openssl genrsa -out server-key.pem 2048
 
# CNにはServerのPrivate IPを指定
openssl req -new -key server-key.pem -out server.csr \
  -subj "/C=JP/O=TestOrg/CN=<SERVER_IP>"
 
# CSRの内容を表示(これをコピー)
cat server.csr

【Admin】CSRを受け取り・サーバー証明書発行

# ServerでコピーしたCSRを貼り付け
cat > server.csr << 'EOF'
-----BEGIN CERTIFICATE REQUEST-----
(Serverでコピーした内容を貼り付け)
-----END CERTIFICATE REQUEST-----
EOF
 
SERVER_CERT_ARN=$(aws acm-pca issue-certificate \
  --certificate-authority-arn $CA_ARN \
  --csr fileb://server.csr \
  --signing-algorithm SHA256WITHRSA \
  --template-arn arn:aws:acm-pca:::template/EndEntityCertificate/V1 \
  --validity Value=365,Type=DAYS \
  --query 'CertificateArn' --output text)
 
aws acm-pca get-certificate \
  --certificate-authority-arn $CA_ARN \
  --certificate-arn $SERVER_CERT_ARN \
  --query 'Certificate' --output text > server-cert.pem
 
# サーバー証明書を表示(これをコピー)
cat server-cert.pem
 
# CA証明書も表示(これもコピー)
cat ca-cert.pem

【Server】証明書を受け取り・配置

# Adminでコピーしたサーバー証明書を貼り付け
cat > /tmp/server-cert.pem << 'EOF'
-----BEGIN CERTIFICATE-----
(Adminでコピーした内容を貼り付け)
-----END CERTIFICATE-----
EOF
 
# Adminでコピーしたca-cert.pemを貼り付け
cat > /tmp/ca-cert.pem << 'EOF'
-----BEGIN CERTIFICATE-----
(Adminでコピーした内容を貼り付け)
-----END CERTIFICATE-----
EOF
 
sudo mkdir -p /etc/ssl/private
sudo cp /tmp/ca-cert.pem /etc/ssl/certs/
sudo cp /tmp/server-cert.pem /etc/ssl/certs/
sudo mv server-key.pem /etc/ssl/private/
sudo chmod 600 /etc/ssl/private/server-key.pem
sudo chmod 644 /etc/ssl/certs/*.pem
rm /tmp/server-cert.pem /tmp/ca-cert.pem

Server設定(Nginxセットアップ)

【Server】Nginxインストール・設定

sudo dnf install -y nginx
sudo systemctl enable nginx
# <SERVER_IP> はServerのPrivate IP(証明書のCNと一致させる)
sudo tee /etc/nginx/conf.d/mtls.conf << 'EOF'
server {
    listen 443 ssl;
    server_name <SERVER_IP>;
    
    ssl_certificate /etc/ssl/certs/server-cert.pem;
    ssl_certificate_key /etc/ssl/private/server-key.pem;
    
    ssl_client_certificate /etc/ssl/certs/ca-cert.pem;
    ssl_verify_client on;
    
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    location /debug-header {
        add_header Content-Type text/plain;
        return 200 "=== mTLS Success ===
Client DN: $ssl_client_s_dn
Client Verify: $ssl_client_verify
TLS Version: $ssl_protocol
";
    }
    
    location /health {
        return 200 "OK";
    }
}
EOF
 
sudo systemctl restart nginx

mTLSテスト

注意: 以下の <SERVER_IP> はServerのPrivate IPに置き換えること

【Client】証明書なしテスト(失敗する)

curl https://<SERVER_IP>/debug-header

以下のエラーが発生する

curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.

【Client】CA証明書のみテスト(失敗する)

curl --cacert ca-cert.pem https://<SERVER_IP>/debug-header
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.28.0</center>
</body>
</html>

【Client】mTLSテスト(成功する)

curl --cacert ca-cert.pem \
     --cert client-cert.pem \
     --key client-key.pem \
     https://<SERVER_IP>/debug-header
=== mTLS Success ===
Client DN: CN=test-client,O=TestOrg,C=JP
Client Verify: SUCCESS
TLS Version: TLSv1.3

成功

【Client】無効な証明書テスト(失敗する)

openssl genrsa -out fake-key.pem 2048
openssl req -new -x509 -days 365 -key fake-key.pem -out fake-cert.pem \
  -subj "/C=JP/O=FakeOrg/CN=fake-client"
 
curl --cacert ca-cert.pem \
     --cert fake-cert.pem \
     --key fake-key.pem \
     https://<SERVER_IP>/debug-header
<html>
<head><title>400 The SSL certificate error</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The SSL certificate error</center>
<hr><center>nginx/1.28.0</center>
</body>
</html>

より詳細を見てみる

opensslコマンドでmTLSのやり取りを確認

openssl s_client を使うとTLSハンドシェイクの詳細が確認できる。

openssl s_client -connect 172.31.32.25:443 \
  -CAfile ~/ca-cert.pem \
  -cert ~/client-cert.pem \
  -key ~/client-key.pem \
  -state

パケットキャプチャして暗号化されているか確認

tcpdumpでパケットをキャプチャし、TLS通信が暗号化されていることを確認する

  • SesrverのプライベートIP: 172.31.32.25
  • ClientのプライベートIP: 172.31.47.173

比較:暗号化なしの場合(HTTP)

Nginx の設定を変更し、http のリクエストを受けるように変更

# Server側でHTTPでもリッスン
sudo tee /etc/nginx/conf.d/http.conf << 'EOF'
server {
    listen 8080;
    location /test {
        return 200 "SECRET DATA\n";
    }
}
EOF
sudo systemctl reload nginx
 
# キャプチャ開始
sudo tcpdump -i any port 8080 -w /tmp/http.pcap &
 
# Client側からHTTPリクエスト
curl http://<SERVER_IP>:8080/test
 
# キャプチャ停止・確認
sudo pkill tcpdump
10 packets captured
11 packets received by filter
0 packets dropped by kernel

HTTPの場合は SECRET DATA が平文で見える

tcpdump -r /tmp/http.pcap -A

...

07:19:04.430549 ens5  Out IP ip-172-31-32-25.ap-northeast-1.compute.internal.webcache > ip-172-31-47-173.ap-northeast-1.compute.internal.33258: Flags [P.], seq 1:175, ack 86, win 489, options [nop,nop,TS val 2601695063 ecr 2836648391], length 174: HTTP: HTTP/1.1 200 OK
E.....@...=L.. .../.......
................
...W....HTTP/1.1 200 OK
Server: nginx/1.28.0
Date: Sat, 29 Nov 2025 07:19:04 GMT
Content-Type: application/octet-stream
Content-Length: 12
Connection: keep-alive

SECRET DATA

...

比較:暗号化ありの場合(mTLS)

【Server】tcpdumpでキャプチャ開始

sudo tcpdump -i any port 443 -w /tmp/mtls.pcap &

【Client】mTLSリクエスト送信

curl --cacert ~/ca-cert.pem \
     --cert ~/client-cert.pem \
     --key ~/client-key.pem \
     https://<SERVER_IP>/debug-header

【Server】キャプチャ停止・内容確認

sudo pkill tcpdump
 
55 packets captured
61 packets received by filter
0 packets dropped by kernel

mTLS(TLS)の場合は SECRET DATA が暗号化されており見えない

$ tcpdump -r /tmp/mtls.pcap -A | grep SECRET
reading from file /tmp/mtls.pcap, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144Warning: interface names might be incorrect
// SECRET DATA がヒットしない

リソース削除

【Admin】Private CA削除

# CA無効化
aws acm-pca update-certificate-authority \
  --certificate-authority-arn $CA_ARN \
  --status DISABLED
 
# CA削除
aws acm-pca delete-certificate-authority \
  --certificate-authority-arn $CA_ARN \
  --permanent-deletion-time-in-days 7

CloudFormationスタック削除

aws cloudformation delete-stack --stack-name mtls-env