The way that AWS WAF works to be very unclear, at the moment, I'm trying to allow all traffic to a certain path.
Lets say everything to /admin should be allowed and not go through the xss or sql filters that I've added from the "common attacks" to my setup via their official guide: https://docs.aws.amazon.com/waf/latest/developerguide/tutorials-common-attacks.html
But the UI and docs makes it really unclear how to do this. Any help or resources would be very useful.
Is /admin part of the URL? If so, can you write a simple string-match rule on URL to whitelist it?
FYI, rules are triggered based on order you put in so put the whitelist rules need to be at top.
# waf using rate-based rule
resource "aws_wafv2_web_acl" "example" {
name = "example"
description = "Example of a regional rate based statement."
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "LoginRateLimit"
priority = 0
action {
count {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "Metric-Limit"
sampled_requests_enabled = true
}
statement {
rate_based_statement {
limit = 500
aggregate_key_type = "IP"
scope_down_statement {
byte_match_statement {
field_to_match {
uri_path {}
}
positional_constraint = "CONTAINS"
search_string = "login"
text_transformation {
priority = 0
type = "NONE"
}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "any-name"
sampled_requests_enabled = true
}
}
You can filter and add different rules to different paths using String Match Conditions.
Configure it to filter on part of the URI (URL that identifies a resource).
Related
I created WAFV2 firewall rules to restrict few IP address to hit API gateways. it is working as expected.
# ipsets
resource "aws_wafv2_ip_set" "example-ip-sets" {
name = "ipsetsexample"
description = "Example IP set"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = ["137.137.43.154/32", "40.121.120.150/32"]
tags = {
Tag1 = "Value1"
Tag2 = "Value2"
}
}
resource "aws_wafv2_web_acl" "firewall" {
name = "firewall"
scope = "REGIONAL"
default_action {
block {}
}
rule {
name = "ip-blacklist"
priority = 1
action {
allow {}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.example-ip-sets.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "BlacklistedIP"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "Allowed"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_web_acl_association" "this" {
resource_arn = aws_api_gateway_stage.api_stage.arn
web_acl_arn = aws_wafv2_web_acl.firewall.arn
}
but now new scenario came in to picture like, block some domain names instead of IP's (abcde.com, us1a.bcde.com) to invoke the API gateways. I have searched many terraform docs but didn't find any related.
is there any docs or source to restrict domain names to hit the API gateway ?
So, I am using a module based on this AWS LB Module
Now, I'm trying to add stickiness option on the default_action forward. Not sure why it's not working. I've tried all sorts of changes. My last change is this:
dynamic "default_action" {
for_each = contains(["authenticate-oidc", "authenticate-cognito"], lookup(var.https_listeners[count.index], "action_type", {})) ? [var.https_listeners[count.index]] : []
content {
type = "forward"
target_group_arn = aws_lb_target_group.main[lookup(default_action.value, "target_group_index", count.index)].id
stickiness {
enabled = lookup(default_action.value, "stickiness", false)
duration = lookup(default_action.value, "duration", null)
}
}
}
I'm trying to pass this:
https_listeners = [
{
port = 443
protocol = "HTTPS"
certificate_arn = var.certificate_arn
stickiness = true
duration = 3600
},
]
I'm now thinking of trying to just use the default stickiness as enabled, and duration 3600.
Any thoughts on this?
EDIT 1: I've tried changing the module block as #bryan mentioned I am passing an empty map. Just for test purposes, I hardcoded the values for stickiness like below:
dynamic "default_action" {
for_each = contains(["authenticate-oidc", "authenticate-cognito"], lookup(var.https_listeners[count.index], "action_type", {})) ? [var.https_listeners[count.index]] : []
content {
type = "forward"
target_group_arn = aws_lb_target_group.main[lookup(default_action.value, "target_group_index", count.index)].id
stickiness {
enabled = true
duration = 3600
}
}
}
Expectation is for the listener default action to set the group stickiness.. but somehow, it doesn't. I've also raised this issue on the AWS LB Module repo
I am trying to rate limit requests to the forgot password change URL using WAFv2 rules attached to an ALB on Cloudfront.
What I think I need to do is..
Create two resources aws_wafv2_web_acl.afv2_rate_limit and another called aws_wafv2_regex_pattern_set.wafv2_password_url
Example of rate: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl
Example of regex: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_regex_pattern_set
Combine these into a rule group, call it aws_wafv2_rule_group.wafv2_rule_group_pw_rate_group
Example of group: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_rule
I've created the rate limit and the regex, but I am failing to create the rule group. I put this rule in to refer to the rate limit
rule {
name = "rate_limit"
priority = 1
action {
block {}
}
statement {
and_statement {
statement {
rule_group_reference_statement { # !!FIXME!! doesn't work
arn = aws_wafv2_web_acl.wafv2_rate_limit.arn
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "password_url"
sampled_requests_enabled = false
}
}
I get the error on the rule_group_reference_statement line:
Blocks of type "rule_group_reference_statement" are not expected here.
I can attach the rule group to the ALB.
of course, the first question is whether this is even the right way to go about it?!
thanks for any thoughts.
working!
resource "aws_wafv2_web_acl" "wafv2_alb_pw5pm_acl" {
name = "wafv2_alb_pw5pm-acl"
description = "prevent brute forcing password setting or changing"
scope = "REGIONAL" # if using this, no need to set provider
default_action {
allow {} # pass traffic until the rules trigger a block
}
rule {
name = "rate_limit_pw5pm"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
#limit = 300 # 5 per sec = 300 per min
limit = 100 # smallest value for testing
aggregate_key_type = "IP"
scope_down_statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.wafv2_password_uri.arn
text_transformation {
priority = 1
type = "NONE"
}
field_to_match {
uri_path {}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "wafv2_alb_pw5pm_acl_rule_vis"
sampled_requests_enabled = false
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "wafv2_alb_pw5pm_acl_vis"
sampled_requests_enabled = false
}
tags = {
managedby = "terraform"
}
}
resource "aws_wafv2_web_acl_association" "web_acl_association_my_lb" {
resource_arn = aws_lb.xxxxxx.arn
web_acl_arn = aws_wafv2_web_acl.wafv2_alb_pw5pm_acl.arn
}
You can't nest a rule_group_reference_statement, for example for use inside a and_statement, not_statement or or_statement. It can only be referenced as a top-level statement within a rule.
yes you can. basically you need to declare an aws_wafv2_regex_pattern_set, in this example I use the URI "/api/*" but it can be a fixed one too.
resource "aws_wafv2_regex_pattern_set" "regex_pattern_api" {
name = "regex-path-api"
scope = "REGIONAL"
regular_expression {
regex_string = "/api/.+"
}
}
and the here is an example on how to use it on a waf declaration:
resource "aws_wafv2_web_acl" "waf" {
name = "waf"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "RateLimit"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
aggregate_key_type = "IP"
limit = 100
scope_down_statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.regex_pattern_api.arn
field_to_match {
uri_path {}
}
text_transformation {
priority = 1
type = "NONE"
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimit"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "waf"
sampled_requests_enabled = false
}
}
The cool part of this is that it is a rate limit that narrows the filter based on client IP using scope_down_statement
I wonder if something like this could be used. I have functional code working, but as pods in kubernetes will grow up fast, I want to convert into templates. This is an example for the creation of each nginx ingress rule for each wordpress site pod.
Now, each pod has its wordpress ingress entry with:
resource "kubernetes_ingress" "ingress_nginx_siteA" {
metadata {
name = "ingress-nginx-siteA"
namespace = "default"
annotations = { "kubernetes.io/ingress.class" = "nginx", "nginx.ingress.kubernetes.io/configuration-snippet" = "modsecurity_rules '\n SecRuleEngine On\n SecRequestBodyAccess On\n SecAuditEngine RelevantOnly\n SecAuditLogParts ABCIJDEFHZ\n SecAuditLog /var/log/modsec_audit.log\n SecRuleRemoveById 932140\n';\n", "nginx.ingress.kubernetes.io/ssl-passthrough" = "true" }
}
spec {
tls {
hosts = ["siteA.test.com"]
secret_name = "wildcard-test-com"
}
rule {
host = "siteA.test.com"
http {
path {
path = "/"
backend {
service_name = "siteA"
service_port = "80"
}
}
}
}
}
}
Now I want to split into variables.tf that contain the whole sites variables, a template file rules.tpl and the main.tf that orchestrate this stuff.
variables.tf:
variable "wordpress_site" {
type = map(object({
name = string
url = string
certificate = string
}))
default = {
siteA = {
name = siteA
url = siteA.test.com
certificate = wildcard-test-com
}
siteB = {
name = siteB
url = siteB.test.com
certificate = wildcard-test-com
}
}
}
rules.tpl:
%{ for name in wordpress_site.name ~}
resource "kubernetes_ingress" "ingress_nginx_${name}" {
metadata {
name = "ingress-nginx-${name}"
namespace = "default"
annotations = { "kubernetes.io/ingress.class" = "nginx", "nginx.ingress.kubernetes.io/configuration-snippet" = "modsecurity_rules '\n SecRuleEngine On\n SecRequestBodyAccess On\n SecAuditEngine RelevantOnly\n SecAuditLogParts ABCIJDEFHZ\n SecAuditLog /var/log/modsec_audit.log\n SecRuleRemoveById 932140\n';\n", "nginx.ingress.kubernetes.io/ssl-passthrough" = "true" }
}
spec {
tls {
hosts = ["${wordpress_site.url}"]
secret_name = "${wordpress_site.certificate}"
}
rule {
host = "${wordpress_site.url}"
http {
path {
path = "/"
backend {
service_name = "${name}"
service_port = "80"
}
}
}
}
}
}
%{ endfor ~}
and now, in main.tf, what is the best way in order to mix it all? I see that new functionalities are added in TF 0.12 like templatefile function, but didn't know at all if I can use it like:
main.tf:
templatefile(${path.module}/rules.tpl, ${module.var.wordpress_site})
Thanks all for your support!
The templatefile function is for generating strings from a template, not for generating Terraform configuration. Although it would be possible to render your given template to produce a string containing Terraform configuration, Terraform would just see the result as a normal string, not as more configuration to be evaluated.
Instead, what we need to get the desired result is resource for_each, which allows creating multiple instances from a single resource based on a map value.
resource "kubernetes_ingress" "nginx" {
for_each = var.wordpress_site
metadata {
name = "ingress-nginx-${each.value.name}"
namespace = "default"
annotations = {
"kubernetes.io/ingress.class" = "nginx"
"nginx.ingress.kubernetes.io/configuration-snippet" = <<-EOT
modsecurity_rules '
SecRuleEngine On
SecRequestBodyAccess On
SecAuditEngine RelevantOnly
SecAuditLogParts ABCIJDEFHZ
SecAuditLog /var/log/modsec_audit.log
SecRuleRemoveById 932140
';
EOT
"nginx.ingress.kubernetes.io/ssl-passthrough" = "true"
}
}
spec {
tls {
hosts = [each.value.url]
secret_name = each.value.certificate
}
rule {
host = each.value.url
http {
path {
path = "/"
backend {
service_name = each.value.name
service_port = "80"
}
}
}
}
}
}
When a resource has for_each set, Terraform will evaluate the given argument to obtain a map, and will then create one instance of the resource for each element in that map, with each one identified by its corresponding map key. In this case, assuming the default value of var.wordpress_site, you'll get two instances with the following addresses:
kubernetes_ingress.nginx["siteA"]
kubernetes_ingress.nginx["siteB"]
Inside the resource block, references starting with each.value refer to the values from the map, which in this case are the objects describing each site.
I created the following AWS WAF ACL and I want to associate it with my ALB using terraform. is there any way I can do it using terraform?
I want to block all requests except the ones that have secret key using amazon web service web application firewalls, aws waf. For that purpose, I created byte_set, aws rule and access control lists, ACL
resource "aws_alb" "app" {
............
}
#waf
resource "aws_waf_byte_match_set" "byte_set" {
name = "tf_waf_byte_match_set"
byte_match_tuples {
text_transformation = "NONE"
target_string = "${var.secret_key}"
positional_constraint = "EXACTLY"
field_to_match {
type = "HEADER"
data = "referer"
}
}
}
resource "aws_waf_rule" "wafrule" {
depends_on = ["aws_waf_byte_match_set.byte_set"]
name = "tfWAFRule"
metric_name = "tfWAFRule"
predicates {
data_id = "${aws_waf_byte_match_set.byte_set.id}"
negated = false
type = "ByteMatch"
}
}
resource "aws_waf_web_acl" "waf_acl" {
depends_on = ["aws_waf_byte_match_set.byte_set", "aws_waf_rule.wafrule"]
name = "tfWebACL"
metric_name = "tfWebACL"
default_action {
type = "BLOCK"
}
rules {
action {
type = "ALLOW"
}
priority = 1
rule_id = "${aws_waf_rule.wafrule.id}"
}
}
Sure, here is an example of the resource for the WAFv2 (I recommend to use this one) with a rate limit example rule and the association with an ALB:
########### This is the creation of an WAFv2 (Web ACL) and a example rate limit rule
resource "aws_wafv2_web_acl" "my_web_acl" {
name = "my-web-acl"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "RateLimit"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
aggregate_key_type = "IP"
limit = 500
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimit"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "my-web-acl"
sampled_requests_enabled = false
}
}
########### This is the association code
resource "aws_wafv2_web_acl_association" "web_acl_association_my_lb" {
resource_arn = aws_lb.my_lb.arn
web_acl_arn = aws_wafv2_web_acl.my_web_acl.arn
}
You can associate a WAF with ALB (Application Load Balancer) and with CloudFront, you cannot associate with an ELB (Classic Elastic Load Balancer).
To associate with ALB this is the piece of code
resource "aws_wafregional_web_acl_association" "foo" {
resource_arn = "${aws_alb.foo.arn}"
web_acl_id = "${aws_wafregional_web_acl.foo.id}"
}
taken from the official documentation
This feature has been proposed but not merged yet.
https://github.com/hashicorp/terraform/issues/10713
https://github.com/hashicorp/terraform/pull/11263