[アップデート] Amazon CloudFrontがmTLSをサポートしたのでクライアント証明書の検証をしてみた

[アップデート] Amazon CloudFrontがmTLSをサポートしたのでクライアント証明書の検証をしてみた

CloudFrontで配信する環境においてクライアント証明書による認証をしたい場合に
2025.11.29

CloudFront側でクライアント証明書による認証を行いたい

こんにちは、のんピ(@non____97)です。

皆さんはCloudFront側でクライアント証明書による認証を行たいなと思ったことはありますか? 私はあります。

CloudFrontには署名付きCookieや署名付きURLといった。一方、そのための認証を考える必要があります。クライアントの実装として署名付きCookieや署名付きURLを発行するための認証に対応することが難しい、そもそも署名付きCookieや署名付きURL自体の相性が悪い場合もあるでしょう。

例えば、IoTデバイスやAPI間通信の認証では証明書ベースの認証を行う場面が多いように感じています。

また、クライアント証明書の検証をオリジン側で行なっている場合、オリジンリクエストを行わせる関係でCloudFrontのキャッシュを上手く活かせない、もしくは活かそうとすると処理が複雑になるケースもあるでしょう。

今回、CloudFrontがmTLSをサポートしました。

https://aws.amazon.com/jp/about-aws/whats-new/2025/11/amazon-cloudfront-mutual-tls-authentication/

AWS Blogsにも投稿されています。

https://aws.amazon.com/blogs/networking-and-content-delivery/trust-goes-both-ways-amazon-cloudfront-now-supports-viewer-mtls/

ALBでは2023年にサポートしていましたが、CloudFrontもついにサポートといった形です。

実際に試してみました。

いきなりまとめ

  • CloudFrontのエッジでクライアント証明書の検証ができるようになった
  • mTLSの設定はCloudFrontディストリビューション単位
    • 有効/無効の切り替えが可能
    • ビヘイビアでパスごとにmTLSを行うということはできない
  • CloudFrontのトラストストアはCRLをサポートしていないが、Connectio FunctionとKeyValueStoreを組み合わせることで同様の処理を実現することは可能
  • クライアント証明書の検証のログはコネクションログとして出力可能
    • 標準ログV2で出力できる
  • クライアント証明書の検証に失敗したものはCloudFrontのアクセスログに記録されない
  • mTLS利用時の追加の料金はなし
    • Connection FunctionはおそらくCloudFront Functionsと同等の料金が発生
  • Connection Functionsを利用する前にはクォータの引き上げが必要
    • デフォルトは0

ドキュメントから仕様の確認

概要

まず、ドキュメントから仕様を確認します。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/mtls-authentication.html

CloudFrontがmTLSをサポートしたことにより、サーバーとクライアントの両方がX.509 証明書を使用して相互認証することを要求することができるようになりました。これにより、信頼できる証明書を提示してきたクライアントのみに接続をさせることが可能です。

mTLSの設定はCloudFrontディストリビューション単位で行います。AWS CLIでもマネジメントコンソールでも簡単に有効/無効の切り替えが可能です。ディストリビューション単位での設定であるため、ビヘイビアでパスごとにmTLSを行うということはできません。

mTLSを設定するにはプライベート認証局の証明書をS3バケットにアップロードし、トラストストアとして指定する必要があります。これはALBのmTLSと同じですね。

ALBのトラストストアと異なる点としてCRLをサポートしていない点が挙げられます。何らかの理由で有効期限前に失効した証明書は後述のConnection FunctionとKeyValue Storeを用いて制御することになります。

CloudFront Connection Function

CloudFront Connection Functionは、mTLSの証明書検証後に実行されるJavaScript関数です。

AWS Client VPNでいうところのクライアント接続ハンドラーのようなもので、クライアント証明書の検証とは別に追加の検証処理を行うことが可能です。

mTLS接続を確立しようとすると、次のフローで処理が行われます。

  1. クライアントとCloudFrontエッジロケーションとのTLSハンドシェイクを開始する
  2. CloudFrontはクライアント証明書を要求し、受信する
  3. CloudFrontはトラストストアに対して証明書検証を実行する
  4. 証明書が検証に合格した場合、Connection Functionを実行する
    • ViewerMtlsConfigIgnoreCertificateExpiryが有効になっている場合、有効期限が切れているものの有効な証明書もConnection Functionに渡される
    • クライアント証明書が無効な場合、Connection Functionは呼び出されない
  5. Connection Functionは解析された証明書情報と接続の詳細を受け取る
  6. Connection Functionにてはカスタムロジックに基づいて許可/拒否の決定を行う
  7. ユーザーの指示に基づいてTLS 接続を完了または終了する

Connection Functionが受け取るイベントは以下のとおりです。

{
  "connectionId": "Fdb-Eb7L9gVn2cFakz7wWyBJIDAD4-oNO6g8r3vXDV132BtnIVtqDA==", // Unique identifier for this TLS connection
  "clientIp": "203.0.113.42", // IP address of the connecting client (IPv4 or IPv6)
  "clientCertificate": {
    "certificates": {
      "leaf": {
        "subject": "CN=client.example.com,O=Example Corp,C=US", // Distinguished Name (DN) of the certificate holder
        "issuer": "CN=Example Corp Intermediate CA,O=Example Corp,C=US", // Distinguished Name (DN) of the certificate authority that issued this certificate
        "serialNumber": "4a:3f:5c:92:d1:e8:7b:6c", // Unique serial number assigned by the issuing CA (hexadecimal)
        "validity": {
 "notBefore": "2024-01-15T00:00:00Z", // Certificate validity start date (ISO 8601 format)
 "notAfter": "2025-01-14T23:59:59Z"   // Certificate expiration date (ISO 8601 format)
        },
        "sha256Fingerprint": "a1b2c3d4e5f6...abc123def456", // SHA-256 hash of the certificate (64 hex characters)
      },
    },
  },
}

抜粋 : Associate a CloudFront Connection Function - Amazon CloudFront

IPアドレスでの制御はAWS WAFでも可能なので、Connection Functionでの検証時の主な判断要素は証明書のシリアル番号やSubjectでしょう。

