Amazon API GatewayでmTLSを試してみた。(1/2)


はじめに

特に金融系のAPI等、より強固な認証を求められるIoTデバイス等からの認証を行う際にはクライアント証明書による認証を求められることがあるかと思います。当記事では、Amazon API Gateway(執筆時点ではRegionalおよび、HTTP API限定)によるmTLS(Mutual TLS authentication)について構成を試したので記載したいと思います。
当機能に関するドキュメントとしてこちらを参考にして構築しました。

なお、当記事は2020年3月にGAしたHTTP APIをベースに記載しており、従来からあるREST APIについては検証・記載しておりません。

(この記事30分くらいの検証と2時間くらいの執筆かな~と持っていたら、苦労したことに書きましたが、自分の構成管理ミスで思ったより手間取りました。そして証明書のチェーン(中間CA)も作ろうと思ってまだ作れてなかったり・・・。)

追記:後続の記事はこちら

サマリ

  • 認証で使用された証明書情報はEvent内で連携される。
  • Lambda AuthorizerにもEventとして証明書情報が連携される。
  • PEMそのものや、解析済みのデータ(例:SubjectDN,Serial Number)を利用することで追加の処理が可能(この部分は別記事で書くことにしました。長くなったので。)

mTLS(Mutual TLS authentication)とは

mTLSはAPIのセキュリティを強化し、クライアントのなりすましや中間者攻撃などの攻撃からデータを保護するのに役立ちます。

mTLSを利用しない通常のTLS

mTLSを利用しない通常のTLSの通信(WebにおいてはHTTPS通信)は、クライアントがサーバに対してX.509証明書を要求し、サーバが自身の証明書をクライアントに提供します。サーバから提供された証明書は代表的な認証局(またはその中間認証局)によって認証(署名)されているケースが一般的なWebでは利用され、それをもって偽物ではないことを確認します。ここで、一般的とつけたのは、企業内でPrivateに利用する証明書については、自社内の認証局で署名された証明書を利用しているケースもあるためです。証明書の発行手順や認証(署名)、中間証明書の概念については世の中にわかりやすく解説されている記事がありますのでそちらをご参照ください。

mTLSを利用する通信

さて、一般的なTLS通信はサーバサイドの証明書を利用した片方の認証のみをする仕組みでした。
mTLSは?というと、クライアントサイドの証明書をサーバサイドに提供し、サーバサイド”も”クライアントを証明書の検証を通じて認証する仕組みとなります。Open Bankingなどの標準で使用されており、金融機関向けに安全なオープンAPI統合が可能になったり、IoT領域では、各デバイスにクライアント証明書を導入していることが多く、証明書を使用してデバイスを認証するのが一般的になっています。
 個々クライアントやデバイスごとに秘密鍵とクライアント証明書(CA証明書で署名)を用意して接続時に利用することで認証を強化します。AWS IoTでもクライアント証明書を利用した認証が採用されていますね。

mTLSを考える上での登場人物

アクター 説明 当記事の場合
クライアント HTTP リクエストを行う人。クライアント証明書と秘密鍵を保持しそれを利用してサーバと通信 cloud9のTerminal/Curlコマンド
サーバ HTTPのサーバ。サーバ証明書を持ち、クライアントと通信。なおmTLSの検証用にクライアント証明書の署名で利用したCA証明書を保持。 Amazon API Gateway + Amazon S3+ Amazon Certificate Manager(ACM)
CA局(ルート/中間) サーバ証明書の作成・署名や、クライアント証明書の署名を行う サーバ証明書はACM/クライアント署名に利用するCA証明書はOpenSSLを利用

Amazon API Gateway におけるmTLS対応

2020年9月18日より、Amazon API Gatewayは、追加費用なしでmTLS認証をサポートするようになりました。
今までは、
1. JWTによる認可
2. Lambda Authorizerによる認可
3. IAMベースの認可

がサポートされていましたが、新たにmTLSを有効化することができるようになりました。既存の認可と組み合わせ利用できるようで今回はLambda Authorizerと組み合わせて試してみました。(IAM/JWTは試してないです。また、構成や内容は別の記事に書きました。)

Amazon API Gatewayの構成

必要ななもの

