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


はじめに

当記事はこちらの続きとして作成しています。Amazon API GatewayがmTLSによる認証をサポートしたため前回の記事では、APIの構成、カスタムドメインの構成、鍵の生成、CA証明書の生成、そしてクライアント証明書の生成、API GatewayのmTLS構成について手順を紹介しました。当記事ではもう少し細部について書きたいと思います。

1.Amazon API GatewayでmTLS構成した際のLambda Authorizerや統合したLambda関数の実装
2. デフォルトのエンドポイントの無効化(重要
3. mTLS設定と絡めたセキュリティ設定

サマリ

  • 証明書の失効対応は自身で必要。Lambda Authorizerや統合されたLambdaで可能
  • 予防・記録・検知と対応をセットでAPI Gatewayの運用をご検討ください

解説

1. Lambda Authorizerならびに、統合されたLambdaの実装

1.1 Lambda関数に送信される情報

Lambda Authorizerならびに、統合されたLambda(NLB/ALB/CloudMapによる連携の場合は送信されません)には、クライアント証明書のPEMデータが送信されます。PEMが送信されることで、Lambda Authorizerによる追加の認可処理や、統合されたLambdaによる業務処理内でのCNの活用などが可能になります。ただ、PEMを解析する必要はないかもしれません。具体的には、下のサンプルコードにあるような方法でPEMを読み込まなくてもEventデータ内に以下の情報が含まれているため、足りない情報があれば利用する形になるかと思います。
- IssuerDN:CA証明書のDN
- SubjectDN:クライアント証明書のDN
- SerialNumber:クライアント証明書のユニークな番号
- Validity:クライアント証明書の有効期間

これらの情報を利用することで例えば、独自に有効期限を確認し、期限が近い場合、運用チームや利用者に対応を通知する警告も出せるかと思います。。
さらに、重要なこととして、AWS ドキュメントには、「API Gatewayは証明書がrevoke(無効化)されたか検証しない」と記載があります。もし、証明書の漏洩や不正利用等の理由で有効期限を前に証明書を無効化する運用や外部で無効化された証明書に対するアクセスを禁止したい場合は、証明書失効リスト(CRL:Certificate Revocation List)の情報を取り込み、Lambda Authorizerにて証明書失効リストをSerialNumberの有無を確認しチェックすることも検討してみてください。
CRLには、証明書のシリアル番号が掲載されるかと思います。

1.2 Lambda関数に送信される情報の処理方法

当記事では、PythonのcryptographyのX.509を利用したPEMの解析と情報取得について例をご紹介します。
(実際にはEventに入っている情報で処理できれば解析は不要)
以下、統合されたLambda関数(つまりAPI Gatewayがリクエストを受け付けて転送する先のLambda)での記載例です。SubjectDNのCommonNameを取得し応答で返しています。

app.py

#required libraries
import json
from cryptography import x509
from cryptography.x509.oid import NameOID

def lambda_handler(event, context):
    try:
        pem = event["requestContext"]["authentication"]["clientCert"]["clientCertPem"]
        cert = x509.load_pem_x509_certificate(pem.encode())
        commonName=cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        return {
            "statusCode": 200,
            "body": json.dumps({
                "message": "hello {}".format(commonName),
                "event":event 

            }),
        }
    except Exception as e:
        raise e

1.3 Lambda関数に送信される情報の例(ダンプ結果の抜粋)

上記を実行し、curlでアクセスすると以下の応答があります。

{"message": "hello qiita 2020 sep 19 user", 
"event": {
 "version": "2.0", "routeKey": "GET /hoge/auth", "rawPath": "/hoge/auth",
 "rawQueryString": "", "headers": {"accept": "*/*", "content-length": "0", 
 "host": "api.mtls.XXXXXXX, "user-agent": "curl/7.61.1", "x-forwarded-for": "18.183.XXX.XXX"},
 "requestContext": {"accountId": "123456789012", "apiId": "abcdefg",

 "authentication": {"clientCert": {"clientCertPem": 
"-----BEGIN CERTIFICATE-----
\nMIIDSDCCAjACFHXxb3nLTzSc+OSArZDsoUNyoW0BMA0GCSqGSIb3DQEBCwUAMFsx
:
:
:
\nZDEfMB0GA1UEAwwWcW1UKRts7pkdmXuwWqXqM0phRM1FxSFtSl2o51oFLqQn/EfY
\ndRScPzz6P48ct09gJ89luyNpol9m4SmIwfoa5B/bzuXAJ53UTplsufNo3saQ5sw9
\nwYBMd/bH5dUe6f6roi8oxZOFQCiz4v6mMLBakg==\n
-----END CERTIFICATE-----\n", 

"issuerDN": "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=mTLS Tester",
"serialNumber": "67333611068738446",
"subjectDN": "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=qiita 2020 sep 19 user", 
"validity": {"notAfter": "Feb  1 06:38:12 2022 GMT", "notBefore": "Sep 19 06:38:12 2020 GMT"}}}, 
"domainName": "api.mtls.XXXXXXXXXXXXXX", 
"http": {"me* Connection #0 to ho
:
:
:

1.4 Lambda Authorizerの構成

さて、上記は統合されたLambda 関数を記載しましたが、仮に追加の認可処理、例えば先ほどのCRL(証明書失効リスト)による無効化チェックをするのであればLambda Authorizerで実装されるケースが多いかなと思います。Lambda Authorizerの構成はこちらを参考にしてください。応答はIAMポリシーで応答することも可能ですし、シンプルにTrue/Falseで応答することも可能です。今回はシンプルな応答形式を選択して実装しました。

先ほど、「認可処理を追加で実施する場合Lambda Authorizerを実装するケースが多い」と記載しましたが、実装することで、Lambda関数がリクエストの都度2回(Authorizer・統合された関数)呼ばれることになります。もちろん、Authorizerにはキャッシュ設定が可能ですので必ずしも2倍にはなりませんが・・。そうした観点で「コスト意識」「同時実行数の上限」等が気になるという方もいるかもしれません。
環境(API Gatewayのルートに対するAuthorizer設定)で認可を強制することを重視するか、コストを考えて統合側の実装の工夫で漏らさずに制御するかはプロジェクトの要件等を考慮して決めていただければと思います。

1.5 Authorizerの関数の実装

今回は何も処理せず常にTrueを指定フォーマットで応答しているため、有効化しても処理に影響はありません。

LambdaAuthozier.py
import json

def lambda_handler(event, context):
    return {
        "isAuthorized": True
    }

1.6 Authorizerをルートに関連付け

1.7 動作確認

以下でアクセスすると変わらず動作します。また、Lambdaの動作確認をCloudWatch Logsで確認し動いていることも確認しました。

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

ちなみに、私の環境では以下の通りのソースで動かしてみました(統合で指定したLambdaとほぼ同じ。returnがAuthorizerの仕様に合わせた形。フォーマットはこちら

LambdaAuthorizerV2.py
import json

#required libraries
import json
from cryptography import x509
from cryptography.x509.oid import NameOID

def lambda_handler(event, context):
    try:
        print(event)
        pem = event["requestContext"]["authentication"]["clientCert"]["clientCertPem"]
        cert = x509.load_pem_x509_certificate(pem.encode())
        commonName=cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        return {
            "isAuthorized": True
        }
    except Exception as e:
        raise e

2. デフォルトエンドポイントの無効化

API Gatewayではステージがデプロイされることで(HTTP APIでは自動デプロイが有効化可能)インターネットからアクセス可能なエンドポイントが公開されます。そのエンドポイントは、前回の記事でもご紹介したカスタムドメインを構成しても残っていますし、mTLS設定を有効化しても残っています。具体的には「https://XXXX.execute-api.ap-northeast-1.amazonaws.com」の形式のエンドポイントです。
mTLSの有効化先はカスタムドメインであり、デフォルトのエンドポイントではないため、mTLS有効化後もアクセスが可能です。仮にLambda Authorizerあるいは、バックエンドの統合されたLambda関数で
- EventデータにAuthorizerが入っているか?
- PEMやSubjectDNが入っているか?
を確認していればはじくことが可能かと思いますが、何もしていないとエンドポイントを知っていれば証明書なしでそのままアクセスできてしまいます。

2.1 無効化の設定

ということで、このデフォルトのエンドポイントを無効化する設定が用意されています。それが以下の画面です。無効化すると記載されているURLではアクセスができなくなりました。

無効化後の画面はこちらです。

2.2 動作確認

実際にアクセスしてみました。以下、404が応答されています。

何等かの理由で必要があれば、有効化のままでもよいかもしれませんが、mTLSを採用したいという要件の背景にはAPIのアクセスを厳密に管理したいという背景があるのではないかと推測します。そうするとこの設定は重要な設定であり、この設定がもし変更されたのであれば重大なインシデントとして運用を回すことも必要か漏れません。

3. mTLS設定と絡めたセキュリティ設定

3.1 CA証明書格納バケットのセキュリティ強化

CA証明書を格納するバケットの保護を強化したいとお考えの方もいるかもしれません。何らかの原因で上書きされ、API側が更新されると悪意のあるクライアントが接続するとこも考えられます(公開鍵の作成も秘密鍵がないとできませんが、、、)。今回の手順でバケットに格納したのはCAの公開鍵証明書であり、CAの秘密鍵ではありません。秘密鍵に比べると重性は相対的には低いと思いますが管理を強化する方法を紹介します。鍵だけでなくバケットの保護強化としても、再確認いただければと思います。

S3バケットに関する保護については、予防的統制の観点から弊社のセキュリティコンサルタントがまとめたこちらのブログを参考にしてみてください。前回の記事で紹介したS3のバージョニング、その中の1つとして紹介されています。アクセス可能なユーザーをバケットポリシーを利用して絞ることや、Versioningの有効化、オブジェクトロックの有効化で削除、上書きを予防することが予防(事前の保護策)として考えられますが、何か起きた場合の検知として

  • GuardDutyのオブション機能であるS3保護の適用による異常の検知
  • AWS Configを利用したリソース変更の記録
  • AWS Config Rulesを利用したリソースのルール適合(準拠/非準拠)判定・検知

3.2 CloudTrailのデータイベント記録によるアクセスの記録

も検討してみてください。またCloudTrailのオプションでS3バケットに対する読み書きのイベント(Get/PutObject等)の記録も追加設定することで可能となります。
追加設定することでログが残ります。私の環境ではログをS3だけでなく、CloudWatch Logsにもストリーミングしており、CloudWatch Insightで検索することが可能です。検索してみると、API GatewayへのmTLS設定の有効化をした時刻の後でGetObjectが起動されています。

3.3 AWS Config によるリソースの状況記録。

CloudTrailは「誰が、いつ、何をしたか」が分かる記録情報として非常に重要です。ただ、「そのAPI」が「その時に」「どのような状態だったか」「何がどう変わったか」を的確に把握するためには、AWS Configレコーダーの有効化が非常に重要です。有効化することで、AWS Configがサポートしているリソースの変更状況が最大7年間保持されます。以下は私が、先ほど上記の「 2. デフォルトエンドポイントの無効果」で操作した際の記録がAPIというリソースを起点として記載されており、視覚的にもログベースで確認するよりもわかりやすいです。ぜひ、この機会にAWS Configについても監査、事後分析の観点からご利用をご検討いただければと思います。

3.4.Event Bridge(CloudWatch Events)にとる異常行動の早期検知と回復

異常な行動があれば、迅速に対処することも重要です。例えばデフォルトのエンドポイントの無効化設定が変更された、予期せぬタイミングで証明書が上書きされた(これはAPI Gateway側で設定を変更しない限りバージョニングをしていれば影響は少ないです)等、検知をしたら迅速に対応することを皆さん考えるでしょう。その場合にはCloudWatch Events/Event Bridgeです。EventBridgeでイベントを指定することでLambdaやSQS、SNSにイベントを通知できます。今回は例として「UpdateApi」「UpdateDomainName」を検知対象として挙げ、通知先としてAmazon SQSに通知する設定としました。

さて、いざ、mTLSの設定を無効化!!
すると、以下のイベントデータ(以下の例では一部抜粋)がSQSに通知されました。以下の例では、mutualTlsAuthenitcationが""、つまり、無効化されていることがわかります。このようにイベント名で検知した上で、Lambda関数等でRequest Parameterのフィルタリングを行い処理することで、例えば、SIEMにアラートを上げたり、あるいは、本来設定されているべき値に自動的に戻すということもAPIを利用すれば可能かと思います。
是非、検討してみてください。

{
  "version": "0",
  "id": "ea541f65-2c5c-0e07-403c-424b2797cb68",
  "detail-type": "AWS API Call via CloudTrail",
  "source": "aws.apigateway",
  "time": "2020-09-19T15:20:27Z",
  "region": "ap-northeast-1",
  "resources": [],
  "detail": {
    "eventVersion": "1.05",
    "userIdentity": {
      "type": "AssumedRole",
      :
    "eventTime": "2020-09-19T15:20:27Z",
    "eventSource": "apigateway.amazonaws.com",
    "eventName": "UpdateDomainName",
    "requestParameters": {
      "domainNameConfigurations": [
        {
          "endpointType": "REGIONAL",
          "certificateArn": "arn:aws:acm:ap-northeast-1:123456789012:certificate/082543e0-96ae-41a4-bd54",
          "securityPolicy": "TLS_1_2"
        }
      ],
      "domainName": "api.mtls.XXXXXXX",
      "mutualTlsAuthentication": {
        "truststoreUri": ""
      }
    },
    "responseElements": {
      "domainNameConfigurations": [
        {
          "endpointType": "REGIONAL",
          "certificateArn": "arn:aws:acm:ap-northeast-1:123456789012:certificate/082543e0-96ae-41a4-bd54",
          "apiGatewayDomainName": "XXXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com",
          "hostedZoneId": "Z1YSHQZHG15GKL",
          "securityPolicy": "TLS_1_2",
          "domainNameStatus": "UPDATING",
          "domainNameStatusMessage": "Update is in progress. Please wait until the update finishes before making another request."
        }
  }
}

まとめ

mTLSの設定の話から、それらに付随する運用回りのお話としてAWSの既存のサービス(CloudTrail/AWS Config/Event Bridge)を絡めてご紹介しました。どれも、「やったほうが良いのはわかるけど・・・・」という感じかもしれません。設定は容易かもしれませんが、コストの兼ね合いや、運用との兼ね合いもあるかと思います。もちろん、対象とする情報資産(取り扱うデータの重要度やインシデント時のビジネス影響)の価値にもよると思います。とはいえ、起きてから過去を分析することはできないという点もありますので、検討をしていただければと思います。