waf.tf 10 KB

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