<<直接的に必要なもの>>とそれを作るために<<間接的に必要なもの>>があります。例えば、クライアントが接続したときに提示する「クライアント証明書」や接続に利用するクライアントの秘密鍵は直接必要なものです。ちなみに、このクライアント証明書はCA証明書で署名されている必要があります。また、サーバサイドではクライアント証明書を検証するためには先ほど言及した「クライアント証明書作成時に署名したCAの証明書」を保持する必要があり、これもまた、直接必要なものとなります。一方、間接的にと表現したのは、例えば、CA証明書の秘密鍵はCA証明書を作るために必要ですが今回の構成で直接利用するものではありません。また、mTLSはカスタムドメインを有効化する必要が前提としてあり、カスタムドメインを有効化するためには、ご自身で管理可能なドメイン(DNS)やそのドメインでHTTP通信するためのサーバ証明書も必要となります。ということで必要なものを列挙しておきました。

API Gateway側

以下、1~8まで挙げましたが1~5はmTLSの設定というよりは、HTTP APIの基本構成(1,2)に加えてカスタムドメインで呼びさせるようにするための構成(3,4)となります。まずは、この1~4が終わっていることがAPI GatewayのmTLS有効化の第一歩です。また5については、今回は入れましたが必須ではありません。6~8がmTLS構成をするために必要な固有の要素で、既にCA証明書をお持ちの場合は、6,7は作る必要はなく、8を作るって構成するのみとなります。
1. APIそのもの
2.呼び出されたときのバックエンドのアプリケーション(今回はLambda)
3.API Gatewayのカスタムドメインで利用するドメイン名
4.API Gatewayのカスタムドメインに対応するサーバ証明書
5.呼び出されたときのLambda Authorizer
6.CA証明書を作るための秘密鍵
7.CA証明書
8.CA証明書を格納するS3バケット

クライアント側

クライアント側は通信時に以下があればmTLSで通信ができます。
1.秘密鍵
2.クライアント証明書(PEM)

構築開始

今回はAWS マネジメントコンソールやCloud9のターミナルを利用して構成しました。
Cloud9 のターミナルはopensslで鍵や証明書の作成、そして、動作確認としてcurlでアクセスするために利用しました。

下準備

まずはサーバサイドを構築しようと、上記のAPI Gateway側の中の1,2を構築し、その後3,4を実施する流れでご説明します。

A.APIの初期設定

A.1.APIやLambdaをSAMで構築(手動でもO.Kです)

手動で構築する手順の参考: HTTP APIの作成チュートリアル

今回は、面倒だったので実は以下のようなSAMのテンプレートで1,2,5,8を作りました。以下は例ですが、SAMのテンプレートを置いたディレクトリ内にhello_worldとauthの二つのフォルダを作ってそれぞれソースを配置しています。sam initで初期化してテンプレートを少しかえて微調整すればOK。

SAMをまだご経験がない方はAWS マネジメントコンソールでHTTP APIの作成、Lambda の作成(2個)、S3バケットの作成をしてみてください。

Globals:
  Function:
    Timeout: 3
Resources:
  mTLSTrustStoreS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'mtls-truststore'
      VersioningConfiguration:
        Status: Enabled 
  TrustStoreS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled 
  mTLSAuthorizerFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: auth/
      Handler: app.lambda_handler
      Runtime: python3.7
  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      Events:
        HttpApiEvent:
          Type: HttpApi
          Properties:
            Path: /hoge/auth
            Method: GET
Outputs:
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/hoge/auth"
  BucketName:
    Value: !Ref 'TrustStoreS3Bucket'
    Description: Name of the TrustStore S3 Bucket.

SAMでbuild とdeployが成功すると以下のように中身が何もない空のS3バケットが出来上がります。

また、API GatewayのHTTP APIとして以下のようなルートの構成を持ったAPIができます。
なお、この時点ではLambda Authorizerによる認可設定はしていません。

一方で統合ではバックエンドのリソースとしてLambdaと統合しています。

Lambdaのコンソールを確認すると、Lambda関数が2個追加されています。1つはLambda Authorizer用,もう一つは前述の統合で指定されたHello Worldの関数です。両方とも処理結果の応答形式が決まっています。Lambda Authorizerはこちらを参考に以下の形式で固定値で応答しています

app.py
#Lambda Authorizer
def lambda_handler(event, context):
    return {
        "isAuthorized": True
    }
