Wafv2 with terraform. how to do I exclude rules? - amazon-web-services

I created a WAFV2 as a modules and this is part of my code
name = var.name
description = "WAFv2 ACL for ${var.name}"
scope = var.scope
default_action {
allow {}
}
visibility_config {
cloudwatch_metrics_enabled = true
sampled_requests_enabled = true
metric_name = var.name
}
dynamic "rule" {
for_each = var.managed_rules
content {
name = rule.value.name
priority = rule.value.priority
override_action {
dynamic "none" {
for_each = rule.value.override_action == "none" ? [1] : []
content {}
}
dynamic "count" {
for_each = rule.value.override_action == "count" ? [1] : []
content {}
}
}
But after setting up kinesis firehouse I noticed some requests are been blocked by WAFV2, can anyone help me to figure out how to exclude some of the AwsManagdRules been blocked? here are some examples of them.
"CrossSiteScripting_BODY", "GenericLFI_BODY"
but I tried something like this one below in the tableau server using the waf is this correct?
name = aws_wafv2_rule_group.aws-wafv2-tableau.name
arn = aws_wafv2_rule_group.aws-wafv2-tableau.arn
priority = 0
override_action = "allow"
excluded_rules = ["CrossSiteScripting_BODY","GenericLFI_BODY"]
}]```

Related

wafv2 firewall rules to restrict domain names

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 ?

Create waf rule if environment is nonprod Terraform

I'm trying to create an IP whitelist in nonprod for load testing, the WAF is dynamically created in prod and nonprod based on the envname/envtype:
resource "aws_waf_ipset" "pwa_cloudfront_ip_restricted" {
name = "${var.envname}-pwa-cloudfront-whitelist"
dynamic "ip_set_descriptors" {
for_each = var.cloudfront_ip_restricted_waf_cidr_whitelist
content {
type = ip_set_descriptors.value.type
value = ip_set_descriptors.value.value
}
}
}
resource "aws_waf_rule" "pwa_cloudfront_ip_restricted" {
depends_on = [aws_waf_ipset.pwa_cloudfront_ip_restricted]
name = "${var.envname}-pwa-cloudfront-whitelist"
metric_name = "${var.envname}PWACloudfrontWhitelist"
predicates {
data_id = aws_waf_ipset.pwa_cloudfront_ip_restricted.id
negated = false
type = "IPMatch"
}
}
resource "aws_waf_ipset" "pwa_cloudfront_ip_restricted_load_testing" {
name = "${var.envname}-pwa-cloudfront-whitelist_load_testing"
count = var.envtype == "nonprod" ? 1 : 0
dynamic "ip_set_descriptors" {
for_each = var.cloudfront_ip_restricted_waf_cidr_whitelist_load_testing
content {
type = ip_set_descriptors.value.type
value = ip_set_descriptors.value.value
}
}
}
resource "aws_waf_rule" "pwa_cloudfront_ip_restricted_load_testing" {
depends_on = [aws_waf_ipset.pwa_cloudfront_ip_restricted_load_testing]
count = var.envtype == "nonprod" ? 1 : 0
name = "${var.envname}-pwa-cloudfront-whitelist-load_testing"
metric_name = "${var.envname}PWACloudfrontWhitelistload_testing"
predicates {
data_id = aws_waf_ipset.pwa_cloudfront_ip_restricted_load_testing[count.index].id
negated = false
type = "IPMatch"
}
}
resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted" {
name = "${var.envname}-pwa-cloudfront-whitelist"
metric_name = "${var.envname}PWACloudfrontWhitelist"
default_action {
type = "BLOCK"
}
rules {
action {
type = "ALLOW"
}
priority = 1
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted.id
type = "REGULAR"
}
rules {
action {
type = "ALLOW"
}
priority = 2
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing.id
type = "REGULAR"
}
}
The second rules block throws and error in the terraform plan:
Error: Missing resource instance key
on waf.tf line 73, in resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted":
73: rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing.id
Because aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing has "count" set,
its attributes must be accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing[count.index]
However if I add [count.index] :
Error: Reference to "count" in non-counted context
on waf.tf line 73, in resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted":
73: rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing[count.index].id
The "count" object can only be used in "module", "resource", and "data"
blocks, and only when the "count" argument is set.
Is there a way to do this that doesn't use the count param? Or am I missing something in the way that I am using it?
Since there is difference between the prod and non-prod environment, the way this should be tackled is by using dynamic [1] and for_each meta-argument [2]:
resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted" {
name = "${var.envname}-pwa-cloudfront-whitelist"
metric_name = "${var.envname}PWACloudfrontWhitelist"
default_action {
type = "BLOCK"
}
dynamic "rules" {
for_each = var.envtype == "nonprod" ? [1] : []
content {
action {
type = "ALLOW"
}
priority = 1
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted[0].id
type = "REGULAR"
}
}
dynamic "rules" {
for_each = var.envtype == "nonprod" ? [1] : []
content {
action {
type = "ALLOW"
}
priority = 2
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing[0].id
type = "REGULAR"
}
}
}
[1] https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks
[2] https://developer.hashicorp.com/terraform/language/expressions/for

When using Terraform with AWS, how can I set a rate limit on a specific URI path (or regex of a URI path) on an ALB

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

is there any way I associate aws ELB/ALB with WAF ACL using terraform?

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

Terraform - Conditional Data Source

In terraform, is there any way to conditionally use a data source? For example:
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
I'm hoping to be able to pass in an environment variable via the command line, and based on that, determine whether or not to fetch this data source.
I know with resources you can use the count property, but it doesn't seem you can use that with data sources.
I would consider tucking this code away in a module, but modules also can't use the count parameter.
Lastly, another option would be to provide a "Default" value for the data source, if it returned null, but I don't think that's doable either.
Are there any other potential solutions for this?
You can use a conditional on data sources the same as you can with resources and also from Terraform 0.13+ on modules as well:
variable "lookup_ami" {
default = true
}
data "aws_ami" "application" {
count = var.lookup_ami ? 1 : 0
most_recent = true
filter {
name = "tag:environment"
values = [var.environment]
}
owners = ["self"]
}
One use case for this in Terraform 0.12+ is to utilise the lazy evaluation of ternary statements like with the following:
variable "internal" {
default = true
}
data "aws_route53_zone" "private_zone" {
count = var.internal ? 1 : 0
name = var.domain
vpc_id = var.vpc_id
private_zone = var.internal
}
data "aws_route53_zone" "public_zone" {
count = var.internal ? 0 : 1
name = var.domain
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id
name = "www.${var.domain}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would create a record in the private zone when var.internal is true and instead create a record in the public zone when var.internal is false.
For this specific use case you could also use Terraform 0.12+'s null to rewrite this more simply:
variable "internal" {
default = true
}
data "aws_route53_zone" "zone" {
name = var.domain
vpc_id = var.internal ? var.vpc_id : null
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = data.aws_route53_zone.zone.zone_id
name = "www.${data.aws_route53_zone.zone.name}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would only pass the vpc_id parameter to the aws_route53_zone data source if var.internal is set to true as you can't set vpc_id when private_zone is false.
Old Terraform 0.11 and earlier answer:
You can in fact use a conditional on the count of data sources but I've yet to manage to work out a good use case for it when I've tried.
As an example I successfully had this working:
data "aws_route53_zone" "private_zone" {
count = "${var.internal == "true" ? 1 : 0}"
name = "${var.domain}"
vpc_id = "${var.vpc_id}"
private_zone = "true"
}
data "aws_route53_zone" "public_zone" {
count = "${var.internal == "true" ? 0 : 1}"
name = "${var.domain}"
private_zone = "false"
}
But then had issues in how to then select the output of it because Terraform will evaluate any variables in the ternary conditional before deciding which side of the ternary to use (instead of lazy evaluation). So something like this doesn't work:
resource "aws_route53_record" "www" {
zone_id = "${var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id}"
name = "www.example.com"
type = "A"
alias {
name = "${aws_elb.lb.dns_name}"
zone_id = "${aws_elb.lb.zone_id }"
evaluate_target_health = "false"
}
}
Because if internal is true then you get the private_zone data source but not the public_zone data source and so the second half of the ternary fails to evaluate because data.aws_route53_zone.public_zone.zone_id isn't defined and equally with the other way around too.
In your case you probably just want to conditionally use the data source so might be able to do something like this:
variable "dynamic_ami" { default = "true" }
variable "default_ami" { default = "ami-123456" }
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
resource "aws_instance" "app" {
ami = "${var.dynamic_ami == "true" ? data.aws_ami.application.id : var.default_ami}"
instance_type = "t2.micro"
}