waf.tf 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. # trussworks/wafv2/aws has a basic WAF with the AWS Managed Ruleset
  2. # See https://registry.terraform.io/modules/trussworks/wafv2/aws/latest
  3. #
  4. # Attempted to add some sane defaults so we can customize as needed
  5. #
  6. # IMPORTANT NOTES:
  7. # An 'Allow' action stops processing immediately. Avoid them!
  8. # Goals:
  9. # - US IPs only - https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-geo-match.html
  10. locals {
  11. waf_name = replace(var.fqdns[0], ".", "_")
  12. # A complicated building of managed rules. Each one builds on the previous.
  13. managed_rules_0 = []
  14. managed_rules_1 = var.excluded_set_AWSManagedRulesCommonRuleSet ? local.managed_rules_0 : concat(local.managed_rules_0, [{
  15. "excluded_rules" : var.excluded_rules_AWSManagedRulesCommonRuleSet,
  16. "name" : "AWSManagedRulesCommonRuleSet",
  17. "override_action" : var.block_settings["AWSManagedRulesCommonRuleSet"] ? "none" : "count",
  18. "priority" : 510
  19. }])
  20. managed_rules_2 = var.excluded_set_AWSManagedRulesAmazonIpReputationList ? local.managed_rules_1 : concat(local.managed_rules_1, [{
  21. "excluded_rules" : var.excluded_rules_AWSManagedRulesAmazonIpReputationList,
  22. "name" : "AWSManagedRulesAmazonIpReputationList",
  23. "override_action" : var.block_settings["AWSManagedRulesAmazonIpReputationList"] ? "none" : "count",
  24. "priority" : 520
  25. }])
  26. managed_rules_3 = var.excluded_set_AWSManagedRulesKnownBadInputsRuleSet ? local.managed_rules_2 : concat(local.managed_rules_2, [{
  27. "excluded_rules" : var.excluded_rules_AWSManagedRulesKnownBadInputsRuleSet,
  28. "name" : "AWSManagedRulesKnownBadInputsRuleSet",
  29. "override_action" : var.block_settings["AWSManagedRulesKnownBadInputsRuleSet"] ? "none" : "count",
  30. "priority" : 530
  31. }])
  32. managed_rules_4 = var.excluded_set_AWSManagedRulesSQLiRuleSet ? local.managed_rules_3 : concat(local.managed_rules_3, [{
  33. "excluded_rules" : var.excluded_rules_AWSManagedRulesSQLiRuleSet,
  34. "name" : "AWSManagedRulesSQLiRuleSet",
  35. "override_action" : var.block_settings["AWSManagedRulesSQLiRuleSet"] ? "none" : "count",
  36. "priority" : 540
  37. }])
  38. managed_rules_5 = var.excluded_set_AWSManagedRulesLinuxRuleSet ? local.managed_rules_4 : concat(local.managed_rules_4, [{
  39. "excluded_rules" : var.excluded_rules_AWSManagedRulesLinuxRuleSet,
  40. "name" : "AWSManagedRulesLinuxRuleSet",
  41. "override_action" : var.block_settings["AWSManagedRulesLinuxRuleSet"] ? "none" : "count",
  42. "priority" : 550
  43. }])
  44. managed_rules_6 = var.excluded_set_AWSManagedRulesUnixRuleSet ? local.managed_rules_5 : concat(local.managed_rules_5, [{
  45. "excluded_rules" : var.excluded_rules_AWSManagedRulesUnixRuleSet,
  46. "name" : "AWSManagedRulesUnixRuleSet",
  47. "override_action" : var.block_settings["AWSManagedRulesUnixRuleSet"] ? "none" : "count",
  48. "priority" : 560
  49. }])
  50. managed_rules = local.managed_rules_6
  51. }
  52. resource "aws_wafv2_ip_set" "blocked" {
  53. name = "${local.waf_name}_blocked_ips"
  54. scope = "REGIONAL"
  55. ip_address_version = "IPV4"
  56. addresses = toset(concat(var.additional_blocked_ips, local.blocked_ips))
  57. lifecycle {
  58. create_before_destroy = true
  59. }
  60. }
  61. resource "aws_wafv2_ip_set" "allowed" {
  62. name = "${local.waf_name}_allowed_ips"
  63. scope = "REGIONAL"
  64. ip_address_version = "IPV4"
  65. addresses = var.allowed_ips
  66. lifecycle {
  67. create_before_destroy = true
  68. }
  69. }
  70. resource "aws_wafv2_ip_set" "admin" {
  71. name = "${local.waf_name}_admin_ips"
  72. scope = "REGIONAL"
  73. ip_address_version = "IPV4"
  74. addresses = var.admin_ips
  75. lifecycle {
  76. create_before_destroy = true
  77. }
  78. }
  79. resource "aws_wafv2_rule_group" "xdr_custom_rules" {
  80. name = "${local.waf_name}_xdr_custom_rules_rev4" # update name when updating
  81. scope = "REGIONAL"
  82. capacity = 60
  83. # Note, there is visibilty config for the group and for the rule
  84. visibility_config {
  85. cloudwatch_metrics_enabled = true
  86. metric_name = "${local.waf_name}_xdr_custom_rules"
  87. #metric_name = "xdr_custom_rules"
  88. sampled_requests_enabled = true
  89. }
  90. rule {
  91. name = "Block_Nonpermitted_Countries"
  92. priority = 100
  93. action {
  94. # WAF rule's strange data format makes this a little complex,
  95. # but the end result is that if block_settings["custom"] is
  96. # set to true, it will block. Otherwise, it will count.
  97. dynamic "block" {
  98. for_each = var.block_settings["custom"] ? ["block"] : []
  99. content {}
  100. }
  101. dynamic "count" {
  102. for_each = var.block_settings["custom"] ? [] : ["count"]
  103. content {}
  104. }
  105. }
  106. statement {
  107. not_statement {
  108. statement {
  109. geo_match_statement {
  110. country_codes = [
  111. "US",
  112. "DE",
  113. ]
  114. }
  115. }
  116. }
  117. }
  118. visibility_config {
  119. cloudwatch_metrics_enabled = true
  120. metric_name = "Block_Nonpermitted_Countries"
  121. sampled_requests_enabled = true
  122. }
  123. # rule_label {
  124. # name = "xdr_custom:nonpermittedcountry"
  125. # }
  126. }
  127. rule {
  128. name = "Block_Admin_Unless_Permitted"
  129. priority = 110
  130. action {
  131. # WAF rule's strange data format makes this a little complex,
  132. # but the end result is that if block_settings["custom"] is
  133. # set to true, it will block. Otherwise, it will count.
  134. dynamic "block" {
  135. for_each = var.block_settings["admin"] ? ["block"] : []
  136. content {}
  137. }
  138. dynamic "count" {
  139. for_each = var.block_settings["admin"] ? [] : ["count"]
  140. content {}
  141. }
  142. }
  143. statement {
  144. and_statement {
  145. statement {
  146. not_statement {
  147. statement {
  148. ip_set_reference_statement {
  149. arn = aws_wafv2_ip_set.admin.arn
  150. }
  151. }
  152. }
  153. }
  154. statement {
  155. byte_match_statement {
  156. field_to_match {
  157. uri_path {}
  158. }
  159. positional_constraint = "STARTS_WITH"
  160. search_string = "/admin"
  161. text_transformation {
  162. priority = 1
  163. type = "URL_DECODE"
  164. }
  165. text_transformation {
  166. priority = 2
  167. type = "LOWERCASE"
  168. }
  169. }
  170. }
  171. }
  172. }
  173. visibility_config {
  174. cloudwatch_metrics_enabled = true
  175. metric_name = "Block_Unallowed_Admin_Access"
  176. sampled_requests_enabled = true
  177. }
  178. # rule_label {
  179. # name = "xdr_custom:nonpermittedcountry"
  180. # }
  181. }
  182. # rule {
  183. # name = "Block_log4j_Exploit_20211210"
  184. # action {
  185. # block {}
  186. # }
  187. # priority = 110
  188. #
  189. # #rule_label {
  190. # # name = "xdr_custom:log4j"
  191. # #}
  192. #
  193. # visibility_config {
  194. # cloudwatch_metrics_enabled = true
  195. # metric_name = "Block_Log4j_exploit_20211210"
  196. # sampled_requests_enabled = true
  197. # }
  198. #
  199. # statement {
  200. # or_statement {
  201. # statement {
  202. # byte_match_statement {
  203. # field_to_match {
  204. # single_header {
  205. # name = "user-agent"
  206. # }
  207. # }
  208. # positional_constraint = "STARTS_WITH"
  209. # search_string = "$${jndi:" # ldap://"
  210. #
  211. # text_transformation {
  212. # priority = 1
  213. # type = "BASE64_DECODE"
  214. # }
  215. #
  216. # #text_transformation {
  217. # # priority = 3
  218. # # type = "HEX_DECODE"
  219. # #}
  220. #
  221. # text_transformation {
  222. # priority = 5
  223. # type = "LOWERCASE"
  224. # }
  225. # }
  226. # }
  227. #
  228. ## statement {
  229. ## byte_match_statement {
  230. ## field_to_match {
  231. ## method {}
  232. ## }
  233. ## positional_constraint = "STARTS_WITH"
  234. ## search_string = "$${jndi:" # ldap://"
  235. ##
  236. ## text_transformation {
  237. ## priority = 1
  238. ## type = "BASE64_DECODE"
  239. ## }
  240. ##
  241. ## text_transformation {
  242. ## priority = 3
  243. ## type = "HEX_DECODE"
  244. ## }
  245. ##
  246. ## text_transformation {
  247. ## priority = 5
  248. ## type = "LOWERCASE"
  249. ## }
  250. ## }
  251. ## }
  252. ##
  253. ## statement {
  254. ## byte_match_statement {
  255. ## field_to_match {
  256. ## query_string {}
  257. ## }
  258. ## positional_constraint = "CONTAINS"
  259. ## search_string = "$${jndi:" # ldap://"
  260. ##
  261. ## text_transformation {
  262. ## priority = 1
  263. ## type = "BASE64_DECODE"
  264. ## }
  265. ##
  266. ## #text_transformation {
  267. ## # priority = 3
  268. ## # type = "HEX_DECODE"
  269. ## #}
  270. ##
  271. ## text_transformation {
  272. ## priority = 5
  273. ## type = "LOWERCASE"
  274. ## }
  275. ## }
  276. ## }
  277. #
  278. # statement {
  279. # byte_match_statement {
  280. # field_to_match {
  281. # uri_path {}
  282. # }
  283. # positional_constraint = "CONTAINS"
  284. # search_string = "$${jndi:" # ldap://"
  285. #
  286. # text_transformation {
  287. # priority = 1
  288. # type = "BASE64_DECODE"
  289. # }
  290. #
  291. # #text_transformation {
  292. # # priority = 3
  293. # # type = "HEX_DECODE"
  294. # #}
  295. #
  296. # text_transformation {
  297. # priority = 5
  298. # type = "LOWERCASE"
  299. # }
  300. # }
  301. # }
  302. # }
  303. # }
  304. # }
  305. # Add additional custom rules here
  306. lifecycle {
  307. create_before_destroy = true
  308. }
  309. }
  310. module "wafv2" {
  311. source = "trussworks/wafv2/aws"
  312. version = "= 2.4.0"
  313. name = local.waf_name
  314. scope = "REGIONAL"
  315. alb_arn = var.resource_arn
  316. associate_alb = true
  317. default_action = var.block_settings["default"] ? "block" : "allow" # Note: Even in block, the final action is actually to 'allow' if the host header is correct
  318. filtered_header_rule = {
  319. header_types = var.fqdns
  320. header_value = "host"
  321. priority = 900
  322. action = "allow"
  323. }
  324. # IP based rules are processed first.
  325. # If an IP is blocked, it is blocked no matter what.
  326. # If an IP is allowed, it is allowed only if it's not blocked, but no other WAF rules are processed.
  327. ip_sets_rule = [
  328. {
  329. name = "blocked_ips"
  330. action = "block"
  331. priority = 10
  332. ip_set_arn = aws_wafv2_ip_set.blocked.arn
  333. },
  334. {
  335. name = "allowed_ips"
  336. action = "allow"
  337. priority = 20
  338. ip_set_arn = aws_wafv2_ip_set.allowed.arn
  339. }
  340. ]
  341. # Custom Rules are defined above
  342. group_rules = [
  343. {
  344. name = aws_wafv2_rule_group.xdr_custom_rules.name
  345. arn = aws_wafv2_rule_group.xdr_custom_rules.arn
  346. priority = 100
  347. override_action = var.block_settings["custom"] ? "none" : "count"
  348. excluded_rules = []
  349. }
  350. ]
  351. # A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span
  352. # Because of zscalar, this is completely ineffective for us.
  353. #ip_rate_based_rule = {
  354. # name = "Rate_Limit"
  355. # priority = 200
  356. # limit = 3000 # 6000 requests per 5 minutes= 10 requests/second (sustained for 5 minutes)
  357. # action = "block"
  358. #}
  359. # AWS managed rulesets
  360. # Baseline was from trussworks/wafv2/aws, but copied here to be customized for our use and renumbered.
  361. managed_rules = local.managed_rules
  362. depends_on = [aws_wafv2_rule_group.xdr_custom_rules]
  363. tags = var.tags
  364. }
  365. resource "aws_wafv2_web_acl_logging_configuration" "waf_logs" {
  366. log_destination_configs = ["arn:${var.aws_partition}:firehose:${var.aws_region}:${var.aws_account_id}:deliverystream/aws-waf-logs-splunk"]
  367. resource_arn = module.wafv2.web_acl_id
  368. # logging_filter {
  369. # default_behavior = "KEEP"
  370. #
  371. # filter {
  372. # behavior = "DROP"
  373. #
  374. # condition {
  375. # action_condition {
  376. # action = "COUNT"
  377. # }
  378. # }
  379. #
  380. # condition {
  381. # label_name_condition {
  382. # label_name = "awswaf:111122223333:rulegroup:testRules:LabelNameZ"
  383. # }
  384. # }
  385. #
  386. # requirement = "MEETS_ALL"
  387. # }
  388. #
  389. # filter {
  390. # behavior = "KEEP"
  391. #
  392. # condition {
  393. # action_condition {
  394. # action = "ALLOW"
  395. # }
  396. # }
  397. #
  398. # requirement = "MEETS_ANY"
  399. # }
  400. # }
  401. }