9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambda関数URLが第三者により直接実行されるのをCloudFront(OAC)+WAFで防ぐCDKを書く

Last updated at Posted at 2024-10-30

はじめに

Lambda関数URLを作成し、第三者からアクセスできてしまうのを防ぐために、CloudFrontからのみアクセスできるように設定しようとしたところ、落とし穴がかなりあったので備忘録として残します。

目次

実現したこと

Before

Lambda関数URLは、認証NONEの場合、誰でもアクセスできてしまう状態でした。
Beforeイメージ図

After

そこで、CloudFront経由でのみアクセス可能になるよう設定・WAFを追加し、かつ、カスタムドメインでアクセスできるように実装しました。
ここではカスタムドメインの変換については省略します。
Afterイメージ図

実装したCDK

ディレクトリ構成

ディレクトリの構成は以下です。
今回mainHandlerでは、POSTメソッドを使用しています。

┣ functions
  ┣ mainHandler.ts
  ┗ lambdaEdgeHandler.ts
┣ ap-northeast-1-stack.ts
┗ us-east-1-stack.ts

コード

下記のサイトを参考にlambdaEdge用のHandlerを作成しました。

'lambdaEdgeHandler.ts'
import { CloudFrontRequestEvent, CloudFrontRequestHandler } from "aws-lambda";

const hashPayload = async (payload: string) => {
  const encoder = new TextEncoder().encode(payload);
  const hash = await crypto.subtle.digest("SHA-256", encoder);
  const hashArray = Array.from(new Uint8Array(hash));
  return hashArray.map((bytes) => bytes.toString(16).padStart(2, "0")).join("");
};

export const handler: CloudFrontRequestHandler = async (
  event: CloudFrontRequestEvent,
  _context,
) => {
  const request = event.Records[0].cf.request;

  if (!request.body) {
    return request;
  }

  const body = request.body.data;
  const decodedBody = Buffer.from(body, "base64").toString("utf-8");

  request.headers["x-amz-content-sha256"] = [
    { key: "x-amz-content-sha256", value: await hashPayload(decodedBody) },
  ];

  return request;
};

ap-northeast-1-stackではmainHandlerの作成、OACの作成、distributionの作成を行なっています。

'ap-northeast-1-stack.ts'
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as ssm from "aws-cdk-lib/aws-ssm";
import { Construct } from "constructs";
import * as cloudfrontOrigins from "aws-cdk-lib/aws-cloudfront-origins";

export class ApNortheast1Stack extends cdk.Stack {
  distribution: cdk.aws_cloudfront.Distribution;
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    // The Lambda Function For Main
    const mainHandlerFunc = new lambda.Function(
      this,
      "MainHandlerFunction",
      {
        runtime: lambda.Runtime.NODEJS_20_X,
        handler: "mainHandler.handler",
        code: lambda.Code.fromAsset("lib/functions"),
        timeout: cdk.Duration.minutes(15),
      }
    );

    // OAC
    const originAccessControlForMain = new cloudfront.CfnOriginAccessControl(
      this,
      "mainHandler-cloudfront-OriginAccessControl",
      {
        originAccessControlConfig: {
          name: "mainHandler-cloudfront-OriginAccessControl",
          originAccessControlOriginType: "lambda",
          signingBehavior: "always",
          signingProtocol: "sigv4",
          description: "Access Control",
        },
      }
    );

    // CloudFront
    const lambdaEdgeFunctionArn = ssm.StringParameter.fromStringParameterAttributes(
      this,
      "LambdaEdgeFunctionArn",
      {
        parameterName: "/main/lambda-edge-function-arn",
        forceDynamicReference: true,
      }
    ).stringValue;
    const webAclId = ssm.StringParameter.fromStringParameterAttributes(
      this,
      "WebAclArnCustomResource",
      {
        parameterName: "/main/web-acl-arn",
        forceDynamicReference: true,
      }
    ).stringValue;
    const distribution = new cloudfront.Distribution(
      this,
      "mainHandler-cloudfront-Distribution",
      {
        comment: "mainHandler distribution.",
        defaultBehavior: {
          origin: new cloudfrontOrigins.FunctionUrlOrigin(
            mainHandlerFunc.addFunctionUrl({
              authType: lambda.FunctionUrlAuthType.AWS_IAM,
            })
          ),
          allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
          edgeLambdas: [
            {
              eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
              functionVersion: cdk.aws_lambda.Version.fromVersionArn(
                this,
                "OriginRequestSigv4SignerFn",
                lambdaEdgeFunctionArn
              ),
              includeBody: true,
            },
          ],
          originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
        },
        httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
        webAclId,
      }
    );
    const cloudfrontDistributionId = distribution.distributionId;
    const cfnDistribution = distribution.node
      .defaultChild as cloudfront.CfnDistribution;

