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 認証での、失効状態が検証されない、という課題を解決する方法を紹介しました。
実際のところ、失効はそこまで頻繁に行われるものでは無いですが、いざというときにはしっかりと検証されていなければなりません。
とても地味ですが、こういった所を突き詰めるのは楽しいですね。