Uploading Files to S3 with CloudFront Pre-Signed URLs
Introduction
CloudFront supports the feature of generating signed URLs. While S3 also offers similar functionality, CloudFront provides the added benefit of enabling uploads through your custom domain, making it especially useful for domain-restricted environments.
Official documentation: Amazon CloudFront Private Content
The architecture uses CloudFront as a front-facing service for secure file uploads, enabling custom domain usage and tighter control in restricted environments.
Specifying Trusted Signers
To begin, you must create a trusted key group for use as a trusted signer.
Official documentation: Trusted Signers for CloudFront
Creating Key Pairs
Key pairs must adhere to the following requirements:
- Type: SSH-2 RSA key pair
- Format: Base64-encoded PEM
- Key Size: 2048-bit
Use the following commands to create a key pair:
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 to provision the required AWS resources.
Key Points in the Template
- Pass the public key to the
PublicKey
parameter (line 5) and use it (line 38). - Ensure the S3 bucket policy allows the
s3:PutObject
action (line 27). - Use the AllViewerExceptHostHeader origin request policy (line 85).
Template Example:
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:
PUBLIC_KEY=$(cat public_key.pem)
aws cloudformation deploy \
--template-file template.yaml \
--stack-name cloudfront-presigned-urls-example \
--parameter-overrides PublicKey=$PUBLIC_KEY
Check the deployed resources:
aws cloudformation describe-stacks \
--stack-name cloudfront-presigned-urls-example \
| jq ".Stacks[0].Outputs"
Testing
Generate a Pre-Signed URL
Set the following variables:
CLOUDFRONT_DOMAIN=<CloudFront domain>
KEYPAIR_ID=<Key pair ID>
UTC_OFFSET=+9
Generate a URL:
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=...
Upload a File
echo 'Hello World' > example.txt
curl -X PUT -d "$(cat example.txt)" $PRESIGNED_URL
Confirm the file is uploaded:
aws s3 cp s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt ./
cat ./upload-test.txt
Cleaning Up
To avoid incurring costs, delete the resources:
aws s3 rm s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt
aws cloudformation delete-stack --stack-name cloudfront-presigned-urls-example
Conclusion
Using CloudFront pre-signed URLs is an effective way to securely upload files to S3 buckets, particularly in domain-restricted environments. With careful configuration, you can streamline workflows and maintain strong security practices.
Happy Coding! 🚀