CoderSathi
  • Tutorial
    • Java Tutorial
    • Swing Tutorial
    • JDBC Tutorial
    • Java String Tutorial
    • Servlet and JSP Tutorial
  • Mongo DB
  • AWS
  • DevOps
  • Linux
  • Git
Home > AWS > How to Use AWS CloudFront Signed URLs in Spring Boot?

How to Use AWS CloudFront Signed URLs in Spring Boot?

Learn the concepts, implementation details, and practical steps with a clean developer-focused walkthrough.

Yuba Raj Kalathoki
By Yuba Raj Kalathoki
Published: June 26, 2026 ยท 9 min read ยท 0 Comments
Share: in X
cloudfront signed url feature image

If you’re serving private files from Amazon S3, you’ve probably used S3 presigned URLs. They work but they expose your S3 bucket endpoint directly, bypass CloudFront’s CDN caching, and can’t be easily invalidated globally. CloudFront signed URLs solve all three problems.

In this guide, you’ll replace S3 presigned URLs with CloudFront signed URLs in a Spring Boot application. You’ll set up a CloudFront distribution from scratch, configure a trusted key group, and write the Java code to generate signed URLs all step by step.

Table of Contents

  • Prerequisites:
  • Why Use CloudFront Signed URLs Instead of S3 Presigned URLs?
  • Step 1: Block All Public Access on Your S3 Bucket
  • Step 2: Create a CloudFront Key Pair for Signing
    • Generate the RSA Key Pair
    • Upload the Public Key to CloudFront
    • Create a Key Group
  • Step 3: Create a CloudFront Distribution
    • Open CloudFront and Create Distribution
    • Configure Distribution Settings
    • Update the S3 Bucket Policy
  • Step 4: Store the Private Key Securely
    • Option A: AWS Secrets Manager (Recommended)
    • Option B: Environment Variable (Dev/Test Only)
  • Step 5: Add Dependencies to Spring Boot
  • Step 6: Configure Application Properties
  • Step 7: Write the Spring Boot Service
    • Service Implementation Class
  • Complete Working Example
  • Common Errors & Fixes
    • Error 1: 403 Forbidden from CloudFront
    • Error 2: InvalidSignature or MissingKey
    • Error 3: java.security.spec.InvalidKeySpecException
    • Error 4: URL Works Once Then Expires Too Fast
  • Best Practices
  • Conclusion

Prerequisites:

  • AWS account with an existing S3 bucket,
  • Java 17+,
  • Spring Boot 3.x, and
  • Basic familiarity with IAM and S3.

Why Use CloudFront Signed URLs Instead of S3 Presigned URLs?

Before jumping into code, understand what you’re gaining.

FeatureS3 Presigned URLCloudFront Signed URL
CDN cachingโŒ Noโœ… Yes
Hides S3 endpointโŒ Noโœ… Yes
Global invalidationโŒ Noโœ… Yes
Custom domainโŒ Hardโœ… Easy
IP restrictionโŒ Noโœ… Via policy
Cost at scaleHigher S3 egressLower (CDN cache hits)

With CloudFront signed URLs, your S3 bucket stays completely private no public access, no bucket policy exceptions. CloudFront acts as the only gateway, and it validates the signed URL before forwarding the request to S3.

Step 1: Block All Public Access on Your S3 Bucket

Your S3 bucket must be fully private. CloudFront will access it through an Origin Access Control (OAC) not through public URLs.

Go to your S3 bucket โ†’ Permissions tab โ†’ Block public access โ†’ Enable all four options โ†’ Save.

Block all public access setting for s3 bucket

If it is not enabled already click Edit button and check Block all public Access

Step 2: Create a CloudFront Key Pair for Signing

CloudFront signed URLs require a RSA key pair. You generate the key pair yourself, upload the public key to CloudFront, and keep the private key in your application.

Generate the RSA Key Pair

Run these commands in your terminal:

# Generate a 2048-bit RSA private key
openssl genrsa -out cloudfromt-private-key.pem

# Extract the public key
openssl rsa -pubout -in cloudfromt-private-key.pem -out cloudfront-public-key.pem