Connection Functionsの一連の処理はAWS Blogsに投稿されている記事の以下図が分かりやすかったです。

6-mtls-overall-diagram.png

抜粋 : 信頼は双方向: Amazon CloudFront がビューワー mTLS をサポート | ネットワーキングとコンテンツ配信

また、Connection Functionは後述のクライアント証明書検証モードで必須かオプショナルかでトリガーされる条件が異なります。

  • 必須の場合 : クライアント証明書の検証を行い、検証に成功した場合に実行
  • オプショナルの場合 : クライアント証明書の検証/失敗に関わらず事項

なお、Connection Functionが受け取るイベントにパスやHostヘッダーの情報は含まれていないため、「オプショナルモードにしてConnection Functionでパスやホスト名ベースで制御を行う」ということはできません。

クライアント証明書の検証モード

クライアント証明書の検証は必須モードとオプショナルモードの2つがあります。

必須モードはその名の通り、クライアント証明書の検証を必ず行います。クライアント証明書を受信しなかった場合は拒否します。

一方、オプショナルモードでは以下の挙動をします。

  • 有効な証明書を持つクライアントへの接続を許可する
  • 証明書のないクライアントへの接続を許可する

そのため、オプションナルモードでは、mTLS 認証への段階的な移行や証明書を持つクライアントと証明書を持たないクライアントの両方のサポートをする必要が場面に使用します。

ログ

ALBのmTLSと同様に通常のアクセスログとは別にコネクションログの設定が可能です。

コネクションログはアクセスログと同様に標準ログV2です。

https://dev.classmethod.jp/articles/cloudfront-access-log-update-202411/

そのため、以下の指定が可能です

  • 出力先 (CloudWatch Logs / S3 / Data Firehose)
  • フィールド
  • パーティション
  • Hive互換フォーマットの有無
  • 出力フォーマット (JSON / Parquet / W3C / Plain-text)

指定可能なフィールドは以下のとおりです。

Field Description Example
eventTimestamp 接続が確立または失敗したときの ISO 8601 タイムスタンプ 1731620046814
connectionId TLS接続の一意の識別子 oLHiEKbQSn8lkvJfA3D4gFowK3_iZ0g4i5nMUjE1Akod8TuAzn5nzg==
connectionStatus mTLS 接続試行のステータス。 Success or Failed
clientIp 接続クライアントのIPアドレス 2001:0db8:85a3:0000:0000:8a2e:0370:7334
clientPort クライアントが使用するポート 12137
serverIp CloudFront エッジサーバーの IP アドレス 99.84.71.136
distributionId CloudFront ディストリビューション ID E2DX1SLDPK0123
distributionTenantId CloudFront ディストリビューションテナント ID(該当する場合) dt_2te1Ura9X3R2iCGNjW123
tlsProtocol 使用されるTLSプロトコルバージョン TLSv1.3
tlsCipher 接続に使用されるTLS暗号スイート TLS_AES_128_GCM_SHA256
tlsHandshakeDuration TLSハンドシェイクの所要時間(ms) 153
tlsSni TLS SNI d111111abcdef8.cloudfront.net
clientLeafCertSerialNumber クライアント証明書のシリアル番号 00:b1:43:ed:93:d2:d8:f3:9d
clientLeafCertSubject クライアント証明書のSubjectフィールド C=US, ST=WA, L=Seattle, O=Amazon.com, OU=CloudFront, CN=client.test.mtls.net
clientLeafCertIssuer クライアント証明書の発行者フィールド C=US, ST=WA, L=Seattle, O=Amazon.com, OU=CloudFront, CN=test.mtls.net
clientLeafCertValidity クライアント証明書の有効期間 NotBefore=2025-06-05T23:28:21Z;NotAfter=2125-05-12T23:28:21Z
connectionLogCustomData Connection Functionを介して追加されたカスタムデータ REVOKED:00:b1:43:ed:93:d2:d8:f3:9d

抜粋 : Observability using connection logs - Amazon CloudFront

connectionStatusに記録されるエラーコードは以下のとおりです。

Code Description
ClientCertMaxChainDepthExceeded 証明書チェーンの最大深度を超えました
ClientCertMaxSizeExceeded 証明書の最大サイズを超えました
ClientCertUntrusted 証明書は信頼できません
ClientCertNotYetValid 証明書はまだ有効ではありません
ClientCertExpired 証明書の有効期限が切れています
ClientCertTypeUnsupported 証明書の種類はサポートされていません
ClientCertInvalid 証明書が無効です
ClientCertIntentInvalid 証明書の意図が無効です
ClientCertRejected カスタム検証により証明書が拒否されました
ClientCertMissing 証明書がありません
TcpError 接続を確立しようとしたときにエラーが発生しました
TcpTimeout タイムアウト期間内に接続を確立できませんでした
ConnectionFunctionError Connection Functionの実行中にキャッチされない例外がスローされました
Internal 内部サービスエラーが発生しました
UnmappedConnectionError 他のカテゴリに該当しないエラーが発生しました

キャッシュポリシーで使用できるヘッダーとオリジンアクセス時に付与されるヘッダー

CloudFrontではキャッシュを効かせていることも多いでしょう。

キャッシュポリシーで使用できるヘッダーは以下のとおりです。

Header name Description Example value
CloudFront-Viewer-Cert-Serial-Number 証明書のシリアル番号の16進表現 4a:3f:5c:92:d1:e8:7b:6c
CloudFront-Viewer-Cert-Issuer 発行者の識別名(DN)のRFC2253文字列表現 CN=rootcamtls.com,OU=rootCA,O=mTLS,L=Seattle,ST=Washington,C=US
CloudFront-Viewer-Cert-Subject Subjectの識別名(DN)のRFC2253文字列表現 CN=client_.com,OU=client-3,O=mTLS,ST=Washington,C=US
CloudFront-Viewer-Cert-Present 証明書が存在するかどうかを示す 1(存在する場合)または 0(存在しない場合)のいずれかの値。必須モードでは、この値は常に 1 です。 1
CloudFront-Viewer-Cert-Sha256 クライアント証明書のSHA256ハッシュ 01fbf94fef5569753420c349f49adbfd80af5275377816e3ab1fb371b29cb586

