はじめに
S3に置かれた画像をCloudFrontで配信するにあたって、画像を読み込めるサイトに制限をかけたいと考えました。
その特定サイトは検索にはヒットせず、しかしIP制限などはかけていないものとします。
かといってAWS lambdaなどを間に挟んでゴニョゴニョするような面倒はかけたくないため、ある程度のガバさは許容するが、特に何の工夫もなしに画像を引っ張ってこれるようにはしたくない。
そんな仕様を叶える手段の一つとしてRefererというヘッダーを見るというのがあります。
Refererヘッダーにはリクエストされているページへのリンク先を持った直前のウェブページのアドレスが含まれています。
つまり画像へのアクセス元ページのアドレスがわかるわけです。
ヘッダーなんてのは簡単に書き換えられてしまうため、ザルではありますが、それでも何の工夫もなしに画像を引っ張ってこれなくはなります。
ヘッダーを見てリクエストを弾いたりするのはWAFでできます。
よって本記事ではRefererヘッダーに特定サイトのURLが含まれている場合のみ、CloudFrontを通してS3にある画像を取ってこれるようにする仕組みをCloudFormationで作成します。
CFn
WAFとS3+CloudFrontそれぞれ分けて作成します。
理由は下記記事にある通り、CloudFront用のWebACLを作成するにはus-east-1 (バージニア北部) に作成する必要があるためです。
【小ネタ】AWS WAF v2 の WebACL (CloudFront用)を東京リージョンから CloudFormation で作成しようとしたら怒られた
WebACL
とりあえず https://example.com
からのアクセスであれば通すようにしています。
複数通したければAWS::WAFv2::WebACL OrStatementで複数指定します。
AWS::WAFv2::WebACL DefaultActionでデフォルトのアクションを設定できるのですが、ここをデフォルトAllowにしてしまうと特定箇所以外からも許可されます。
私は、AWS::WAFv2::WebACL RuleActionを特定サイトからのアクセスは通すからとAllowにして、デフォルトアクションもAllowにしていたのを見逃していたため、どこからでもAllowになってしまい時間を溶かしました。
AWSTemplateFormatVersion: "2010-09-09"
Resources:
RefererWebACL:
Type: AWS::WAFv2::WebACL
Properties:
Scope: CLOUDFRONT
Name: WafCloudFrontForImageDelivery
DefaultAction:
Block: {}
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: AllowSpecificReferrersFoImageDelivery
Rules:
- Name: AllowSpecificReferrersRule
Priority: 0
Action:
Allow: {}
Statement:
ByteMatchStatement:
SearchString: "https://example.com"
FieldToMatch:
SingleHeader:
Name: "Referer"
TextTransformations:
- Priority: 0
Type: NONE
PositionalConstraint: "CONTAINS"
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: AllowSpecificReferrersRuleMetricForImageDelivery
作成します。
regionに注意。
aws cloudformation create-stack --stack-name web-acl --template-body file://web-acl.yaml --region us-east-1
S3, CloudFront
これでS3とCloudFrontがいい感じに作成され、CloudFrontには上で作成したWAFが付いてくれます。
WAFは別で作成しているので、WebAclのARNをパラメータで与えてやる必要があるので注意が必要です。
AWS::CloudFront::Distribution DefaultCacheBehavior CachePolicyIdにはマネージドキャッシュポリシーのCachingOptimizedのポリシーIDを指定しています。
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
WebAclARN:
Description: WAF v2 ARN
Type: String
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: "いい感じの一意な名前"
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: "Access identity for S3 bucket"
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowCloudFrontServicePrincipalReadOnly
Effect: Allow
Action: s3:GetObject
Resource: !Join [ "/", [ !GetAtt S3Bucket.Arn, "*" ] ]
Principal:
AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Origins:
- Id: S3Origin
DomainName: !GetAtt S3Bucket.DomainName
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
HttpVersion: http2and3
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: allow-all
AllowedMethods:
- GET
- HEAD
Compress: true
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
WebACLId: !Ref WebAclARN
パラメータをいちいちコマンドで突っ込むのが面倒なのでparam.jsonを作ります。
[
{
"ParameterKey": "WebAclARN",
"ParameterValue": "%WebAclのARN%"
}
]
param.jsonを注入してstackを作成します。
aws cloudformation create-stack --stack-name s3-cloudfront --template-body file://s3-cloudfront.yaml --parameters=file://./params/s3-cloudfront.param.json
確認
できたと思うので確認します。
$ curl -I -L https://xxx.cloudfront.net/path/to/image
HTTP/2 403
server: CloudFront
content-type: text/html
x-cache: Error from cloudfront
$ curl -I -L -H 'Referer:https://qiita.com' https://xxx.cloudfront.net/path/to/image
HTTP/2 403
server: CloudFront
content-type: text/html
x-cache: Error from cloudfront
ブロックはできてそう。
正常系を試します。
curl -I -L -H 'Referer:https://example.com' https://xxx.cloudfront.net/path/to/image
HTTP/2 307
content-type: application/xml
location: https://s3バケット名.s3-ap-northeast-1.amazonaws.com/path/to/image
x-amz-bucket-region: ap-northeast-1
server: AmazonS3
x-cache: Miss from cloudfront
HTTP/1.1 403 Forbidden
Server: AmazonS3
!?!?
リダイレクトされてる!?
わからんので「CloudFront」と「307」をキーワードにして検索します。
ありました。
S3+CloudFrontでS3のURLにリダイレクトされてしまう場合の対処法
S3をバージニア北部(us-east-1)以外で作成した場合、24時間以内のアクセスだとリダイレクトが発生するようですね。
待ちます。
待ちました。
もう一度叩きます。
$ curl -I -L -H 'Referer:https://example.com' https://xxx.cloudfront.net/path/to/image
HTTP/2 200
content-type: image/png
x-amz-server-side-encryption: AES256
server: AmazonS3
x-cache: Miss from cloudfront
取ってこれてる!
https://example.com
で開発者ツールを使ってDOMを追加して、取れているかを確認します。
やったぜ!