By Andrico Karoulla - 15 August 2025
5 Min Read
A few months back I was working on a project that required a complete server rewrite. Before writing any features, I used SST to define the infrastructure and deploy it to AWS, adding features incrementally.
At this stage, the server was live on AWS but used only for internal testing. So it was a surprise to start seeing strange, unexpected requests hitting the server. These came in a various forms, but a particularly alarming one looked like this:
<SERVER_URL>/?asset=../../../../WINDOWS/system32/config/SAM
A malicious actor had discovered the address of our server and began hunting for vulnerabilities, using query parameters to guess the location of (and subsequently access) sensitive files. This is what’s known as a path traversal attack.
While this specific attempt wasn’t an issue for us, it made clear the gaps in the default security setup.
It didn’t stop there though, another malicious actor began hitting the server hundreds of times per minute. The screenshot below shows a sharp spike in number of requests at 4am.
These are exactly the kind of requests that a firewall could block, but SST doesn’t include a firewall construct out of the box. In the blog, I’ll through how I addressed this.
💡 SST is a framework that makes it easy to build modern full-stack applications on your own infrastructure. It offers a great developer experience for those who want control over their infra without an overly complex infrastructure-as-code syntax/API.
I’ve used SST to build standalone servers, load balancers, fullstack applications, serverless functions, and I’ve used it to orchestrate clusters of servers. But despite the variety of use cases, I never needed to go beyond what the SST SDK offers out of the box.
That changed when I observed those malicious requests. While SST lets developers spin up load balancers with routing rules, it doesn’t offer first class support for setting up an AWS web application firewall (WAF). The firewall can block suspicious looking requests AND offer rate limiting capabilities.
💡 This article assumes you have a little knowledge about AWS, so I won’t dive into things like what clusters and VPCs are, etc.
The minimal code required to spin up a server using SST is as follows:
const vpc = new sst.aws.Vpc("MyVpc");
const cluster = new sst.aws.Cluster("MyCluster", { vpc });
const service = cluster.addService("MyAppService", {
image: {
context: "./",
dockerfile: "packages/server/Dockerfile",
},
});
The above 7 lines of code will create a docker image of your server and run the container as a service within your VPC. If you’ve spun up a no-frills server in SST, it’s likely that you’ve written very similar code.
What’s great about the above is that even though I’ve explicitly instantiated 2 resources, a Vpc and Cluster, SST creates manages dozens of resources under the hood. The resources includes IAM roles, security groups, load balancers and more.
💡 You can check out my talk at Svelte Society if you want to learn a little more about SST and why I love it.
The SST documentation mentions how it’s built on top of Pulumi, another IaC tool. Pulumi serves a different purpose, being a more general IaC tool that offers a lower-level API, giving developers more control like allowing them to provision infra with other cloud providers.
This lets developers define infrastructure using Pulumi when they reach the limits of what SST offers out-of-the-box. SST makes it easy to link these Pulumi resources with SST-defined infra. It’s just like doing a little DIY carpentry to customise your flat packed Ikea table.
Pulumi offers much more comprehensive coverage for provisioning AWS infrastructure and as such it offers a construct for managing WAF.
Setting up a firewall in SST involves just a couple of key steps.
Define a web access control list (web ACL)
Define rate limiting rules to a AWS’s firewall
Enable rulesets to counter common vulnerabilities
Associate a web ACL with the application’s load balancer.
The web ACL is a set of rules that defines which requests the firewall allows and blocks. We’ll need to create a web ACL, define some rules, and then associate it to our load balancer.
In addition to creating rules from scratch, AWS offers some pre-defined rulesets. These rulesets vary from generic web server security, to rate limits, to rulesets specialised for specific technologies.
You’ll need to define the web ACL, here’s how I did it:
const webAcl = new aws.wafv2.WebAcl("AppAlbWebAcl", {
defaultAction: { allow: {} },
scope: "REGIONAL",
visibilityConfig: {
cloudwatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "AppAlbWebAcl",
},
rules: [],
});
Here’s what’s happening above:
I’m using the global aws
object which all access the Pulumi constructs, i.e., wafv2
.
I’m creating a new instance of a Web ACL. The first argument is the name I want to give to this resource.
The second argument is an object of options.
I want to allow all traffic by default. The alternative is to block all traffic, and enable traffic through rules.
The scope
option defines whether our application is a regional application or a Cloudfront distribution.
The visibilityConfig
option defines the configuration for Cloudwatch metrics.
The rules
option defines the ACL’s rules. Let’s create some rules…
The goal here was to add a rate limit and enforce AWS’s managed ruleset, defined as follows:
const rateLimitRule = {
name: "RateLimitRule",
statement: {
rateBasedStatement: {
limit: 200,
aggregateKeyType: "IP",
},
},
priority: 1,
action: { block: {} },
visibilityConfig: {
cloudwatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "MyAppRateLimitRule",
},
};
const awsManagedRules = {
name: "AWSManagedRules",
statement: {
managedRuleGroupStatement: {
name: "AWSManagedRulesCommonRuleSet",
vendorName: "AWS",
},
},
priority: 2,
overrideAction: {
none: {},
},
visibilityConfig: {
cloudwatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "MyAppAWSManagedRules",
},
};
There’s a lot going on in the code above, so let’s break things down.
For starters, I’ve created two objects, for each set of rules. The first defines a rule for a rate limit and the other for AWS’s managed ruleset.
Both rules have a statement
option, which defines either a brand new rule, or reference a managed ruleset.
As you’d guess, the rate limit rule sets up the infrastructure needed to blocks requests from IP addresses that have made too many requests within a time limit. In this case, the limit is 200 requests within a 5 minute window.
Each rule has a priority
value. This determines the order in which the rules run against a request. The lower the value, the earlier the rule gets run. In this case, the rate limit rule runs before the AWS managed rules do.
Lastly, the rate limit rule requires an action
object, which defines what the desired behaviour is for when a request activates a rule. I want to block requests that breach the rate limit rule.
The structure of the AWS managed ruleset option is similar to that of the rate limit rule. The key difference is the existence of the overrideAction
in lieu of the action
property. overrideAction
action determines whether to override the action of ruleset’s rules. There are two available choices: none
and count
. none
lets the ruleset do its thing, but count
logs the offending request, but still allows it to proceed. As a result, count
won’t block offending requests, but helps evaluate the effectiveness of a new rule.
Once the rules have been defined, I reference them in the web ACL definition:
const webAcl = new aws.wafv2.WebAcl("AppAlbWebAcl", {
defaultAction: { allow: {} },
scope: "REGIONAL",
visibilityConfig: {
cloudwatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "AppAlbWebAcl",
},
rules: [
rateLimitRule,
awsManagedRules
],
});
After defining the web ACL, it has to be associated to a load balancer. This can be done by passing through the Amazon Resource Name (ARN) for both the web ACL and the load balancer.
💡 An ARN is a unique identifier for a resource, which allows developers to reference resources from different regions, services, and accounts without worry about collisions.
Accessing the ARN of the web ACL can be done like so:
const webAclArn = webAcl.arn
We can then pass it through to the WebAclAssociation
construct provided to us by Pulumi:
new aws.wafv2.WebAclAssociation("MyAppAlbWebAclAssociation", {
webAclArn: webAcl.arn,
resourceArn: ...
});
You might think that accessing the load balancer’s ARN may be as simple as:
const loadBalancerArn = service.nodes.loadBalancer.arn;
This isn’t guaranteed to work as the load balancer’s details may not be known immediately available. Instead, use the apply
helper to get access to the ARN once it’s available:
service.nodes.loadBalancer.arn.apply((arn) => {
new aws.wafv2.WebAclAssociation("MyAppAlbWebAclAssociation", {
resourceArn: arn,
webAclArn: webAcl.arn,
});
});
You can see that apply
takes a callback function that fires once the ARN is available. Within the callback, I finish setting up the infrastructure needed to provision a firewall.
After provisioning the infrastructure, you jump over to AWS and see it associated to your firewall.
To validate that everything worked, I needed to deploy our server. Once that’s done, I can go over to my load balancer’s resource in AWS, open the extensions tab and see that the firewall has been enabled.
You can validate that the managed rules are working as expected by sending requests that violate one of the managed rules. Here I am attempting a path traversal attack:
curl <SERVER_URL>\?assets\=../../../WINDOWS/system32/config/sam
The response I get from the server is a 403 forbidden, showing that the firewall works as expected.
In the introduction, I mentioned SST’s strengths in letting developers spin up infrastructure quickly while offering more control than most other out-of-the-box tools. That was before I had to go a layer deeper for this project, which made me appreciate SST even more. If you find yourself hitting a wall with what SST offers, check the Pulumi docs, as it may offer the very constructs you’re looking for.
Finally, if you find yourself trying to implement an WAF like I’ve done in this article, take a look at all the managed rule sets that AWS provides. There are dozens that are all geared toward different technologies and security considerations.
If you have any questions about using SST in your next project, you can email me at andrico@planes.agency
We use cookies to ensure you get the best experience on our website. For more information on how we use cookies, please see our cookie policy.