Uploading to S3 Through CloudFront 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.
Both CloudFront and S3 support signed URLs. A CloudFront signed URL can send an upload through a CloudFront distribution and its custom domain, which is useful when clients are allowed to access only that domain.
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.
This example uses an RSA key pair with the following properties. CloudFront key groups also supported ECDSA 256 signatures in 2024.
- 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. The date -v syntax shown here is for macOS:
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 private key used to sign these URLs is not uploaded to AWS. CloudFront receives only the public key through CloudFrontPublicKey, while the private key remains in the environment that runs aws cloudfront sign. With a trusted key group, a compromised key can be replaced without changing IAM credentials.
For an upload to succeed, the cache behavior must allow PUT and trust the signing key group, while the origin access control and S3 bucket policy must permit CloudFront to call s3:PutObject. Test the complete flow—sign, PUT, and read the object back from S3—rather than treating successful URL generation as proof that the upload path is configured correctly.
Related posts
Handling s3:TestEvent in Amazon S3 Event Notifications
Amazon S3 sends an s3:TestEvent message when an event notification is configured. Consumers must handle its structure separately from ordinary event messages.
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
Containerizing a FastAPI backend and deploying it to a single Lambda function with Lambda Web Adapter and AWS CDK.
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.
