Uploading Files to S3 with CloudFront Pre-Signed URLs

Uploading Files to S3 with CloudFront Pre-Signed URLs

Takahiro Iwasa
Takahiro Iwasa
3 min read
CloudFront S3

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.

Architecture Diagram

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! 🚀

Takahiro Iwasa

Takahiro Iwasa

Software Developer at KAKEHASHI Inc.
Involved in the requirements definition, design, and development of cloud-native applications using AWS. Now, building a new prescription data collection platform at KAKEHASHI Inc. Japan AWS Top Engineers 2020-2023.