はじめに

2022年4月に一般提供が開始されたLambda Function URLsには「APIGatewayやALBなしで、Lambdaを使ったAPIを作成できるだと・・・」と驚かされた方も多いんじゃないでしょうか。かくいう私もその一人です。

簡易なAPIを作成するには便利なLambda Function URLsですが、認証タイプの選択には注意が必要です。

AWS_IAM 指定のIAMのみに、API(Lambda)の実行を許可する。
NONE 認証をしない。エンドポイントを知っているユーザであれば、誰でもAPI(Lambda)の実行可能。

他のAWSサービスからAPIを実行するような場合はAWS_IAMが使えますが、パブリックに公開し、一般ユーザにAPIの実行を許可する場合はNONEを選択する形になります。
NONEを選択した場合、エンドポイントを知っているユーザであれば、誰でもAPI(Lambda)の実行が可能です。

その為、そのままだとL7の攻撃に対して無防備になってしまいます(L4までの攻撃はAWS Shieldが防いでくれる)。Lambdaのコード内で何かしらの防御の仕組みを実装するのも一つの手ではありますが、これはあまり現実的ではありません。
また、どんなリクエストであってもLambdaの実行自体はされるので、DDoSによる意図しないLambdaの大量実行は防げません。
これらへの対処としてはAWS WAFを活用するのが一般的ですが、LambdaにはAWS WAFをアタッチできません。
Lambda Function URLsを使用しないのであれば、APIGateway or ALB + Lambdaの構成にし、APIGateway or ALBにWAFをアタッチすればOKですが、
今回はLambda Function URLsを使用しつつ、AWS WAFを活用する方法を紹介します。

それが、CloudFront + Lambda@Edgeを使用した以下構成です。

構成図

設定手順

Lambda Function URLsの設定

関数URLを認証タイプAWS_IAMで有効化します。

LambdaFunctionURLs設定画面

今回は、Lambda Function URLsの実行許可をLambda@EdgeのIAMロールに付与するので、リソースベースポリシーは未設定のまま進めます。

Lambdaリソースベースポリシー設定画面

Lambda@Edgeの設定

署名V4でCloudFrontのオリジンリクエストに署名を実施するLambdaをバージニアリージョンにデプロイします。

コード

const AWS = require('aws-sdk');

exports.handler = async event => {
    console.log("event=" + JSON.stringify(event));

    const request = event.Records[0].cf.request;
    let headers = request.headers;
    // remove the x-forwarded-for from the signature
    delete headers['x-forwarded-for'];

    // build the request to sign
    const host = request.headers['host'][0].value;
    const region = host.split(".")[2];
    const path = request.uri + (request.querystring ? '?'+ request.querystring : '');
    const req = new AWS.HttpRequest(new AWS.Endpoint(`https://${host}${path}`), region);
    req.body = (request.body && request.body.data) ? Buffer.from(request.body.data, request.body.encoding) : undefined;
    req.method = request.method;
    for (const header of Object.values(headers)) {
        req.headers[header[0].key] = header[0].value;
    }
    console.log(JSON.stringify(req));

    // sign the request with Signature V4 and the credentials of the edge function itself
    // the edge function must have lambda:InvokeFunctionUrl permission for the target URL
    const signer = new AWS.Signers.V4(req, 'lambda', true);
    signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());

    // reformat the headers for CloudFront
    for (const header in req.headers){
        request.headers[header.toLowerCase()] = [{
            key: header,
            value: req.headers[header].toString(),
        }];
    }
    console.log("signedRequest="+JSON.stringify(request));
    return request;
}

Lambdaに付与したIAMロールの信頼ポリシーと許可ポリシーは以下のように設定します。

信頼ポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "edgelambda.amazonaws.com",
                    "lambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

許可ポリシー

{
    "Statement": [
        {
            "Action": "lambda:InvokeFunctionUrl",
            "Condition": {
                "StringEquals": {
                    "lambda:FunctionUrlAuthType": "AWS_IAM"
                }
            },
            "Effect": "Allow",
            "Resource": "${関数URLを有効化したLambdaのARN}"
        }
    ],
    "Version": "2012-10-17"
}

CloudFrontの設定

Lambda Function URLs用のビヘイビアを作成し、オリジンにLambda Function URLsのエンドポイントを選択します。

CloudFrontビヘイビア設定画面

関数の関連付けのオリジンリクエストに、先程作成したLambda@Edgeを関連付けます。

CloudFrontビヘイビア設定画面

これにて設定完了です。
CloudFront経由だと200がレスポンスされ、Lambda Function URLs直だと403がレスポンスされるようになります。

あとはCloudFrontにAWS WAFをアタッチして、そのWAFで好きなだけL7攻撃をシバいちゃって下さい。

最後に

今回は、Lambda Function URLsをセキュアにパブリック公開する方法を紹介しました。
本記事の元ネタはAWSの公式ブログになるので、詳しくはこちらをご参照下さい。