Lambda関数URLのBot対策をCloudFormationで実装してみた (OAC + Basic認証 + noindex)

Lambda関数URLのBot対策をCloudFormationで実装してみた (OAC + Basic認証 + noindex)

Lambda関数URLへの直接アクセスをOACで防ぎ、CloudFront FunctionsでBasic認証、robots.txt、noindexの実装を行うCloudFormationテンプレートを紹介します。
2025.11.04

2024年、Lambda関数URLがCloudFront Origin Access Control (OAC)をサポートしました。

https://dev.classmethod.jp/articles/cloudfront-oac-lambda-url/

これにより、Lambda関数URLへの直接アクセスを防ぎ、CloudFront経由のアクセスのみを許可できるようになりました。CDKを利用した実装例は以下の記事で紹介されています。

https://dev.classmethod.jp/articles/aws-cdk-cloudfront-oac-lambda-function-url/

今回、これらのアップデートに対応した、Lambda関数URLが検索エンジンにインデックスされたり、Botにアクセスされたりすることを防ぐための仕組みを備えたCloudFormationテンプレートを作成、試す機会がありました。

本記事では、テンプレートに実装した以下の機能について解説します。

  • CloudFront OACによるLambda関数URLの保護
  • CloudFront FunctionsによるBasic認証
  • robots.txtの自動配信(認証不要)
  • noindexヘッダーの付与

アーキテクチャ

主要コンポーネント

  1. Lambda Function + Function URL: バックエンドロジック
  2. CloudFront Distribution: エッジでの配信とセキュリティ制御
  3. Origin Access Control (OAC): Lambda関数URLへの直接アクセスを禁止
  4. CloudFront Functions: Basic認証とrobots.txt配信
  5. Response Headers Policy: noindexヘッダーの付与

実装のポイント

1. Lambda関数URLの保護(OAC)

IAM認証なしで設定したLambda関数URL、便利に利用する事が出来ますが、デフォルトではURLを知った第三者によりアクセス可能です。
OACを使用することで、CloudFront経由のアクセスのみを許可し、Lambda関数URLへの直アクセスの禁止を実現しました。

# Lambda Function URL(AWS_IAM認証)
LambdaFunctionUrl:
  Type: AWS::Lambda::Url
  Properties:
    TargetFunctionArn: !GetAtt HelloWorldFunction.Arn
    AuthType: AWS_IAM  # OACで署名されたリクエストのみ許可

# Origin Access Control
OriginAccessControl:
  Type: AWS::CloudFront::OriginAccessControl
  Properties:
    OriginAccessControlConfig:
      Name: !Sub '${AWS::StackName}-lambda-oac'
      OriginAccessControlOriginType: lambda
      SigningBehavior: always
      SigningProtocol: sigv4

# Lambda権限(CloudFrontからの呼び出しのみ許可)
# 2025年10月より lambda:InvokeFunctionUrl と lambda:InvokeFunction の両方が必要
LambdaInvokeFunctionUrlPermission:
  Type: AWS::Lambda::Permission
  Properties:
    FunctionName: !Ref HelloWorldFunction
    Action: lambda:InvokeFunctionUrl
    Principal: cloudfront.amazonaws.com
    SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'

LambdaInvokeFunctionPermission:
  Type: AWS::Lambda::Permission
  Properties:
    FunctionName: !Ref HelloWorldFunction
    Action: lambda:InvokeFunction
    Principal: cloudfront.amazonaws.com
    SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'

2025年10月より、Lambda関数URLを利用する際の権限要件が変更されました。

https://dev.classmethod.jp/articles/aws-lambda-function-url-change-policy/

2. Basic認証の実装(CloudFront Functions)

CloudFront Functionsを使用し、簡易なBasic認証を実現しました。

