🆔

Lambda@Edge+パラメータストアでちょっと汎用的なBasic認証を実装してみる

2023/02/19に公開

こんにちは。深緑です。

今回はBasic認証です。
令和になってもBasic認証はちょいちょい求められます。
最近はCloudFrontのLambda@Edgeを使うことが多いのですが、
Lambda関数内にBasic認証のID・パスワードをベタで書いており、
なかなかその状態から脱出できずサイトを作るたびにLambda関数が増えるのが悩みでした。
今回、パラメータストアを併用してちょっと汎用的にできた気がするので書き記してみます。

概要

Pythonで書いてます。
https://github.com/satoshi256kbyte/aws-code/tree/master/lambda/basic-auth

Lambdaのイベントハンドラのソースはこちらです。
https://github.com/satoshi256kbyte/aws-code/blob/master/lambda/basic-auth/src/app.py

パラメータストアがこちらです。
SecureStringにしています。
パラメータストアの画面

パラメータストアでホストごとのユーザーID・パスワードを管理します。
Lambda@Edgeはイベント情報からホスト名が取れるので、
パラメータストアからそのホストに応じたユーザーID・パスワードを取って認証を行います。

app.py
def check_authorization_header(authorization_header: list, host: str) -> bool:
    """Basic認証のヘッダーをチェックする
    """
    if not authorization_header:
        return False
        
    ssm_response = ssm.get_parameters(
        Names=['basicauth-parameters'],
        WithDecryption=True
    )

    print('--- ssm_response ---')
    print(ssm_response)

    # パラメータを格納する配列を準備
    params = {}

    # 復号化したパラメータを配列に格納
    for param in ssm_response['Parameters']:
        params[param['Name']] = param['Value']
        
    parameters = json.loads(params['basicauth-parameters'])

    print('--- parameters ---')
    print(parameters)

    if host not in parameters:
        return False

    for account in parameters.get(host):
        user_id = account.get('user_id')
        user_password = account.get('user_password')

        encoded_value = base64.b64encode('{0}:{1}'.format(
            user_id, user_password).encode('utf-8'))
        check_value = 'Basic {0}'.format(encoded_value.decode(encoding='utf-8'))

        if authorization_header[0].get('value') == check_value:
            return True

    return False

パラメータストアをSecureStringにしているので、
パラメータストアからデータを取る時はWithDecryption=Trueを付けるのがポイントです。

    ssm_response = ssm.get_parameters(
        Names=['basicauth-parameters'],
        WithDecryption=True
    )

リージョンに関する注意点

Lambdaはバージニアに作るけど、動くのはバージニアとは限らない

まず、CloudFrontとLambda@Edgeはバージニアリージョンで設定するものなので、
Lambdaもバージニアで作る必要があります。

しかし、セットしたLambdaがバージニアで動くとは限りません。
CloudFrontにはエッジロケーションの仕様があるので、Lambda@Edgeはアクセスした場所に応じたリージョンで動作するようです。

AWS公式 - 関数を使用してエッジでカスタマイズ

これらは単一の AWS リージョンに公開しますが、関数を CloudFront ディストリビューションに関連付けると、Lambda@Edge がコードを世界中に自動でレプリケートします。

クラスメソッド様の記事でもそのように読み取ることができます。
DevelopersIO - AWS Lambda@Edgeのログはどこ?AWS Lambda@Edgeのログ出力先について

AWS Lambda@Edgeでは関数自体はバージニアリージョンに作成する必要がありますが、
必ず、バージニアリージョンに出力されるわけではなく、 CloudFrontにアクセスしたとき、AWS Lambda@Edgeを動作させたエッジロケーションが何処かによってログ出力されるリージョンが決定されます。
例えば、日本でCloudFrontにアクセスしてAWS Lambda@Edgeを動作させた場合は、東京リージョン近辺のエッジロケーションにアクセスされるので、東京リージョンのCloudWatch Logsに出力されます。

どこのリージョンのパラメータストアを見るか指定する必要がある

上述の通り、Lambda@Edgeで設定したLambda関数はアクセスに応じて色んなリージョンで実行されます。
従って、パラメータストアのデータをロードする際にリージョンを指定していなければ、
バージニアで呼ばれたらバージニアのパラメータストアを見に行く動きをします。

今回の例ではパラメータストアは東京リージョンに作りました。
なので、パラメータストアのクライアントを生成する際に東京リージョンを固定で指定しています。
(環境変数にしてもいいかもしれませんね。)
これでどのリージョンで動いても東京リージョンのパラメータストアを見に行くはずです。

ssm = boto3.client('ssm', region_name='ap-northeast-1')

権限もリージョンが可変であること前提で設定する

本記事のLambdaを動かすには、Lambdaにパラメータストアのデータロード権限とCloudWatchへのログ書き込み権限が必要です。
パラメータストアの方は、ソースコードで東京リージョンを固定で指定したので権限も東京リージョンに対してあれば大丈夫です。
一方、CloudWatchの方はLambdaがどこのリージョンで動くかわからないので全リージョンに書き込み権限を与える必要があります。

具体的にポリシーのjsonで表すとこのようになります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ssm1",
            "Effect": "Allow",
            "Action": "ssm:DescribeParameters",
            "Resource": "*"
        },
        {
            "Sid": "ssm2",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-1:アカウント:parameter/basicauth-parameters"
            ]
        },
        {
            "Sid": "log1",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup"
            ],
            "Resource": [
                "arn:aws:logs:*:アカウント:*"
            ]
        },
        {
            "Sid": "log2",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:アカウント:log-group:/aws/lambda/*"
        }
    ]
}

なお、Lambda@Edgeのログは、Lambdaのモニタリングからは見えません。
こちらもクラスメソッド様の記事を参考にして見てください。
DevelopersIO - AWS Lambda@Edgeのログはどこ?AWS Lambda@Edgeのログ出力先について

最後に

個人的に以前からこういうコードがあったらちょっと嬉しいというものを書いたつもりです。
また、Lambda@Edgeのリージョンに関する挙動はだいぶ悩んだので本記事の内容がお役に立てれば幸いです。

Discussion