How to Upload Files to S3 Using CloudFront Pre-Signed URLs

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.
Serve private content with signed URLs and signed cookies
Specifying Trusted Signers
To begin, you must create a trusted key group for use as a trusted signer.
Specify signers that can create signed URLs and signed cookies
While you can use your AWS account as a trusted signer, AWS recommends using a key group. Refer to Choose between trusted key groups (recommended) and AWS accounts for details.
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 2048openssl rsa -pubout -in private_key.pem -out public_key.pem
Building
- 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).
AWSTemplateFormatVersion: 2010-09-09Description: 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
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.txtcurl -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
Clean up all the AWS resources provisioned during this example with the following command:
Disabling the CloudFront distribution may take several minutes.
aws s3 rm s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txtaws cloudformation delete-stack --stack-name cloudfront-presigned-urls-example