app.py
#Hello World
import json
def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world"
        }),
    }

さて、ここまでで、API Gatewayが提供するエンドポイントにアクセスするとLambda Authorizerは通らず(まだ未構成)、HelloWorldのLambdaが呼び出され、応答を受け取ることが可能です。Lambda Authorizerの構成は別の記事で記載します。

A.2 動作確認

CurlやブラウザでAPIにアクセスしてみてください。URLが分からない方は、画面で確認できますので確認してみてください。

B.カスタムドメイン設定

B.1 カスタムドメイン構成のために証明書を作成

さて、ここからは、カスタムドメインでアクセスできるようにするための設定です。
必要なことは、ドメインを手に入れ、証明書を作ることです。
今回は、ドメインはRoute 53を利用して登録することも可能ですし、ご自身、または会社がAWS外,または別のAWSアカウント内部で管理されているドメイン、オンプレで管理されているドメインを利用することも可能です。今回は私は、外部で販売されている1円のドメインを購入しました。そのドメインのネームサーバをRoute 53のネームサーバに登録したうえで、証明書の作成を実施しました。以下は証明書を作った状態です。こちらの証明書はAWSコンソール上で、私の場合は5分程度で作成できました。手順はこちらを参考にしてみてください。

ちなみに今作成した証明書はサーバ証明書と一般的に呼ばれるもので、Amazon Certificate Manager(ACM)をご利用いただくことで無料で発行ができ、API Gatewayだけでなく、Application Load BalancerやCDN(Contents Delivery Network)の役割を果たすCloudFront等のサービスとシームレスに連携でき便利です。発行される証明書の要件をご確認の上、ご活用ください。(注意;発行自体は容易ですが、発行するためにはそのドメインの管理者であること、つまり、DNS管理者ならできるであろう操作や、管理者へのメールによる確認が行われます)

B.2 カスタムドメイン構成

証明書を作成したのでACMの証明書を利用してAPI Gatewayにカスタムドメイン登録を行います。HTTP APIのカスタムドメインは1つのドメイン内に複数のドメインをルーティングでき、統合が可能になっています(私は実は、昨日社内の同僚たちのチャットで知り、試したところです)。以下の画面が構成したときの例です。ACM証明書は作成済みなため、選択できます。画面上にはMutual TLS authenticationというタブがありますが、この時点では有効化していません。もちろん有効化していただいてもよいですが、カスタムドメイン構成が失敗したまま、mTLS構成したときデバッグがしにくいかなと考え段階を踏んでいます。

さてドメイン名を構成すると裏では、Route 53にレコードが追加されています どうやら、私は無意識でやっていたみたいでした。自身で追加する必要があるようです。

1つのドメインに複数のAPIを統合することができると先ほど記載しましたが、以下が2つのAPIを統合する構成例です。ということで、今回構築しているAPIは
https://カスタムドメイン/qiita/hoge/auth
で動くように設定しました。

B.3 動作確認

ということで、動作確認をブラウザ、またはcurlで実施します。

mTLS構成開始

API Gatewayの構成

C.CA証明書の準備とS3への格納

ここからが本格的にmTLSの構成作業となっていきます。まずは、CA証明書ですね。こちらは、Public/PriateなCAでも自己署名で作成したCAの証明書でも問題とドキュメント上記載があります。
私は今回用に自己署名によるCA証明書を作成することにしました。
以下、コマンドで実施しました。
OpenSSL は標準のオープンソースライブラリーで、x509 証明書の作成と署名を含む包括的な暗号関数をサポートしています。OpenSSL の詳細については、www.openssl.org を参照してください。

C.1 CA証明書用の秘密鍵の生成

openssl genrsa -out rootCA.key 2048

C.2 CA証明書の作成

こちらは、自身の秘密鍵で自己署名をするコマンドとなっています。 -keyが自身の秘密鍵なので、自己署名。つまり、ルート認証局というわけです。

openssl req -x509 -new -nodes \
    -key rootCA.key \
    -sha256 -days 1024 \
    -out rootCA.pem

C.3 CA証明書をS3バケットに保管

以下のコマンドでS3に保存可能です。こ

aws s3api put-object --bucket XXX --key rootCA.pem --body rootCA.pem