Keep cloudfront-private-key.pem secret. Never commit it to version control.

Upload the Public Key to CloudFront

  1. Go to CloudFront โ†’ Key management โ†’ Public keys
  2. Click Create public key
  3. Name: my-app-signing-key
  4. Key value: Paste the contents of cloudfront-public-key.pem
  5. Click Create โ€” note the Public key ID (e.g., K2JCJMDEHXQW5F)

Create a Key Group

  1. Go to CloudFront โ†’ Key management โ†’ Key groups
  2. Click Create key group
  3. Name: my-app-key-group
  4. Public keys: Select the key you just created
  5. Click Create key group โ€” note the Key group ID

Step 3: Create a CloudFront Distribution

Open CloudFront and Create Distribution

  1. Go to AWS Console โ†’ CloudFront โ†’ Create distribution -> You can select Free plan and click Next.

image 28

  1. Give Distribution name and if you want to use your custom domain you can provide that name as well anc click Next.
image 29

  1. Under Origin domain, select your S3 bucket from the dropdown (e.g., my-private-bucket.s3.amazonaws.com)
  2. Under Origin access, select Origin access control settings (recommended)
  3. Click Next -> If you want you can enable Security but let’s leave it for now.

image 30

Configure Distribution Settings

  1. Click on the Behaviors tab of your distribution
  2. Select Behaviors and click Edit

image 31

Now verify following:

  • Viewer protocol policy: Redirect HTTP to HTTPS
  • Allowed HTTP methods: GET, HEAD (for read-only file access)
  • Cache policy: CachingOptimized (or create a custom one)
  • Restrict viewer access: Yes โ€” this enforces signed URLs
  • Trusted authorization type: Trusted key groups (recommended over trusted signers)
  • Select the Key group created in the Step 2 and Save.

CloudFront distribution behavior with key group restriction enabled

CloudFront will take 5โ€“10 minutes to deploy. Note your Distribution domain name (e.g., randomvalue.cloudfront.net).

Update the S3 Bucket Policy

After creating the distribution, CloudFront shows a banner: “Copy policy”. Click it, then:

  1. Go to your S3 bucket โ†’ Permissions โ†’ Bucket policy
  2. Paste the copied policy and save

The policy grants CloudFront’s OAC permission to s3:GetObject on your bucket. It looks like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipal",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-private-bucket/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
        }
      }
    }
  ]
}

Replace my-private-bucket and the distribution ARN with your actual values.

Step 4: Store the Private Key Securely

Never hardcode your private key. Store it in AWS Secrets Manager or as an environment variable.

Option A: AWS Secrets Manager (Recommended)

How to Connect Spring Boot to AWS Secrets Manager Dynamically (Without Git Exposure)

  1. From the services list select Serects Manager
  2. Click Store a new secret button
  3. Select Other type of secret
  4. If you are storing multiple key/values under a single secret select Plaintext and create json object and store. This will be the easiest way to store. I prefer this.
  5. Click Next

image 34

  1. Configure secret and give secret name it will required later to load from our code.

image 35

  1. Click Next and leave everything default and click Store.

Option B: Environment Variable (Dev/Test Only)

export CLOUDFRONT_PRIVATE_KEY="$(cat cloudfront-private-key.pem)"

Step 5: Add Dependencies to Spring Boot

Add the AWS SDK CloudFront and also spring cloud starter dependency for AWS in your pom.xml.

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.awspring.cloud</groupId>
                <artifactId>spring-cloud-aws-dependencies</artifactId>
                <version>3.3.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>



<dependencies>
        
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-cloudfront</artifactId>
            <version>1.12.787</version>
        </dependency>

        <dependency>
            <groupId>io.awspring.cloud</groupId>
            <artifactId>spring-cloud-aws-starter-secrets-manager</artifactId>
        </dependency>

    </dependencies>

Step 6: Configure Application Properties

Since we are using spring cloud aws starter it will be easier for us to read secrets. So add your CloudFront settings to application.yml:

spring:
  config:
    # Instructs Spring to pull the specific JSON payload block at startup.
    import: aws-secretsmanager:cloudfront/private-key # Secrets Manager secret name
  cloud:
    aws:
      region:
        # Explicitly designates which cloud data region contains the secret.
        static: us-west-2
      secretsmanager:
        enabled: true

cloudfront:
  distribution-domain: randomstring.cloudfront.net
  key-pair-id: K2JCJMDEHXQW5F          # Your Public Key ID from Step 2
  url-expiry-minutes: 60

Step 7: Write the Spring Boot Service

Service Implementation Class

import com.amazonaws.auth.PEM;
import com.amazonaws.services.cloudfront.CloudFrontUrlSigner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Date;

/**
 * Signed URL provider backed by CloudFront signed URLs.
 */
@Service
public class CloudFrontSignedUrlService {

	private final String privateKey;
	private final Long expiryMinutes;
	private final String publicKeyId;
	private final String domain;

	public CloudFrontSignedUrlService(
			@Value("${cloudfront-private-key}") String privateKey,
			@Value("${cloudfront.url-expiry-minutes}") Long expiryMinutes,
			@Value("${cloudfront.key-pair-id}") String publicKeyId,
			@Value("${cloudfront.distribution-domain}") String domain) {
		this.privateKey = privateKey;
		this.expiryMinutes=expiryMinutes;
		this.publicKeyId=publicKeyId;
		this.domain=domain;
	}

	/**
	 * You can call this method to get signed download url for the file.
	 */
	public String getSignedDownloadUrl(String s3FileKey) {
		validateConfiguration();

		Date expirationDateTime = new Date(
				System.currentTimeMillis() + (1000L * 60L * expiryMinutes));
		String resourceUrl = buildResourceUrl(s3FileKey);

		try {
			PrivateKey privateKey = loadPrivateKey();
			return CloudFrontUrlSigner.getSignedURLWithCannedPolicy(resourceUrl,
					publicKeyId, privateKey, expirationDateTime);
		}
		catch (InvalidKeySpecException | IOException exception) {
			throw new RuntimeException("Unable to generate CloudFront signed URL.", exception);
		}
	}

	private void validateConfiguration() {
		if (!StringUtils.hasText(domain)) {
			throw new IllegalStateException("CloudFront domain is required.");
		}
		if (!StringUtils.hasText(publicKeyId)) {
			throw new IllegalStateException("CloudFront public key id is required.");
		}
		if (!StringUtils.hasText(privateKey)) {
			throw new IllegalStateException("CloudFront private key is required.");
		}
	}


	private String buildResourceUrl(String key) {
		UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(
				trimTrailingSlash(domain));
		for (String pathSegment : key.split("/")) {
			if (StringUtils.hasText(pathSegment)) {
				builder.pathSegment(pathSegment);
			}
		}
		return builder.build().encode(StandardCharsets.UTF_8).toUriString();
	}

	private PrivateKey loadPrivateKey() throws InvalidKeySpecException, IOException {
		byte[] privateKeyBytes = privateKey.replace("\\n", "\n")
				.getBytes(StandardCharsets.UTF_8);
		try (ByteArrayInputStream inputStream = new ByteArrayInputStream(privateKeyBytes)) {
			return PEM.readPrivateKey(inputStream);
		}
	}

	private String trimTrailingSlash(String value) {
		if (!StringUtils.hasText(value)) {
			return value;
		}
		return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
	}
}

For local development, the SDK picks up credentials from ~/.aws/credentials or environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). For production on EC2/ECS/Lambda, use IAM roles โ€” no hardcoded credentials needed.

Complete Working Example

Here’s the full flow in action. Given an S3 object at key uploads/documents/Q3-report.pdf:

Request:

GET /api/files/signed-url?key=uploads/documents/Q3-report.pdf

Response:

{
  "url": "https://d1abc2defg3hij.cloudfront.net/uploads/documents/Q3-report.pdf?Expires=1719316800&Signature=ABCD...XYZ&Key-Pair-Id=K2JCJMDEHXQW5F"
}