抜粋 : Viewer mTLS headers for cache policies and forwarded to origin - Amazon CloudFront

また、オリジンリクエスト時には上述のヘッダーに加えて、以下ヘッダーも付与することが可能です。

Header name Description Example value
CloudFront-Viewer-Cert-Validity notBefore と notAfter 日付の ISO8601 形式 CloudFront-Viewer-Cert-Validity: NotBefore=2023-09-21T01:50:17Z;NotAfter=2024-09-20T01:50:17Z
CloudFront-Viewer-Cert-Pem リーフ証明書のURLエンコードされたPEM形式 CloudFront-Viewer-Cert-Pem: -----BEGIN%20CERTIFICATE-----%0AMIIG<...reduced...>NmrUlw%0A-----END%20CERTIFICATE-----%0A

クライアント証明書ごとにもキャッシュを効かせられるのは個人的には便利そうで嬉しいです。

料金

無料です。やったね。

なお、Connection Functionの料金についての言及はAWS公式ドキュメントにはありませんでした。

CloudFront Functionsと同じエッジコンピューティングではあると思うので、Connection Functionにおいても以下料金が適用されると想像しています。

Info Price
Requests $0.60 per 1M requests
Duration $0.00005001 for every GB-second

抜粋 : Amazon CloudFront CDN - Plans & Pricing - Try For Free

注意点

その他の注意点は以下のとおりです。

  • HTTPは非サポート
    • HTTPS onlyもしくはRedirect HTTP to HTTPSとする必要がある
  • HTTP/3は未サポート
  • Connection FunctionはTLS ハンドシェイク中にクライアント接続ごとに1回だけ実行される
  • Connection Functionは接続を許可または拒否することしかできず、HTTPリクエスト/レスポンスを変更することはできない
  • CloudFrontディストリビューションに関連付けることができるのは、パブリッシュ済みのConnection Functionのみ
  • 各CloudFrontディストリビューションに設定可能なConnection Functionは1つまで

やってみた

検証環境

実際に試してみましょう。

検証環境は以下のとおりです。

検証環境.png

mTLS設定とコネクションログの出力設定以外は全て設定済みです。

CA証明書とクライアント証明書の発行

以下記事を参考にCA証明書とクライアント証明書の発行を行います。

https://dev.classmethod.jp/articles/mutual-authentication-for-application-load-balancer-trust-store/

適当にAmazon Linux 2023のEC2インスタンスを立てます。

このEC2インスタンス上でCA証明書を作成します。

$ openssl req \
    -x509 \
    -new \
    -days 7 \
    -keyout rootCA_key.pem \
    -out rootCA_cert.pem
..+++++++++++++++++++++++++++++++++++++++*..
.
.
(中略)
.
.
.....++++++
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:root-ca
Email Address []:

$ cat rootCA_cert.pem
-----BEGIN CERTIFICATE-----
MIIDqTCCApGgAwIBAgIUdEGHxctVOm29JJnc35PRA4jT4tEwDQYJKoZIhvcNAQEL
BQAwZDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMRUwEwYDVQQHDAxEZWZh
dWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEDAOBgNVBAMM
B3Jvb3QtY2EwHhcNMjUxMTI3MTAxNjIxWhcNMjUxMjA0MTAxNjIxWjBkMQswCQYD
VQQGEwJKUDEOMAwGA1UECAwFVG9reW8xFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc
MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEQMA4GA1UEAwwHcm9vdC1jYTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIbee0pd1JakvdeCxoTQrz1C
qes4f4Sva5s75V19+aWqHMICljpplQUZUrSbWBNzG4WL8PTZe1elQIrC0invDQE+
cq1Ol/iv8QSOeKgiTiYFLBpsKxQuZ6hnfl4jwimEoytdJlBdmJ1CIXLjAJBQmrgj
NkBMH3KE6EyiY/u/vBOEvF3zA8lPq2/KSq8SiW9u+WYRHgxS6WNNAkeHXyMPBPi+
5AcBIaT5ZpN4Ay5pQ0B010YTWo1ZLWuwg9YEcFLvDTe5wll0LxGOvUNICKYKtsX2
/PJOU0WvUPspHI0+Poek6SCWfAodt+iZVXvh5WRIsh8NMqNeGLul7jHHeUNdR8UC
AwEAAaNTMFEwHQYDVR0OBBYEFNx8y9Y/6GGFz09UrJcQFZVnGfydMB8GA1UdIwQY
MBaAFNx8y9Y/6GGFz09UrJcQFZVnGfydMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
hvcNAQELBQADggEBABzPLW3lttnvHHj9YfMDvSEE0RLqyRSCp163vD3JvxewigSE
RmsO7NTPt0rKgHg21jFGSX0Bv/h7MePzrbWiJD/QSwJIYnzx+IYGZLnl8sd76syu
c0yRD8ABmVo+n7W+kTYzAZhCFDviLPG+04CergDtDBAG8efzKpKxpfbBTOC23h64
k7qcaizjQHWM0bL7MNDG13YaOAnMdcMRNp4cj468FzCYHAzCBC5QVjk0A1U4p9qJ
SSYOqLwyHzYfUebiuJp1+d/pGFWZRswovQUcqZYIgNnEWCVdIQsUDJgIKbnxdpip
P+dwjlFcaEUqPEtai9ptNIUtb8be1W42Z1DB0aA=
-----END CERTIFICATE-----

発行したCA証明書は後ほどトラストストアとして登録します。トラストストア用のS3バケットにアップロードしておきます。

クライアント証明書も発行します。

$ openssl req \
    -x509 \
    -CA rootCA_cert.pem \
    -CAkey rootCA_key.pem \
    -days 7 \
    -new \
    -nodes \
    -keyout client_key_test1.pem \
    -out client_cert_test1.pem
