CloudFrontとLambda@Edge ( Python3 )とS3で静的ページにIPアドレス制限とBasic認証を設定する
はじめに
今回はCloudFrontとLambda@Edge ( Python3.8 )を使ってオリジンに対してIP制限とBasic認証を設定しました。 LambdaEdgeとIAMについてはServerlessFrameworkを利用してデプロイしています。
構成
CloudFrontについて
Basic認証とIP制限を設定するためにCloudFrontとLambda@Edge(us-east-1:バージニア北部)を連携する。 CloudFrontには以下の機能があるので利用する。
CloudFront ディストリビューションの各キャッシュ動作に、特定の CloudFront イベントの発生時に Lambda 関数を実行させるトリガー (関連付け) を 4 つまで追加できます。
ビューワーリクエスト
CloudFront がビューワーからリクエストを受け取ると、リクエストされたオブジェクトが CloudFront キャッシュにあるかどうかを確認する前に関数が実行されます。
→ Basic認証とIPアドレス制限には、CloudFrontがキャッシュを確認する前に実行する必要があるので、Viewer Request
で連携します。
公式のドキュメントは下記です。
docs.aws.amazon.com
Lambda@Edgeについて
- Lambda
- Functionの設定
- ランタイムはPython3.8
- ServerlessFrameworkのyamlで設定:
handler: basic_auth.lambda_handler
- IAMの設定
- lambda関数をCloudFrontに関連付けるために必要な許可設定
- Cloudwatchにログを出力するための権限を設定
- Functionの設定
sls deployすると以下のIAMロールとポリシーが作成される。
- serverless.yml
service: basicauth frameworkVersion: ">=2.68.0" provider: name: aws stage: dev runtime: python3.8 region: us-east-1 lambdaHashingVersion: 20201221 functions: function: handler: basic_auth.lambda_handler role: LambdaEdgeBasicAuth timeout: 5 description: LambdaEdge Basic Auth Python3 memorySize: 128 resources: Resources: LambdaEdgeBasicAuth: Type: AWS::IAM::Role Properties: RoleName: LambdaEdgeBasicAuth AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com - edgelambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: LambdaEdgeBasicAuth PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "lambda:GetFunction" - "lambda:EnableReplication*" - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*"
コードについて( Python3 )
コードについては以下のとおり。
- basic_auth.py
""" - 許可IPアドレス元の場合は認証なし - 許可されたIPアドレス以外の場合はBasic認証を実施 """ import base64 ALLOW_USERS = [ { "user": "admin", "password": "pass1" }, { "user": "dev", "password": "pass2" } ] ALLOW_IP = ['X.X.X.X', 'X.X.X.X'] ERROR_RESPONSE_AUTH = { 'status': '401', 'statusDescription': 'Unauthorized', 'body': 'Authentication Failed', 'headers': { 'www-authenticate': [ { 'key': 'WWW-Authenticate', 'value': 'Basic Authentication' } ] } } def lambda_handler(event, context): request = event['Records'][0]['cf']['request'] headers = request['headers'] client_ip = request['clientIp'] if validate_client_ip(client_ip): return request # Authorizationヘッダーの有無をチェック if 'authorization' not in headers: return ERROR_RESPONSE_AUTH encode_auth = headers['authorization'][0]['value'].split(" ") decode_auth = base64.b64decode(encode_auth[1]).decode().split(":") (user, password) = (decode_auth[0], decode_auth[1]) if validate_auth(user, password): return request else: return ERROR_RESPONSE_AUTH def validate_auth(user, password): exist_flag = False for allow_user in ALLOW_USERS: if user == allow_user.get('user') and password == allow_user.get('password'): exist_flag = True return True if exist_flag == False: return False def validate_client_ip(client_ip): print('CLIENT_IP=%s' % client_ip) if client_ip in ALLOW_IP: return True else: return False
- (参考)Lambdaで受け取るCloudFrontのイベント構造について docs.aws.amazon.com
CloudFrontとS3について
詳しくは触れませんが、オリジンにS3を指定してCloudFrontからS3へのアクセス許可を設定するために OAIを作成します。 CloudFrontの設定時に "バケットポリシーを自動で更新する" にチェックをいれると、オリジンに指定したS3のバケットポリシーが以下のように自動で更新されます。
- bucket-policy-sample.
{ "Version": "2008-10-17", "Id": "PolicyForCloudFrontPrivateContent", "Statement": [ { "Sid": "1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {$CLOUDFRONT_DISTRIBUTION_ID}" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::${ORIGIN_S3_BUCKET_NAME}/*" } ] }
Behaviorの設定
- CachePolicy
- Basic認証とIP制限なのでキャッシュはDisabledに設定
- LambdaEdgeの関連付け
- Viewer-Request に設定
- テストページに表示するページ
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <p>CloudFront LambdaEdge BasicAuth Test</p> </body> </html>
動作確認
Lambdaの機能を使ったテストイベント
CloudFrontと連携する前にこのテストを使って動作確認をすることができます。
イベントはプルダウンで事前に用意されているので色々なパターンを利用できます。
今回はBasic認証を確認したかったので、 cloudfront-access-request-in-response
を利用して
authorization
のブロックを追加して確認しました。
❯ echo -n dev:pass2 | base64 ZGV2OnBhc3My
"authorization": [ { "value": "Basic ZGV2OnBhc3My" } ],
{ "Records": [ { "cf": { "config": { "distributionId": "EXAMPLE" }, "request": { "headers": { "host": [ { "key": "Host", "value": "d123.cf.net" } ], "authorization": [ { "value": "Basic ZGV2OnBhc3My" } ], "user-name": [ { "key": "User-Name", "value": "CloudFront" } ] }, "clientIp": "0.0.0.0", "uri": "/index.html", "method": "GET" }, "response": { "status": "200", "statusDescription": "OK", "headers": { "x-cache": [ { "key": "X-Cache", "value": "Hello from Cloudfront" } ] } } } } ] }
クライアントからの実行
■ パターン1 adminユーザの場合 ❯ curl -u admin:pass1 http://{$CLOUDFRONT_DISTRIBUTION_ID}.cloudfront.net/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <p>CloudFront S3 LambdaEdge BasicAuth Test</p> </body> </html>% ■ パターン2 devユーザの場合 ❯ curl -u dev:pass2 http://{$CLOUDFRONT_DISTRIBUTION_ID}.cloudfront.net/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <p>CloudFront S3 LambdaEdge BasicAuth Test</p> </body> </html> ■ パターン3 パスワードを間違えた場合 ❯ curl -u dev:pass http://{$CLOUDFRONT_DISTRIBUTION_ID}.cloudfront.net/index.html Authentication Failed
さいごに
サーバレス環境(CloudFront + Lambda@Edge)を利用してサイトのBasic認証とIP制限を検証しました。 今回は静的ページで確認しましたが,SPAの環境でも活用できる仕組みなので利用ケースは多いと思います。 Basic認証のID/PASSをハードコーディングしていますが、よりセキュアにするためにもSSMのパラメータストアから取得してもいいと思います。