feat: add options to restrict ssr lambda functions to cloudfront-only access using OAC#5581
Conversation
|
Hmm that file was a WIP as a part of the v3.10 release. Would appreciate if you made these changes to just the updated ssr-site.ts |
d033795 to
9797838
Compare
|
@jayair I have updated the PR |
|
Appreciate the PR @MrHertal! Could u try out POST and PUT requests? According to this thread, might need to calculate and set the content hash header - #4684 (comment) |
|
@MrHertal you can't use OAC, only supports GET, not POST/PUT etc etc. Mostly useless. Copy the implementation from sst v2, and use my PR anomalyco/v2#50 that fixes an import bug in the current implementation. @fwang OAC is not good enough, as it requires clients to compute hashes so it's not transparent. Use the sst v2 method with my PR: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html |
|
@berenddeboer this comment suggests calculating the hash in CF function, does that work? |
ah thanks, got that now. Not sure I have a good setup to try this right now, but first question would be: any memory limits for CF functions? Probably ok in most cases, but how big can the body be? |
|
@fwang @berenddeboer Thanks for your reply 🙂 You cannot access request body in CloudFront function, so the It seems doable in a Lambda@Edge, but that would introduce a lot of changes in the architecture. The solution I propose is to offer an option to use OAC and protect lambda URL, by default it does not change anything, lambda URL will still be public. But if you enable it, it will setup the OAC and protect the URL behind CloudFront. You must then add the The implementation depends on your application framework. Here is an example of a async fetchThroughOAC(
input: string | Request | URL,
init: RequestInit | undefined,
) {
const body = init?.body?.toString() || "";
const digestHex = await digestMessage(body);
const headers = new Headers(init?.headers);
headers.append("x-amz-content-sha256", digestHex);
return fetch(input, { ...init, headers });
}
// See https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
async function digestMessage(message: string) {
const msgUint8 = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashHex;
}I can add an example for I understand that it is a bit cumbersome, but it's a good first step for users who don't want to deploy public lambda URL. I work in a company where cloud resources are monitored by tools like Prisma, deploying unprotected lambda URL is a red flag. With that option, I can at least enable the protection, knowing that for POST and PUT requests, I will need to add the payload hash value of the request body in the x-amz-content-sha256 header. |
|
@MrHertal lambda@edge is another service which looks free on the surface, but starts popping up in the bills at scale. Possibly would work, but something to take into account. |
|
@MrHertal this thing:
That's not great, as it would require changes to applications. Imagine having to add this to all your graphql requests... Very very painful. |
|
I think that having an option is better than no option 🙂 |
|
We recently upgraded to SST v3 especially because of the AOC support. Our security team flags public lambda urls as a critical security issue due to potential resource exhaustion. We would like to upgrade to Nextjs 15 but for that we need to upgrade to the latest SST version, which no longer supports AOC. So we would really like to have this support. It makes sense to keep it optional because of the additional header required for POST and PUT requests. |
|
Question, does this solution also secure the Image Optimization lambda? |
|
Hi, is there a way to use this, even before it merges? Like a patch? We are stuck on SST 3.7 until this merges, because of the changes to how NextJS runs on Cloudfront... Until we have this alternate way of making lambdas private, we can't update.... Most NextJS users will not be making POST endpoints... we would use API Gateway for that... NextJS is mostly focused on GET. Since this PR is providing an option, not changing the default, it should be fairly low risk, right? I could help test it if possible? |
|
I'll bump it up internally. |
|
I've implemented a more complete approach. The original option has been evolved into a comprehensive // Simple protection (my use case)
new sst.aws.Nextjs("MyApp", {
protection: "oac" // Manual header signing required
});
// Full protection (external webhooks/SAML)
new sst.aws.Nextjs("MyApp", {
protection: "oac-with-edge-signing" // Automatic Lambda@Edge signing
});
// No protection (default)
new sst.aws.Nextjs("MyApp", {
protection: "none" // Public access
});Key improvements:
The Lambda@Edge mode automatically computes SHA256 headers for POST requests, making it work seamlessly with external services while keeping the infrastructure overhead optional for those who don't need it. |
|
planning on testing this by next week! |
vimtor
left a comment
There was a problem hiding this comment.
thanks for the contribution @MrHertal
the only issue i found is file uploads with "oac-with-edge-signing":
this happens because the body bytes are being corrupted by converting to UTF-8 before hashing. which means that any request with non-UTF-8 bytes will fail, not just file uploads. i've suggested a fixed that seems to work
i've left other minor suggestions but everything else worked like a charm
| runtime: "nodejs22.x", | ||
| handler: "index.handler", | ||
| role: edgeRole.arn, | ||
| code: new pulumi.asset.AssetArchive({ |
There was a problem hiding this comment.
could we move this function code to platform/functions instead of inline? just to keep it consistent with the rest of the codebase.
you'll find more examples in there
There was a problem hiding this comment.
Indeed, much better. I applied the change here: 61f63e2
| // Get the request body | ||
| let bodyString = ''; | ||
|
|
||
| if (request.body && request.body.data) { | ||
| // Lambda@Edge provides body as base64-encoded string | ||
| if (request.body.encoding === 'base64') { | ||
| // Decode base64 to get the actual body content | ||
| bodyString = Buffer.from(request.body.data, 'base64').toString('utf8'); | ||
| } else { | ||
| // If not base64 encoded, use as-is | ||
| bodyString = request.body.data; | ||
| } | ||
| } | ||
|
|
||
| // Compute SHA256 hash of the body | ||
| const hash = crypto.createHash('sha256').update(bodyString, 'utf8').digest('hex'); |
There was a problem hiding this comment.
the below code fixed the file upload issue
still, lambda@edge has a 1mb payload cap, so big uploads return the failed to verify hash error
maybe we can return a different error or put a heads-up in the docs
| // Get the request body | |
| let bodyString = ''; | |
| if (request.body && request.body.data) { | |
| // Lambda@Edge provides body as base64-encoded string | |
| if (request.body.encoding === 'base64') { | |
| // Decode base64 to get the actual body content | |
| bodyString = Buffer.from(request.body.data, 'base64').toString('utf8'); | |
| } else { | |
| // If not base64 encoded, use as-is | |
| bodyString = request.body.data; | |
| } | |
| } | |
| // Compute SHA256 hash of the body | |
| const hash = crypto.createHash('sha256').update(bodyString, 'utf8').digest('hex'); | |
| // Get the request body as raw bytes (never convert to UTF-8 string) | |
| const bodyBuffer = request.body?.data | |
| ? (request.body.encoding === 'base64' | |
| ? Buffer.from(request.body.data, 'base64') | |
| : Buffer.from(request.body.data)) | |
| : Buffer.alloc(0); | |
| // Compute SHA256 hash of the raw bytes | |
| const hash = crypto.createHash('sha256').update(bodyBuffer).digest('hex'); |
There was a problem hiding this comment.
Nice catch, I had not tested the file upload, shame on me.
I've added your suggested code, a note about the file size limit and also a better error type: 0791c64
There was a problem hiding this comment.
don't worry. your pr is one of the few that worked first try haha
| * } | ||
| * ``` | ||
| */ | ||
| lambdaProtection?: Input< |
There was a problem hiding this comment.
it’d be cool if we had this in the Router component too, so you’ve got protection when using it with an api for example.
not something to change for this pr, but maybe it makes sense to move this type to Router
There was a problem hiding this comment.
Good idea! I'll keep this PR focused on SSR sites, but I can open a follow-up issue to add lambdaProtection support to Router. At that point we can refactor the type to a shared location.
There was a problem hiding this comment.
good! let me know if you want me to create the issue
| | "oac" | ||
| | "oac-with-edge-signing" | ||
| | { | ||
| mode: "oac-with-edge-signing"; |
There was a problem hiding this comment.
when deleting a stage that uses "oac-with-edge-signing" it takes ~5-10 minutes for the replicated functions to be deleted, so the edge function deletion takes a while
maybe we can add a small ::note in the docs
|
Thank you @vimtor for your relevant comments. Could you test my last changes? I could not find a way to properly build the lambda@edge. |
| * ```js | ||
| * // No protection (default) | ||
| * { | ||
| * lambdaProtection: "none" |
There was a problem hiding this comment.
i was thinking about how we'd name this attribute once we move it to the Router component
as far as i know you can protect lambdas and buckets for the most part. would we have a bucketProtection? not sure if that makes sense
this got me thinking maybe we should move this to the router and require people to pass the router instance to the SsrSite if they wanna use this feature
what do you think @MrHertal?
There was a problem hiding this comment.
Thanks for the feedback @vimtor
Regarding buckets: Looking at the Router code, bucket routes already have OAC protection enabled by default (via originAccessControlId). The specific security violation I'm addressing is public Lambda Function URLs - tools like Prisma Cloud flag these as security risks, but S3 buckets behind CloudFront with OAC are fine.
Regarding Router support: Agreed this would be useful there too! The Router's url routes currently use customOriginConfig without OAC. In a future PR, we could add something like:
router.route("/api/*", api.url, {
protection: "oac" // or "oac-with-edge-signing"
})
For this PR, I'd prefer to keep the scope limited to SSR sites since:
- SSR sites are the most common case for Lambda function URLs
- The protection is tightly coupled with how SSR sites create their Lambda functions (setting authorization: "iam" on the function URL)
- It works today without requiring users to change how they create their sites
Once this lands, we can discuss the best API for Router in a follow-up. The type definitions and Lambda@Edge function could be shared. What do you think?
There was a problem hiding this comment.
@MrHertal i love this api you suggested for the router. you're right, let's focus first on ssr sites and then we do the router.
the only thing missing then (i promise) is the lambdaProtection arg name
given that the router will have protection i wonder if it would be a better, more elegant name (in the sst philosophy style) for the arg. what do you think?
There was a problem hiding this comment.
I agree, let me rename it
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
|
Anyone else running in to issues with DELETE request? "message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details." |
|
i remember something about the DELETE requests when i was looking into this @masterFlippy can you open an issue? ideally add a sample of your sst.config.ts @MrHertal any ideas? |
… access using OAC (anomalyco#5581) * feat: add option to protect ssr server with origin access control * feat: use metadata in key value store * fix: parent self * docs: add an example of usage with tanstack start * fix: add a better example * chore: remove outdated example * feat: protect nextjs image optimiser * feat: lambda at edge * chore: trim comments * chore(review): move function to right folder * chore(review): handle file upload * chore(review): add note about lambda@edge long deletion * chore(review): update platform/functions/oac-edge-signer/index.ts Co-authored-by: Victor Navarro <vn4varro@gmail.com> * chore(review): prettier * chore(review): rename arg --------- Co-authored-by: Victor Navarro <vn4varro@gmail.com>
Yeah sure! I'll do it during the day! |
… access using OAC (anomalyco#5581) * feat: add option to protect ssr server with origin access control * feat: use metadata in key value store * fix: parent self * docs: add an example of usage with tanstack start * fix: add a better example * chore: remove outdated example * feat: protect nextjs image optimiser * feat: lambda at edge * chore: trim comments * chore(review): move function to right folder * chore(review): handle file upload * chore(review): add note about lambda@edge long deletion * chore(review): update platform/functions/oac-edge-signer/index.ts Co-authored-by: Victor Navarro <vn4varro@gmail.com> * chore(review): prettier * chore(review): rename arg --------- Co-authored-by: Victor Navarro <vn4varro@gmail.com>







Lambda Protection for SSR Components
This PR adds a comprehensive
protectionoption to SSR components (Nextjs, TanStackStart, etc.) to control Lambda function URL access through CloudFront using AWS Origin Access Control.Key Motivation
Enterprise Security Compliance: In many companies, deploying public Lambda URLs is prohibited by cybersecurity policies. Security tools like Prisma Cloud detect public Lambda URLs as security violations, preventing deployment of SSR applications.
With this PR, you can now use SST to deploy Next.js and other SSR frameworks in enterprise environments with strict security requirements!
See the AWS documentation: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html
API Design
The
protectionoption provides three security modes:1.
"none"(Default)2.
"oac"x-amz-content-sha256header for POST requests3.
"oac-with-edge-signing"Advanced Configuration
For the
"oac-with-edge-signing"mode, you can:Use auto-created Lambda@Edge function:
Customize the auto-created function:
Use existing Lambda@Edge function:
Technical Implementation
Lambda@Edge Integration
lambda.amazonaws.comandedgelambda.amazonaws.comtrust policiesConsistent Type System
Size,DurationSeconds)FunctionArntype andparseLambdaEdgeArn()helperVisibleErrorpatternsMulti-Region Support
Robust Permission Management
lambda:FunctionUrlAuthType=NONEconditionsourceArnconditionsExample Usage
Benefits
✅ Enterprise-ready security - Meets strict cybersecurity compliance requirements
✅ Flexible protection levels - Choose the right balance of security vs complexity
✅ External webhook support - Lambda@Edge mode works with any external service
✅ Multi-region ready - All modes work seamlessly across regions
✅ Type-safe configuration - Consistent with SST's type system
✅ Reliable state management - No issues when switching between modes
This enhancement makes SST's SSR components suitable for enterprise environments while maintaining the developer experience SST is known for.