...+...+..............
.
.
(中略)
.
.
...+..++++++
-----
Enter pass phrase for rootCA_key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:Test 1 BU
Common Name (eg, your name or your server's hostname) []:Test 1
Email Address []:

$ openssl x509 -text -noout -in client_cert_test1.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
   01:e5:f0:60:df:9a:92:dd:40:c7:31:61:2e:72:7b:ae:0e:20:00:b9
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, ST=Tokyo, L=Default City, O=Default Company Ltd, CN=root-ca
        Validity
   Not Before: Nov 27 10:18:17 2025 GMT
   Not After : Dec  4 10:18:17 2025 GMT
        Subject: C=JP, ST=Tokyo, L=Default City, O=Default Company Ltd, OU=Test 1 BU, CN=Test 1
        Subject Public Key Info:
   Public Key Algorithm: rsaEncryption
       Public-Key: (2048 bit)
       Modulus:
  00:ea:ff:73:9a:2d:b1:94:44:2e:a2:7d:ef:29:28:
  78:19:3a:06:c8:5c:7e:7b:ef:74:67:96:e7:64:05:
  d9:d7:b5:43:10:a4:de:c0:99:1e:57:1f:6b:3e:18:
  08:a9:ac:f9:8d:29:fe:f8:74:82:c1:67:f3:48:51:
  08:56:4a:6f:00:c1:f4:80:68:41:fa:89:38:33:6a:
  5b:c8:a1:67:d0:8a:32:e1:29:e1:ce:8a:31:62:f0:
  57:57:9f:de:29:4a:ba:ea:b8:63:cf:b2:60:5e:b9:
  be:78:a6:e3:6a:da:7b:dc:98:d2:58:1b:e8:85:42:
  a5:56:bc:bf:33:eb:7e:73:0e:82:51:f8:2f:c3:57:
  e7:27:b6:f2:60:73:18:70:cb:52:7c:b1:b6:08:6f:
  f8:34:3e:7c:37:44:af:2b:9d:b0:ec:44:f5:0b:4a:
  09:96:f9:ba:f3:92:92:e7:a3:ba:65:cf:77:12:66:
  4b:1e:68:07:35:91:85:74:9e:ac:be:8a:92:02:b0:
  df:fb:71:9b:0d:a2:3b:b5:d8:15:bf:e5:92:15:27:
  c5:f2:9d:a9:f6:dd:78:9d:60:4a:74:0b:2f:12:6d:
  3e:f1:c4:59:01:73:29:54:b7:8b:f8:e1:69:f1:72:
  95:47:58:36:c0:57:5f:9a:49:c2:db:e6:73:25:0d:
  78:07
       Exponent: 65537 (0x10001)
        X509v3 extensions:
   X509v3 Subject Key Identifier:
       34:45:36:CE:E4:1B:A6:1F:5F:EE:81:A2:C0:70:CA:58:7A:E4:B0:4A
   X509v3 Authority Key Identifier:
       DC:7C:CB:D6:3F:E8:61:85:CF:4F:54:AC:97:10:15:95:67:19:FC:9D
   X509v3 Basic Constraints: critical
       CA:TRUE
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        7f:b5:f5:a7:3c:4b:7f:dc:b5:f2:4c:cf:61:87:3c:4a:14:0a:
        35:eb:fd:ba:e7:22:f6:74:46:ef:b3:3d:a9:28:93:36:9d:f9:
        3f:a5:9f:d0:74:e2:32:ba:16:67:d4:89:23:29:52:66:c9:da:
        42:e9:ad:35:b0:58:96:d8:b7:50:8a:15:4f:a4:62:65:e3:e5:
        7e:d8:0c:71:0e:a6:44:15:01:bc:69:68:bd:19:27:2f:d5:9e:
        ed:1d:9e:f2:6e:51:1d:b4:04:6a:bf:66:c0:46:69:6f:af:77:
        14:f5:80:f5:94:77:16:5a:12:be:dc:2a:ee:90:8d:ad:77:5c:
        b7:5b:65:19:53:2d:f9:00:ba:4b:bb:df:05:73:29:09:d0:26:
        3d:3c:be:53:b1:b2:82:82:49:f8:7c:e8:6e:ca:f7:00:87:fc:
        a1:3d:ae:de:31:aa:38:00:b6:9d:6e:91:e7:73:48:55:b8:ad:
        70:25:81:86:9a:09:bb:fc:1a:e8:e1:c1:52:76:6d:fb:19:8b:
        a9:47:d4:82:ba:47:6a:44:b9:22:dd:38:bc:97:a6:93:74:11:
        76:4b:6c:e9:90:4e:11:a0:cf:44:cd:7a:ce:ee:3f:43:59:0c:
        ef:5f:bb:86:e9:4b:4c:9c:1b:48:2e:67:79:95:58:c1:b8:d9:
        ee:2d:40:9b

トラストストアの作成

トラストストアの作成をします。

CloudFrontのコンソールのTrust storesから作成します。

1.Trust stores.png

トラストストア名とCA証明書を配置している場所を指定します。

2. Create trust store.png

トラストストアの作成が完了しました。

3.Successfully created trust store- rootCA.png

Associate to distributionをクリックすると、そのままCloudFrontディストリビューションと関連付けができるようでした。

4. rootCA- Associate to distribution.png

現在はCloudFrontディストリビューションでHTTP/3をサポートしている関係でグレーアウトしています。

CloudFrontディストリビューションでのmTLSの設定

mTLSの設定をします。

CloudFrontディストリビューションを選択して編集をクリックします。

5.ELGQOVCCUO3ME.png

HTTP/3が有効である関係でmTLSの有効化ができない状態です。

6.Viewer mutual authentication (mTLS).png

HTTP/3のチェックを外すとmTLSを有効化できるようになりました。先ほどのトラストストアを指定して更新します。

7.mTLSの有効化.png

デプロイが開始されました。トラストストア名が確認できますね。

8.mTLSが有効.png

状態を確認します。

9.Associated distributionsが1.png

1-2分ほどでデプロイが完了しました。かなり早いですね。

10.Deployed.png

コネクションログの設定

mTLSの設定が完了したので、続いてコネクションログの設定をします。

Loggingタブをクリックすると、Connection log destinationsと項目が生えていました。

11.Connection log destinations.png

mTLS有効化前はこちらの項目はなかったので、有効化すると生えてくるのでしょう。

今回はS3バケットにParquet形式で出力します。フィールドはデフォルトで有効なものをそのまま採用します。

13.Add connection log destination.png

設定が完了しました。パーティションは未指定だったのですが、自動でcloudfront-connection/{region}/{yyyy}/{MM}/{dd}/{HH}/が付与されていました。

14.Partitioning.png

動作確認

それでは動作確認です。

まず、クライアント証明書を渡さない場合です。

$ curl https://www.non-97.net -v
* Host www.non-97.net:443 was resolved.
* IPv6: (none)
* IPv4: 108.138.85.24, 108.138.85.20, 108.138.85.82, 108.138.85.44
*   Trying 108.138.85.24:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=www.non-97.net
*  start date: Feb 24 00:00:00 2025 GMT
*  expire date: Mar 25 23:59:59 2026 GMT
*  subjectAltName: host "www.non-97.net" matched cert's "www.non-97.net"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to www.non-97.net (108.138.85.24) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://www.non-97.net/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: www.non-97.net]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.11.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: www.non-97.net
> User-Agent: curl/8.11.1
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS alert, close notify (256):
* closing connection #0
curl: (16) Error in the HTTP2 framing layer

はい、エラーになりました。

続いてブラウザからアクセスします。

アクセスすると証明書の選択を行うポップアップが表示されました。

15.証明書の選択.png

キャンセルしても適当な証明書を選択しても接続できませんでした。

16.ERR_CONNECTION_CLOSED.png

いいですね。

では、証明書を指定してアクセスします。

$ curl https://www.non-97.net \
    --key client_key_test1.pem \
    --cert client_cert_test1.pem \
    -v
* Host www.non-97.net:443 was resolved.
* IPv6: (none)
* IPv4: 108.138.85.44, 108.138.85.24, 108.138.85.82, 108.138.85.20
*   Trying 108.138.85.44:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=www.non-97.net
*  start date: Feb 24 00:00:00 2025 GMT
*  expire date: Mar 25 23:59:59 2026 GMT
*  subjectAltName: host "www.non-97.net" matched cert's "www.non-97.net"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to www.non-97.net (108.138.85.44) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://www.non-97.net/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: www.non-97.net]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.11.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: www.non-97.net
> User-Agent: curl/8.11.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< content-type: text/html
< content-length: 12
< date: Thu, 27 Nov 2025 10:53:15 GMT
< last-modified: Tue, 25 Feb 2025 02:38:39 GMT
< etag: "56aec8b7843df637b3fb2ec0b027e5b6"
< x-amz-server-side-encryption: AES256
< accept-ranges: bytes
< server: AmazonS3
< x-cache: Miss from cloudfront
< via: 1.1 c6bba20dc3ec8526b729f039a2fdf7ae.cloudfront.net (CloudFront)
< x-amz-cf-pop: IAD12-P2
< x-amz-cf-id: QrU7g3CmAEB-yv3lzqK3hHjV8bWZq61lFrqd-GLB9Kfktc1BTz92Ww==
< x-xss-protection: 1; mode=block
< x-frame-options: SAMEORIGIN
< referrer-policy: strict-origin-when-cross-origin
< x-content-type-options: nosniff
< strict-transport-security: max-age=31536000
<
/index.html
* Connection #0 to host www.non-97.net left intact

正常にアクセスできましたね。

この時のログの確認をします。

AWSLogs/<AWSアカウントID>/cloudfront-connection/us-east-1/2025/11/28/10/<AWSアカウントID>_cloudfront-connection_us-east-1_ELGQOVCCUO3ME_20251128T10Z_79e73179.log.parquetというオブジェクトが出力されていました。

AWSLogs/<AWSアカウントID>はパーティションで指定していないですが自動で挿入されるようです。

ログには以下のように記録されていました。

curlから証明書を渡さずにアクセスしたとき
{
  "eventTimestamp": 1764240547413,
  "connectionId": "jcjbyIT6plZdRHKVAUunc0OAntEtmgNhiXbI2nuWOvu9n4J5my4jhA==",
  "distributionId": "ELGQOVCCUO3ME",
  "connectionStatus": "Failed:ClientCertMissing",
  "clientIp": "18.207.134.47",
  "clientPort": "37092",
  "serverIp": "108.138.85.24",
  "distributionTenantId": "-",
  "tlsProtocol": "TLSv1.3",
  "tlsCipher": "TLS_AES_128_GCM_SHA256",
  "tlsHandshakeDuration": 15,
  "tlsSni": "www.non-97.net",
  "clientLeafCertSerialNumber": "-",
  "clientLeafCertSubject": "-",
  "clientLeafCertIssuer": "-",
  "clientLeafCertValidity": "-",
  "connectionLogCustomData": "-"
}
ブラウザから証明書選択でキャンセルしたとき
{
  "eventTimestamp": 1764240588632,
  "connectionId": "oztvKdh3ftSYifMJHkcbnX6jFgKfFlKE6BT7PDc-KcuQ8PBUNt5_LA==",
  "distributionId": "ELGQOVCCUO3ME",
  "connectionStatus": "Failed:UnmappedConnectionError",
  "clientIp": "104.28.236.107",
  "clientPort": "40170",
  "serverIp": "3.173.254.16",
  "distributionTenantId": "-",
  "tlsProtocol": "TLSv1.3",
  "tlsCipher": "TLS_AES_128_GCM_SHA256",
  "tlsHandshakeDuration": 821,
  "tlsSni": "www.non-97.net",
  "clientLeafCertSerialNumber": "-",
  "clientLeafCertSubject": "-",
  "clientLeafCertIssuer": "-",
  "clientLeafCertValidity": "-",
  "connectionLogCustomData": "-"
}
curlから証明書を選択してアクセスしたとき
{
  "eventTimestamp": 1764240794284,
  "connectionId": "e8Cx1UpCdn9lp2ZaUxQaUQ47O_oo7C1ezy5JBcG-wLCP_Wlt7IU-1w==",
  "distributionId": "ELGQOVCCUO3ME",
  "connectionStatus": "Success",
  "clientIp": "18.207.134.47",
  "clientPort": "47384",
  "serverIp": "108.138.85.44",
  "distributionTenantId": "-",
  "tlsProtocol": "TLSv1.3",
  "tlsCipher": "TLS_AES_128_GCM_SHA256",
  "tlsHandshakeDuration": 11,
  "tlsSni": "www.non-97.net",
  "clientLeafCertSerialNumber": "01:e5:f0:60:df:9a:92:dd:40:c7:31:61:2e:72:7b:ae:0e:20:00:b9",
  "clientLeafCertSubject": "C=JP,%20ST=Tokyo,%20L=Default%20City,%20O=Default%20Company%20Ltd,%20OU=Test%201%20BU,%20CN=Test%201",
  "clientLeafCertIssuer": "C=JP,%20ST=Tokyo,%20L=Default%20City,%20O=Default%20Company%20Ltd,%20CN=root-ca",
  "clientLeafCertValidity": "NotBefore=2025-11-27T10:18:17Z;NotAfter=2025-12-04T10:18:17Z",
  "connectionLogCustomData": "-"
}

いい具合ですね。

なお、CloudFrontのアクセスログを確認すると、正常にクライアント証明書の検証が成功したもの以外の通信は記録されていませんでした。mTLSを導入した際に、うまく接続できないという場合はまずコネクションログを見るという形が良いでしょう。

CA アドバタイズメント

次に、CAアドバタイズメントを試します。

こちらの機能を有効化すると、TLS ハンドシェイク中に信頼できるCA名のリストをクライアントに送信してくれます。

有効にしてみます。

17.Advertise trust store CA names.png

この状態でアクセスをします。

$ curl https://www.non-97.net -v
* Host www.non-97.net:443 was resolved.
* IPv6: (none)
* IPv4: 108.138.85.20, 108.138.85.24, 108.138.85.82, 108.138.85.44
*   Trying 108.138.85.20:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=www.non-97.net
*  start date: Feb 24 00:00:00 2025 GMT
*  expire date: Mar 25 23:59:59 2026 GMT
*  subjectAltName: host "www.non-97.net" matched cert's "www.non-97.net"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to www.non-97.net (108.138.85.20) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://www.non-97.net/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: www.non-97.net]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.11.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: www.non-97.net
> User-Agent: curl/8.11.1
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS alert, close notify (256):
* closing connection #0
curl: (16) Error in the HTTP2 framing layer

特にCA名は表示されないですね。

ログの詳細度をもう少しあげます。

$ curl https://www.non-97.net -vvv
04:19:21.727292 [0-x] == Info: [READ] client_reset, clear readers
04:19:21.743075 [0-0] == Info: Host www.non-97.net:443 was resolved.
04:19:21.743401 [0-0] == Info: IPv6: (none)
04:19:21.743478 [0-0] == Info: IPv4: 108.138.85.44, 108.138.85.82, 108.138.85.24, 108.138.85.20
04:19:21.743728 [0-0] == Info: [HTTPS-CONNECT] added
04:19:21.743872 [0-0] == Info: [HTTPS-CONNECT] connect, init
04:19:21.744063 [0-0] == Info: [HTTPS-CONNECT] connect, check h21
04:19:21.744530 [0-0] == Info:   Trying 108.138.85.44:443...
04:19:21.744746 [0-0] == Info: [HTTPS-CONNECT] connect -> 0, done=0
04:19:21.744936 [0-0] == Info: [HTTPS-CONNECT] adjust_pollset -> 1 socks
04:19:21.745285 [0-0] == Info: [HTTPS-CONNECT] connect, check h21
04:19:21.745480 [0-0] == Info: [SSL] cf_connect()
04:19:21.747474 [0-0] == Info: [SSL] No cached session ID for https://www.non-97.net:443
04:19:21.747742 [0-0] == Info: ALPN: curl offers h2,http/1.1
04:19:21.748162 [0-0] => Send SSL data, 5 bytes (0x5)
0000: .....
04:19:21.748386 [0-0] == Info: TLSv1.3 (OUT), TLS handshake, Client hello (1):
04:19:21.748579 [0-0] => Send SSL data, 512 bytes (0x200)
0000: ............3......-^.....#.....>..... wvH.......~.w......^..PvF
.
.
(中略)
.
.
01c0: ................................................................
04:19:21.750354 [0-0] == Info: [SSL] ossl_bio_cf_out_write(len=517) -> 517, err=0
04:19:21.750573 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=5) -> -1, err=81
04:19:21.750782 [0-0] == Info: [SSL] populate_x509_store, path=/etc/pki/tls/certs/ca-bundle.crt, blob=0
04:19:21.759457 [0-0] == Info:  CAfile: /etc/pki/tls/certs/ca-bundle.crt
04:19:21.759582 [0-0] == Info:  CApath: none
04:19:21.759700 [0-0] == Info: [SSL] SSL_connect() -> err=-1, detail=2
04:19:21.759889 [0-0] == Info: [SSL] SSL_connect() -> want recv
04:19:21.760023 [0-0] == Info: [SSL] cf_connect() -> 0, done=0
04:19:21.760126 [0-0] == Info: [HTTPS-CONNECT] connect -> 0, done=0
04:19:21.760257 [0-0] == Info: [SSL] adjust_pollset, POLLIN fd=4
04:19:21.760413 [0-0] == Info: [HTTPS-CONNECT] adjust_pollset -> 1 socks
04:19:21.760589 [0-0] == Info: [HTTPS-CONNECT] connect, check h21
04:19:21.760765 [0-0] == Info: [SSL] cf_connect()
04:19:21.760904 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=5) -> 5, err=0
04:19:21.761095 [0-0] <= Recv SSL data, 5 bytes (0x5)
0000: ....z
04:19:21.761315 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=122) -> 122, err=0
04:19:21.761553 [0-0] == Info: TLSv1.3 (IN), TLS handshake, Server hello (2):
04:19:21.761775 [0-0] <= Recv SSL data, 122 bytes (0x7a)
0000: ...v...#..}Y@!.J[...L-:...jU %.J...... wvH.......~.w......^..PvF
0040: ....WD.......+.....3.$... Y..*g......\.....5.+..FVD.....c.
04:19:21.762589 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=5) -> 5, err=0
04:19:21.762795 [0-0] <= Recv SSL data, 5 bytes (0x5)
0000: .....
04:19:21.762962 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=1) -> 1, err=0
04:19:21.763171 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=5) -> 5, err=0
04:19:21.763382 [0-0] <= Recv SSL data, 5 bytes (0x5)
0000: ....$
04:19:21.763556 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=36) -> 36, err=0
04:19:21.763789 [0-0] <= Recv SSL data, 1 bytes (0x1)
0000: .
04:19:21.763971 [0-0] == Info: TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
04:19:21.764129 [0-0] <= Recv SSL data, 19 bytes (0x13)
0000: .................h2
04:19:21.764310 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=5) -> 5, err=0
04:19:21.764511 [0-0] <= Recv SSL data, 5 bytes (0x5)
0000: .....
04:19:21.764680 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=164) -> 164, err=0
04:19:21.764907 [0-0] <= Recv SSL data, 1 bytes (0x1)
0000: .
04:19:21.765077 [0-0] == Info: TLSv1.3 (IN), TLS handshake, Request CERT (13):
04:19:21.765272 [0-0] <= Recv SSL data, 147 bytes (0x93)
0000: ....................................../.j.h.f0d1.0...U....JP1.0.
0040: ..U....Tokyo1.0...U....Default City1.0...U....Default Company Lt
0080: d1.0...U....root-ca
04:19:21.765962 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=5) -> 5, err=0
04:19:21.766148 [0-0] <= Recv SSL data, 5 bytes (0x5)
0000: .....
04:19:21.766341 [0-0] == Info: [SSL] ossl_bio_cf_in_read(len=3820) -> 3820, err=0
04:19:21.766570 [0-0] <= Recv SSL data, 1 bytes (0x1)
0000: .
04:19:21.766662 [0-0] == Info: TLSv1.3 (IN), TLS handshake, Certificate (11):
04:19:21.766863 [0-0] <= Recv SSL data, 3803 bytes (0xedb)
0000: ...........0...0..............}.r.5.U.....0...*.H........0<1.0..
.
.
(以下略)
.
.

