Lambda関数URLのBot対策をCloudFormationで実装してみた (OAC + Basic認証 + noindex)
2024年、Lambda関数URLがCloudFront Origin Access Control (OAC)をサポートしました。
これにより、Lambda関数URLへの直接アクセスを防ぎ、CloudFront経由のアクセスのみを許可できるようになりました。
CDKを利用した実装例は以下の記事で紹介されています。
今回、これらのアップデートに対応した、Lambda関数URLが検索エンジンにインデックスされたり、Botにアクセスされたりすることを防ぐための仕組みを備えたCloudFormationテンプレートを作成、試す機会がありました。
本記事では、テンプレートに実装した以下の機能について解説します。
- CloudFront OACによるLambda関数URLの保護
- CloudFront FunctionsによるBasic認証
- robots.txtの自動配信(認証不要)
- noindexヘッダーの付与
アーキテクチャ
主要コンポーネント
- Lambda Function + Function URL: バックエンドロジック
- CloudFront Distribution: エッジでの配信とセキュリティ制御
- Origin Access Control (OAC): Lambda関数URLへの直接アクセスを禁止
- CloudFront Functions: Basic認証とrobots.txt配信
- 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を利用する際の権限要件が変更されました。
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