BasicAuthFunction:
  Type: AWS::CloudFront::Function
  Properties:
    Name: !Sub '${AWS::StackName}-basic-auth-function'
    AutoPublish: true  # 自動公開を有効化
    FunctionConfig:
      Comment: 'Basic Authentication Function'
      Runtime: cloudfront-js-2.0
    FunctionCode: !Sub |
      function handler(event) {
        var request = event.request;
        var headers = request.headers;

        // 認証情報の検証
        var expectedAuth = "Basic " + btoa("${BasicAuthUser}:${BasicAuthPassword}");

        if (
          typeof headers.authorization === "undefined" ||
          headers.authorization.value !== expectedAuth
        ) {
          return {
            statusCode: 401,
            statusDescription: "Unauthorized",
            headers: { 
              "www-authenticate": { value: "Basic realm=\"Protected Area\"" },
              "content-type": { value: "text/plain" }
            },
            body: "Authentication required"
          };
        }

        return request;
      }
  • ポイント
    • AutoPublish: true で、CloudFront Functions の 自動公開を実施しています。
    • 認証情報は、テンプレートの引数で定義するようにしました。

3. robots.txtの配信

全てのクローラーに対してサイト全体のクロールを禁止する内容の robots.txt を生成する CloudFront Functions を用意しました。

RobotsTxtFunction:
  Type: AWS::CloudFront::Function
  Properties:
    Name: !Sub '${AWS::StackName}-robots-txt-function'
    AutoPublish: true
    FunctionConfig:
      Comment: 'Robots.txt function'
      Runtime: cloudfront-js-2.0
    FunctionCode: |
      function handler(event) {
        return {
          statusCode: 200,
          statusDescription: 'OK',
          headers: {
            'content-type': { value: 'text/plain' },
            'cache-control': { value: 'public, max-age=86400' }
          },
          body: 'User-agent: *\nDisallow: /'
        };
      }

4. CloudFront Behaviorの設定

異なるパスに対して異なる動作を設定します。

CloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      # デフォルトの動作(Basic認証必須)
      DefaultCacheBehavior:
        TargetOriginId: LambdaOrigin
        ViewerProtocolPolicy: redirect-to-https
        CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad  # CachingDisabled
        ResponseHeadersPolicyId: !Ref NoIndexResponseHeadersPolicy
        FunctionAssociations:
          - EventType: viewer-request
            FunctionARN: !GetAtt BasicAuthFunction.FunctionMetadata.FunctionARN

      # robots.txt専用の動作(認証不要)
      CacheBehaviors:
        - PathPattern: robots.txt
          TargetOriginId: LambdaOrigin
          ViewerProtocolPolicy: redirect-to-https
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6  # Managed-CachingOptimized
          FunctionAssociations:
            - EventType: viewer-request
              FunctionARN: !GetAtt RobotsTxtFunction.FunctionMetadata.FunctionARN
  • ポイント
    • robots.txtには最適化されたキャッシュポリシーを適用
    • デフォルトはキャッシュ無効化、Lambda関数URLの開発環境はキャッシュ影響を受けないようにしました。

5. noindexヘッダーの付与

Response Headers Policyを使用して、全てのレスポンスにnoindexヘッダーを追加しました。
Botによる 関数URLのアクセスが発生した場合でも、当該コンテンツのインデックス登録は回避する事を意図した指定としました。

NoIndexResponseHeadersPolicy:
  Type: AWS::CloudFront::ResponseHeadersPolicy
  Properties:
    ResponseHeadersPolicyConfig:
      Name: !Sub '${AWS::StackName}-noindex-policy'
      Comment: 'Add noindex meta tag to responses'
      CustomHeadersConfig:
        Items:
          - Header: X-Robots-Tag
            Value: noindex
            Override: false

デプロイ方法

1. テンプレートのデプロイ

aws cloudformation create-stack \
  --stack-name lambda-cloudfront-auth \
  --template-body file://template.yaml \
  --parameters \
    ParameterKey=BasicAuthUser,ParameterValue=admin \
    ParameterKey=BasicAuthPassword,ParameterValue=your-secure-password \
  --capabilities CAPABILITY_IAM \
  --region us-east-1

2. デプロイ完了の確認

aws cloudformation wait stack-create-complete \
  --stack-name lambda-cloudfront-auth \
  --region us-east-1

3. 出力の取得