Recv SSL dataroot-caとCA証明書の名前が表示されていますね。

Wiresharkでも確認したいので、手元の端末からTLS 1.2でアクセスしてみます。

TLS 1.2とした都合はTLS 1.3だと証明書のやり取りが暗号化されてしまい、私のWiresharkの設定だとCA証明書名がアドバタイズされてているか確認できないためです。なお、TLS 1.2をサポートする関係でセキュリティポリシーをTLSv1.2_2025に変更しています。

Wiresharkを確認すると、確かにCA証明書のDNを確認できました。

18.CAアドバタイズの確認.png

個人的には別に有効にしなくとも良いかなと思いました。

Connection Functionを用いた失効した証明書の対応

せっかくなのでConnection Functionを用いた失効した証明書の対応をしてみます。

まずCRLの役割を果たすKeyValueStoresを準備します。

20.KeyValueStores.png

適当に名前を入力して作成します。

21.Create KeyValueStore.png

数十秒ほどで作成完了しました。

23.作成完了.png

KeyValueStoreにCNがTest 1のクライアント証明書のシリアル番号を登録します。

24.KeyValueStore update.png

これでKeyValueStoreは準備完了です。

続いて、Connection Functionの準備です。

25.Connection functions.png

名前を入力して作成します。

26.Create connection function.png

