aptpod Advent Calendar 2021 の 6 日目を担当する、SRE チームの柏崎です。
弊社では、intdash を組み合わせたプロジェクトが多くあります。
とあるプロジェクトでは、車両に設置するエッジコンピュータが Amazon API Gateway を利用した API と通信する、というカスタマイズ部分があります。
先日このプロジェクトで、エッジコンピュータと Amazon API Gateway の通信に、セキュリティ強化のため相互 TLS 認証を導入することになりました。
今回は、Amazon API Gateway の相互 TLS 認証での課題を解決し、より厳格に導入する方法をご紹介します。
「相互 TLS 認証」とは
例えば、ブラウザで一般的なウェブサイトを閲覧するとき、HTTPS (HTTP+TLS) が利用されます。
TLS のレイヤーでは、サーバから提示された証明書をクライアントが検証することで、クライアントがサーバを認証しています。
この認証によって、接続先のサーバが正当なものであるかどうかの確認ができ、安心して通信を行うことができます。
TLS のデフォルトでは、 クライアントがサーバを 認証するのみですが、オプションで サーバがクライアントを 認証することもできます。
クライアントとサーバが相互に相手を認証することから、「相互 TLS 認証」「Mutual-TLS」「mTLS」などと呼ばれています。
また、オプションであるクライアントの認証を有効にしているので、単に「クライアント認証」と呼ばれることもあります。
Amazon API Gateway での相互 TLS 認証と課題
Amazon API Gateway での相互 TLS 認証は、カスタムドメイン名の設定にて、簡単に有効にすることができます。
トラストストア 1 を S3 にアップロードし、カスタムドメイン名の設定にてその S3 URI を指定するだけです。
ただし、ここで課題となるのは、(現時点で) Amazon API Gateway の相互 TLS 認証は、証明書が失効したかどうかの検証を行わない という点です。 2
証明書は、「対応する秘密鍵が漏洩した可能性がある」「誤って発行したので取り消したい」等の理由で失効される事があります。
特に今回のようなクライアントに設定する証明書は、プライベート認証局から 10 年等の長めの有効期間で発行されることも多く、 失効させてもその間は接続できてしまう という問題がおきてしまいます。
Lambda オーソライザーでの解決
Amazon API Gateway には、Lambda オーソライザーという、Lambda 関数を利用してアクセス制御を行う機能があります。
この Lambda 関数に入力される内容には、クライアントから提示された証明書が含まれています。 3
ここでは、Lambda オーソライザーを利用して証明書の失効状態を検証する方法を示します。
1. Lambda オーソライザーを実装する
Python 3.8 にて、下記の Lambda 関数を実装します。(長ったらしいですがご勘弁を…!)
環境変数 TRUSTSTORE_URI
には、Amazon API Gateway のカスタムドメイン名のトラストストアと同一の S3 URI を指定してください。
また、Lambda 関数の実行ロールには、この S3 URI を読める権限を付与してください。
CertificateValidator()
で allow_fetching=True
を与えると、CRL または OCSP 4 を利用した証明書の失効状態が検証されるようになります。
ここでのポイントは、「lambda_handler の外で、S3 からトラストストアを読み込んでいる」という点です。
これにより、S3 との通信が Lambda 関数のコールドスタート時のみ実行されるようにしています。
import os from urllib.parse import urlparse import boto3 from asn1crypto import pem import json from certvalidator import ValidationContext, CertificateValidator trust_roots = [] truststore_uri = os.environ.get('TRUSTSTORE_URI') if truststore_uri is not None: u = urlparse(truststore_uri) bucket = u.netloc key = u.path.lstrip('/') truststore = boto3.client('s3').get_object(Bucket=bucket, Key=key)['Body'].read() for _, _, der in pem.unarmor(truststore, multiple=True): trust_roots.append(der) def lambda_handler(event, context): print("Event: " + json.dumps(event)) principalId = event['requestContext']['identity']['clientCert']['serialNumber'] cert = event['requestContext']['identity']['clientCert']['clientCertPem'].encode() context = ValidationContext(trust_roots=trust_roots, allow_fetching=True, revocation_mode='hard-fail') validator = CertificateValidator(end_entity_cert=cert, validation_context=context) try: validator.validate_usage(key_usage=None) except Exception as e: print("The certificate could not be validated: " + str(e)) return generate_policy(principalId, EFFECT_DENY) else: print("The certificate has been validated") return generate_policy(principalId, EFFECT_ALLOW) EFFECT_ALLOW = 'Allow' EFFECT_DENY = 'Deny' def generate_policy(principalId, effect): return { 'principalId': principalId, 'policyDocument': { 'Version': '2012-10-17', 'Statement': [ { 'Action': 'execute-api:Invoke', 'Effect': effect, 'Resource': '*' } ] } }
2. Amazon API Gateway に Lambda オーソライザーを設定する
API に Lambda オーソライザーを設定します。
ここでは、認可のキャッシュを有効にしておくと良いでしょう。
クライアントからのリクエストの都度、Lambda オーソライザーが実行されてしまうことを回避できます。
続いて、メソッドリクエストに Lambda オーソライザーを設定します。
これで、設定は完了です。
確認
実際に、失効済みの証明書を利用して、Lambda オーソライザー設定前後の Amazon API Gateway からのレスポンスを確認してみました。
Lambda オーソライザーの設定前は、失効済みの証明書でも下記のように 204 が返ってきていましたが、
$ curl -i --cert cert-revoked.pem --key privkey.pem https://my-great-api.example.com/ HTTP/2 204 x-amzn-requestid: cba741e3-e2eb-4ced-8332-efae0a6f4910 x-amz-apigw-id: JpzGqG_rtjMF1jg= content-type: application/json date: Wed, 01 Dec 2021 04:49:58 GMT
設定後は、下記のように 403 が返るようになりました。
$ curl -i --cert cert-revoked.pem --key privkey.pem https://my-great-api.example.com/ HTTP/2 403 x-amzn-requestid: 0c4e363e-25f3-4206-8d67-665641787512 x-amzn-errortype: AccessDeniedException x-amz-apigw-id: JpzipH4CNjMFiWw= content-type: application/json content-length: 82 date: Wed, 01 Dec 2021 04:52:58 GMT {"Message":"User is not authorized to access this resource with an explicit deny"}
まとめ
今回は、Amazon API Gateway の相互 TLS 認証での、失効状態が検証されない、という課題を解決する方法を紹介しました。
実際のところ、失効はそこまで頻繁に行われるものでは無いですが、いざというときにはしっかりと検証されていなければなりません。
とても地味ですが、こういった所を突き詰めるのは楽しいですね。