はじめに
カミナシでID管理・認証基盤を開発しているmanaty(@manaty0226)です。
カミナシではAmazon Web Services(AWS)上で認証基盤を構築して動かしています。特にOpenID Providerのコア機能含めて内製しており、さまざまなOAuth拡張仕様を駆使してお客様の要求やプロダクト機能の実現に寄与しています。今回は、相互TLS(mTLS: mutual Transport Layer Security)と呼ばれる、クライアントとサーバーがともに証明書を提示して安全に接続する技術に立脚したRFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens(以下、OAuth mTLSと呼びます)を実現するために検討した内容を書きます。
仕様自体の話はこの記事の本筋ではありませんが、前提としてOAuth mTLSについて簡単に説明します。OAuth mTLSは「1. クライアント証明書を使ったOAuthクライアント認証」と、「2. クライアント証明書による送信者制約付きトークン発行およびリソースサーバーにおける検証方法」について定めています。OAuth mTLSを使うことでクライアント証明書および関連する秘密鍵を持つクライアントのみがアクセストークンを利用できるよう制約をつけることができるため、トークンが悪意ある第三者に漏洩しても不正に利用されず強固なセキュリティを実現できます。
RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
AWSにおける認可サーバーの構成例
AWSを利用して内製した認可サーバーをデプロイする場合、一般的なWebサーバーと同じようにCloudFrontおよびApplication Load Balancer(ALB)を前段に立てた上でElastic Container ServiceやEC2などのコンピューティングリソースに認可サーバーを乗せて動かすのが一般的な構成ではないでしょうか。構成例を以下に示します。
しかしながら、AWSの各種サービス仕様を読み解きながら設計検討を進めていくと、このような構成でOAuth mTLSに対応することが難しいことがわかってきました。
AWSにおけるOAuth mTLS対応の難しさ
CloudFrontがmTLSに対応していない
CloudFrontはmTLSに対応していません。ALBやバックエンドサーバーなどオリジンがmTLSのためにクライアント証明書要求を出しても、CloudFrontがリクエストを削除する仕様になっています。そのため、前提としていた構成で実現しようとすると、まずCloudFrontを外さなければなりません。
CloudFront はクライアント側の SSL 証明書を使用したクライアント認証をサポートしていません。オリジンがクライアント側証明書をリクエストした場合、CloudFront はリクエストを削除します。
カスタムオリジンの場合のリクエストおよびレスポンスの動作 - Amazon CloudFront
ALBのmTLS対応モードが歯痒い
CloudFrontと異なり、ALBは2023年11月にmTLS対応を開始しています。ALBにはクライアント証明書の取り扱い方として、「mTLS検証」と「パススルー」の2つのmTLS対応モードが存在します。
Application Load Balancer での TLS による相互認証 - Elastic Load Balancing
mTLS検証モードでは常にクライアントに対して証明書を要求し、あらかじめALBのトラストストアに登録されたCA証明書を使って有効性を検証します。この時、クライアントから証明書が提示されないとALBでコネクションを拒否します。一方で、パススルーモードではクライアントに対して証明書を要求しますが、クライアントから提示されなくてもコネクションは拒否されません。パススルーモードで証明書が提示された場合は、TLSハンドシェイクで提示された証明書をPEM形式でヘッダーに付与してバックエンドサーバーに転送します。
さて、ここで私たちの認可サーバーについて考えてみます。認可サーバーにはさまざまなOAuthクライアントが登録されています。一部のクライアントはクライアント証明書を使って認証しようとしますが、他のクライアントはクライアントシークレットなどで認証します。そのため、認可サーバーは複数のクライアント認証方式に対応する必要があります。
したがって、「mTLS検証モード」を選択した場合、クライアント証明書を持たずクライアントシークレットなどでトークンエンドポイントに接続しようとするクライアントの接続をALBで拒否してしまうことになります。一方で、「パススルーモード」を選択した場合、証明書の有効性はALBで検証されずバックエンドサーバーに委ねられます。そのため、認可サーバーに証明書検証の責務も持たせなければなりません。
このように、ALBの2つのmTLS対応モードはどちらも痛し痒しなところがあり、上述した構成のまま単純にサービスを利用するだけでは解決できそうにないとわかりました。
ちなみに、nginxではmTLSのクライアント証明書検証モード(ssl_verify_client)にoptionalという設定値があります。これは、クライアント証明書が提示されなくても通信は拒否せずバックエンドサーバーに転送するが、提示された場合はあらかじめ構成されたトラストストアのCA証明書で検証するという、ALBの2つのモードの中間に位置するような仕様です。将来もしAWSでnginxのssl_verify_client=optional相当のmTLSモードが追加されたら、1つのエンドポイントでmTLSクライアントと通常のクライアントを収容する仕様を素直な形で実現できるかもしれません。
その他のAWSサービスを使った場合
では、その他のAWSサービスを利用する場合はどうでしょうか。ALBの代替となる認可サーバーの前段に配置するサービスとして、例えばAPI GatewayとNetwork Load Balancer(NLB)が考えられます。
しかしながら、API GatewayはmTLS対応しているもののALBにおけるmTLS検証モード相当の仕様しかありません。NLBではクライアント証明書検証には対応しておらず、TLSパススルーでバックエンドサーバーに証明書検証の責務を委ねることになります。TLSパススルーモードで設定したELBの後段にnginxコンテナを配置してnginxの設定をssl_verify_client=optionalとしてTLS終端する構成も考えられます。ただし、NLBの後ろでコンテナタスクとしてnginxロードバランサーを管理するのは、仕様実現とのトレードオフを鑑みてシステム構成の複雑性が必要以上に大きくなるデメリットがあります。
したがって、これらのAWSサービスでも私たちの要求を完全に満たすことは難しいと結論づけました。
API Gateway で HTTP API の相互 TLS 認証を有効にする方法 - Amazon API Gateway
Network Load Balancer のリスナー - Elastic Load Balancing
今のAWSでOAuth mTLSを実現するために
mTLS対応専用のALBを配置する
上述した仕様の把握やAWSサポートへの技術質問を通して、当初考えていた1つのエンドポイントでmTLSと通常のリクエストを受け付ける仕様を諦め、mTLSエンドポイント専用のALBを配置する以下のような構成に至りました。各クライアントは自身がクライアントシークレットなどで認証したい場合には通常のトークンエンドポイントに対応しているALB(またはCloudFront)へアクセスし、クライアント証明書を使った認証および証明書による送信者制約付きトークンが欲しい場合はmTLS対応しているALBにアクセスします。ALBが1つ増えることによるコスト増はありますが、システムアーキテクチャと認可サーバーの責務の観点で適度に複雑性を抑えた構成になっていると考えています。
RFC 8705のmtls_endpoint_aliasesの意図
実はOAuth mTLS RFCの5章にはmtls_endpoint_aliasesという、mTLSに対応した認可サーバーのエイリアスエンドポイントを示すメタデータについて定義されています。これは、mTLS対応したトークンエンドポイント等について、mTLS対応していないエンドポイントと異なる場合に定義できるものです。たとえばサーバーが/.well-known/openid-configurationに公開することでクライアントから読み取ってもらい、クライアント認証方式に合わせてアクセス先を変えることができます。
{ "issuer": "https://server.example.com", "authorization_endpoint": "https://server.example.com/authz", "token_endpoint": "https://server.example.com/token", "introspection_endpoint": "https://server.example.com/introspect", "revocation_endpoint": "https://server.example.com/revo", ... "mtls_endpoint_aliases": { "token_endpoint": "https://mtls.example.com/token", "revocation_endpoint": "https://mtls.example.com/revo", "introspection_endpoint": "https://mtls.example.com/introspect" } }
このようなエイリアスが定義された背景としてRFCではクライアントのUXのためと記載されています。
Although a server can be configured such that client certificates are optional, meaning that the connection is allowed to continue when the client does not provide a certificate, the act of a server requesting a certificate can result in undesirable behavior from some clients. This is particularly true of web browsers as TLS clients, which will typically present the end user with an intrusive certificate selection interface when the server requests a certificate.
実際にローカル環境でnginxを使ってmTLS要求してみると、ブラウザでは以下のようなポップアップが出てきます。たとえば本来クライアント証明書が不要なクライアントのアプリケーションにおいて、事前説明もない状態でこのようなポップアップが出てくるのは、確かに多くの利用者を混乱させてしまうように感じます。
はじめは1つのエンドポイントで全てのクライアントを収容する構成を考えていましたが、このRFCの記述と実際の動作を確認した上で考えると、最終的な構成も妥当な選択と思っています。
おわりに
AWS上で動く認可サーバーへOAuth mTLS対応する構成設計について書きました。一見サービスの仕様制約で実現が難しいと思うことも、各種サービスの仕様を把握することで妥当な構成に落とし込むこともできますし、仕様を隅から隅まで読んでみると実はあまり悩む必要がなかったということも稀にあります。
カミナシでは今回紹介したようなOAuth mTLSだけでなく、OAuthおよびOpenID Connectの関連仕様を輪読して日々私たちのID管理・認証基盤を進化させています。自分の手でOpenID Providerを育てたいという方は是非カジュアル面談しましょう!