๐๐ถ๐ฟ๐ฒ๐ฐ๐ ๐๐ฝ๐น๐ผ๐ฎ๐ฑ๐. ๐ญ๐ฒ๐ฟ๐ผ ๐ฏ๐ฎ๐ฐ๐ธ๐ฒ๐ป๐ฑ ๐ฏ๐ผ๐๐๐น๐ฒ๐ป๐ฒ๐ฐ๐ธ๐. ๐๐ป๐๐ฒ๐ฟ๐ฝ๐ฟ๐ถ๐๐ฒ-๐ด๐ฟ๐ฎ๐ฑ๐ฒ ๐๐ฒ๐ฐ๐๐ฟ๐ถ๐๐.
๐ง ๐ ๐ฅ๐ฒ๐ฎ๐น-๐ช๐ผ๐ฟ๐น๐ฑ ๐ฃ๐ฟ๐ผ๐ฏ๐น๐ฒ๐บ (๐ง๐ต๐ฎ๐ ๐ ๐๐ฒ๐ฒ๐ฝ ๐ฆ๐ฒ๐ฒ๐ถ๐ป๐ด)
A few weeks ago, I was reviewing a system where users were uploading files (some >300MB).
The original flow looked โreasonableโ:
- Frontend uploads the file to the backend
- Backend processes the request
- Backend uploads the file to S3
- Backend responds
But in practice, the system was ๐ณ๐ฎ๐น๐น๐ถ๐ป๐ด ๐ฎ๐ฝ๐ฎ๐ฟ๐:
โ Timeouts
โ Lambda memory spikes
โ High AWS bills
โ Angry users
And the root cause was always the same:
๐ง๐ต๐ฒ ๐ฏ๐ฎ๐ฐ๐ธ๐ฒ๐ป๐ฑ ๐๐ต๐ผ๐๐น๐ฑ ๐ก๐๐ฉ๐๐ฅ ๐ต๐ฎ๐ป๐ฑ๐น๐ฒ ๐ณ๐ถ๐น๐ฒ ๐๐ฝ๐น๐ผ๐ฎ๐ฑ๐ ๐ถ๐ป ๐ฎ ๐๐ฒ๐ฟ๐๐ฒ๐ฟ๐น๐ฒ๐๐ ๐ฎ๐ฟ๐ฐ๐ต๐ถ๐๐ฒ๐ฐ๐๐๐ฟ๐ฒ.
๐ก ๐ง๐ต๐ฒ ๐ฃ๐ฎ๐๐๐ฒ๐ฟ๐ป ๐ง๐ต๐ฎ๐ ๐๐ถ๐
๐ฒ๐ ๐๐๐ฒ๐ฟ๐๐๐ต๐ถ๐ป๐ด
The solution is a ๐๐ฒ๐น๐น-๐ธ๐ป๐ผ๐๐ป ๐ฏ๐๐ ๐ผ๐ณ๐๐ฒ๐ป ๐บ๐ถ๐๐๐๐ฒ๐ฑ ๐ฝ๐ฎ๐๐๐ฒ๐ฟ๐ป:
๐ ๐ฆ๐ฏ ๐ฃ๐ฟ๐ฒ๐๐ถ๐ด๐ป๐ฒ๐ฑ ๐จ๐ฅ๐๐
Instead of uploading files ๐ต๐ฉ๐ณ๐ฐ๐ถ๐จ๐ฉ your backend, you let the client upload ๐ฑ๐ถ๐ฟ๐ฒ๐ฐ๐๐น๐ ๐๐ผ ๐ฆ๐ฏ, but in a ๐ฐ๐ผ๐ป๐๐ฟ๐ผ๐น๐น๐ฒ๐ฑ, ๐๐ฒ๐บ๐ฝ๐ผ๐ฟ๐ฎ๐ฟ๐, ๐ฎ๐ป๐ฑ ๐๐ฒ๐ฐ๐๐ฟ๐ฒ ๐๐ฎ๐.
This is the same pattern used by:
โ Fintech platforms
โ Healthcare systems
โ Large SaaS products
๐งฉ ๐๐ผ๐ ๐๐ต๐ฒ ๐๐ฟ๐ฐ๐ต๐ถ๐๐ฒ๐ฐ๐๐๐ฟ๐ฒ ๐ช๐ผ๐ฟ๐ธ๐
๐๐ถ๐ด๐ต-๐น๐ฒ๐๐ฒ๐น ๐ณ๐น๐ผ๐:
1๏ธโฃ Client asks permission to upload a file
2๏ธโฃ API Gateway โ Lambda generates a ๐ฃ๐ฟ๐ฒ๐๐ถ๐ด๐ป๐ฒ๐ฑ ๐จ๐ฅ๐
3๏ธโฃ Client uploads ๐ฑ๐ถ๐ฟ๐ฒ๐ฐ๐๐น๐ ๐๐ผ ๐ฆ๐ฏ using PUT
4๏ธโฃ Backend never touches the file
๐ฏ Result:
โ Zero bottlenecks
โ Minimal Lambda execution time
โ Lower cost
โ Better UX
๐ ๐ช๐ต๐ฎ๐ ๐๐ ๐ฎ ๐ฃ๐ฟ๐ฒ๐๐ถ๐ด๐ป๐ฒ๐ฑ ๐จ๐ฅ๐ (๐๐ป ๐ฆ๐ถ๐บ๐ฝ๐น๐ฒ ๐ง๐ฒ๐ฟ๐บ๐)?
Think of a presigned URL as:
๐๏ธ ๐ ๐๐ฒ๐บ๐ฝ๐ผ๐ฟ๐ฎ๐ฟ๐, ๐๐ถ๐ป๐ด๐น๐ฒ-๐ฝ๐๐ฟ๐ฝ๐ผ๐๐ฒ ๐๐ถ๐ฐ๐ธ๐ฒ๐
โ Valid only for a few minutes
โ Allows only one specific action (e.g. PUT)
โ Scoped to a single object
โ No AWS credentials exposed
Once it expires โ ๐ถ๐โ๐ ๐๐๐ฒ๐น๐ฒ๐๐.
โ๏ธ ๐๐ฎ๐บ๐ฏ๐ฑ๐ฎ: ๐๐ฒ๐ป๐ฒ๐ฟ๐ฎ๐๐ถ๐ป๐ด ๐๐ต๐ฒ ๐ฃ๐ฟ๐ฒ๐๐ถ๐ด๐ป๐ฒ๐ฑ ๐จ๐ฅ๐ (๐ก๐ผ๐ฑ๐ฒ.๐ท๐)
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: "us-east-1" });
export const handler = async (event) => {
const { fileName, contentType } = JSON.parse(event.body);
const key = uploads/${Date.now()}-${fileName};
const command = new PutObjectCommand({
Bucket: process.env.BUCKET_NAME,
Key: key,
ContentType: contentType,
});
const uploadUrl = await getSignedUrl(s3, command, {
expiresIn: 300, // 5 minutes
});
return {
statusCode: 200,
body: JSON.stringify({ uploadUrl, key }),
};
};
[!WARNING]
โ ๏ธ ๐๐บ๐ฝ๐ผ๐ฟ๐๐ฎ๐ป๐:
The Lambda never sees the file, it only authorizes the upload.
๐ก๏ธ ๐ฆ๐ฒ๐ฐ๐๐ฟ๐ถ๐๐ ๐๐ฎ๐๐ฒ๐ฟ๐ (๐ช๐ต๐ฎ๐ ๐ ๐ฎ๐ธ๐ฒ๐ ๐ง๐ต๐ถ๐ ๐ฃ๐ฟ๐ผ๐ฑ๐๐ฐ๐๐ถ๐ผ๐ป-๐ฅ๐ฒ๐ฎ๐ฑ๐)
This PoC follows ๐ฟ๐ฒ๐ฎ๐น ๐ฒ๐ป๐๐ฒ๐ฟ๐ฝ๐ฟ๐ถ๐๐ฒ ๐ฝ๐ฟ๐ฎ๐ฐ๐๐ถ๐ฐ๐ฒ๐, not just demos.
๐ ๐ญ. ๐ฆ๐ต๐ผ๐ฟ๐-๐น๐ถ๐๐ฒ๐ฑ ๐จ๐ฅ๐๐
โ 5โ10 minutes max
โ Enough for upload, useless afterward
๐ ๐ฎ. ๐๐๐ ๐๐ฒ๐ฎ๐๐ ๐ฃ๐ฟ๐ถ๐๐ถ๐น๐ฒ๐ด๐ฒ
Lambda role can ๐ผ๐ป๐น๐ do:
{
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/uploads/*"
}
Nothing else.
๐ ๐ฏ. ๐ฃ๐ฟ๐ถ๐๐ฎ๐๐ฒ ๐ฆ๐ฏ ๐๐๐ฐ๐ธ๐ฒ๐
โ No public access
โ No ACL tricks
โ Only presigned URLs work
๐ ๐ฐ. ๐ฆ๐ฎ๐ป๐ถ๐๐ถ๐๐ฒ๐ฑ ๐ข๐ฏ๐ท๐ฒ๐ฐ๐ ๐ก๐ฎ๐บ๐ฒ๐
โ UUID-based keys
โ No user-controlled paths
โ No path traversal risks
๐ ๐ฑ. ๐๐ข๐ฅ๐ฆ ๐๐ผ๐ฟ๐ฟ๐ฒ๐ฐ๐๐น๐ ๐๐ผ๐ป๐ณ๐ถ๐ด๐๐ฟ๐ฒ๐ฑ
Because ๐๐ข๐ฅ๐ฆ ๐ถ๐ ๐ฎ๐น๐๐ฎ๐๐ ๐๐ต๐ฒ ๐ณ๐ถ๐ฟ๐๐ ๐๐ต๐ถ๐ป๐ด ๐๐ต๐ฎ๐ ๐ฏ๐ฟ๐ฒ๐ฎ๐ธ๐ ๐
๐งฑ ๐๐ป๐ณ๐ฟ๐ฎ๐๐๐ฟ๐๐ฐ๐๐๐ฟ๐ฒ ๐ฎ๐ ๐๐ผ๐ฑ๐ฒ ๐๐ถ๐๐ต ๐ง๐ฒ๐ฟ๐ฟ๐ฎ๐ณ๐ผ๐ฟ๐บ
Everything is deployed using ๐ง๐ฒ๐ฟ๐ฟ๐ฎ๐ณ๐ผ๐ฟ๐บ, so the architecture is:
โ Reproducible
โ Auditable
โ Ready for CI/CD
Core components:
โ S3 bucket
โ Lambda
โ IAM roles
โ API Gateway (HTTP API)
๐ ๐๐ฏ ๐ต๐ฉ๐ฆ ๐ฑ๐ฐ๐ด๐ต ๐ ๐ง๐ฐ๐ค๐ถ๐ด ๐ฐ๐ฏ ๐ต๐ฉ๐ฆ ๐ฎ๐ฟ๐ฐ๐ต๐ถ๐๐ฒ๐ฐ๐๐๐ฟ๐ฒ ๐ฎ๐ป๐ฑ ๐ฝ๐ฎ๐๐๐ฒ๐ฟ๐ป.
๐๐ฉ๐ฆ ๐ณ๐๐น๐น ๐ง๐ฒ๐ฟ๐ฟ๐ฎ๐ณ๐ผ๐ฟ๐บ ๐ถ๐บ๐ฝ๐น๐ฒ๐บ๐ฒ๐ป๐๐ฎ๐๐ถ๐ผ๐ป ๐ญ๐ช๐ท๐ฆ๐ด ๐ช๐ฏ ๐ต๐ฉ๐ฆ ๐ณ๐ฆ๐ฑ๐ฐ๐ด๐ช๐ต๐ฐ๐ณ๐บ.
๐ ๐ฅ๐ฒ๐ฝ๐ผ๐๐ถ๐๐ผ๐ฟ๐ ๐ฆ๐๐ฟ๐๐ฐ๐๐๐ฟ๐ฒ (๐ฆ๐ถ๐บ๐ฝ๐น๐ฒ ๐ฏ๐ ๐๐ฒ๐๐ถ๐ด๐ป)
aws-s3-presigned-url-lambda-terraform/
โโโ 01-s3.tf
โโโ 02-lambda.tf
โโโ 04-api.tf
โโโ client/
โ โโโ index.html
โ โโโ index.html.tpl
โ โโโ logo-client.png
โ โโโ logo-s3.png
โโโ dev.tfvars
โโโ drawio/
โ โโโ aws-s3-presignend.gif
โ โโโ aws-s3-url.drawio
โ โโโ image-2.png
โ โโโ image.png
โโโ lambda-function.zip
โโโ main.tf
โโโ Makefile
โโโ provider.tf
โโโ README.md
โโโ security/
โ โโโ checkov.yaml
โ โโโ trivy.yaml
โโโ src/
โ โโโ index.mjs
โ โโโ node_modules/
โ โโโ package-lock.json
โ โโโ package.json
โโโ terraform.tfstate
โโโ terraform.tfstate.backup
โโโ tfplan
โโโ tfplan.json
โโโ variables.tf
โโโ versions.tf
๐ฏ Minimal, readable, and focused on the pattern.
๐งช ๐ฅ๐ฒ๐พ๐๐ถ๐ฟ๐ฒ๐บ๐ฒ๐ป๐๐ ๐๐ผ ๐ฅ๐๐ป ๐๐ต๐ฒ ๐ฃ๐ผ๐
This PoC assumes:
โ
macOS
โ
Terraform already installed
โ
AWS CLI already authenticated
No extra setup. No magic.
๐ ๐ช๐ฎ๐ป๐ ๐๐ต๐ฒ ๐๐๐น๐น ๐ช๐ผ๐ฟ๐ธ๐ถ๐ป๐ด ๐ฃ๐ผ๐?
I published the complete implementation here:
๐ ๐ต๐๐๐ฝ๐://๐ด๐ถ๐๐ต๐๐ฏ.๐ฐ๐ผ๐บ/๐ณ๐ฟ๐ฎ๐ป๐ฐ๐ผ๐๐ฒ๐น/๐ฎ๐๐-๐๐ฏ-๐ฝ๐ฟ๐ฒ๐๐ถ๐ด๐ป๐ฒ๐ฑ-๐๐ฟ๐น-๐น๐ฎ๐บ๐ฏ๐ฑ๐ฎ-๐๐ฒ๐ฟ๐ฟ๐ฎ๐ณ๐ผ๐ฟ๐บ (๐ต๐๐๐ฝ๐://๐ด๐ถ๐๐ต๐๐ฏ.๐ฐ๐ผ๐บ/๐ณ๐ฟ๐ฎ๐ป๐ฐ๐ผ๐๐ฒ๐น/๐ฎ๐๐-๐๐ฏ-๐ฝ๐ฟ๐ฒ๐๐ถ๐ด๐ป๐ฒ๐ฑ-๐๐ฟ๐น-๐น๐ฎ๐บ๐ฏ๐ฑ๐ฎ-๐๐ฒ๐ฟ๐ฟ๐ฎ๐ณ๐ผ๐ฟ๐บ)
Youโll find:
โ Full Terraform code
โ Lambda (Node.js 20)
โ Test scripts
โ Architecture diagram
Clone it, deploy it, break it, improve it.
๐ฏ ๐๐ถ๐ป๐ฎ๐น ๐ง๐ต๐ผ๐๐ด๐ต๐
If your backend is still handling file uploads, youโre paying more ๐ฎ๐ป๐ฑ scaling less.
This pattern:
โ Reduces cost
โ Improves reliability
โ Scales naturally
โ Matches real-world cloud architectures
Inspiration and References
This project was inspired by the AWS blog post:
- Securing Amazon S3 presigned URLs for serverless applications https://aws.amazon.com/blogs/compute/securing-amazon-s3-presigned-urls-for-serverless-applications/
The article explores several best practices for securing presigned
URLs in serverless architectures, including checksum validation,
expiration strategies, and least-privilege IAM policies.
๐ค Let's Connect!
If you find this repository useful and want to see more content like this, follow me on LinkedIn to stay updated on more projects and resources!
If youโd like to support my work, you can buy me a coffee. Thank you for your support!
Thank you for reading! ๐







Top comments (0)