CloudFront署名付きURL経由でS3にアップロードする

CloudFront署名付きURL経由でS3にアップロードする

CloudFrontの署名付きURLを使えば、独自ドメイン経由でS3にアップロードできます。S3の直接の署名付きURLが使えない場合に有用です。

Takahiro Iwasa
5 min read

CloudFrontには署名付きURLを生成する機能があります。S3にも同様の機能がありますが、CloudFrontを使うと独自ドメイン経由でもアップロードできるため、ドメイン制限のある環境では特に有用です。

Serve private content with signed URLs and signed cookies

Architecture Diagram

信頼された署名者の指定

信頼された署名者として使用するキーグループを作成します。

Specify signers that can create signed URLs and signed cookies

Important

AWSアカウントを信頼された署名者として使用することも可能ですが、AWSはキーグループの使用を推奨しています。詳細はChoose between trusted key groups (recommended) and AWS accountsを参照してください。

キーペアは次の要件を満たす必要があります。

  • タイプ: SSH-2 RSAキーペア
  • フォーマット: Base64エンコードされたPEM
  • キーサイズ: 2048ビット

次のコマンドでキーペアを作成します。

Terminal window
openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

構築

  • 公開鍵をPublicKeyパラメータ(5行目)に渡し、それを使用します(38行目)。
  • S3バケットポリシーがs3:PutObjectアクションを許可していることを確認します(27行目)。
  • AllViewerExceptHostHeaderオリジンリクエストポリシーを使用します(85行目)。
template.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Example of CloudFront pre-signed URLs to upload files to S3 Bucket
Parameters:
PublicKey:
Type: String
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub uploaded-files-${AWS::AccountId}-${AWS::Region}
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: 2008-10-17
Id: PolicyForCloudFrontPrivateContent
Statement:
- Sid: AllowCloudFrontServicePrincipal
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action:
- s3:PutObject
Resource: !Sub ${S3Bucket.Arn}/*
Condition:
StringEquals:
"AWS:SourceArn": !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
CloudFrontPublicKey:
Type: AWS::CloudFront::PublicKey
Properties:
PublicKeyConfig:
Name: signer1
EncodedKey: !Ref PublicKey
CallerReference: cloudfront-caller-reference-example
CloudFrontKeyGroup:
Type: AWS::CloudFront::KeyGroup
Properties:
KeyGroupConfig:
Name: cloudfront-key-group-1
Items:
- !Ref CloudFrontPublicKey
CloudFrontOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Ref S3Bucket
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
HttpVersion: http2and3
Origins:
- Id: !GetAtt S3Bucket.DomainName
DomainName: !GetAtt S3Bucket.DomainName
OriginAccessControlId: !Ref CloudFrontOriginAccessControl
S3OriginConfig:
OriginAccessIdentity: ''
DefaultCacheBehavior:
AllowedMethods:
- HEAD
- DELETE
- POST
- GET
- OPTIONS
- PUT
- PATCH
Compress: true
# CachingDisabled
# See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policy-caching-disabled
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
# AllViewerExceptHostHeader
# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-except-host-header
OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac
TargetOriginId: !GetAtt S3Bucket.DomainName
TrustedKeyGroups:
- !Ref CloudFrontKeyGroup
ViewerProtocolPolicy: https-only
Outputs:
CloudFrontDistributionDomainName:
Value: !GetAtt CloudFrontDistribution.DomainName
CloudFrontPublicKeyId:
Value: !Ref CloudFrontPublicKey
S3BucketName:
Value: !Ref S3Bucket

次のコマンドでCloudFormationスタックをデプロイします。

Terminal window
PUBLIC_KEY=$(cat public_key.pem)
aws cloudformation deploy \
--template-file template.yaml \
--stack-name cloudfront-presigned-urls-example \
--parameter-overrides PublicKey=$PUBLIC_KEY

デプロイされたリソースを確認します。

Terminal window
aws cloudformation describe-stacks \
--stack-name cloudfront-presigned-urls-example \
| jq ".Stacks[0].Outputs"

テスト

次の変数を設定します。

Terminal window
CLOUDFRONT_DOMAIN=<CloudFront domain>
KEYPAIR_ID=<Key pair ID>
UTC_OFFSET=+9

URLを生成します。

Terminal window
PRESIGNED_URL=$(aws cloudfront sign \
--url https://$CLOUDFRONT_DOMAIN/upload-test.txt \
--key-pair-id $KEYPAIR_ID \
--private-key file://private_key.pem \
--date-less-than $(date -v +5M "+%Y-%m-%dT%H:%M:%S$UTC_OFFSET"))
echo $PRESIGNED_URL
# https://<distribution-id>.cloudfront.net/upload-test.txt?Expires=...&Signature=...Key-Pair-Id=...

ファイルをアップロードします。

Terminal window
echo 'Hello World' > example.txt
curl -X PUT -d "$(cat example.txt)" $PRESIGNED_URL

ファイルがアップロードされたことを確認します。

Terminal window
aws s3 cp s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt ./
cat ./upload-test.txt

クリーンアップ

次のコマンドで、この例でプロビジョニングしたすべてのAWSリソースをクリーンアップします。

ℹ️ Note

CloudFrontディストリビューションの無効化には数分かかる場合があります。

Terminal window
aws s3 rm s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt
aws cloudformation delete-stack --stack-name cloudfront-presigned-urls-example

まとめ

信頼されたキーグループでCloudFrontのURLに署名し、それを使ってファイルをPUTしたところ、S3への直接の署名付きURLではなく、独自ドメイン経由でアップロードがS3に到達することがわかりました。これらのURLの署名に使うキーペアはAWSに一切触れません。CloudFrontが受け取るのはCloudFrontPublicKey経由の公開鍵だけで、秘密鍵はaws cloudfront signを実行する側の環境にとどまります。これが、AWSアカウントを署名者として使うよりも信頼されたキーグループのモデルが推奨される理由です。秘密鍵が漏洩しても、IAMの設定に触れることなくキーグループから外してローテーションできます。オリジンリクエストポリシー(AllViewerExceptHostHeader)とバケットポリシーのs3:PutObject条件は、どちらもディストリビューションのARNと一致していないとアップロードが成功しません。そのため、CloudFrontがURLを返したからといってそれが有効だと決めつけず、上で行ったように署名、PUT、S3からの読み戻しという一連の流れを通しでテストしておく価値があります。

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.