aws cloudformation describe-stacks \
  --stack-name lambda-cloudfront-auth \
  --query 'Stacks[0].Outputs'

動作確認

robots.txtへのアクセス(認証不要)

curl https://your-distribution.cloudfront.net/robots.txt

期待される結果:

User-agent: *
Disallow: /

メインコンテンツへのアクセス(認証なし)

curl -I https://your-distribution.cloudfront.net

期待される結果: 401 Unauthorized

メインコンテンツへのアクセス(認証あり)

curl -u admin:your-secure-password https://your-distribution.cloudfront.net

期待される結果: Lambda関数のレスポンス(JSON)

noindexヘッダーの確認

curl -I -u admin:your-secure-password https://your-distribution.cloudfront.net | grep X-Robots-Tag

期待される結果: X-Robots-Tag: noindex

Lambda関数URLへの直接アクセス

curl https://your-lambda-url.lambda-url.us-east-1.on.aws/

期待される結果: 403 Forbidden(OACによる保護)

まとめ

本記事では 固定費が発生しない、低コストで 開発環境のLambda関数URLを保護する仕組みを、OAC、CloudFront Functionsで実現しました。

今回のBasic認証は、機密情報を扱わない開発環境などでの利用を想定した、簡易的な認証方式となります。本番環境では、Cognito、Lambda Authorizerや、APIGatewayを活用したより堅牢な認証方式や、必要に応じAWS WAFの活用もご検討ください。

テンプレート

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Lambda Function URL with robots.txt support and Basic Auth protected by CloudFront OAC'

Parameters:
  BasicAuthUser:
    Type: String
    Default: admin
    Description: Basic Authentication Username
    MinLength: 3
    MaxLength: 20

  BasicAuthPassword:
    Type: String
    NoEcho: true
    Description: Basic Authentication Password
    MinLength: 8
    MaxLength: 50