はい、To create your first connection function, you must first contact AWS Support to request a limit increase.とエラーになりました。

27.To create your first connection function, you must first contact AWS Support to request a limit increase.png

どうやらConnection Functionを初めて作成する場合はAWSサポートにクォータ引き上げのリクエストを行う必要があるようです。

Service Quotasを確認すると、Maximum number of connection functions per AWS accountでクォータコードL-D0422299のものがありました。

28.Maximum number of connection functions per AWS account.png

デフォルトクォータは0となっていますね。

引き上げのリクエストをしましょう。

29.クォータの引き上げをリクエストする.png

1をリクエストしましたが、裏側でサポートケースが上がるタイプでした。

30分ほどで引き上げが完了しました。AWSサポートのご担当者の方、いつもありがとうございます。

30.クォータ引き上げ完了.png

クォータ引き上げ完了後Connection Functionを作成しようとすると、正常に受け付けられました。

31.Successfully created new connection function non-97-crl..png

KeyValueStoreに該当するキーがあれば拒否するコードをデプロイします。

import cf from 'cloudfront';

async function connectionHandler(connection) {
    const kvsHandle = cf.kvs();
    
    // Get client certificate serial number
    const clientSerialNumber = connection.clientCertificate.certificates.leaf.serialNumber;
    
    // Check if the serial number exists in the KeyValueStore
    const isRevoked = await kvsHandle.exists(clientSerialNumber);
    
    if (isRevoked) {
        console.log(`Certificate ${clientSerialNumber} is revoked. Denying connection.`);
        connection.logCustomData(`REVOKED:${clientSerialNumber}`);
        connection.deny();
    } else {
        connection.logCustomData(`Valid certificate: ${clientSerialNumber}`);
        console.log(`Certificate ${clientSerialNumber} is valid. Allowing connection.`);
        connection.allow();
    }
    
}

