My Note

自己理解のためのブログ

CloudFrontとLambda@Edge ( Python3 )とS3で静的ページにIPアドレス制限とBasic認証を設定する

はじめに

今回はCloudFrontとLambda@Edge ( Python3.8 )を使ってオリジンに対してIP制限とBasic認証を設定しました。 LambdaEdgeとIAMについてはServerlessFrameworkを利用してデプロイしています。

構成

f:id:yhidetoshi:20211206170730p:plain

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について

sls deployすると以下のIAMロールとポリシーが作成される。

f:id:yhidetoshi:20211207104302p:plain

  • 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のバケットポリシーが以下のように自動で更新されます。

{
    "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 に設定

f:id:yhidetoshi:20211207110707p:plain

  • テストページに表示するページ
<!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"
      }
],

f:id:yhidetoshi:20211207112320p:plain

{
    "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のパラメータストアから取得してもいいと思います。

ソースコード一式は以下のGitHubにあります。

github.com