This guide will walk you through uploading images to an AWS S3 bucket using pre-signed URLs and retrieving them securely via CloudFront signed URLs.
Step 1: Create an S3 Bucket
- Log in to the AWS Console.
- Search for S3 and navigate to the S3 Dashboard.
- Click on Create Bucket.
- An S3 bucket name must be globally unique
- Block all public access (important for security).
- Keep all other settings as default and click Create Bucket.
Step 2: Generate Public and Private Keys
Since CloudFront requires a key pair for secure access, generate a public-private key pair using OpenSSL:
openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem
Step 3: Upload the Public Key to CloudFront
- Go to CloudFront in the AWS Console.
- In the sidebar, navigate to Key Management > Public Keys.
- Click Add Public Key and upload the public key generated earlier.
Step 4: Create a Key Group in CloudFront
- In Key Management, go to Key Groups.
- Click Create Key Group and add the public key you just uploaded.
Note: Select the public key from the dropdown that we created in Step 3.
Step 5: Set Up a CloudFront Distribution
- In CloudFront, click Create Distribution.
- Choose your S3 bucket as the origin.
- In Origin Access, select the recommended control setting, then create a new OAC (Origin Access Control).
- Change the Viewer Protocol Policy from 'HTTP and HTTPS' to 'Redirect HTTP to HTTPS'.
- Restrict viewer access: Yes, and select the Key Group created earlier.
- Keep other settings default and click Create Distribution.
Step 6: Update S3 Bucket Policy and CORS
Go to S3 Bucket > Permissions and add the following Bucket Policy:
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::your-account-id:distribution/your-distribution-id"
}
}
}
]
}
Then, configure CORS Policy:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedOrigins": ["http://yourlocalhost", "https://yourdomain.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
Step 7: Create a Node.js Backend to Generate Pre-Signed URLs
Initialize a Node.js project:
npm init -y
npm i express dotenv @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @aws-sdk/cloudfront-signer
import express from "express";
import dotenv from "dotenv";
import cors from "cors";
import { S3Client } from "@aws-sdk/client-s3";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl as uploads3Object } from "@aws-sdk/s3-request-presigner";
import { getSignedUrl as readCDNImage } from "@aws-sdk/cloudfront-signer";
dotenv.config();
const port = 7000;
// Configure the S3 client with credentials and region
export const s3Client = new S3Client({
region: "YOUR_REGION",
credentials: {
accessKeyId: "YOUR_ACCESS_KEY", // Replace with your AWS access key
secretAccessKey: "YOUR_SECRET_KEY", // Replace with your AWS secret key
},
});
const app = express();
app.use(
cors({
origin: "http://your_localhost:", // Allow requests from frontend
})
);
app.use(express.json());
// This private key is used for signing CloudFront URLs
let key = `-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_HERE
-----END PRIVATE KEY-----`;
// API route to generate a pre-signed URL for uploading images to S3
app.get("/get-presigned-url", async (req, res) => {
const { fileName, fileType } = req.query; // Extract filename and file type from request
const params = {
Bucket: "YOUR_S3_BUCKET_NAME", // Your S3 bucket name
Key: `uploads/${fileName}`, // File path inside the bucket
ContentType: fileType, // The type of file being uploaded
};
try {
const command = new PutObjectCommand(params); // Create an S3 upload command
// Generate a pre-signed URL valid for 60 seconds to upload image to S3
const uploadUrl = await uploads3Object(s3Client, command, {
expiresIn: 60,
});
res.status(200).json({
uploadUrl, // Pre-signed S3 URL for uploading the image
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Error generating pre-signed URL" });
}
});
app.get("/get-cdn-url", async (req, res) => {
const { fileName } = req.query;
// Generate a signed URL for accessing the image via CloudFront, valid for 1 hour
try {
const readImage = readCDNImage({
url: `https://${process.env.CLOUDFRONT_DOMAIN}/uploads/${fileName}`,
keyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID, // CloudFront Key Pair ID from .env
privateKey: key, // Private key for signing the URL
dateLessThan: new Date(Date.now() + 60 * 60 * 1000), // URL expires in 1 hour
});
res.status(200).json({ readImage });
} catch (error) {
res
.status(500)
.json({ error: "Error generating signed URL" });
}
});
// Start the Express server
app.listen(port, () => {
console.log(`Your application running on port ${port}`);
});
Step 8: Create a React Frontend to Upload Images
Initialize a React project:
npm create vite@latest
cd project-name
npm install
npm install axios
import React, { useState } from "react";
import axios from "axios";
const BASE_URL = "http://localhost:7000";
const app = () => {
const [file, setFile] = useState("");
const [imageUrl, setImageUrl] = useState("");
console.log(file);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async () => {
const { data } = await axios.get(BASE_URL + "/get-presigned-url", {
params: { fileName: file.name, fileType: file.type },
});
await axios.put(data.uploadUrl, file, {
headers: { "Content-Type": file.type },
});
};
const getImageFromCdn = async () => {
const { data } = await axios.get(BASE_URL + "/get-cdn-url", {
params: { fileName: file.name },
});
console.log(data.readImage);
setImageUrl(data.readImage);
};
return (
<>
<div>
<input type="file" onChange={handleFileChange} />
<div>
<button onClick={handleUpload}>Upload Image to S3</button>
</div>
</div>
<br />
<div>
<input type="text" value={file.name} />
<button onClick={getImageFromCdn}>Get Image from CDN</button>
<div>
{imageUrl && <img src={imageUrl} alt="Uploaded" width="200" />}
</div>
</div>
</>
);
};
export default app;
Conclusion
You have successfully set up a secure system to upload images to S3 using pre-signed URLs and retrieve them via CloudFront signed URLs.
Why Use This Approach?
β
Security: No direct public access to S3.
β
Performance: CloudFront caches images, reducing load times.
β
Cost-Effective: CloudFront reduces the number of direct S3 requests.
Now, your app can securely upload and serve images efficiently!
Top comments (4)
great article @burhanuddin_rampura
You can add syntax highlight too
docs.github.com/en/get-started/wri...
Like this.
Overall great
Thanks a lot! @scorcism π That's a great suggestionβIβll look into adding syntax highlighting to improve readability. Appreciate the feedback! π
Nice one.. :)
Glad you liked it! π Appreciate your support! π