今回作成したこのバケットはバケット作成時にバージョニングを有効化したため、戻り値にVersionIdが返ってきました。
例えば、間違って上書きや削除されたときにバージョニングを指定しておくことでそのバージョンのオブジェクトが参照できるため有効化しています。ただ、バージョン自体の削除も権限があれば可能になるため、それを防ぎたい場合はS3 のObject Lockをご利用いただくと一段階、強固な設定となります。
なお、バージョニングを有効化するとその分のオブジェクトの料金が加算されますのでご注意ください(参考)
バージョニングは適宜S3のライフサイクルポリシー等を活用しながら有効活用してください。

D.API Gateway のmTLS構成

D.1 mTLS 有効化設定

S3 Bucketおよび格納したファイル名(オブジェクト名)、必要に応じてオブジェクトのバージョンを指定しmTLSを有効化します。

さあ、これでサーバサイドはmTLS対応しました。念のため接続確認をしてみましょう。

ステータスが以下の通りUpdatingではなく、Availableになってからお試しください。

D.2 mTLS 有効化設定後の動作確認

Curlコマンドで-vをつけてログを出力いながら確認しましたが、接続できなくなりました。サーバサイドはたぶんOK。エラーの理由は接続時にクライアント証明書を送付していないからです。(構成が正しければ)

クライアント証明書の準備と接続

ここまでくると後は証明書を作って接続するだけです。

E. クライアント証明書の作成

E.1 クライアントの秘密鍵の作成

openssl genrsa -out device.key 2048

E.2 CA証明書で証明したクライアント証明書の生成のための要求(CSR)の作成

openssl req -new \
    -key device.key \
    -out deviceCA.csr

E.3 CSR からクライアント証明書を作成

今回は有効期限が1日のものと有効期限が切れたものの二種類を用意しました。

openssl x509 -req \
-in deviceCA.csr \
-CA rootCA.pem \
-CAkey rootCA.key \
-CAcreateserial \
-out deviceCert.pem \
-days 1 -sha256

openssl x509 -req \
    -in deviceCA.csr \
    -CA rootCA.pem \
    -CAkey rootCA.key \
    -CAcreateserial \
    -out deviceCertExpire.pem \
    -days 0 -sha256

E.3 クライアント証明書を利用した接続確認

有効期限内

 curl -v --key device.key --cert deviceCert.pem  https://api.mtls.XXXXXX/qiita/hoge/auth

有効期限切れ

有効期限切れの証明書では以下の通りHTTP403 が応答されました。

curl -v --key device.key --cert deviceCertExpire.pem https://api.mtls.XXXXXX/qiita/hoge/auth

また、CloudWatch Logsにログを出力するように構成した結果、以下の通りログでも接続できていないことが確認できました。

苦労した点

構成を一通りまとめましたが、私がハマったのは、
1. CA証明書を複数作っていて、クライアント証明書との組み合わせが間違ったままテストしていた。
2. クライアント証明書の有効期限を逆に設定していてつながらない!って苦労した
3. 20分くらいでできるかな~とおもって梅酒を飲みながら挑戦したら、証明書をACMで発行するところで寝落ちしてどこまで何をしたか忘れてしまった。

ということで、構成自体は横道にずれずに構成していればすんなりいけました。ちなみに、CLIで操作する方やSAMをご利用になる方は最新バージョンにUpdateしてご利用ください。

まとめ

手順は色々ありますが、mTLSの構成だけであれば、簡単に実装できます。mTLS機能がリリースされる前は、例えば、自身でNLBを構築し、さらには、Farate or EC2を運用する形で利用されていたのではないでしょうか?そのアンマネージドな部分をサーバレスアーキテクチャに置き換えることも可能です。置き換えることでInternet Facingな自身のリソースを減らすことができたり、運用コストを減らすことが可能になるかと思います。ぜひご検討ください。

本当はLambda Authorizerや後続のLambdaの処理について記載しようと思いましが、長くなったのでいったん区切ります。別記事で記載します。

続きも書く予定です。 書きました。後続の記事はこちら

次に書きたいこと

・Lambda内で実施できること、すること
・セキュアなバケット
・運用回り
・ログ

参考

こちらのブログ、英文ですが丁寧に手順が書かれてました。書いてから気づいた・・・。