The user clicks this URL. CloudFront validates the signature, checks the expiry, then fetches from S3 via the OAC โ€” all transparently. The S3 bucket URL is never exposed.

cloudfront signed url sequence

Common Errors & Fixes

Error 1: 403 Forbidden from CloudFront

Cause: The S3 bucket policy doesn’t grant the OAC permission, or the behavior doesn’t have Restrict viewer access enabled.

Fix: Re-copy the bucket policy from CloudFront’s banner and paste it into your S3 bucket policy. Double-check the distribution ARN in the Condition block.

Error 2: InvalidSignature or MissingKey

Cause: The Key-Pair-Id in the signed URL doesn’t match any key in the trusted key group attached to the distribution behavior.

Fix: Verify the keyPairId in your application.yml matches the Public Key ID (not the key group ID) in CloudFront โ†’ Key management โ†’ Public keys.

Error 3: java.security.spec.InvalidKeySpecException

Cause: The private key is in PKCS#1 format (starts with -----BEGIN RSA PRIVATE KEY-----) but the Java PKCS8EncodedKeySpec expects PKCS#8.

Fix: Convert the key before storing it:

openssl pkcs8 -topk8 -nocrypt -in cloudfront-private-key.pem -out cloudfront-private-key-pkcs8.pem

Store and use the PKCS#8 version.

Error 4: URL Works Once Then Expires Too Fast

Cause: Server clock skew โ€” your EC2 or container clock is drifting, making the expiry appear in the past.

Fix: Sync your system clock with NTP. On Amazon Linux:

sudo yum install -y chrony
sudo systemctl enable chronyd --now

Best Practices

  • Cache the private key in memory. Loading from Secrets Manager on every request adds latency. Load once at startup or use a @PostConstruct cache.
  • Keep expiry times short 15 to 60 minutes is usually sufficient. Shorter windows reduce the window of abuse if a URL leaks.
  • Use custom policies for sensitive files when you need IP restriction or access windows that don’t start immediately.
  • Never block CloudFront’s IP ranges in your S3 bucket policy the OAC uses AWS service principals, not IP addresses.
  • Use CloudFront invalidations sparingly they cost money after the first 1,000 per month. Design your key paths to avoid needing mass invalidations.
  • Log CloudFront access enable CloudFront access logs to an S3 bucket for auditing who accessed what and when.

Conclusion

You’ve replaced S3 presigned URLs with CloudFront signed URLs in Spring Boot. Your S3 bucket is now fully private, your files are served through a CDN with global caching, and your signing key is safely stored in Secrets Manager.

For further reading, see the official AWS CloudFront signed URLs documentation and the AWS SDK for Java v2 CloudFront module.

Related Posts:

  • Most Frequently Asked Spring Boot Interview…
  • Setting Up an EC2 Instance with Amazon CloudFront…
  • Host Static Website on S3 AWS: Step-by-Step Guide
  • Pseudo Parameters in AWS CloudFormation
  • What is Caching?
  • How To Enable Cross Account Access In AWS S3?
Tags:awscloudfrontjavaspring-boot
Was this article helpful?
โ† Previous ArticleHow to Fix SSH Agent Forwarding on macOS: The Ultimate Guide for Developers

Recent Posts

  • How to Use AWS CloudFront Signed URLs in Spring Boot?
  • How to Fix SSH Agent Forwarding on macOS: The Ultimate Guide for Developers
  • How to Read AWS Secrets Manager in Spring Boot (Step-by-Step)
  • How to Fix “Public Key Retrieval is not allowed” MySQL JDBC Error
  • Complete Guide to JaCoCo: How to Measure Java Code Coverage Accurately
CoderSathi

Your go-to resource for Java, Spring Boot, Microservices, AWS, and modern development tutorials.

Linkedin

Quick Links

  • About
  • Contact

Popular Topics

  • Java
  • Spring Boot
  • AWS
  • DevOps
  • MongoDB
  • Linux
  • Git
  • How to
ยฉ 2026 CoderSathi. All rights reserved. Privacy Policy ยท Sitemap