Uploading to S3 Through CloudFront Pre-Signed URLs
CloudFront signed URLs let you upload to S3 through a custom domain—useful when direct S3 pre-signed URLs are not an option.
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.pemBuilding
- Pass the public key to the
PublicKeyparameter (line 5) and use it (line 38). - Ensure the S3 bucket policy allows the
s3:PutObjectaction (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 S3BucketDeploy 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_KEYCheck 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=+9Generate 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_URLConfirm the file is uploaded:
aws s3 cp s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt ./cat ./upload-test.txtCleaning 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-exampleConclusion
Signing a CloudFront URL with a trusted key group and using it to PUT a file confirmed uploads reaching S3 through a custom domain rather than a direct S3 pre-signed URL. The key pair used to sign these URLs never touches AWS — CloudFront only ever receives the public key via CloudFrontPublicKey, and the private key stays wherever aws cloudfront sign runs, which is what makes the trusted key group model recommended over using an AWS account as signer: a compromised private key can be rotated out of the key group without touching any IAM configuration. Since the origin request policy (AllViewerExceptHostHeader) and the bucket policy’s s3:PutObject condition both have to line up with the distribution’s ARN for uploads to succeed, it’s worth testing the full round trip — sign, PUT, then read back from S3 as done above — rather than assuming the URL is valid just because CloudFront returns one.
Related posts
Avoiding Common Pitfalls with s3:TestEvent in AWS S3 Notifications
When configuring event notifications for S3 buckets, s3:TestEvent messages are automatically sent by S3. If this test message is not handled properly, it may cause unexpected issues.
Sign in with Slack Using Cognito User Pools and OIDC
Federating Cognito user pools with Slack over OIDC and wiring "Sign in with Slack" into a Next.js app with Amplify.
Deploying FastAPI on AWS Lambda with Lambda Web Adapter
This example guides you through the process of developing API backends with FastAPI using Lambda Web Adapter.
API Gateway WebSocket: Implementing a Mock Integration
Building an API Gateway WebSocket API entirely with mock integrations, returning canned responses with no backend Lambda involved.
AWS EventBridge Scheduler: Starting and Stopping EC2 on a Schedule
Starting and stopping EC2 instances on a cron schedule with EventBridge Scheduler calling the EC2 API directly, no Lambda involved.