Resources:
  # Lambda Execution Role
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  # Lambda Function with robots.txt support
  HelloWorldFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-hello-world-function'
      Runtime: python3.11
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import json
          import os

          def lambda_handler(event, context):
              return {
                  'statusCode': 200,
                  'headers': {
                      'Content-Type': 'application/json'
                  },
                  'body': json.dumps({
                      'message': 'Hello World from Lambda!',
                      'requestId': context.aws_request_id,
                      'timestamp': context.get_remaining_time_in_millis()
                  })
              }

  # Lambda Function URL
  LambdaFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      TargetFunctionArn: !GetAtt HelloWorldFunction.Arn
      AuthType: AWS_IAM
      Cors:
        AllowCredentials: false
        AllowMethods: [GET, POST]
        AllowOrigins: ["*"]

  # CloudFront Function for robots.txt
  RobotsTxtFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: !Sub '${AWS::StackName}-robots-txt-function'
      AutoPublish: true
      FunctionConfig:
        Comment: !Sub 'Robots.txt function for ${AWS::StackName}'
        Runtime: cloudfront-js-2.0
      FunctionCode: |
        function handler(event) {
          return {
            statusCode: 200,
            statusDescription: 'OK',
            headers: {
              'content-type': { value: 'text/plain' },
              'cache-control': { value: 'public, max-age=86400' }
            },
            body: 'User-agent: *\nDisallow: /'
          };
        }

  # CloudFront Function for Basic Authentication
  BasicAuthFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: !Sub '${AWS::StackName}-basic-auth-function'
      AutoPublish: true
      FunctionConfig:
        Comment: !Sub 'Basic Authentication Function for ${AWS::StackName}'
        Runtime: cloudfront-js-2.0
      FunctionCode: !Sub |
        function handler(event) {
          var request = event.request;
          var headers = request.headers;

          // Expected credentials: ${BasicAuthUser}:${BasicAuthPassword}
          var expectedAuth = "Basic " + btoa("${BasicAuthUser}:${BasicAuthPassword}");

          if (
            typeof headers.authorization === "undefined" ||
            headers.authorization.value !== expectedAuth
          ) {
            return {
              statusCode: 401,
              statusDescription: "Unauthorized",
              headers: { 
                "www-authenticate": { value: "Basic realm=\"Protected Area\"" },
                "content-type": { value: "text/plain" }
              },
              body: "Authentication required"
            };
          }

          return request;
        }

  # Response Headers Policy for noindex
  NoIndexResponseHeadersPolicy:
    Type: AWS::CloudFront::ResponseHeadersPolicy
    Properties:
      ResponseHeadersPolicyConfig:
        Name: !Sub '${AWS::StackName}-noindex-policy'
        Comment: 'Add noindex meta tag to responses'
        CustomHeadersConfig:
          Items:
            - Header: X-Robots-Tag
              Value: noindex
              Override: false

  # Origin Access Control
  OriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: !Sub 'OAC for ${AWS::StackName} Lambda Function URL'
        Name: !Sub '${AWS::StackName}-lambda-oac'
        OriginAccessControlOriginType: lambda
        SigningBehavior: always
        SigningProtocol: sigv4

  # CloudFront Distribution
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    DependsOn:
      - LambdaFunctionUrl
      - OriginAccessControl
      - NoIndexResponseHeadersPolicy
    Properties:
      DistributionConfig:
        Enabled: true
        Comment: !Sub '${AWS::StackName} - Lambda Function URL with OAC and robots.txt'
        Origins:
          - Id: LambdaOrigin
            DomainName: !Select [2, !Split ['/', !GetAtt LambdaFunctionUrl.FunctionUrl]]
            CustomOriginConfig:
              HTTPPort: 443
              OriginProtocolPolicy: https-only
            OriginAccessControlId: !GetAtt OriginAccessControl.Id
        DefaultCacheBehavior:
          TargetOriginId: LambdaOrigin
          ViewerProtocolPolicy: redirect-to-https
          CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad  # CachingDisabled
          ResponseHeadersPolicyId: !Ref NoIndexResponseHeadersPolicy
          FunctionAssociations:
            - EventType: viewer-request
              FunctionARN: !GetAtt BasicAuthFunction.FunctionMetadata.FunctionARN
        CacheBehaviors:
          - PathPattern: robots.txt
            TargetOriginId: LambdaOrigin
            ViewerProtocolPolicy: redirect-to-https
            CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6  # Managed-CachingOptimized
            FunctionAssociations:
              - EventType: viewer-request
                FunctionARN: !GetAtt RobotsTxtFunction.FunctionMetadata.FunctionARN

  # Lambda Permission for CloudFront Distribution (Function URL)
  LambdaInvokeFunctionUrlPermission:
    Type: AWS::Lambda::Permission
    DependsOn: CloudFrontDistribution
    Properties:
      FunctionName: !Ref HelloWorldFunction
      Action: lambda:InvokeFunctionUrl
      Principal: cloudfront.amazonaws.com
      SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'

  # Lambda Permission for CloudFront Distribution (Function Invoke)
  LambdaInvokeFunctionPermission:
    Type: AWS::Lambda::Permission
    DependsOn: CloudFrontDistribution
    Properties:
      FunctionName: !Ref HelloWorldFunction
      Action: lambda:InvokeFunction
      Principal: cloudfront.amazonaws.com
      SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'

Outputs:
  LambdaFunctionUrl:
    Description: Lambda Function URL (Direct access - use CloudFront URL instead)
    Value: !GetAtt LambdaFunctionUrl.FunctionUrl

  CloudFrontURL:
    Description: CloudFront Distribution URL (Use this URL for access)
    Value: !Sub 'https://${CloudFrontDistribution.DomainName}'

  RobotsTxtURL:
    Description: Robots.txt URL (No Basic Auth required)
    Value: !Sub 'https://${CloudFrontDistribution.DomainName}/robots.txt'

  CloudFrontDistributionId:
    Description: CloudFront Distribution ID
    Value: !Ref CloudFrontDistribution

  BasicAuthUsername:
    Description: Basic Authentication Username
    Value: !Ref BasicAuthUser

参考資料

この記事をシェアする

FacebookHatena blogX

関連記事