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 ResponseHeadersPolicyId: !Ref NoIndexResponseHeadersPolicy FunctionAssociations: - EventType: viewer-request FunctionARN: !GetAtt BasicAuthFunction.FunctionMetadata.FunctionARN # Legacy cache settings for query string support ForwardedValues: QueryString: true Cookies: Forward: none MinTTL: 0 DefaultTTL: 0 MaxTTL: 0 # 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 ResponseHeadersPolicyId: !Ref NoIndexResponseHeadersPolicy FunctionAssociations: - EventType: viewer-request FunctionARN: !GetAtt BasicAuthFunction.FunctionMetadata.FunctionARN # Legacy cache settings for query string support ForwardedValues: QueryString: true Cookies: Forward: none MinTTL: 0 DefaultTTL: 0 MaxTTL: 0 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

関連記事