Your App’s Bouncer: A No-BS Guide to AWS WAF

Look, I’ve been doing this for over a decade. If there’s one thing I know, it’s that the internet is a dumpster fire of malicious requests, and your beautiful, lovingly-crafted application is the target.

You can have the cleanest code in the world (you don’t) and the most robust infrastructure (it’s not), but at 3 AM, you’re still going to get that PagerDuty alert. Why? Because some script kiddie in a basement just found a simple SQL injection (SQLi) vector, or a botnet decided your login page looks like a fun thing to hammer.

Enter the bouncer: AWS WAF (Web Application Firewall).

What is AWS WAF? (The 1-Minute Version)

Let’s get this out of the way. It’s a firewall. But unlike your network-level security groups (Layer 4), WAF operates at Layer 7 (HTTP/S).

It sits in front of your key resources:

  • Application Load Balancers (ALB)
  • Amazon CloudFront distributions
  • API Gateway

It inspects every single HTTP request coming in before it ever touches your application code. It’s the bouncer at the club door checking IDs, dress codes, and attitudes. Your app servers are the VIP room, and they don’t need to be bothered by the riff-raff.

Why You’re Naked Without It: The Uses

Your security group says, “I only allow traffic on port 443.”

Your WAF says, “I see you’re on port 443, but your request looks like you’re trying to dump my entire user database. Denied.”

Here’s what you use it for:

  • Blocking the OWASP Top 10: This is the big one. SQLi, Cross-Site Scripting (XSS), command injection, etc. Instead of trusting that your devs sanitized every single input (spoiler: they didn’t), you block these patterns at the edge.
  • Beating the Bots: Stop web scrapers, content thieves, and comment spammers. WAF can detect and block traffic that looks like it’s coming from an automated script, not a human user.
  • Rate Limiting: Is a single IP address hitting your /login endpoint 1,000 times a second? That’s a brute-force attack. WAF can see that, count the requests, and tell that IP to “cool off” (i.e., block it for a while).
  • Geo-Blocking: Don’t do business in (or get a ton of spam from) a specific country? You can drop all traffic from that geographic region in two clicks. Sorry, Antarctica.
  • Patching Zero-Days (Virtually): Remember that huge Log4j vulnerability? While everyone was scrambling to patch their servers, the smart teams had a WAF rule deployed in minutes that blocked the malicious string (jndi:ldap...) from ever reaching their code. That’s virtual patching. It buys you time to patch properly.

Let’s Get Our Hands Dirty: The Commands

Talk is cheap. Let’s build.

You can click around in the console, but we’re pros. We use the AWS CLI (for quick stuff) or Terraform/CDK (for real, repeatable infrastructure).

Let’s do a common scenario: Block a list of known-bad IP addresses.

The Components:

  1. IPSet: A list of IPs you want to target.
  2. Rule: The logic (e.g., “IF request comes from IP in MyBadIPSet, THEN block“).
  3. WebACL: A collection of rules. This is the “firewall” you actually attach to your ALB or CloudFront.

Scope: WAF rules have a “scope.”

  • REGIONAL: For ALBs and API Gateways.
  • CLOUDFRONT: For… well, CloudFront.

We’ll use REGIONAL for an ALB.

1. Create the IPSet

First, get your AWS Account ID.

ACCOUNT_ID=$(aws sts get-caller-identity — query Account — output text)

Now, create the set. We’ll add a dummy IP, 1.2.3.4.

aws wafv2 create-ip-set \
    --name "MyBadIPSet" \
    --scope "REGIONAL" \
    --region "us-east-1" \
    --ip-address-version "IPV4" \
    --addresses "1.2.3.4/32" "192.0.2.0/24"

(Grab the Arn from the output! You’ll need it.)

2. Create the WebACL with a Rule

This is one command, but it’s a beast of JSON. This is where you see why IaC (Infrastructure as Code) is better.

We’ll create a WebACL that:

  • Default Action: allow (It lets everything through unless a rule matches).
  • Rule: A rule named BlockBadIPs that uses our MyBadIPSet. If it matches, it will block the request.

Bash

# Replace with the ARN from the previous step
IP_SET_ARN="arn:aws:wafv2:us-east-1:${ACCOUNT_ID}:regional/ipset/MyBadIPSet/..."
aws wafv2 create-web-acl \
--name "MyProdWebACL" \
--scope "REGIONAL" \
--region "us-east-1" \
--default-action "Allow={}" \
--visibility-config "SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=MyProdWebACL" \
--rules '[
{
"Name": "BlockBadIPs",
"Priority": 1,
"Statement": {
"IPSetReferenceStatement": {
"ARN": "'"${IP_SET_ARN}"'"
}
},
"Action": {
"Block": {}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BlockBadIPs"
}
}
]'

(Again, grab the Arn of the WebACL from the output.)

3. Associate the WebACL with Your ALB

Finally, attach your new bouncer to the club door (your ALB).

# Get your ALB ARN
ALB_ARN="arn:aws:elasticloadbalancing:us-east-1:${ACCOUNT_ID}:loadbalancer/app/my-prod-alb/..."
# Get your WebACL ARN
WEB_ACL_ARN="arn:aws:wafv2:us-east-1:${ACCOUNT_ID}:regional/webacl/MyProdWebACL/..."
aws wafv2 associate-web-acl \
--web-acl-arn "${WEB_ACL_ARN}" \
--resource-arn "${ALB_ARN}"

Boom. You’re now actively blocking those IPs.

The “Real World” Way (Terraform)

Now, let’s be honest. Nobody types that JSON blob into the CLI for production. We define it in code so it’s version-controlled and repeatable.

Here’s the same thing in Terraform. See how much cleaner this is?

resource "aws_wafv2_ip_set" "bad_ips" {
name = "MyBadIPSet"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = ["1.2.3.4/32", "192.0.2.0/24"]
}

resource "aws_wafv2_web_acl" "prod_acl" {
name = "MyProdWebACL"
scope = "REGIONAL"

default_action {
allow {}
}

rule {
name = "BlockBadIPs"
priority = 1

action {
block {}
}

statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.bad_ips.arn
}
}

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "BlockBadIPs"
sampled_requests_enabled = true
}
}

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "MyProdWebACL"
sampled_requests_enabled = true
}
}

resource "aws_wafv2_web_acl_association" "prod_alb_assoc" {
resource_arn = aws_lb.my_prod_alb.arn # (Your ALB resource)
web_acl_arn = aws_wafv2_web_acl.prod_acl.arn
}

The Most Important Advice You’ll Get

You’re excited. You’re ready to block all the things. DON’T.

If you deploy a new rule in Block mode, I guarantee you will block legitimate users. You will break production. You will be the reason for the 3 AM PagerDuty alert.

USE. COUNT. MODE.

When you create a rule, set its action to Count instead of Block.

"Action": {
"Count": {}
}

WAF will now log every request that would have been blocked. You can watch your CloudWatch metrics or WAF logs (send them to S3 or Kinesis Firehose!) for a few days.

  • See a bunch of false positives? Tune your rule.
  • See only malicious traffic being counted? Then, and only then, do you flip the switch to Block.

Oh, and use the AWS Managed Rules. Don’t try to write your own regex for SQLi. AWS and their partners (F5, Fortinet) have already done the hard work. Start with AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet. Set them to Count Mode first!

The Bottom Line

AWS WAF isn’t “set it and forget it.” It’s a critical, active part of your security posture. It’s not magic, and it won’t save you from terrible application code.

But it’s a damn good bouncer. It’ll handle the common drunks and troublemakers so your application can focus on serving the real customers.

Leave a Reply

Your email address will not be published. Required fields are marked *