KeyValueStoreのアタッチ完了後にテストをすると、意図したとおり拒否してくれました。

34.test.png

Liveステージにパブリッシュします。

35.Deployed.png

この状態でKeyValueStoreに登録されている証明書を指定してアクセスします。

$ curl https://www.non-97.net \
    --key client_key_test1.pem \
    --cert client_cert_test1.pem \
    -v
* Host www.non-97.net:443 was resolved.
* IPv6: (none)
* IPv4: 108.138.85.24, 108.138.85.20, 108.138.85.44, 108.138.85.82
*   Trying 108.138.85.24:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / secp256r1 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=www.non-97.net
*  start date: Feb 24 00:00:00 2025 GMT
*  expire date: Mar 25 23:59:59 2026 GMT
*  subjectAltName: host "www.non-97.net" matched cert's "www.non-97.net"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to www.non-97.net (108.138.85.24) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://www.non-97.net/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: www.non-97.net]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.11.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: www.non-97.net
> User-Agent: curl/8.11.1
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS alert, close notify (256):
* closing connection #0
curl: (16) Error in the HTTP2 framing layer

はい、失敗しました。

次に別のクライアント証明書を指定してアクセスする場合です。

$ openssl req \
    -x509 \
    -CA rootCA_cert.pem \
    -CAkey rootCA_key.pem \
    -days 7 \
    -new \
    -nodes \
    -keyout client_key_test2.pem \
    -out client_cert_test2.pem
