Generating CloudFront Pre-signed URLs for Uploading to S3
CloudFront supports a feature to generate signed URLs. You can use it to upload files to your S3 bucket.
While S3 also offers the similar functionality, the important difference is, not covered in this post, ability to upload files with our custom domain assigned to the CloudFront distribution. This is particularly beneficial for domain-restricted environments.
Overview
Specifying Trusted Signers
First of all, you must create a trusted key group as a trusted signer.
Creating Key Pairs
Key pairs must meet the following requirements.
- It must be an SSH-2 RSA key pair.
- It must be in base64-encoded PEM format.
- It must be a 2048-bit key pair.
You can create your new key pair with the following command.
openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem
Creating AWS Resources
Create a CloudFormation template with the following content.
The key points are the following.
- The public key should be passed to the
PublicKey
parameter on line 5 and used on line 38. - The S3 bucket policy must allow the
s3:PutObject
action on line 27. - The CloudFront origin request policy must be AllViewerExceptHostHeader on line 85 because the S3 bucket needs the query strings.
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
Deploy the CloudFormation stack with the following command.
Note that the public key itself is passed to the PublicKey
parameter.
PUBLIC_KEY=$(cat public_key.pem)
aws cloudformation deploy \
--template-file template.yaml \
--stack-name example-of-cloudfront-presigned-urls-to-upload-files-to-s3-bucket \
--parameter-overrides PublicKey=$PUBLIC_KEY
To check the values for testing, run the following command.
$ aws cloudformation describe-stacks \
--stack-name example-of-cloudfront-presigned-urls-to-upload-files-to-s3-bucket \
| jq ".Stacks[0].Outputs"
[
{
"OutputKey": "CloudFrontPublicKeyId",
"OutputValue": "<id>"
},
{
"OutputKey": "CloudFrontDistributionDomainName",
"OutputValue": "<id>.cloudfront.net"
},
{
"OutputKey": "S3BucketName",
"OutputValue": "uploaded-files-<AWS::AccountId>-<AWS::Region>"
}
]
Testing
To generate a pre-signed URL to upload a file to the S3 bucket, set the actual values to the CLOUDFRONT_DOMAIN
, KEYPAIR_ID
and UTC_OFFSET
variables.
CLOUDFRONT_DOMAIN=
KEYPAIR_ID=
UTC_OFFSET=+9
Then, run the following command. You will see the pre-signed URL generated.
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=...
To upload a file using the pre-signed URL, run the following command.
PUT
. echo 'Hello World' > example.txt
curl -X PUT -d "$(cat example.txt)" $PRESIGNED_URL
Finally, confirm the uploaded file on the S3 bucket.
aws s3 cp s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt ./
cat ./upload-test.txt
# Hello World
rm ./upload-test.txt
Cleaning Up
Clean up the provisioned AWS resources with the following command.
aws s3 rm s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt
aws cloudformation delete-stack --stack-name example-of-cloudfront-presigned-urls-to-upload-files-to-s3-bucket
Conclusion
When you want to upload files to S3 buckets under domain-restricted environments, the CloudFront pre-signed url feature is useful.
I hope you will find this post useful.