Generating CloudFront Pre-signed URLs for Uploading to S3

Generating CloudFront Pre-signed URLs for Uploading to S3

Takahiro Iwasa
Takahiro Iwasa
4 min read
CloudFront S3

CloudFront supports a feature to generate signed URLs. You can use it to upload files to your S3 bucket.

While S3 also offers the similar functionality, the important difference is, not covered in this post, ability to upload files with our custom domain assigned to the CloudFront distribution. This is particularly beneficial for domain-restricted environments.

Overview

Specifying Trusted Signers

First of all, you must create a trusted key group as a trusted signer.

Creating Key Pairs

Key pairs must meet the following requirements.

  • It must be an SSH-2 RSA key pair.
  • It must be in base64-encoded PEM format.
  • It must be a 2048-bit key pair.

You can create your new key pair with the following command.

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 with the following content.

The key points are the following.

  • The public key should be passed to the PublicKey parameter on line 5 and used on line 38.
  • The S3 bucket policy must allow the s3:PutObject action on line 27.
  • The CloudFront origin request policy must be AllViewerExceptHostHeader on line 85 because the S3 bucket needs the query strings.
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. Note that the public key itself is passed to the PublicKey parameter.

PUBLIC_KEY=$(cat public_key.pem)
aws cloudformation deploy \
--template-file template.yaml \
--stack-name example-of-cloudfront-presigned-urls-to-upload-files-to-s3-bucket \
--parameter-overrides PublicKey=$PUBLIC_KEY

To check the values for testing, run the following command.

$ aws cloudformation describe-stacks \
--stack-name example-of-cloudfront-presigned-urls-to-upload-files-to-s3-bucket \
| jq ".Stacks[0].Outputs"

[
  {
    "OutputKey": "CloudFrontPublicKeyId",
    "OutputValue": "<id>"
  },
  {
    "OutputKey": "CloudFrontDistributionDomainName",
    "OutputValue": "<id>.cloudfront.net"
  },
  {
    "OutputKey": "S3BucketName",
    "OutputValue": "uploaded-files-<AWS::AccountId>-<AWS::Region>"
  }
]

Testing

To generate a pre-signed URL to upload a file to the S3 bucket, set the actual values to the CLOUDFRONT_DOMAIN, KEYPAIR_ID and UTC_OFFSET variables.

CLOUDFRONT_DOMAIN=
KEYPAIR_ID=
UTC_OFFSET=+9

Then, run the following command. You will see the pre-signed URL generated.

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=...

To upload a file using the pre-signed URL, run the following command.

echo 'Hello World' > example.txt
curl -X PUT -d "$(cat example.txt)" $PRESIGNED_URL

Finally, confirm the uploaded file on the S3 bucket.

aws s3 cp s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt ./
cat ./upload-test.txt
# Hello World
rm ./upload-test.txt

Cleaning Up

Clean up the provisioned AWS resources with the following command.

aws s3 rm s3://uploaded-files-<AWS::AccountId>-<AWS::Region>/upload-test.txt
aws cloudformation delete-stack --stack-name example-of-cloudfront-presigned-urls-to-upload-files-to-s3-bucket

Conclusion

When you want to upload files to S3 buckets under domain-restricted environments, the CloudFront pre-signed url feature is useful.

I hope you will find this post useful.

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.