.....+..........+
.
.
(中略)
.
.
....+...++++++
-----
Enter pass phrase for rootCA_key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server\'s hostname) []:Test 2
Email Address []:

$ curl https://www.non-97.net \
    --key client_key_test2.pem \
    --cert client_cert_test2.pem \
    -v
* Host www.non-97.net:443 was resolved.
* IPv6: (none)
* IPv4: 108.138.85.20, 108.138.85.24, 108.138.85.82, 108.138.85.44
*   Trying 108.138.85.20:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/pki/tls/certs/ca-bundle.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / secp256r1 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=www.non-97.net
*  start date: Feb 24 00:00:00 2025 GMT
*  expire date: Mar 25 23:59:59 2026 GMT
*  subjectAltName: host "www.non-97.net" matched cert's "www.non-97.net"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to www.non-97.net (108.138.85.20) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://www.non-97.net/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: www.non-97.net]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.11.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: www.non-97.net
> User-Agent: curl/8.11.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< content-type: text/html
< content-length: 12
< date: Fri, 28 Nov 2025 04:45:38 GMT
< last-modified: Tue, 25 Feb 2025 02:38:39 GMT
< etag: "56aec8b7843df637b3fb2ec0b027e5b6"
< x-amz-server-side-encryption: AES256
< accept-ranges: bytes
< server: AmazonS3
< x-cache: Hit from cloudfront
< via: 1.1 4685cae701bd588fa0176a1c8b1e52f4.cloudfront.net (CloudFront)
< x-amz-cf-pop: IAD12-P2
< x-amz-cf-id: 9vsFTFRwsg-vdtNIcxV-BTDtq7wNdjdNz-YgYrqsAQlMOg5lQjf10g==
< age: 19269
< x-xss-protection: 1; mode=block
< x-frame-options: SAMEORIGIN
< referrer-policy: strict-origin-when-cross-origin
< x-content-type-options: nosniff
< strict-transport-security: max-age=31536000
<
/index.html
* Connection #0 to host www.non-97.net left intact

はい、こちらは正常にアクセスできました。

この時のコネクションログは以下のとおりです。

KeyValueStoreに登録されているクライアント証明書を使用したとき
{
  "eventTimestamp": 1764324261055,
  "connectionId": "dHAOAFd3fARRQ3dvj1ldEAkGSZPaDw8GLOud1LB17xYY_Wahx076cg==",
  "distributionId": "ELGQOVCCUO3ME",
  "connectionStatus": "Failed:ConnectionFunctionDenied",
  "clientIp": "44.220.54.32",
  "clientPort": "43670",
  "serverIp": "108.138.85.24",
  "distributionTenantId": "-",
  "tlsProtocol": "TLSv1.3",
  "tlsCipher": "TLS_AES_128_GCM_SHA256",
  "tlsHandshakeDuration": 127,
  "tlsSni": "www.non-97.net",
  "clientLeafCertSerialNumber": "01:e5:f0:60:df:9a:92:dd:40:c7:31:61:2e:72:7b:ae:0e:20:00:b9",
  "clientLeafCertSubject": "C=JP,%20ST=Tokyo,%20L=Default%20City,%20O=Default%20Company%20Ltd,%20OU=Test%201%20BU,%20CN=Test%201",
  "clientLeafCertIssuer": "C=JP,%20ST=Tokyo,%20L=Default%20City,%20O=Default%20Company%20Ltd,%20CN=root-ca",
  "clientLeafCertValidity": "NotBefore=2025-11-27T10:18:17Z;NotAfter=2025-12-04T10:18:17Z",
  "connectionLogCustomData": "REVOKED:01:e5:f0:60:df:9a:92:dd:40:c7:31:61:2e:72:7b:ae:0e:20:00:b9"
}
KeyValueStoreに登録されていないクライアント証明書を使用したとき
{
  "eventTimestamp": 1764324406448,
  "connectionId": "K85pU88FYUohutYb1vpTJdxdoW8LMTik5lqCmmyBllkVXHevWVR_kA==",
  "distributionId": "ELGQOVCCUO3ME",
  "connectionStatus": "Success",
  "clientIp": "44.220.54.32",
  "clientPort": "37594",
  "serverIp": "108.138.85.20",
  "distributionTenantId": "-",
  "tlsProtocol": "TLSv1.3",
  "tlsCipher": "TLS_AES_128_GCM_SHA256",
  "tlsHandshakeDuration": 64,
  "tlsSni": "www.non-97.net",
  "clientLeafCertSerialNumber": "72:ef:fd:8b:79:26:e7:11:d1:36:f2:80:f0:61:75:82:90:34:d7:d5",
  "clientLeafCertSubject": "C=JP,%20ST=Tokyo,%20L=Default%20City,%20O=Default%20Company%20Ltd,%20CN=Test%202",
  "clientLeafCertIssuer": "C=JP,%20ST=Tokyo,%20L=Default%20City,%20O=Default%20Company%20Ltd,%20CN=root-ca",
  "clientLeafCertValidity": "NotBefore=2025-11-28T09:52:08Z;NotAfter=2025-12-05T09:52:08Z",
  "connectionLogCustomData": "Valid%20certificate:%2072:ef:fd:8b:79:26:e7:11:d1:36:f2:80:f0:61:75:82:90:34:d7:d5"
}

connectionLogCustomDataにしっかり記録されていますね。

CloudFrontで配信する環境においてクライアント証明書による認証をしたい場合に

Amazon CloudFrontがmTLSをサポートしたアップデートを紹介しました。

エッジ側でクライアント証明書による認証をさせたい場合は積極的に使っていきましょう。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

この記事をシェアする

FacebookHatena blogX

関連記事