    // OAC設定(OAIが自動で設定されてしまうため、削除してOACを追加)
    cfnDistribution.addPropertyOverride(
      "DistributionConfig.Origins.0.OriginAccessControlId",
      originAccessControlForMain.attrId
    );

    // リソースベースのポリシーステートメントを追加
    new lambda.CfnPermission(this, "AllowCloudFrontServicePrincipalForMain", {
      action: "lambda:InvokeFunctionUrl",
      functionName: mainHandlerFunc.functionArn,
      principal: "cloudfront.amazonaws.com",
      sourceArn: `arn:aws:cloudfront::${cdk.Aws.ACCOUNT_ID}:distribution/${cloudfrontDistributionId}`,
    });
  }
}


us-east-1-stack.tsでは、Lambda@Edgeの設定とWAFの設定を行っています。
スタックのリージョンを分けている理由は、Lambda@Edge、CloudFrontのWAFが共にus-east-1しか対応していないためです。
今回WAFのルールはレートベースでかけていますが、お好みで設定して下さい。

'us-east-1-stack.ts'
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as wafv2 from "aws-cdk-lib/aws-wafv2";
import { CrossRegionParameter } from "@alma-cdk/cross-region-parameter";
import { Construct } from "constructs";

export class UsEast1Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    // Lambda@Edge
    const lambdaEdgeFunction = new lambda.Function(this, "LambdaEdgeFunction", {
      runtime: lambda.Runtime.NODEJS_20_X,
      code: lambda.Code.fromAsset("lib/functions"),
      handler: "cloudFrontRequestHandler.handler",
      memorySize: 128,
      timeout: cdk.Duration.seconds(5),
      role: new iam.Role(this, "LambdaEdgeFunctionRole", {
        assumedBy: new iam.CompositePrincipal(
          new iam.ServicePrincipal("lambda.amazonaws.com"),
          new iam.ServicePrincipal("edgelambda.amazonaws.com")
        ),
        managedPolicies: [
          iam.ManagedPolicy.fromAwsManagedPolicyName(
            "service-role/AWSLambdaBasicExecutionRole"
          ),
          iam.ManagedPolicy.fromAwsManagedPolicyName("AWSLambda_FullAccess"),
        ],
      }),
    });
    new CrossRegionParameter(this, `${id}-Lambda-Edge-Function-Arn`, {
      description: "The ARN of the Lambda@Edge function",
      name: "/main/lambda-edge-function-arn",
      value: lambdaEdgeFunction.currentVersion.functionArn,
      region: "ap-northeast-1",
    });

    // Web ACL
    const blockHighRateAccessRuleName = "block-high-rate-access-rule";
    const blockHighRateAccessRule: wafv2.CfnWebACL.RuleProperty = {
      priority: 100,
      name: blockHighRateAccessRuleName,
      action: { block: {} },
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: `${blockHighRateAccessRuleName}`,
      },
      statement: {
        rateBasedStatement: {
          aggregateKeyType: "IP",
          limit: 1000,
        },
      },
    };
    const webACLForMain = new wafv2.CfnWebACL(this, "WebACLForMain", {
      name: "main-web-acl",
      scope: "CLOUDFRONT",
      defaultAction: {
        block: {},
      },
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: "main-web-acl",
      },
      rules: [blockHighRateAccessRule],
    });
    new CrossRegionParameter(this, `${id}-WebAcl-Arn`, {
      description: "The ID of the WebACL",
      name: "/main/web-acl-arn",
      value: webACLForMain.attrArn,
      region: "ap-northeast-1",
    });
  }
}

ハマったところ

Lambda関数URLでPUT/POSTメソッドを使用する場合、x-amz-content-sha256ヘッダーにペイロードする必要がある

GETメソッドだけの場合は下記のサイトの実装のみで問題ありませんでした。

しかし、今回POSTメソッドを使用していたため、x-amz-content-sha256ヘッダーにペイロードハッシュ値を含める必要があり、下記のサイトを参考にさせていただき、実装を修正しました。

Lambda@EdgeのNodeバージョンは20

Lambda@EdgeのNodeバージョンは20だとうまくいき、18だとうまくリクエストを投げられませんでした。

リージョンの制限

今回使用したLambda@EdgeやCloudFrontのWAFはus-east-1しか対応していないとのことで、CDKのスタックの作成に苦戦しました。
CrossRegionParameterを利用して、SSMのパラメータの保存先を変えることで取り出しが簡単にできるようになったので共有します。

参考

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?