From fc0714e26048764e41fab1601827959137d4033b Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 10 Apr 2024 14:22:33 -0400 Subject: [PATCH 01/57] Removing pre-existing code --- infrastructure/.terraform.lock.hcl | 84 - infrastructure/Makefile | 8 - infrastructure/cloudops_role.tf | 4 - infrastructure/customer_role.tf | 17 - infrastructure/eks.tf | 102 - infrastructure/eks_nodegroups.tf | 576 ---- infrastructure/example.auto.tfvars | 58 - infrastructure/kms.tf | 11 - infrastructure/lb.tf | 0 infrastructure/local.tf | 160 -- infrastructure/main.tf | 23 - infrastructure/outputs.tf | 254 -- infrastructure/rds.tf | 48 - infrastructure/redis.tf | 41 - infrastructure/s3.tf | 2315 ----------------- infrastructure/s3_backend_configuration.conf | 3 - infrastructure/secrets.tfvars | 5 - infrastructure/security_groups.tf | 46 - infrastructure/state.tf | 6 - infrastructure/variables.tf | 693 ----- infrastructure/versions.tf | 9 - infrastructure/vpc.tf | 56 - kubernetes-config/.terraform.lock.hcl | 65 - kubernetes-config/Makefile | 8 - kubernetes-config/eks_sigs.tf | 398 --- kubernetes-config/example.auto.tfvars | 15 - .../helm-charts/aws-ebs-csi-driver-2.14.1.tgz | Bin 11028 -> 0 bytes .../helm-charts/aws-efs-csi-driver-2.3.3.tgz | Bin 6106 -> 0 bytes .../aws-load-balancer-controller-1.4.6.tgz | Bin 21655 -> 0 bytes .../helm-charts/cluster-autoscaler-9.21.1.tgz | Bin 18380 -> 0 bytes .../helm-charts/external-dns-1.11.0.tgz | Bin 6955 -> 0 bytes kubernetes-config/iam/aws-ebs-csi-driver.json | 133 - kubernetes-config/iam/aws-efs-csi-driver.json | 37 - .../iam/aws-load-balancer-controller.json | 219 -- kubernetes-config/iam/cluster-autoscaler.json | 20 - kubernetes-config/iam/external-dns.json | 24 - kubernetes-config/local.tf | 3 - kubernetes-config/main.tf | 47 - kubernetes-config/output.tf | 0 .../s3_backend_configuration.conf | 3 - kubernetes-config/state.tf | 6 - kubernetes-config/variables.tf | 91 - kubernetes-config/versions.tf | 17 - 43 files changed, 5605 deletions(-) delete mode 100644 infrastructure/.terraform.lock.hcl delete mode 100644 infrastructure/Makefile delete mode 100644 infrastructure/cloudops_role.tf delete mode 100644 infrastructure/customer_role.tf delete mode 100644 infrastructure/eks.tf delete mode 100644 infrastructure/eks_nodegroups.tf delete mode 100644 infrastructure/example.auto.tfvars delete mode 100644 infrastructure/kms.tf delete mode 100644 infrastructure/lb.tf delete mode 100644 infrastructure/local.tf delete mode 100644 infrastructure/main.tf delete mode 100644 infrastructure/outputs.tf delete mode 100644 infrastructure/rds.tf delete mode 100644 infrastructure/redis.tf delete mode 100644 infrastructure/s3.tf delete mode 100644 infrastructure/s3_backend_configuration.conf delete mode 100644 infrastructure/secrets.tfvars delete mode 100644 infrastructure/security_groups.tf delete mode 100644 infrastructure/state.tf delete mode 100644 infrastructure/variables.tf delete mode 100644 infrastructure/versions.tf delete mode 100644 infrastructure/vpc.tf delete mode 100644 kubernetes-config/.terraform.lock.hcl delete mode 100644 kubernetes-config/Makefile delete mode 100644 kubernetes-config/eks_sigs.tf delete mode 100644 kubernetes-config/example.auto.tfvars delete mode 100644 kubernetes-config/helm-charts/aws-ebs-csi-driver-2.14.1.tgz delete mode 100644 kubernetes-config/helm-charts/aws-efs-csi-driver-2.3.3.tgz delete mode 100644 kubernetes-config/helm-charts/aws-load-balancer-controller-1.4.6.tgz delete mode 100644 kubernetes-config/helm-charts/cluster-autoscaler-9.21.1.tgz delete mode 100644 kubernetes-config/helm-charts/external-dns-1.11.0.tgz delete mode 100644 kubernetes-config/iam/aws-ebs-csi-driver.json delete mode 100644 kubernetes-config/iam/aws-efs-csi-driver.json delete mode 100644 kubernetes-config/iam/aws-load-balancer-controller.json delete mode 100644 kubernetes-config/iam/cluster-autoscaler.json delete mode 100644 kubernetes-config/iam/external-dns.json delete mode 100644 kubernetes-config/local.tf delete mode 100644 kubernetes-config/main.tf delete mode 100644 kubernetes-config/output.tf delete mode 100644 kubernetes-config/s3_backend_configuration.conf delete mode 100644 kubernetes-config/state.tf delete mode 100644 kubernetes-config/variables.tf delete mode 100644 kubernetes-config/versions.tf diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl deleted file mode 100644 index 2d82e9a..0000000 --- a/infrastructure/.terraform.lock.hcl +++ /dev/null @@ -1,84 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "4.49.0" - constraints = ">= 3.0.0, >= 3.63.0, >= 3.72.0, >= 4.47.0, 4.49.0" - hashes = [ - "h1:1vbzsJ8TBjjotPQKWZQUrX2AII8+te1ih4bYJYD2VS4=", - "zh:09803937f00fdf2873eccf685eec7854408925cbf183c9b683df7c5825be463f", - "zh:2af1575e538fb0b669266f8d1385b17bfdaf17c521b6b6329baa1f2971fc4a4d", - "zh:3f71882b438cde3108fe68cfe2637839d3eed08157a9721bd97babf3912247a8", - "zh:577af1b38f5da8a9f29eebe5eebec9279d26e757cd03b0c8c59311f9ce8a859b", - "zh:60160d39094973beefb9b10cfd6aaa5b63a2e68c32445ecffcd1b101356e6f9b", - "zh:762656454722548baeccf35cbaa23b887976337e1ed321682df7390419fdf22d", - "zh:7f6d7887821659bf3bef815949077dc91ffcdb0d911644a887b6683b264a5ca6", - "zh:8f16a352cc903f8951fa4619c36233b3e66e27d724817b131f2035dd8896f524", - "zh:8f768f65e370366c8b91c00d01c9a6264fe26ea9ae1819f14bdcd12c066272bc", - "zh:95ad78c689a83c08ef7c3e544c3c9aca93ed528054aa77cc968ddd9efa3a1023", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a47097ab6a4ca8302da82964303ffdd2310ed65e8f8524bfe4058816cf1addb7", - "zh:b66d820c70cd5fd628ffe882d2b97e76b969dca4e6827ac2ba0f8d3bc5d6e9c6", - "zh:b80f713a4f3e1355c3dd1600e9d08b9f15ed2370054ec792ad2c01f2541abe02", - "zh:ce065bc3962cb71fa7652562226b9d486f3d7fcb88285c1020ebe2f550e28641", - ] -} - -provider "registry.terraform.io/hashicorp/cloudinit" { - version = "2.2.0" - constraints = ">= 2.0.0" - hashes = [ - "h1:Id6dDkpuSSLbGPTdbw49bVS/7XXHu/+d7CJoGDqtk5g=", - "zh:76825122171f9ea2287fd27e23e80a7eb482f6491a4f41a096d77b666896ee96", - "zh:795a36dee548e30ca9c9d474af9ad6d29290e0a9816154ad38d55381cd0ab12d", - "zh:9200f02cb917fb99e44b40a68936fd60d338e4d30a718b7e2e48024a795a61b9", - "zh:a33cf255dc670c20678063aa84218e2c1b7a67d557f480d8ec0f68bc428ed472", - "zh:ba3c1b2cd0879286c1f531862c027ec04783ece81de67c9a3b97076f1ce7f58f", - "zh:bd575456394428a1a02191d2e46af0c00e41fd4f28cfe117d57b6aeb5154a0fb", - "zh:c68dd1db83d8437c36c92dc3fc11d71ced9def3483dd28c45f8640cfcd59de9a", - "zh:cbfe34a90852ed03cc074601527bb580a648127255c08589bc3ef4bf4f2e7e0c", - "zh:d6ffd7398c6d1f359b96f5b757e77b99b339fbb91df1b96ac974fe71bc87695c", - "zh:d9c15285f847d7a52df59e044184fb3ba1b7679fd0386291ed183782683d9517", - "zh:f7dd02f6d36844da23c9a27bb084503812c29c1aec4aba97237fec16860fdc8c", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.4.3" - constraints = ">= 2.2.0" - hashes = [ - "h1:saZR+mhthL0OZl4SyHXZraxyaBNVMxiZzks78nWcZ2o=", - "zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752", - "zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b", - "zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3", - "zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5", - "zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda", - "zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6", - "zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1", - "zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d", - "zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8", - "zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93", - ] -} - -provider "registry.terraform.io/hashicorp/tls" { - version = "4.0.4" - constraints = ">= 2.2.0" - hashes = [ - "h1:GZcFizg5ZT2VrpwvxGBHQ/hO9r6g0vYdQqx3bFD3anY=", - "zh:23671ed83e1fcf79745534841e10291bbf34046b27d6e68a5d0aab77206f4a55", - "zh:45292421211ffd9e8e3eb3655677700e3c5047f71d8f7650d2ce30242335f848", - "zh:59fedb519f4433c0fdb1d58b27c210b27415fddd0cd73c5312530b4309c088be", - "zh:5a8eec2409a9ff7cd0758a9d818c74bcba92a240e6c5e54b99df68fff312bbd5", - "zh:5e6a4b39f3171f53292ab88058a59e64825f2b842760a4869e64dc1dc093d1fe", - "zh:810547d0bf9311d21c81cc306126d3547e7bd3f194fc295836acf164b9f8424e", - "zh:824a5f3617624243bed0259d7dd37d76017097dc3193dac669be342b90b2ab48", - "zh:9361ccc7048be5dcbc2fafe2d8216939765b3160bd52734f7a9fd917a39ecbd8", - "zh:aa02ea625aaf672e649296bce7580f62d724268189fe9ad7c1b36bb0fa12fa60", - "zh:c71b4cd40d6ec7815dfeefd57d88bc592c0c42f5e5858dcc88245d371b4b8b1e", - "zh:dabcd52f36b43d250a3d71ad7abfa07b5622c69068d989e60b79b2bb4f220316", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - ] -} diff --git a/infrastructure/Makefile b/infrastructure/Makefile deleted file mode 100644 index 45bd889..0000000 --- a/infrastructure/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -init: - terraform init -backend-config=s3_backend_configuration.conf -plan: - terraform plan -var-file="example.auto.tfvars" -var-file="secrets.tfvars" -apply: - terraform apply -var-file="example.auto.tfvars" -var-file="secrets.tfvars" -destroy: - terraform destroy -var-file="example.auto.tfvars" -var-file="secrets.tfvars" diff --git a/infrastructure/cloudops_role.tf b/infrastructure/cloudops_role.tf deleted file mode 100644 index f0ec55c..0000000 --- a/infrastructure/cloudops_role.tf +++ /dev/null @@ -1,4 +0,0 @@ -data "aws_iam_roles" "cloudops_iam_role" { - name_regex = "AWSReservedSSO_AdministratorAccess_.*" - path_prefix = "/aws-reserved/sso.amazonaws.com/" -} diff --git a/infrastructure/customer_role.tf b/infrastructure/customer_role.tf deleted file mode 100644 index 07fda19..0000000 --- a/infrastructure/customer_role.tf +++ /dev/null @@ -1,17 +0,0 @@ -data "aws_iam_policy_document" "aws_cluster_access" { - statement { - actions = ["sts:AssumeRole"] - - principals { - type = "AWS" - identifiers = ["${var.iam_role.customer_arn}"] - } - } -} - -resource "aws_iam_role" "customer_iam_role" { - count = "${var.iam_role.customer_arn}" != "" ? 1 : 0 - - name = "${var.deployment_id}-iam-role" - assume_role_policy = data.aws_iam_policy_document.aws_cluster_access.json -} \ No newline at end of file diff --git a/infrastructure/eks.tf b/infrastructure/eks.tf deleted file mode 100644 index 5553eeb..0000000 --- a/infrastructure/eks.tf +++ /dev/null @@ -1,102 +0,0 @@ -module "eks" { - source = "terraform-aws-modules/eks/aws" - version = "19.5.1" - - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - cluster_enabled_log_types = ["audit", "api", "authenticator", "scheduler"] - - cluster_endpoint_private_access = true - cluster_endpoint_public_access = true - - vpc_id = local.vpc_id - subnet_ids = local.subnets - - enable_irsa = true - - cluster_addons = { - coredns = { - most_recent = true - } - kube-proxy = { - most_recent = true - } - vpc-cni = { - most_recent = true - } - } - - create_kms_key = false - cluster_encryption_config = { - "resources" = ["secrets"] - provider_key_arn = local.kms_arn - } - - # EKS Managed Node Group(s) - eks_managed_node_group_defaults = { - vpc_security_group_ids = [ - module.eks.cluster_security_group_id, - local.sig_k8s_to_dbs_id - ] - } - - # aws-auth configmap - create_aws_auth_configmap = true - manage_aws_auth_configmap = true - - aws_auth_roles = [ - # { - # rolearn = "${var.iam_role.cloudops_arn}" == true ? element(tolist(data.aws_iam_roles.cloudops_iam_role.arns),0) : aws_iam_role.customer_iam_role[0].arn - # username = "AWSAdministratorAccess:{{SessionName}}" - # groups = ["system:masters"] - # }, - { - rolearn = module.ast_default.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.ast_sast_engines.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.ast_sast_medium_engines.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.ast_sast_large_engines.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.ast_sast_extra_large_engines.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.ast_sast_xxl_engines.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.kics_nodes_engines.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.minio_gateway_nodes.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - { - rolearn = module.repostore_nodes.iam_role_arn - username = "system:node:{{EC2PrivateDNSName}}" - groups = ["system:bootstrappers", "system:nodes"] - }, - ] - - depends_on = [aws_kms_key.eks] -} \ No newline at end of file diff --git a/infrastructure/eks_nodegroups.tf b/infrastructure/eks_nodegroups.tf deleted file mode 100644 index f967de3..0000000 --- a/infrastructure/eks_nodegroups.tf +++ /dev/null @@ -1,576 +0,0 @@ -# Workaround for the issues that tags are not propagated from EKS managed node group to auto-scaling groups -#https://github.com/terraform-aws-modules/terraform-aws-eks/issues/1886#issuecomment-1044154307 - -# AST Default -module "ast_default" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.ast_nodes.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.ast_nodes.min_size - max_size = var.ast_nodes.max_size - desired_size = var.ast_nodes.desired_size - - instance_types = var.ast_nodes.instance_types - capacity_type = var.ast_nodes.capacity_type - - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.ast_nodes.device_name - ebs = { - volume_size = var.ast_nodes.disk_size_gib - volume_type = var.ast_nodes.volume_type - iops = var.ast_nodes.disk_iops - throughput = var.ast_nodes.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - tags = { - Name = "${var.ast_nodes.name}-${local.deployment_id}" - } - -} -resource "aws_autoscaling_group_tag" "ast_default" { - for_each = { for k, v in local.cluster_autoscaler_ast_default_asg_tags : k => v } - - autoscaling_group_name = module.ast_default.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# sast_nodes-m5.2xlarge -module "ast_sast_engines" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.sast_nodes.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.sast_nodes.min_size - max_size = var.sast_nodes.max_size - desired_size = var.sast_nodes.desired_size - - instance_types = var.sast_nodes.instance_types - capacity_type = var.sast_nodes.capacity_type - - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.sast_nodes_large.device_name - ebs = { - volume_size = var.sast_nodes.disk_size_gib - volume_type = var.sast_nodes.volume_type - iops = var.sast_nodes.disk_iops - throughput = var.sast_nodes.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - labels = { - "${var.sast_nodes.label_name}" = var.sast_nodes.label_value - } - - taints = { - dedicated = { - key = var.sast_nodes.key - value = var.sast_nodes.value - effect = var.sast_nodes.effect - } - } - - tags = { - Name = "${var.sast_nodes.name}-${local.deployment_id}" - } -} - -resource "aws_autoscaling_group_tag" "ast_sast_engines" { - for_each = { for k, v in local.cluster_autoscaler_ast_sast_engines_asg_tags : k => v } - - autoscaling_group_name = module.ast_sast_engines.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# sast_nodes_medium-m6a.xlarge -module "ast_sast_medium_engines" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.sast_nodes_medium.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.sast_nodes_medium.min_size - max_size = var.sast_nodes_medium.max_size - desired_size = var.sast_nodes_medium.desired_size - - instance_types = var.sast_nodes_medium.instance_types - capacity_type = var.sast_nodes_medium.capacity_type - - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.sast_nodes_medium.device_name - ebs = { - volume_size = var.sast_nodes_medium.disk_size_gib - volume_type = var.sast_nodes_medium.volume_type - iops = var.sast_nodes_medium.disk_iops - throughput = var.sast_nodes_medium.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - taints = { - dedicated = { - key = var.sast_nodes_medium.key - value = var.sast_nodes_medium.value - effect = var.sast_nodes_medium.effect - } - } - - labels = { - "${var.sast_nodes_medium.label_name}" = var.sast_nodes_medium.label_value - } - tags = { - Name = "${var.sast_nodes_medium.name}-${local.deployment_id}" - } -} - -resource "aws_autoscaling_group_tag" "ast_sast_medium_engines" { - for_each = { for k, v in local.cluster_autoscaler_ast_sast_medium_engines_asg_tags : k => v } - - autoscaling_group_name = module.ast_sast_medium_engines.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# sast_nodes_large-m5.2xlarge -module "ast_sast_large_engines" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.sast_nodes_large.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.sast_nodes_large.min_size - max_size = var.sast_nodes_large.max_size - desired_size = var.sast_nodes_large.desired_size - - instance_types = var.sast_nodes_large.instance_types - capacity_type = var.sast_nodes_large.capacity_type - - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.sast_nodes_large.device_name - ebs = { - volume_size = var.sast_nodes_large.disk_size_gib - volume_type = var.sast_nodes_large.volume_type - iops = var.sast_nodes_large.disk_iops - throughput = var.sast_nodes_large.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - taints = { - dedicated = { - key = var.sast_nodes_large.key - value = var.sast_nodes_large.value - effect = var.sast_nodes_large.effect - } - } - - labels = { - "${var.sast_nodes_large.label_name}" = var.sast_nodes_large.label_value - } - tags = { - Name = "${var.sast_nodes_large.name}-${local.deployment_id}" - } -} - -resource "aws_autoscaling_group_tag" "ast_sast_large_engines" { - for_each = { for k, v in local.cluster_autoscaler_ast_sast_large_engines_asg_tags : k => v } - - autoscaling_group_name = module.ast_sast_large_engines.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# sast_nodes_extra_large-r5.2xlarge -module "ast_sast_extra_large_engines" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.sast_nodes_extra_large.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.sast_nodes_extra_large.min_size - max_size = var.sast_nodes_extra_large.max_size - desired_size = var.sast_nodes_extra_large.desired_size - - instance_types = var.sast_nodes_extra_large.instance_types - capacity_type = var.sast_nodes_extra_large.capacity_type - - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.sast_nodes_extra_large.device_name - ebs = { - volume_size = var.sast_nodes_extra_large.disk_size_gib - volume_type = var.sast_nodes_extra_large.volume_type - iops = var.sast_nodes_extra_large.disk_iops - throughput = var.sast_nodes_extra_large.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - taints = { - dedicated = { - key = var.sast_nodes_extra_large.key - value = var.sast_nodes_extra_large.value - effect = var.sast_nodes_extra_large.effect - } - } - labels = { - "${var.sast_nodes_extra_large.label_name}" = var.sast_nodes_extra_large.label_value - } - tags = { - Name = "${var.sast_nodes_extra_large.name}-${local.deployment_id}" - } -} -resource "aws_autoscaling_group_tag" "ast_sast_extra_large_engines" { - for_each = { for k, v in local.cluster_autoscaler_ast_sast_extra_large_engines_asg_tags : k => v } - - autoscaling_group_name = module.ast_sast_extra_large_engines.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# sast_nodes_xxl-r5.4xlarge -module "ast_sast_xxl_engines" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.sast_nodes_xxl.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.sast_nodes_xxl.min_size - max_size = var.sast_nodes_xxl.max_size - desired_size = var.sast_nodes_xxl.desired_size - - instance_types = var.sast_nodes_xxl.instance_types - capacity_type = var.sast_nodes_xxl.capacity_type - - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.sast_nodes_xxl.device_name - ebs = { - volume_size = var.sast_nodes_xxl.disk_size_gib - volume_type = var.sast_nodes_xxl.volume_type - iops = var.sast_nodes_xxl.disk_iops - throughput = var.sast_nodes_xxl.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - taints = { - dedicated = { - key = var.sast_nodes_xxl.key - value = var.sast_nodes_xxl.value - effect = var.sast_nodes_xxl.effect - } - } - labels = { - "${var.sast_nodes_xxl.label_name}" = var.sast_nodes_xxl.label_value - } - tags = { - Name = "${var.sast_nodes_xxl.name}-${local.deployment_id}" - } -} - -resource "aws_autoscaling_group_tag" "ast_sast_xxl_engines" { - for_each = { for k, v in local.cluster_autoscaler_ast_sast_xxl_engines_asg_tags : k => v } - - autoscaling_group_name = module.ast_sast_xxl_engines.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# Kics -module "kics_nodes_engines" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.kics_nodes.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.kics_nodes.min_size - max_size = var.kics_nodes.max_size - desired_size = var.kics_nodes.desired_size - - instance_types = var.kics_nodes.instance_types - capacity_type = var.kics_nodes.capacity_type - - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.kics_nodes.device_name - ebs = { - volume_size = var.kics_nodes.disk_size_gib - volume_type = var.kics_nodes.volume_type - iops = var.kics_nodes.disk_iops - throughput = var.kics_nodes.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - taints = { - dedicated = { - key = var.kics_nodes.key - value = var.kics_nodes.value - effect = var.kics_nodes.effect - } - } - labels = { - "${var.kics_nodes.label_name}" = var.kics_nodes.label_value - } - tags = { - Name = "${var.kics_nodes.name}-${local.deployment_id}" - } -} - -resource "aws_autoscaling_group_tag" "kics_nodes_engines" { - for_each = { for k, v in local.cluster_autoscaler_kics_nodes_engines_asg_tags : k => v } - - autoscaling_group_name = module.kics_nodes_engines.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# MINIO GATEWAY -module "minio_gateway_nodes" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.minio_gateway_nodes.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.minio_gateway_nodes.min_size - max_size = var.minio_gateway_nodes.max_size - desired_size = var.minio_gateway_nodes.desired_size - - instance_types = var.minio_gateway_nodes.instance_types - capacity_type = var.minio_gateway_nodes.capacity_type - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.minio_gateway_nodes.device_name - ebs = { - volume_size = var.minio_gateway_nodes.disk_size_gib - volume_type = var.minio_gateway_nodes.volume_type - iops = var.minio_gateway_nodes.disk_iops - throughput = var.minio_gateway_nodes.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - taints = { - dedicated = { - key = var.minio_gateway_nodes.key - value = var.minio_gateway_nodes.value - effect = var.minio_gateway_nodes.effect - } - } - labels = { - "${var.minio_gateway_nodes.label_name}" = var.minio_gateway_nodes.label_value - } - tags = { - Name = "${var.minio_gateway_nodes.name}-${local.deployment_id}" - } -} - -resource "aws_autoscaling_group_tag" "minio_gateway_nodes" { - for_each = { for k, v in local.cluster_autoscaler_minio_gateway_nodes_asg_tags : k => v } - - autoscaling_group_name = module.minio_gateway_nodes.node_group_autoscaling_group_names[0] - tag { - key = each.value.tagKey - value = each.value.tagValue - propagate_at_launch = false - } -} - -# REPOSTORE -module "repostore_nodes" { - source = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group" - - name = var.repostore_nodes.name - cluster_name = local.deployment_id - cluster_version = var.eks_cluster_version - - subnet_ids = local.subnets - - cluster_primary_security_group_id = module.eks.cluster_primary_security_group_id - - min_size = var.repostore_nodes.min_size - max_size = var.repostore_nodes.max_size - desired_size = var.repostore_nodes.desired_size - - instance_types = var.repostore_nodes.instance_types - capacity_type = var.repostore_nodes.capacity_type - metadata_options = { - http_endpoint = "enabled" - http_tokens = "optional" - instance_metadata_tags = "disabled" - http_put_response_hop_limit = "2" - } - - block_device_mappings = { - xvda = { - device_name = var.repostore_nodes.device_name - ebs = { - volume_size = var.repostore_nodes.disk_size_gib - volume_type = var.repostore_nodes.volume_type - iops = var.repostore_nodes.disk_iops - throughput = var.repostore_nodes.disk_throughput - encrypted = true - delete_on_termination = true - } - } - } - - taints = { - dedicated = { - key = var.repostore_nodes.key - value = var.repostore_nodes.value - effect = var.repostore_nodes.effect - } - } - labels = { - "${var.repostore_nodes.label_name}" = var.repostore_nodes.label_value - } - tags = { - Name = "${var.repostore_nodes.name}-${local.deployment_id}" - } -} \ No newline at end of file diff --git a/infrastructure/example.auto.tfvars b/infrastructure/example.auto.tfvars deleted file mode 100644 index a698a27..0000000 --- a/infrastructure/example.auto.tfvars +++ /dev/null @@ -1,58 +0,0 @@ - -# METADATA -deployment_id = "" -environment = "" -owner = "" -aws_profile = "" -aws_region = "" - -#VPC -vpc_cidr = "" - -vpc = { - create = true - single_nat = true - nat_per_az = false - existing_vpc_id = "" - existing_subnet_ids = [] - existing_db_subnets_group = "" - existing_db_subnets = [] -} - -# Security -sig = { - create = true - existing_sig_k8s_to_dbs_id = "" -} - -# IAM-ROLE -iam_role = { - cloudops_arn = true - customer_arn = "" -} - -# KMS -kms = { - create = true - existing_kms_arn = "" -} - -# EKS -eks_cluster_version = "1.24" - -# RDS -postgres_nodes = { - create = true - auto_scaling_enable = false - count = 1 - max_count = 0 - instance_type = "db.r6g.xlarge" -} - -# REDIS -redis_nodes = { - create = true - instance_type = "cache.t4g.medium" - number_of_shards = 3 - replicas_per_shard = 1 -} diff --git a/infrastructure/kms.tf b/infrastructure/kms.tf deleted file mode 100644 index 28ddc97..0000000 --- a/infrastructure/kms.tf +++ /dev/null @@ -1,11 +0,0 @@ -resource "aws_kms_key" "eks" { - count = var.kms.create ? 1 : 0 - description = "EKS Secret Encryption Key" - deletion_window_in_days = 7 - enable_key_rotation = true -} - - -locals { - kms_arn = var.kms.create == true ? aws_kms_key.eks[0].arn : var.kms.existing_kms_arn -} \ No newline at end of file diff --git a/infrastructure/lb.tf b/infrastructure/lb.tf deleted file mode 100644 index e69de29..0000000 diff --git a/infrastructure/local.tf b/infrastructure/local.tf deleted file mode 100644 index 97aeec4..0000000 --- a/infrastructure/local.tf +++ /dev/null @@ -1,160 +0,0 @@ -locals { - deployment_id = var.deployment_id - - #ast-default - cluster_autoscaler_ast_default_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - #ast_sast_engines - cluster_autoscaler_ast_sast_engines_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/label/${var.sast_nodes.label_name}" - tagValue = var.sast_nodes.label_value - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/taint/${var.sast_nodes.key}" - tagValue = "${var.sast_nodes.value}:${var.sast_nodes_large.effect}" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - #sast_nodes_medium - cluster_autoscaler_ast_sast_medium_engines_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/label/${var.sast_nodes_medium.label_name}" - tagValue = var.sast_nodes_medium.label_value - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/taint/${var.sast_nodes_medium.key}" - tagValue = "${var.sast_nodes_medium.value}:${var.sast_nodes_medium.effect}" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - #sast_nodes_large - cluster_autoscaler_ast_sast_large_engines_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/label/${var.sast_nodes_large.label_name}" - tagValue = var.sast_nodes_large.label_value - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/taint/${var.sast_nodes_large.key}" - tagValue = "${var.sast_nodes_large.value}:${var.sast_nodes_large.effect}" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - #sast_nodes_extra_large - cluster_autoscaler_ast_sast_extra_large_engines_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/label/${var.sast_nodes_extra_large.label_name}" - tagValue = var.sast_nodes_extra_large.label_value - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/taint/${var.sast_nodes_extra_large.key}" - tagValue = "${var.sast_nodes_extra_large.value}:${var.sast_nodes_extra_large.effect}" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - #sast_nodes_xxl - cluster_autoscaler_ast_sast_xxl_engines_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/label/${var.sast_nodes_xxl.label_name}" - tagValue = var.sast_nodes_xxl.label_value - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/taint/${var.sast_nodes_xxl.key}" - tagValue = "${var.sast_nodes_xxl.value}:${var.sast_nodes_xxl.effect}" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - #Kics - cluster_autoscaler_kics_nodes_engines_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/label/${var.kics_nodes.label_name}" - tagValue = var.kics_nodes.label_value - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/taint/${var.kics_nodes.key}" - tagValue = "${var.kics_nodes.value}:${var.kics_nodes.effect}" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - # MINIO - cluster_autoscaler_minio_gateway_nodes_asg_tags = [ - { - tagKey = "k8s.io/cluster-autoscaler/enabled" - tagValue = "true" - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/label/${var.minio_gateway_nodes.label_name}" - tagValue = var.minio_gateway_nodes.label_value - }, - { - tagKey = "k8s.io/cluster-autoscaler/node-template/taint/${var.minio_gateway_nodes.key}" - tagValue = "${var.minio_gateway_nodes.value}:${var.minio_gateway_nodes.effect}" - }, - { - tagKey = "k8s.io/cluster-autoscaler/${var.deployment_id}" - tagValue = "owned" - }, - ] - - private_subnets = slice(cidrsubnets(var.vpc_cidr, 2, 2, 2, 5, 5, 5, 6, 6, 6), 0, 3) - public_subnets = slice(cidrsubnets(var.vpc_cidr, 2, 2, 2, 5, 5, 5, 6, 6, 6), 6, 9) - database_subnets = slice(cidrsubnets(var.vpc_cidr, 2, 2, 2, 5, 5, 5, 6, 6, 6), 3, 6) - -} \ No newline at end of file diff --git a/infrastructure/main.tf b/infrastructure/main.tf deleted file mode 100644 index 39b9e21..0000000 --- a/infrastructure/main.tf +++ /dev/null @@ -1,23 +0,0 @@ -provider "aws" { - region = var.aws_region - profile = var.aws_profile - - default_tags { - tags = { - Terraform = "true" - DeploymentID = var.deployment_id - Owner = var.owner - Environment = var.environment - } - } -} - -provider "kubernetes" { - host = module.eks.cluster_endpoint - cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) - exec { - api_version = "client.authentication.k8s.io/v1beta1" - args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name] - command = "aws" - } -} \ No newline at end of file diff --git a/infrastructure/outputs.tf b/infrastructure/outputs.tf deleted file mode 100644 index a39cd0c..0000000 --- a/infrastructure/outputs.tf +++ /dev/null @@ -1,254 +0,0 @@ -output "aws_region" { - description = "AWS Region where the infra was deployed" - value = var.aws_region -} -output "postgres-primary-endpoint" { - description = "Postgres cluster primary endpoint" - value = module.rds-aurora.cluster_endpoint -} - -output "postgres-reader-endpoint" { - description = "Postgres cluster reader endpoint" - value = module.rds-aurora.cluster_reader_endpoint -} - -output "postgres-database-name" { - description = "Name of the database" - value = nonsensitive(var.database_name) -} - -output "redis-private-endpoint" { - description = "Redis cluster private endpoint" - value = try(aws_elasticache_replication_group.redis[0].configuration_endpoint_address, null) -} - -output "eks_cluster_id" { - value = module.eks.cluster_name - description = "EKS Cluster ID" -} - -output "eks_cluster_arn" { - value = module.eks.cluster_arn - description = "EKS Cluster ARN" -} - -output "eks_cluster_endpoint" { - value = module.eks.cluster_endpoint - description = "EKS Cluster Endpoint" -} - -output "oidc_provider_arn" { - value = module.eks.oidc_provider_arn -} -# Nodegroups -# AST -output "ast_default_autoscaling_group_name" { - value = module.ast_default.node_group_autoscaling_group_names[0] -} -output "ast_default_autoscaling_group_min_size" { - value = var.ast_nodes.min_size -} -output "ast_default_autoscaling_group_max_size" { - value = var.ast_nodes.max_size -} - -# SAST -output "ast_sast_engines_autoscaling_group_name" { - value = module.ast_sast_engines.node_group_autoscaling_group_names[0] -} -output "ast_sast_engines_autoscaling_group_min_size" { - value = var.sast_nodes.min_size -} -output "ast_sast_engines_autoscaling_group_max_size" { - value = var.sast_nodes.max_size -} - -# SAST Medium -output "ast_sast_medium_engines_autoscaling_group_name" { - value = module.ast_sast_medium_engines.node_group_autoscaling_group_names[0] -} -output "ast_sast_medium_engines_autoscaling_group_min_size" { - value = var.sast_nodes_medium.min_size -} -output "ast_sast_medium_engines_autoscaling_group_max_size" { - value = var.sast_nodes_medium.max_size -} - -# SAST Large -output "ast_sast_large_engines_autoscaling_group_name" { - value = module.ast_sast_large_engines.node_group_autoscaling_group_names[0] -} -output "ast_sast_large_engines_autoscaling_group_min_size" { - value = var.sast_nodes_large.min_size -} -output "ast_sast_large_engines_autoscaling_group_max_size" { - value = var.sast_nodes_large.max_size -} - -# SAST ExtraLarge -output "ast_sast_extra_large_engines_autoscaling_group_name" { - value = module.ast_sast_extra_large_engines.node_group_autoscaling_group_names[0] -} -output "ast_sast_extra_large_engines_autoscaling_group_min_size" { - value = var.sast_nodes_extra_large.min_size -} -output "ast_sast_extra_large_engines_autoscaling_group_max_size" { - value = var.sast_nodes_extra_large.max_size -} - -# SAST XXL -output "ast_sast_xxl_engines_autoscaling_group_name" { - value = module.ast_sast_xxl_engines.node_group_autoscaling_group_names[0] -} -output "ast_sast_xxl_engines_autoscaling_group_min_size" { - value = var.sast_nodes_xxl.min_size -} -output "ast_sast_xxl_engines_autoscaling_group_max_size" { - value = var.sast_nodes_xxl.max_size -} - -# KICS -output "kics_nodes_engines_autoscaling_group_name" { - value = module.kics_nodes_engines.node_group_autoscaling_group_names[0] -} -output "kics_nodes_engines_autoscaling_group_min_size" { - value = var.kics_nodes.min_size -} -output "kics_nodes_engines_autoscaling_group_max_size" { - value = var.kics_nodes.max_size -} - -# MINIO -output "minio_gateway_nodes_autoscaling_group_name" { - value = module.minio_gateway_nodes.node_group_autoscaling_group_names[0] -} -output "minio_gateway_nodes_autoscaling_group_min_size" { - value = var.minio_gateway_nodes.min_size -} -output "minio_gateway_nodes_autoscaling_group_max_size" { - value = var.minio_gateway_nodes.max_size -} - -# UPLOADS BUCKET -output "uploads_s3_bucket_name" { - value = aws_s3_bucket.uploads_bucket.id - description = "S3 Bucket Name" -} -# QUERIES BUCKET -output "queries_s3_bucket_name" { - value = aws_s3_bucket.queries_bucket.id - description = "S3 Bucket Name" -} -# MISC BUCKET -output "misc_s3_bucket_name" { - value = aws_s3_bucket.misc_bucket.id - description = "S3 Bucket Name" -} - -# REPOSTORE BUCKET -output "repostore_s3_bucket_name" { - value = aws_s3_bucket.repostore_bucket.id - description = "S3 Bucket Name" -} - -# SAST-METADATA BUCKET -output "sast_metadata_s3_bucket_name" { - value = aws_s3_bucket.sast_metadata_bucket.id - description = "S3 Bucket Name" -} - -# SCANS BUCKET -output "scans_s3_bucket_name" { - value = aws_s3_bucket.scans_bucket.id - description = "S3 Bucket Name" -} - -# SAST-WORKER BUCKET -output "sast_worker_s3_bucket_name" { - value = aws_s3_bucket.sast_worker_bucket.id - description = "S3 Bucket Name" -} - -# KICS-WORKER BUCKET -output "kics_worker_s3_bucket_name" { - value = aws_s3_bucket.kics_worker_bucket.id - description = "S3 Bucket Name" -} - -# SCA-WORKER BUCKET -output "sca_worker_s3_bucket_name" { - value = aws_s3_bucket.sca_worker_bucket.id - description = "S3 Bucket Name" -} - -# LOGS BUCKET -output "logs_s3_bucket_name" { - value = aws_s3_bucket.logs_bucket.id - description = "S3 Bucket Name" -} - -# ENGINE-LOGS BUCKET -output "engine_logs_s3_bucket_name" { - value = aws_s3_bucket.engine_logs_bucket.id - description = "S3 Bucket Name" -} - -# REPORTS BUCKET -output "reports_s3_bucket_name" { - value = aws_s3_bucket.reports_bucket.id - description = "S3 Bucket Name" -} - -# REPORT-TEMPLATES BUCKET -output "report_templates_s3_bucket_name" { - value = aws_s3_bucket.report_templates_bucket.id - description = "S3 Bucket Name" -} - -# CONFIGURATION BUCKET -output "configuration_s3_bucket_name" { - value = aws_s3_bucket.configuration_bucket.id - description = "S3 Bucket Name" -} - -# IMPORTS BUCKET -output "imports_s3_bucket_name" { - value = aws_s3_bucket.imports_bucket.id - description = "S3 Bucket Name" -} - -# AUDIT BUCKET -output "audit_s3_bucket_name" { - value = aws_s3_bucket.audit_bucket.id - description = "S3 Bucket Name" -} - -# SOURCE-RESOLVER BUCKET -output "source_resolver_s3_bucket_name" { - value = aws_s3_bucket.source_resolver_bucket.id - description = "S3 Bucket Name" -} - -# APISEC BUCET -output "apisec_s3_bucket_name" { - value = aws_s3_bucket.apisec_bucket.id - description = "S3 Bucket Name" -} - -# KICS-MATADATA BUCKET -output "kics_metadata_s3_bucket_name" { - value = aws_s3_bucket.kics_metadata_bucket.id - description = "S3 Bucket Name" -} - -# REDIS-SHARED-BUCKET -output "redis_shared_s3_bucket_name" { - value = aws_s3_bucket.redis_shared_bucket.id - description = "S3 Bucket Name" -} - -# SCAN RESULTS STORAGE BUCKET -output "scan_results_storage_s3_bucket_name" { - value = aws_s3_bucket.scan_results_storage_bucket.id - description = "S3 Bucket Name" -} diff --git a/infrastructure/rds.tf b/infrastructure/rds.tf deleted file mode 100644 index fa2829a..0000000 --- a/infrastructure/rds.tf +++ /dev/null @@ -1,48 +0,0 @@ -module "rds-aurora" { - source = "terraform-aws-modules/rds-aurora/aws" - version = "6.1.4" - create_cluster = var.postgres_nodes.create - - vpc_id = local.vpc_id - create_db_subnet_group = false - db_subnet_group_name = local.db_subnet_group - vpc_security_group_ids = [ - local.sig_k8s_to_dbs_id - ] - - name = local.deployment_id - - engine = "aurora-postgresql" - engine_mode = "provisioned" - engine_version = "13.8" - - create_security_group = false - allowed_cidr_blocks = module.vpc.private_subnets_cidr_blocks - - instance_class = var.postgres_nodes.instance_type - instances = { - 1 = { - instance_class = var.postgres_nodes.instance_type - publicly_accessible = false - } - } - - autoscaling_enabled = var.postgres_nodes.auto_scaling_enable - autoscaling_min_capacity = var.postgres_nodes.count - autoscaling_max_capacity = var.postgres_nodes.max_count - - storage_encrypted = true - kms_key_id = local.kms_arn - - apply_immediately = true - skip_final_snapshot = true - auto_minor_version_upgrade = true - performance_insights_enabled = true - iam_database_authentication_enabled = false - - master_username = var.database_username - master_password = var.database_password - create_random_password = false - database_name = var.database_name - -} \ No newline at end of file diff --git a/infrastructure/redis.tf b/infrastructure/redis.tf deleted file mode 100644 index a616f4b..0000000 --- a/infrastructure/redis.tf +++ /dev/null @@ -1,41 +0,0 @@ -resource "aws_elasticache_subnet_group" "redis" { - count = var.redis_nodes.create ? 1 : 0 - name = local.deployment_id - subnet_ids = local.db_subnets -} - -# tfsec:ignore:aws-elasticache-enable-in-transit-encryption -resource "aws_elasticache_replication_group" "redis" { - count = var.redis_nodes.create ? 1 : 0 - replication_group_id = local.deployment_id - description = "Redis cluster for AST application" - - subnet_group_name = aws_elasticache_subnet_group.redis[0].name - - security_group_ids = [ - local.sig_k8s_to_dbs_id - ] - - node_type = var.redis_nodes.instance_type - - engine = "redis" - engine_version = "6.x" - - port = 6379 - snapshot_retention_limit = 2 - - automatic_failover_enabled = true - - replicas_per_node_group = var.redis_nodes.replicas_per_shard - num_node_groups = var.redis_nodes.number_of_shards - - transit_encryption_enabled = var.redis_auth_token != "" ? true : false #BUG - AST can't work with TLS enabled - auth_token = var.redis_auth_token != "" ? var.redis_auth_token : null - - kms_key_id = local.kms_arn - at_rest_encryption_enabled = true - - - apply_immediately = true - -} \ No newline at end of file diff --git a/infrastructure/s3.tf b/infrastructure/s3.tf deleted file mode 100644 index 5fe6c13..0000000 --- a/infrastructure/s3.tf +++ /dev/null @@ -1,2315 +0,0 @@ -resource "random_string" "random_suffix" { - length = 6 - special = false - upper = false -} - -locals { - s3_bucket_name_suffix = "${var.deployment_id}-${random_string.random_suffix.result}" -} - -# UPLOADS BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "uploads_bucket" { - bucket = "uploads-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} uploads bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "uploads_ownership_controls" { - bucket = aws_s3_bucket.uploads_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "uploads_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.uploads_ownership_controls] - - bucket = aws_s3_bucket.uploads_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "uploads_bucket_versioning" { - bucket = aws_s3_bucket.uploads_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "uploads_bucket_encryption_configuration" { - bucket = aws_s3_bucket.uploads_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "uploads_bucket_lifecycle" { - bucket = aws_s3_bucket.uploads_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "uploads_public_access_block" { - bucket = aws_s3_bucket.uploads_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "uploads_permissive_access" { - bucket = aws_s3_bucket.uploads_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.uploads_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.uploads_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# QUERIES BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "queries_bucket" { - bucket = "queries-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} queries bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "queries_ownership" { - bucket = aws_s3_bucket.queries_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "queries_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.queries_ownership] - - bucket = aws_s3_bucket.queries_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "queries_bucket_versioning" { - bucket = aws_s3_bucket.queries_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "queries_bucket_encryption_configuration" { - bucket = aws_s3_bucket.queries_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "queries_bucket_lifecycle" { - bucket = aws_s3_bucket.queries_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "queries_public_access_block" { - bucket = aws_s3_bucket.queries_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "queries_permissive_access" { - bucket = aws_s3_bucket.queries_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.queries_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.queries_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# MISC BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "misc_bucket" { - bucket = "misc-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} misc bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "misc_ownership" { - bucket = aws_s3_bucket.misc_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "misc_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.misc_ownership] - - bucket = aws_s3_bucket.misc_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "misc_bucket_versioning" { - bucket = aws_s3_bucket.misc_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "misc_bucket_encryption_configuration" { - bucket = aws_s3_bucket.misc_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "misc_bucket_lifecycle" { - bucket = aws_s3_bucket.misc_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - - - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "misc_public_access_block" { - bucket = aws_s3_bucket.misc_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "misc_permissive_access" { - bucket = aws_s3_bucket.misc_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.misc_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.misc_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# REPOSTORE BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "repostore_bucket" { - bucket = "repostore-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} repostore bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "repostore_ownership_controls" { - bucket = aws_s3_bucket.repostore_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "repostore_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.repostore_ownership_controls] - - bucket = aws_s3_bucket.repostore_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "repostore_bucket_versioning" { - bucket = aws_s3_bucket.repostore_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "repostore_bucket_encryption_configuration" { - bucket = aws_s3_bucket.repostore_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "repostore_bucket_lifecycle" { - bucket = aws_s3_bucket.repostore_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "repostore_public_access_block" { - bucket = aws_s3_bucket.repostore_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "repostore_permissive_access" { - bucket = aws_s3_bucket.repostore_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.repostore_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.repostore_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# SAST-METADATA BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "sast_metadata_bucket" { - bucket = "sast-metadata-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} sast metadata bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "sast_metadata_ownership_controls" { - bucket = aws_s3_bucket.sast_metadata_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "sast_metadata_acl" { - depends_on = [aws_s3_bucket_ownership_controls.sast_metadata_ownership_controls] - - bucket = aws_s3_bucket.sast_metadata_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "sast_metadata_versioning" { - bucket = aws_s3_bucket.sast_metadata_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "sast_metadata_encryption_configuration" { - bucket = aws_s3_bucket.sast_metadata_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "sast_metadata_lifecycle" { - bucket = aws_s3_bucket.sast_metadata_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "sast_metadata_public_access_block" { - bucket = aws_s3_bucket.sast_metadata_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "sast_metadata_permissive_access" { - bucket = aws_s3_bucket.sast_metadata_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.sast_metadata_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.sast_metadata_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# SCANS BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "scans_bucket" { - bucket = "scans-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} scans bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "scans_ownership_controls" { - bucket = aws_s3_bucket.scans_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "scans_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.scans_ownership_controls] - - bucket = aws_s3_bucket.scans_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "scans_bucket_versioning" { - bucket = aws_s3_bucket.scans_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "scans_bucket_encryption_configuration" { - bucket = aws_s3_bucket.scans_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "scans_bucket_lifecycle" { - bucket = aws_s3_bucket.scans_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "scans_public_access_block" { - bucket = aws_s3_bucket.scans_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "scans_permissive_access" { - bucket = aws_s3_bucket.scans_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.scans_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.scans_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# SAST-WORKER BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "sast_worker_bucket" { - bucket = "sast-worker-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} sast worker bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "sast_worker_ownership_controls" { - bucket = aws_s3_bucket.sast_worker_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "sast_worker_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.sast_worker_ownership_controls] - - bucket = aws_s3_bucket.sast_worker_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "sast_worker_bucket_versioning" { - bucket = aws_s3_bucket.sast_worker_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "sast_worker_bucket_encryption_configuration" { - bucket = aws_s3_bucket.sast_worker_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "sast_worker_bucket_lifecycle" { - bucket = aws_s3_bucket.sast_worker_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "sast_worker_public_access_block" { - bucket = aws_s3_bucket.sast_worker_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "sast_worker_permissive_access" { - bucket = aws_s3_bucket.sast_worker_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.sast_worker_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.sast_worker_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# KICS-WORKER BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "kics_worker_bucket" { - bucket = "kics-worker-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} kics worker bucket" - Environment = "${var.deployment_id}" - } -} -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "kics_worker_ownership_controls" { - bucket = aws_s3_bucket.kics_worker_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "kics_worker_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.kics_worker_ownership_controls] - - bucket = aws_s3_bucket.kics_worker_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "kics_worker_bucket_versioning" { - bucket = aws_s3_bucket.kics_worker_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "kics_worker_bucket_encryption_configuration" { - bucket = aws_s3_bucket.kics_worker_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "kics_worker_bucket_lifecycle" { - bucket = aws_s3_bucket.kics_worker_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "kics_worker_public_access_block" { - bucket = aws_s3_bucket.kics_worker_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "kics_worker_permissive_access" { - bucket = aws_s3_bucket.kics_worker_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.kics_worker_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.kics_worker_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# SCA-WORKER BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "sca_worker_bucket" { - bucket = "sca-worker-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} sca worker bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "sca_worker_ownership_controls" { - bucket = aws_s3_bucket.sca_worker_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "sca_worker_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.sca_worker_ownership_controls] - - bucket = aws_s3_bucket.sca_worker_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "sca_worker_bucket_versioning" { - bucket = aws_s3_bucket.sca_worker_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "sca_worker_bucket_encryption_configuration" { - bucket = aws_s3_bucket.sca_worker_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "sca_worker_bucket_lifecycle" { - bucket = aws_s3_bucket.sca_worker_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "sca_worker_public_access_block" { - bucket = aws_s3_bucket.sca_worker_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "sca_worker_permissive_access" { - bucket = aws_s3_bucket.sca_worker_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.sca_worker_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.sca_worker_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# LOGS BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "logs_bucket" { - bucket = "logs-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} logs bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "logs_bucket_ownership_controls" { - bucket = aws_s3_bucket.logs_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "logs_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.logs_bucket_ownership_controls] - - bucket = aws_s3_bucket.logs_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "logs_bucket_versioning" { - bucket = aws_s3_bucket.logs_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "logs_bucket_encryption_configuration" { - bucket = aws_s3_bucket.logs_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "logs_bucket_lifecycle" { - bucket = aws_s3_bucket.logs_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "logs_bucket_public_access_block" { - bucket = aws_s3_bucket.logs_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "logs_permissive_access" { - bucket = aws_s3_bucket.logs_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.logs_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.logs_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# ENGINE-LOGS BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "engine_logs_bucket" { - bucket = "engine-logs-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} engine logs bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "engine_logs_ownership_controls" { - bucket = aws_s3_bucket.engine_logs_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "engine_logs_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.engine_logs_ownership_controls] - - bucket = aws_s3_bucket.engine_logs_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "engine_logs_bucket_versioning" { - bucket = aws_s3_bucket.engine_logs_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "engine_logs_bucket_encryption_configuration" { - bucket = aws_s3_bucket.engine_logs_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "engine_logs_bucket_lifecycle" { - bucket = aws_s3_bucket.engine_logs_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "engine_logs_public_access_block" { - bucket = aws_s3_bucket.engine_logs_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "engine_logs_permissive_access" { - bucket = aws_s3_bucket.engine_logs_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.engine_logs_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.engine_logs_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# REPORTS BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "reports_bucket" { - bucket = "reports-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} reports bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "reports_ownership_controls" { - bucket = aws_s3_bucket.reports_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "reports_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.reports_ownership_controls] - - bucket = aws_s3_bucket.reports_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "reports_bucket_versioning" { - bucket = aws_s3_bucket.reports_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "reports_bucket_encryption_configuration" { - bucket = aws_s3_bucket.reports_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "reports_bucket_lifecycle" { - bucket = aws_s3_bucket.reports_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "reports_public_access_block" { - bucket = aws_s3_bucket.reports_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "reports_permissive_access" { - bucket = aws_s3_bucket.reports_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.reports_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.reports_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# REPORT-TEMPLATES BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "report_templates_bucket" { - bucket = "report-templates-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} report templates bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "report_templates_ownership_controls" { - bucket = aws_s3_bucket.report_templates_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "report_templates_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.report_templates_ownership_controls] - - bucket = aws_s3_bucket.report_templates_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "report_templates_bucket_versioning" { - bucket = aws_s3_bucket.report_templates_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "report_templates_bucket_encryption_configuration" { - bucket = aws_s3_bucket.report_templates_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "report_templates_bucket_lifecycle" { - bucket = aws_s3_bucket.report_templates_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "report_templates_public_access_block" { - bucket = aws_s3_bucket.report_templates_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "report_templates_permissive_access" { - bucket = aws_s3_bucket.report_templates_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.report_templates_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.report_templates_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# CONFIGURATION BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "configuration_bucket" { - bucket = "configuration-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} configuration bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "configuration_ownership_controls" { - bucket = aws_s3_bucket.configuration_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "configuration_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.configuration_ownership_controls] - - bucket = aws_s3_bucket.configuration_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "configuration_bucket_versioning" { - bucket = aws_s3_bucket.configuration_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "configuration_bucket_encryption_configuration" { - bucket = aws_s3_bucket.configuration_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "configuration_bucket_lifecycle" { - bucket = aws_s3_bucket.configuration_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - - - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "configuration_public_access_block" { - bucket = aws_s3_bucket.configuration_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "configuration_permissive_access" { - bucket = aws_s3_bucket.configuration_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.configuration_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.configuration_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# IMPORTS BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "imports_bucket" { - bucket = "imports-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} imports bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "imports_ownership_controls" { - bucket = aws_s3_bucket.imports_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "imports_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.imports_ownership_controls] - - bucket = aws_s3_bucket.imports_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "imports_bucket_versioning" { - bucket = aws_s3_bucket.imports_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "imports_bucket_encryption_configuration" { - bucket = aws_s3_bucket.imports_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "imports_bucket_lifecycle" { - bucket = aws_s3_bucket.imports_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "imports_public_access_block" { - bucket = aws_s3_bucket.imports_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "imports_permissive_access" { - bucket = aws_s3_bucket.imports_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.imports_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.imports_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# AUDIT BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "audit_bucket" { - bucket = "audit-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} audit bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "audit_ownership_controls" { - bucket = aws_s3_bucket.audit_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "audit_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.audit_ownership_controls] - - bucket = aws_s3_bucket.audit_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "audit_bucket_versioning" { - bucket = aws_s3_bucket.audit_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "audit_bucket_encryption_configuration" { - bucket = aws_s3_bucket.audit_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "audit_bucket_lifecycle" { - bucket = aws_s3_bucket.audit_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "audit_public_access_block" { - bucket = aws_s3_bucket.audit_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "audit_permissive_access" { - bucket = aws_s3_bucket.audit_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.audit_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.audit_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - - - - - - - -# SOURCE-RESOLVER BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "source_resolver_bucket" { - bucket = "source-resolver-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} source resolver bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "source_resolver_ownership_controls" { - bucket = aws_s3_bucket.source_resolver_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "asource_resolver_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.source_resolver_ownership_controls] - - bucket = aws_s3_bucket.source_resolver_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "source_resolver_bucket_versioning" { - bucket = aws_s3_bucket.source_resolver_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "source_resolver_bucket_encryption_configuration" { - bucket = aws_s3_bucket.source_resolver_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "source_resolver_bucket_lifecycle" { - bucket = aws_s3_bucket.source_resolver_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "source_resolver_public_access_block" { - bucket = aws_s3_bucket.source_resolver_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "source_resolver_permissive_access" { - bucket = aws_s3_bucket.source_resolver_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.source_resolver_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.source_resolver_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - - - - -# APISEC BUCKET -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "apisec_bucket" { - bucket = "apisec-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} apisec bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "apisec_ownership_controls" { - bucket = aws_s3_bucket.apisec_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "apisec_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.apisec_ownership_controls] - - bucket = aws_s3_bucket.apisec_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "apisec_bucket_versioning" { - bucket = aws_s3_bucket.apisec_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "apisec_bucket_encryption_configuration" { - bucket = aws_s3_bucket.apisec_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "apisec_bucket_lifecycle" { - bucket = aws_s3_bucket.apisec_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "apisec_public_access_block" { - bucket = aws_s3_bucket.apisec_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "apisec_permissive_access" { - bucket = aws_s3_bucket.apisec_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.apisec_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.apisec_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# KICS-MATADATA BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "kics_metadata_bucket" { - bucket = "kics-metadata-${lower(local.s3_bucket_name_suffix)}" - force_destroy = true - - tags = { - Name = "${var.deployment_id} kics metadata bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "kics_metadata_ownership_controls" { - bucket = aws_s3_bucket.kics_metadata_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "kics_metadata_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.kics_metadata_ownership_controls] - - bucket = aws_s3_bucket.kics_metadata_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "kics_metadata_bucket_versioning" { - bucket = aws_s3_bucket.kics_metadata_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "kics_metadata_bucket_encryption_configuration" { - bucket = aws_s3_bucket.kics_metadata_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "kics_metadata_bucket_lifecycle" { - bucket = aws_s3_bucket.kics_metadata_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "kics_metadata_public_access_block" { - bucket = aws_s3_bucket.kics_metadata_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "kics_metadata_permissive_access" { - bucket = aws_s3_bucket.kics_metadata_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.kics_metadata_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.kics_metadata_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# REDIS-SHARED-BUCKET -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "redis_shared_bucket" { - bucket = "redis-shared-bucket-${lower(local.s3_bucket_name_suffix)}" - force_destroy = false - - tags = { - Name = "${var.deployment_id} redis shared bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "redis_shared_bucket_ownership_controls" { - bucket = aws_s3_bucket.redis_shared_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "redis_shared_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.redis_shared_bucket_ownership_controls] - - bucket = aws_s3_bucket.redis_shared_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "redis_shared_bucket_versioning" { - bucket = aws_s3_bucket.redis_shared_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "redis_shared_bucket_encryption_configuration" { - bucket = aws_s3_bucket.redis_shared_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "redis_shared_bucket_lifecycle" { - bucket = aws_s3_bucket.redis_shared_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "redis_shared_bucket_public_access_block" { - bucket = aws_s3_bucket.redis_shared_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "redis_shared_bucket_permissive_access" { - bucket = aws_s3_bucket.redis_shared_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.redis_shared_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.redis_shared_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# SCAN-RESULTS-STORAGE -# S3 Bucket -# tfsec:ignore:aws-s3-encryption-customer-key -resource "aws_s3_bucket" "scan_results_storage_bucket" { - bucket = "scan-results-storage-${lower(local.s3_bucket_name_suffix)}" - force_destroy = false - - tags = { - Name = "${var.deployment_id} scan results storage bucket" - Environment = "${var.deployment_id}" - } -} - -# S3 Bucket - Ownership Control -resource "aws_s3_bucket_ownership_controls" "scan_results_storage_bucket_ownership_controls" { - bucket = aws_s3_bucket.scan_results_storage_bucket.id - rule { - object_ownership = "BucketOwnerPreferred" - } -} - -# S3 Bucket - acl -resource "aws_s3_bucket_acl" "scan_results_storage_bucket_acl" { - depends_on = [aws_s3_bucket_ownership_controls.scan_results_storage_bucket_ownership_controls] - - bucket = aws_s3_bucket.scan_results_storage_bucket.id - acl = "private" -} - -# S3 Bucket - versioning -resource "aws_s3_bucket_versioning" "scan_results_storage_bucket_versioning" { - bucket = aws_s3_bucket.scan_results_storage_bucket.id - - versioning_configuration { - status = var.s3_bucket_versioning_status - } -} - -# S3 Bucket - encryption configuration -resource "aws_s3_bucket_server_side_encryption_configuration" "scan_results_storage_bucket_encryption_configuration" { - bucket = aws_s3_bucket.scan_results_storage_bucket.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "AES256" - } - } -} - -# S3 Bucket - lifecycle -resource "aws_s3_bucket_lifecycle_configuration" "scan_results_storage_bucket_lifecycle" { - bucket = aws_s3_bucket.scan_results_storage_bucket.id - - rule { - id = "Transition-To-Intelligent-Tiering" - status = "Enabled" - # abort_incomplete_multipart_upload_days = 1 (not expected here) - transition { - days = 0 - storage_class = "INTELLIGENT_TIERING" - } - } - rule { - id = "${var.s3_retention_period}-Days-Non-Current-Expiration" - status = "Enabled" - noncurrent_version_expiration { - noncurrent_days = var.s3_retention_period - } - expiration { - expired_object_delete_marker = true - } - } -} - -# S3 Bucket - Block Public Access -resource "aws_s3_bucket_public_access_block" "scan_results_storage_bucket_public_access_block" { - bucket = aws_s3_bucket.scan_results_storage_bucket.id - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -# S3 Bucket Policy - Deny Non-HTTPS only -resource "aws_s3_bucket_policy" "scan_results_storage_bucket_permissive_access" { - bucket = aws_s3_bucket.scan_results_storage_bucket.id - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "denyInsecureTransport", - "Effect" : "Deny", - "Principal" : "*", - "Action" : "s3:*", - "Resource" : [ - "arn:aws:s3:::${aws_s3_bucket.scan_results_storage_bucket.id}/*", - "arn:aws:s3:::${aws_s3_bucket.scan_results_storage_bucket.id}" - ], - "Condition" : { - "Bool" : { - "aws:SecureTransport" : "false" - } - } - } - ] - }) -} - -# Policy to Allow Minio Nodegroup to aceess the S3 Buckets -resource "aws_iam_policy" "ast_s3_buckets_policy" { - name = "${local.deployment_id}-eks-ng-minio-gateway-S3-${random_string.random_suffix.result}" - policy = jsonencode({ - "Version" : "2012-10-17", - "Statement" : [ - { - "Action" : [ - "s3:*" - ], - "Effect" : "Allow", - "Resource" : [ - "arn:aws:s3:::*${lower(local.s3_bucket_name_suffix)}", - "arn:aws:s3:::*${lower(local.s3_bucket_name_suffix)}/*" - ] - } - ] - }) -} - -resource "aws_iam_role_policy_attachment" "ast_s3_buckets_policy_attachment" { - role = module.minio_gateway_nodes.iam_role_name - policy_arn = aws_iam_policy.ast_s3_buckets_policy.arn -} \ No newline at end of file diff --git a/infrastructure/s3_backend_configuration.conf b/infrastructure/s3_backend_configuration.conf deleted file mode 100644 index b5dc95a..0000000 --- a/infrastructure/s3_backend_configuration.conf +++ /dev/null @@ -1,3 +0,0 @@ -bucket="" -region="" -key="" \ No newline at end of file diff --git a/infrastructure/secrets.tfvars b/infrastructure/secrets.tfvars deleted file mode 100644 index 4f85ea1..0000000 --- a/infrastructure/secrets.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -database_username = "" -database_password = "" -database_name = "" - -redis_auth_token = "" \ No newline at end of file diff --git a/infrastructure/security_groups.tf b/infrastructure/security_groups.tf deleted file mode 100644 index 3924877..0000000 --- a/infrastructure/security_groups.tf +++ /dev/null @@ -1,46 +0,0 @@ -# TODO - create one for Postgres and one For Redis instead of all to all -module "internal_security_group" { - source = "terraform-aws-modules/security-group/aws" - version = "4.8.0" - - name = "internal-${local.deployment_id}-sg" - description = "Internal security group for AST deployment called ${local.deployment_id}." - vpc_id = local.vpc_id - - ingress_cidr_blocks = [ - module.vpc.vpc_cidr_block] - ingress_rules = [ - "all-all"] - egress_rules = [ - "all-all"] - - create = var.sig.create -} - -module "external_security_group" { - source = "terraform-aws-modules/security-group/aws" - version = "4.8.0" - - name = "external-${local.deployment_id}-sg" - description = "External Security group for AST deployment called ${local.deployment_id}." - vpc_id = local.vpc_id - - ingress_cidr_blocks = [ - "0.0.0.0/0"] - ingress_rules = [ - "http-80-tcp", - "https-443-tcp", - "kubernetes-api-tcp", - "ssh-tcp", - "all-icmp"] - egress_rules = [ - "all-all"] - - create = var.sig.create -} - - - -locals { - sig_k8s_to_dbs_id = var.sig.create == true ? module.internal_security_group.security_group_id : var.sig.existing_sig_k8s_to_dbs_id -} \ No newline at end of file diff --git a/infrastructure/state.tf b/infrastructure/state.tf deleted file mode 100644 index f5c6d7c..0000000 --- a/infrastructure/state.tf +++ /dev/null @@ -1,6 +0,0 @@ - -terraform { - backend "s3" { - encrypt = true - } -} \ No newline at end of file diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf deleted file mode 100644 index 87683a8..0000000 --- a/infrastructure/variables.tf +++ /dev/null @@ -1,693 +0,0 @@ -# PROVIDERS -# AWS -variable "aws_region" { - type = string - description = "AWS region to use" -} -variable "aws_profile" { - description = "The aws profile used to run terraform." - type = string - nullable = false - - validation { - condition = (length(var.aws_profile) > 2) - error_message = "Must have at least 3 characters length." - } -} - -# METADATA VARIABLES -variable "deployment_id" { - description = "the id of the deployment. It's goint to be the EKS cluster name" - type = string - nullable = false - validation { - condition = (length(var.deployment_id) > 0) - error_message = "The deployment_id is required." - } -} - -#EKS -variable "environment" { - description = "the name of the environment. for example: production / dev / stanging/ QA and etc." - type = string - validation { - condition = (length(var.environment) > 0) - error_message = "The environment variable is required." - } - nullable = false -} - -variable "owner" { - description = "the name of the deployment owner. for example: ast-team" - type = string - validation { - condition = (length(var.owner) > 0) - error_message = "The owner variable is required." - } - nullable = false -} - -variable "eks_cluster_version" { - description = "EKS Kubernetes version to be used" - type = string - default = "1.24" - nullable = false -} - -# S3 -variable "s3_retention_period" { - description = "S3 Retention Period" - type = string - default = "90" -} - -# NODEGROUPS -variable "ast_nodes" { - type = object({ - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - name = string - }) - default = { - name = "ast" - min_size = 3 - desired_size = 3 - max_size = 10 - instance_types = ["c5.2xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - } - validation { - condition = var.ast_nodes.desired_size >= 1 - error_message = "You must have at least 1 instance in this node group." - } - validation { - condition = var.ast_nodes.disk_size_gib >= 20 - error_message = "You must provide at least 20 GiB per node." - } - - validation { - condition = alltrue([var.ast_nodes.disk_iops >= 3000, var.ast_nodes.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.ast_nodes.disk_throughput >= 125, var.ast_nodes.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the AST nodes" -} - -variable "sast_nodes" { - type = object({ - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - name = string - label_name = string - label_value = string - }) - - default = { - name = "sast-eng" - min_size = 1 - desired_size = 1 - max_size = 100 - instance_types = ["c6i.2xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "sast-engine" - value = "true" - effect = "NO_SCHEDULE" - label_name = "sast-engine" - label_value = "true" - } - validation { - condition = var.sast_nodes.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.sast_nodes.disk_iops >= 3000, var.sast_nodes.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.sast_nodes.disk_throughput >= 125, var.sast_nodes.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the SAST nodes" -} - -# ------------------- - -variable "sast_nodes_medium" { - type = object({ - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - name = string - label_name = string - label_value = string - }) - - default = { - name = "sast-eng-m" - min_size = 0 - desired_size = 0 - max_size = 100 - instance_types = ["m5.2xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "sast-engine-medium" - value = "true" - effect = "NO_SCHEDULE" - label_name = "sast-engine-medium" - label_value = "true" - } - validation { - condition = var.sast_nodes_medium.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.sast_nodes_medium.disk_iops >= 3000, var.sast_nodes_medium.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.sast_nodes_medium.disk_throughput >= 125, var.sast_nodes_medium.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the SAST nodes" -} - -# ------------------- - -variable "sast_nodes_large" { - type = object({ - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - name = string - label_name = string - label_value = string - }) - - default = { - name = "sast-eng-l" - min_size = 0 - desired_size = 0 - max_size = 300 - instance_types = ["r6i.xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "sast-engine-large" - value = "true" - effect = "NO_SCHEDULE" - label_name = "sast-engine-large" - label_value = "true" - } - validation { - condition = var.sast_nodes_large.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.sast_nodes_large.disk_iops >= 3000, var.sast_nodes_large.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.sast_nodes_large.disk_throughput >= 125, var.sast_nodes_large.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the SAST nodes" -} - -variable "sast_nodes_extra_large" { - type = object({ - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - name = string - label_name = string - label_value = string - }) - - default = { - name = "sast-eng-xl" - min_size = 0 - desired_size = 0 - max_size = 100 - instance_types = ["r6i.2xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "sast-engine-extra-large" - value = "true" - effect = "NO_SCHEDULE" - label_name = "sast-engine-extra-large" - label_value = "true" - } - validation { - condition = var.sast_nodes_extra_large.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.sast_nodes_extra_large.disk_iops >= 3000, var.sast_nodes_extra_large.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.sast_nodes_extra_large.disk_throughput >= 125, var.sast_nodes_extra_large.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the SAST nodes" -} - -variable "sast_nodes_xxl" { - type = object({ - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - name = string - label_name = string - label_value = string - }) - - default = { - name = "sast-eng-xxl" - min_size = 0 - desired_size = 0 - max_size = 50 - instance_types = ["r6i.4xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "sast-engine-xxl" - value = "true" - effect = "NO_SCHEDULE" - label_name = "sast-engine-xxl" - label_value = "true" - } - validation { - condition = var.sast_nodes_xxl.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.sast_nodes_xxl.disk_iops >= 3000, var.sast_nodes_xxl.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.sast_nodes_xxl.disk_throughput >= 125, var.sast_nodes_xxl.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the SAST nodes" -} - -variable "kics_nodes" { - type = object({ - name = string - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - label_name = string - label_value = string - }) - - default = { - name = "kics" - min_size = 1 - desired_size = 1 - max_size = 10 - instance_types = ["c5.2xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "kics-engine" - value = "true" - effect = "NO_SCHEDULE" - label_name = "kics-engine" - label_value = "true" - } - validation { - condition = var.kics_nodes.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.kics_nodes.disk_iops >= 3000, var.kics_nodes.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.kics_nodes.disk_throughput >= 125, var.kics_nodes.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the SAST nodes" -} - -# MINIO -variable "minio_gateway_nodes" { - type = object({ - name = string - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - label_name = string - label_value = string - }) - - default = { - name = "minio-gateway" - min_size = 1 - desired_size = 1 - max_size = 10 - instance_types = ["c6i.2xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "minio-gateway" - value = "true" - effect = "NO_SCHEDULE" - label_name = "minio-gateway" - label_value = "true" - } - validation { - condition = var.minio_gateway_nodes.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.minio_gateway_nodes.disk_iops >= 3000, var.minio_gateway_nodes.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.minio_gateway_nodes.disk_throughput >= 125, var.minio_gateway_nodes.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the Minio Gateway nodes" -} - -# REPOSTORE -variable "repostore_nodes" { - type = object({ - name = string - instance_types = list(string) - min_size = number - desired_size = number - max_size = number - capacity_type = string - disk_size_gib = number - disk_iops = number - disk_throughput = number - device_name = string - volume_type = string - key = string - value = string - effect = string - label_name = string - label_value = string - }) - - default = { - name = "repostore" - min_size = 1 - desired_size = 1 - max_size = 10 - instance_types = ["c5.2xlarge"] - disk_size_gib = 50 - disk_iops = 3000 # this should be the default - disk_throughput = 125 # this should be the default - capacity_type = "ON_DEMAND" - device_name = "/dev/xvda" - volume_type = "gp3" - key = "repostore" - value = "true" - effect = "NO_SCHEDULE" - label_name = "repostore" - label_value = "true" - } - validation { - condition = var.repostore_nodes.disk_size_gib >= 8 - error_message = "You must provide at least 8 GiB per node." - } - validation { - condition = alltrue([var.repostore_nodes.disk_iops >= 3000, var.repostore_nodes.disk_iops <= 16000]) - error_message = "You must provide a value between 3000 and 16000." - } - - validation { - condition = alltrue([var.repostore_nodes.disk_throughput >= 125, var.repostore_nodes.disk_throughput <= 1000]) - error_message = "You must provide a value between 125 and 1000." - } - description = "Configuration for the Minio Gateway nodes" -} - -#RDS -variable "database_name" { - type = string - validation { - condition = can(regex("^(?:[a-z]|[a-z][a-z0-9]+)$", var.database_name)) - error_message = "The database_name is not valid." - } - sensitive = true - description = "Name of the database" -} - -variable "database_username" { - type = string - validation { - condition = var.database_username != "" - error_message = "The database_username is not valid." - } - sensitive = true - description = "Username of the database" -} - -variable "database_password" { - type = string - validation { - condition = var.database_password != "" - error_message = "The database_password is not valid." - } - sensitive = true - description = "Password of the database" -} - -variable "postgres_nodes" { - type = object({ - create = bool - auto_scaling_enable = bool - instance_type = string - count = number - max_count = number - }) - validation { - condition = (var.postgres_nodes.create == false) || (var.postgres_nodes.create == true && var.postgres_nodes.instance_type != "" && var.postgres_nodes.count > 0) - error_message = "The field instance_type cannot be empty and the number of db nodes must be greater than zero." - } - description = "Configuration for the Aurora Postgres DB nodes" -} - -# REDIS -variable "redis_auth_token" { - type = string - sensitive = true - description = "Auth token for Elasticache Redis DB" - default = "" -} - -variable "redis_nodes" { - type = object({ - create = bool - instance_type = string - replicas_per_shard = number - number_of_shards = number - }) - validation { - condition = (var.redis_nodes.create == false) || (var.redis_nodes.create == true && var.redis_nodes.instance_type != "" && var.redis_nodes.number_of_shards > 0) - error_message = "The field instance_type cannot be empty and the number of db shards must be greater than zero." - } - description = "Configuration for the Elasticache Redis DB nodes" -} - -# VPC -variable "vpc" { - type = object({ - create = bool - nat_per_az = bool - single_nat = bool - existing_vpc_id = string - existing_subnet_ids = list(string) - existing_db_subnets_group = string - existing_db_subnets = list(string) - }) - validation { - condition = (var.vpc.create == true) || ((length(var.vpc.existing_subnet_ids) > 0) && (length(var.vpc.existing_vpc_id) > 0) && var.vpc.existing_db_subnets_group != "" && (length(var.vpc.existing_db_subnets) > 0)) - error_message = "You must create a VPC or provide existing VPC information." - } - description = "Configuration for the VPC. If you want to use your own VPC you must follow AWS instructions for VPC-EKS" -} - -variable "vpc_cidr" { - description = "the main cidr of the vpc" - type = string - default = "10.1.0.0/16" - validation { - condition = can(regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))?$", var.vpc_cidr)) - error_message = "Valid cidr is required. The subnet masking must be /21 ." - } - nullable = false -} - -# Security groups -variable "sig" { - type = object({ - create = bool - existing_sig_k8s_to_dbs_id = string - }) - validation { - condition = (var.sig.create == true) || ((length(var.sig.existing_sig_k8s_to_dbs_id) > 0)) - error_message = "You must create a Security groups or provide existing VPC information." - } - description = "Configuration for the Security groups." -} - -# IAM-ROLE -variable "iam_role" { - type = object({ - cloudops_arn = bool - customer_arn = string - }) - description = "CloudOps and Customer iam role" - validation { - condition = (var.iam_role.cloudops_arn == true && length(var.iam_role.customer_arn) == 0) || (var.iam_role.cloudops_arn == false && length(var.iam_role.customer_arn) > 0 && can(regex("^arn:aws:iam::[[:digit:]]{12}:role/.+", var.iam_role.customer_arn))) - error_message = "Error: If cloudops_iam_role is true then customer_iam_role_arn must be an empty string. If cloudops_iam_role is false then customer_iam_role_arn must be a valid role arn string." - } -} - -# KMS -variable "kms" { - type = object({ - create = bool - existing_kms_arn = string - }) - validation { - condition = (var.kms.create == true) || (length(var.kms.existing_kms_arn) > 0) - error_message = "You must create a KMS or provide existing KMS." - } - description = "Configuration for the KMS." -} - - -# S3 -variable "s3_bucket_versioning_status" { - type = string - description = "S3 Bucket versioning Status" - default = "Disabled" -} \ No newline at end of file diff --git a/infrastructure/versions.tf b/infrastructure/versions.tf deleted file mode 100644 index cdcdc35..0000000 --- a/infrastructure/versions.tf +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - required_version = ">= 1.1.0" - required_providers { - aws = { - source = "hashicorp/aws" - version = "4.49.0" - } - } -} \ No newline at end of file diff --git a/infrastructure/vpc.tf b/infrastructure/vpc.tf deleted file mode 100644 index 6e8ab57..0000000 --- a/infrastructure/vpc.tf +++ /dev/null @@ -1,56 +0,0 @@ -module "vpc" { - create_vpc = var.vpc.create - - source = "terraform-aws-modules/vpc/aws" - version = "3.12.0" - - name = local.deployment_id - cidr = var.vpc_cidr - - azs = ["${var.aws_region}a", "${var.aws_region}b", "${var.aws_region}c"] - - - private_subnets = local.private_subnets - public_subnets = local.public_subnets - database_subnets = local.database_subnets - - enable_nat_gateway = true - single_nat_gateway = var.vpc.single_nat - one_nat_gateway_per_az = var.vpc.nat_per_az - - enable_dns_hostnames = true - enable_dns_support = true - - public_subnet_tags = { - "kubernetes.io/cluster/${local.deployment_id}" = "shared" - "kubernetes.io/role/elb" = "1" - } - - private_subnet_tags = { - "kubernetes.io/cluster/${local.deployment_id}" = "shared" - "kubernetes.io/role/internal-elb" = "1" - } - - enable_flow_log = true - create_flow_log_cloudwatch_iam_role = true - create_flow_log_cloudwatch_log_group = true - -} - -locals { - vpc_id = var.vpc.create == true ? module.vpc.vpc_id : var.vpc.existing_vpc_id - subnets = var.vpc.create == true ? module.vpc.private_subnets : var.vpc.existing_subnet_ids - db_subnets = var.vpc.create == true ? module.vpc.database_subnets : var.vpc.existing_db_subnets - db_subnet_group = var.vpc.create == true ? module.vpc.database_subnet_group_name : var.vpc.existing_db_subnets_group -} - -# VPC S3 Gateway Endpoints -resource "aws_vpc_endpoint" "s3_gateway_private" { - vpc_endpoint_type = "Gateway" - service_name = "com.amazonaws.${var.aws_region}.s3" - vpc_id = local.vpc_id - route_table_ids = module.vpc.private_route_table_ids - tags = { - Name = "${var.deployment_id}-s3-gateway-private" - } -} \ No newline at end of file diff --git a/kubernetes-config/.terraform.lock.hcl b/kubernetes-config/.terraform.lock.hcl deleted file mode 100644 index 1e1efa5..0000000 --- a/kubernetes-config/.terraform.lock.hcl +++ /dev/null @@ -1,65 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "4.49.0" - constraints = ">= 3.0.0, >= 4.0.0, 4.49.0" - hashes = [ - "h1:1vbzsJ8TBjjotPQKWZQUrX2AII8+te1ih4bYJYD2VS4=", - "zh:09803937f00fdf2873eccf685eec7854408925cbf183c9b683df7c5825be463f", - "zh:2af1575e538fb0b669266f8d1385b17bfdaf17c521b6b6329baa1f2971fc4a4d", - "zh:3f71882b438cde3108fe68cfe2637839d3eed08157a9721bd97babf3912247a8", - "zh:577af1b38f5da8a9f29eebe5eebec9279d26e757cd03b0c8c59311f9ce8a859b", - "zh:60160d39094973beefb9b10cfd6aaa5b63a2e68c32445ecffcd1b101356e6f9b", - "zh:762656454722548baeccf35cbaa23b887976337e1ed321682df7390419fdf22d", - "zh:7f6d7887821659bf3bef815949077dc91ffcdb0d911644a887b6683b264a5ca6", - "zh:8f16a352cc903f8951fa4619c36233b3e66e27d724817b131f2035dd8896f524", - "zh:8f768f65e370366c8b91c00d01c9a6264fe26ea9ae1819f14bdcd12c066272bc", - "zh:95ad78c689a83c08ef7c3e544c3c9aca93ed528054aa77cc968ddd9efa3a1023", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a47097ab6a4ca8302da82964303ffdd2310ed65e8f8524bfe4058816cf1addb7", - "zh:b66d820c70cd5fd628ffe882d2b97e76b969dca4e6827ac2ba0f8d3bc5d6e9c6", - "zh:b80f713a4f3e1355c3dd1600e9d08b9f15ed2370054ec792ad2c01f2541abe02", - "zh:ce065bc3962cb71fa7652562226b9d486f3d7fcb88285c1020ebe2f550e28641", - ] -} - -provider "registry.terraform.io/hashicorp/helm" { - version = "2.6.0" - constraints = "2.6.0" - hashes = [ - "h1:i+fbwv8Vk8n5kQc+spEtzvCNF4yo2exzSAZhL0ipFuo=", - "zh:0ac248c28acc1a4fd11bd26a85e48ab78dd6abf0f7ac842bf1cd7edd05ac6cf8", - "zh:3d32c8deae3740d8c5310136cc11c8afeffc350fbf88afaca0c34a223a5246f5", - "zh:4055a27489733d19ca7fa2dfce14d323fe99ae9dede7d0fea21ee6db0b9ca74b", - "zh:58a8ed39653fd4c874a2ecb128eccfa24c94266a00e349fd7fb13e22ad81f381", - "zh:6c81508044913f25083de132d0ff81d083732aba07c506cc2db05aa0cefcde2c", - "zh:7db5d18093047bfc4fe597f79610c0a281b21db0d61b0bacb3800585e976f814", - "zh:8269207b7422db99e7be80a5352d111966c3dfc7eb98511f11c8ff7b2e813456", - "zh:b1d7ababfb2374e72532308ff442cc906b79256b66b3fe7a98d42c68c4ddf9c5", - "zh:ca63e226cbdc964a5d63ef21189f059ce45c3fa4a5e972204d6916a9177d2b44", - "zh:d205a72d60e8cc362943d66f5bcdd6b6aaaa9aab2b89fd83bf6f1978ac0b1e4c", - "zh:db47dc579a0e68e5bfe3a61f2e950e6e2af82b1f388d1069de014a937962b56a", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - ] -} - -provider "registry.terraform.io/hashicorp/kubernetes" { - version = "2.16.1" - constraints = "2.16.1" - hashes = [ - "h1:kO/d+ZMZYM2tNMMFHZqBmVR0MeemoGnI2G2NSN92CrU=", - "zh:06224975f5910d41e73b35a4d5079861da2c24f9353e3ebb015fbb3b3b996b1c", - "zh:2bc400a8d9fe7755cca27c2551564a9e2609cfadc77f526ef855114ee02d446f", - "zh:3a479014187af1d0aec3a1d3d9c09551b801956fe6dd29af1186dec86712731b", - "zh:73fb0a69f1abdb02858b6589f7fab6d989a0f422f7ad95ed662aaa84872d3473", - "zh:a33852cd382cbc8e06d3f6c018b468ad809d24d912d64722e037aed1f9bf39db", - "zh:b533ff2214dca90296b1d22eace7eaa7e3efe5a7ae9da66a112094abc932db4f", - "zh:ddf74d8bb1aeb01dc2c36ef40e2b283d32b2a96db73f6daaf179fa2f10949c80", - "zh:e720f3a15d34e795fa9ff90bc755e838ebb4aef894aa2a423fb16dfa6d6b0667", - "zh:e789ae70a658800cb0a19ef7e4e9b26b5a38a92b43d1f41d64fc8bb46539cefb", - "zh:e8aed7dc0bd8f843d607dee5f72640dbef6835a8b1c6ea12cea5b4ec53e463f7", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:fb3ac4f43c8b0dfc0b0103dd0f062ea72b3a34518d4c8808e3a44c9a3dd5f024", - ] -} diff --git a/kubernetes-config/Makefile b/kubernetes-config/Makefile deleted file mode 100644 index 5d0cdd3..0000000 --- a/kubernetes-config/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -init: - terraform init -backend-config=s3_backend_configuration.conf -plan: - terraform plan -var-file="example.auto.tfvars" -apply: - terraform apply -var-file="example.auto.tfvars" -destroy: - terraform destroy -var-file="example.auto.tfvars" diff --git a/kubernetes-config/eks_sigs.tf b/kubernetes-config/eks_sigs.tf deleted file mode 100644 index 2ad761a..0000000 --- a/kubernetes-config/eks_sigs.tf +++ /dev/null @@ -1,398 +0,0 @@ -## AWS LOAD BALANCER CONTROLLER -# AWS IAM POLICY FOR AWS LOAD BALANCER CONTROLLER -resource "aws_iam_policy" "aws_load_balancer_controller" { - name = "${local.deployment_id}-eks-aws-load-balancer-controller-${var.aws_region}" - description = "EKS Cluster AWS Load Balancer Controller Policy for ${local.deployment_id}" - policy = file("iam/aws-load-balancer-controller.json") -} -# AWS IAM ROLE FOR AWS LOAD BALANCER CONTROLLER -module "load_balancer_controller_irsa" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "4.13.1" - - role_name = "load_balancer_controller-${var.deployment_id}" - role_description = "IRSA role for cluster load balancer controller" - - # setting to false because we don't want to rely on exeternal policies - attach_load_balancer_controller_policy = false - oidc_providers = { - main = { - provider_arn = data.terraform_remote_state.infra.outputs.oidc_provider_arn - namespace_service_accounts = ["kube-system:aws-load-balancer-controller"] - } - } -} -# ATTACHE POLICY FOR AWS AIM ROLE FOR AWS LOAD BALANCER CONTROLLER -resource "aws_iam_role_policy_attachment" "aws-load-balancer-controller-policy-attachment" { - role = module.load_balancer_controller_irsa.iam_role_name - policy_arn = aws_iam_policy.aws_load_balancer_controller.arn -} -# HELM CHART AWS LOAD BALANCER CONTROLLER -resource "helm_release" "aws-load-balancer-controller" { - depends_on = [ - module.load_balancer_controller_irsa, - aws_iam_role_policy_attachment.aws-load-balancer-controller-policy-attachment - ] - name = "aws-load-balancer-controller" - chart = "./helm-charts/aws-load-balancer-controller-1.4.6.tgz" - version = "1.4.6" - namespace = "kube-system" - - set { - name = "clusterName" - value = local.deployment_id - } - set { - name = "serviceAccount.create" - value = "true" - } - set { - name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.load_balancer_controller_irsa.iam_role_arn - } - set { - name = "serviceAccount.name" - value = "aws-load-balancer-controller" - } - set { - name = "region" - value = var.aws_region - } -} - -## CLUSTER AUTO SCALLER -# AWS IAM POLICY FOR CLUSTER AUTOSCALER -resource "aws_iam_policy" "cluster_autoscaler" { - name = "${local.deployment_id}-eks-cluster-autoscaler-${var.aws_region}" - description = "EKS Cluster Auto Scalers Policy for ${local.deployment_id}" - policy = file("iam/cluster-autoscaler.json") -} -# AWS IAM ROLE FOR CLUTER AUTOSCALER -module "cluster_autoscaler_irsa" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "4.13.1" - - role_name = "cluster-autoscaler-${var.deployment_id}" - role_description = "IRSA role for cluster autoscaler" - - # setting to false because we don't want to rely on exeternal policies - attach_cluster_autoscaler_policy = false - cluster_autoscaler_cluster_ids = [data.terraform_remote_state.infra.outputs.eks_cluster_id] - oidc_providers = { - main = { - provider_arn = data.terraform_remote_state.infra.outputs.oidc_provider_arn - namespace_service_accounts = ["kube-system:cluster-autoscaler"] - } - } -} -# ATTACHE POLICY FOR AWS AIM ROLE FOR CLUSTER AUTOSCALER -resource "aws_iam_role_policy_attachment" "aws-cluster-autoscaler-policy-attachment" { - role = module.cluster_autoscaler_irsa.iam_role_name - policy_arn = aws_iam_policy.cluster_autoscaler.arn -} -# HELM CLUSTER AUTOSCALER -resource "helm_release" "cluster-autoscaler" { - depends_on = [ - module.cluster_autoscaler_irsa, - aws_iam_role_policy_attachment.aws-cluster-autoscaler-policy-attachment - ] - count = 1 - name = "cluster-autoscaler" - chart = "./helm-charts/cluster-autoscaler-9.21.1.tgz" - version = "9.21.1" - namespace = "kube-system" - - set { - name = "image.tag" - value = "v1.23.0" - } - - set { - name = "autoDiscovery.clusterName" - value = var.deployment_id - } - set { - name = "awsRegion" - value = var.aws_region - } - - set { - name = "rbac.create" - value = "true" - } - set { - name = "rbac.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.cluster_autoscaler_irsa.iam_role_arn - } - set { - name = "rbac.serviceAccount.create" - value = "true" - } - set { - name = "rbac.serviceAccount.name" - value = "cluster-autoscaler" - } -} - -## AWS EBS CSI DRIVER -# AWS IAM POLICY FOR AWS EBS CSI DRIVER -resource "aws_iam_policy" "aws-ebs-csi-driver-policy" { - name = "${local.deployment_id}-aws-ebs-csi-driver-${var.aws_region}" - description = "AWS ebs csi driver Policy for ${local.deployment_id}" - policy = file("iam/aws-ebs-csi-driver.json") -} -# AWS EBS CSI DRIVER ROLE -module "aws_ebs_csi_driver_role" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.9.2" - - role_name = "aws_ebs_csi_driver_role-${var.deployment_id}" - role_description = "IRSA role for ebs csi driver role" - - # setting to false because we don't want to rely on exeternal policies - attach_ebs_csi_policy = false - oidc_providers = { - main = { - provider_arn = data.terraform_remote_state.infra.outputs.oidc_provider_arn - namespace_service_accounts = ["kube-system:aws-ebs-csi-driver-controller", "kube-system:aws-ebs-csi-driver-node"] - } - } -} -# ATTACHE POLICY FOR AWS AIM ROLE FOR AWS EBS CSI DRIVER -resource "aws_iam_role_policy_attachment" "aws-ebs-csi-driver-policy-attachment" { - role = module.aws_ebs_csi_driver_role.iam_role_name - policy_arn = aws_iam_policy.aws-ebs-csi-driver-policy.arn -} -# HELM CHART EBS CSI DRIVER -resource "helm_release" "aws_ebs_csi_driver" { - depends_on = [ - module.aws_ebs_csi_driver_role, - aws_iam_role_policy_attachment.aws-ebs-csi-driver-policy-attachment - ] - count = 1 - name = "aws-ebs-csi-driver" - chart = "./helm-charts/aws-ebs-csi-driver-2.14.1.tgz" - version = "2.14.1" - namespace = "kube-system" - - set { - name = "node.tolerateAllTaints" - value = "true" - } - set { - name = "controller.serviceAccount.create" - value = "true" - } - set { - name = "controller.serviceAccount.name" - value = "aws-ebs-csi-driver-controller" - } - set { - name = "controller.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.aws_ebs_csi_driver_role.iam_role_arn - } - set { - name = "node.serviceAccount.create" - value = "true" - } - set { - name = "node.serviceAccount.name" - value = "aws-ebs-csi-driver-node" - } - set { - name = "node.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.aws_ebs_csi_driver_role.iam_role_arn - } - set { - name = "enableVolumeSnapshot" - value = "true" - } -} - -## AWS EFS CSI DRIVER -# AWS IAM POLICY FOR AWS EFS CSI DRIVER -resource "aws_iam_policy" "aws-efs-csi-driver-policy" { - name = "${local.deployment_id}-aws-efs-csi-driver-${var.aws_region}" - description = "AWS efs csi driver Policy for ${local.deployment_id}" - policy = file("iam/aws-efs-csi-driver.json") -} -# AWS EFS CSI DRIVER ROLE -module "aws_efs_csi_driver_role" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.9.2" - - role_name = "aws_efs_csi_driver_role-${var.deployment_id}" - role_description = "IRSA role for efs csi driver role" - - # setting to false because we don't want to rely on exeternal policies - attach_efs_csi_policy = false - oidc_providers = { - main = { - provider_arn = data.terraform_remote_state.infra.outputs.oidc_provider_arn - namespace_service_accounts = ["kube-system:efs-csi-controller-sa", "kube-system:efs-csi-node-sa"] - } - } -} -# ATTACHE POLICY FOR AWS AIM ROLE FOR AWS EFS CSI DRIVER -resource "aws_iam_role_policy_attachment" "aws-efs-csi-driver-policy-attachment" { - role = module.aws_efs_csi_driver_role.iam_role_name - policy_arn = aws_iam_policy.aws-efs-csi-driver-policy.arn -} -# # HELM CHART EFS CSI DRIVER -resource "helm_release" "aws_efs_csi_driver" { - depends_on = [ - module.aws_ebs_csi_driver_role, - aws_iam_role_policy_attachment.aws-efs-csi-driver-policy-attachment - ] - count = 1 - name = "aws-efs-csi-driver" - chart = "./helm-charts/aws-efs-csi-driver-2.3.3.tgz" - version = "2.3.3" - namespace = "kube-system" - - set { - name = "controller.serviceAccount.create" - value = "true" - } - set { - name = "controller.serviceAccount.name" - value = "efs-csi-controller-sa" - } - set { - name = "controller.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.aws_efs_csi_driver_role.iam_role_arn - } - set { - name = "node.serviceAccount.create" - value = "true" - } - set { - name = "node.serviceAccount.name" - value = "efs-csi-node-sa" - } - set { - name = "node.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.aws_efs_csi_driver_role.iam_role_arn - } - set { - name = "node.tolerateAllTaints" - value = "true" - } - set { - name = "serviceAccount.snapshot.create" - value = "true" - } - set { - name = "serviceAccount.snapshot.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.aws_ebs_csi_driver_role.iam_role_arn - } - set { - name = "serviceAccount.snapshot.name" - value = "aws-ebs-csi-driver" - } - set { - name = "enableVolumeScheduling" - value = "true" - } - set { - name = "enableVolumeResizing" - value = "true" - } - set { - name = "enableVolumeSnapshot" - value = "true" - } -} - -## STORAGE CLASS -# AWS STORAGE CLASS GP3 -resource "kubernetes_storage_class" "storage_class_gp3" { - depends_on = [ - helm_release.aws_ebs_csi_driver, - helm_release.aws_efs_csi_driver - ] - metadata { - name = "gp3" - annotations = { - "storageclass.kubernetes.io/is-default-class" = "true" - } - } - storage_provisioner = "ebs.csi.aws.com" - volume_binding_mode = "WaitForFirstConsumer" - allow_volume_expansion = "true" - parameters = { - type = "gp3" - fstype = "xfs" - } -} -# AWS STORAGE CLASS GP2 -resource "kubernetes_annotations" "gp2" { - depends_on = [ - helm_release.aws_ebs_csi_driver, - helm_release.aws_efs_csi_driver - ] - api_version = "storage.k8s.io/v1" - kind = "StorageClass" - metadata { - name = "gp2" - } - annotations = { - "storageclass.kubernetes.io/is-default-class" = "false" - } - force = true -} - -## EXTERNAL DNS -# AWS IAM POLICY FOR EXTERNAL DNS -resource "aws_iam_policy" "external-dns-policy" { - name = "${local.deployment_id}-external-dns-${var.aws_region}" - description = "external dns Policy for ${local.deployment_id}" - policy = file("iam/external-dns.json") -} -# AWS IAM Role FOR ExternalDNS -module "external_dns_irsa" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.9.2" - - role_name = "external-dns-${var.deployment_id}" - role_description = "IRSA role for cluster external dns controller" - - # setting to false because we don't want to rely on exeternal policies - attach_external_dns_policy = false - oidc_providers = { - main = { - provider_arn = data.terraform_remote_state.infra.outputs.oidc_provider_arn - namespace_service_accounts = ["kube-system:external-dns"] - } - } -} -# ATTACHE POLICY FOR AWS AIM ROLE FOR EXTERNAL DNS -resource "aws_iam_role_policy_attachment" "external-dns-policy-attachment" { - role = module.external_dns_irsa.iam_role_name - policy_arn = aws_iam_policy.external-dns-policy.arn -} -# HELM ExternalDNS -resource "helm_release" "external-dns" { - depends_on = [ - module.external_dns_irsa, - aws_iam_role_policy_attachment.external-dns-policy-attachment - ] - count = 1 - name = "external-dns" - chart = "./helm-charts/external-dns-1.11.0.tgz" - version = "1.11.0" - namespace = "kube-system" - - set { - name = "serviceAccount.create" - value = "true" - } - set { - name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.external_dns_irsa.iam_role_arn - } - set { - name = "serviceAccount.name" - value = "external-dns" - } -} \ No newline at end of file diff --git a/kubernetes-config/example.auto.tfvars b/kubernetes-config/example.auto.tfvars deleted file mode 100644 index 42af3b6..0000000 --- a/kubernetes-config/example.auto.tfvars +++ /dev/null @@ -1,15 +0,0 @@ -# METADATA -deployment_id = "" -environment = "" -owner = "" -aws_profile = "" -aws_region = "" - - -#Infra Backend info -s3_backend_infra_bucket = "" -s3_backend_infra_remote_config_key = "" -s3_backend_infra_bucket_region = "" - -# External DNS Helm -hosted_zone_id = "" \ No newline at end of file diff --git a/kubernetes-config/helm-charts/aws-ebs-csi-driver-2.14.1.tgz b/kubernetes-config/helm-charts/aws-ebs-csi-driver-2.14.1.tgz deleted file mode 100644 index b58c262ffb5d59866c229f933560a8a8c09f172a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11028 zcmV+vE9=xBiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PMZ_bK^GhD84`IujsFwt=RJ&Qircu)n~SzV|zA!nX5*V%JHcMkk^BSaYpnke(`1ir^8hCs6j47q@l zpZjqBT^~;S7jS0yr!nR+_F$}3B73dYh^lcq@B<#VuF?Sth6<%8=}5Nbd9QJW(`zn5 z*=u+}u4Tgm9O(Z8NgKL)j@-9@f7$Un$9~&uf7^%&Wr|RS!c=5LBk)5G4Ng)u=0Y|A zQW4z}wN}G6TZ6RZsR+=3zFn^;S+Ba9#;3#UpN{8$0lEOeIhW1payQ=CwEEA|^^<2tyj7E^vlHLCgI2nmrAL!eA?nDSJB!Z?YDLg_aeG$teLH2?%ATvElwvFp&B(5`cD94lOFu?FY|j&HvKHb)qXBg_UeW7M}hDu zZ%I1xq~d~%u%&S*z;hKXH75Ih$8R^yyNHio;{>A~bQ`wmO!?XF=?;EO8A604M4^93 z1dK4y_QNn&1(fe6G3FQvH!F!BA#3+rCDQT%6VhC|6mF8l~fG{9IfQi!JK+zce_e4lMO9jdL z7ZWvW@&gWM<|ers`6KR!IB5+dGTO;%<)sZ`+1nm8JBPBFe-y~@`V2`JQHJ_syYx|s z>3bBEhdpR!)g%U~plW)`nZg^@%h`d1?^rbbz`1%wBa~CAFfI{~L=nG!FX)6uIKp!o zkjNo5B$3pmh*BgNKN4qVz^ZI{kg?@jOqaE?<+umqxYg?6A$d|VlE^VvDr3tXIA_if zjj{L{oSp@oAOkuqeTUNzXA-m_V_X4cQjsXa(6l(@B0$^d4DN&Te*yEd2psuszw;DG zAJGXil=8jc1K0U`rb7lH(Gbd1==w*QB6!?h3zYP?N78`~R=MY$nF{>68LaF zY?u;9ZXWmHXVA2lIQN^i6S zibE6v!?RRSHtGjs4AY3R(Zz`I{Pp<_25HeYdT@<&G*&(6xHnl-$bx+Q^%D~njL&Zp zfl@nzviQw|E1dQKucS|6@*8L5TAFw|;Bk^F^wc!LLU{n5pdgBi9=zr1f|b5mgWM~< z^I%BCh$w7iwm6FshWQYKh63CghzXC<7`0W;{a3giBZHwd#i)do$piyBq$b`mwfdG_ zPa`6wZ$Mqx<_ZysHZ&3y5EhmjLsi$zbNz+^<5}Tr8b#V;kf`OyHT+6`oF>=+ClUIVrx;y#cz$s@#I(kDAX>f6j2qX|(O3mY-+09P1WSE7RM0^@s*>$bv zkS>NAsplpJWNyCEh51EMh8Go1JF^gS?C*IZm_QfYzEtR>J; zI1xNXHO5qG;t3i^K-ZpW&J4du8SLT>i4*OcT=rhHU$igbdQ5{c1Y8J|31^`pO~Qo& z{arh}5f4ZNV=mR6KPL%P0>+7&f>c`8pS$*ABy1iRT{@)Fc*aG7T4(uzRqLomrCU7y5zOZXyauH;1&+t4xHqQGYjf<+)%-Q5B`mTHBnI>*INT&TBSp8Fc_NRV}v2sHifd09hTGgIi-8hZ2RV4Z-2jS ztlNxxFU~TBRhR+|qEu?fH--HeF)TTpHcuGVXB%?zv6yq?3@3j|eKZcX1b z{j{CuOcN=U=xY)i^_lBkqZ@ViUBQgJ5$9JJLYlyY5>voTUY~YnQ#KleW6QEeuub(K zl^znQypHkyM$YO9Od=YPQ=aOf(d0;zkSOdcK@^Ury=?AB8HN0TN7{=1Xv)~;i*GV@ z6O)^MDn{6Y_Uz4%j7$iPEdN?8%0A2v{EU#w)F?WE0gq#3p&^mtj!JomJF9Kfz2Z6| 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 gBl6;Jv^acvo}Q=YAN~Aq00030|HDc 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�aalq$kCXQk&D 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 yXa64z4sS&t`Wn*j1kw?%{Dg45Dc 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 diff --git a/kubernetes-config/iam/aws-ebs-csi-driver.json b/kubernetes-config/iam/aws-ebs-csi-driver.json deleted file mode 100644 index c5b38f4..0000000 --- a/kubernetes-config/iam/aws-ebs-csi-driver.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "ec2:CreateSnapshot", - "ec2:AttachVolume", - "ec2:DetachVolume", - "ec2:ModifyVolume", - "ec2:DescribeAvailabilityZones", - "ec2:DescribeInstances", - "ec2:DescribeSnapshots", - "ec2:DescribeTags", - "ec2:DescribeVolumes", - "ec2:DescribeVolumesModifications" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateTags" - ], - "Resource": [ - "arn:aws:ec2:*:*:volume/*", - "arn:aws:ec2:*:*:snapshot/*" - ], - "Condition": { - "StringEquals": { - "ec2:CreateAction": [ - "CreateVolume", - "CreateSnapshot" - ] - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:DeleteTags" - ], - "Resource": [ - "arn:aws:ec2:*:*:volume/*", - "arn:aws:ec2:*:*:snapshot/*" - ] - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateVolume" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "aws:RequestTag/ebs.csi.aws.com/cluster": "true" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateVolume" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "aws:RequestTag/CSIVolumeName": "*" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:DeleteVolume" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:DeleteVolume" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "ec2:ResourceTag/CSIVolumeName": "*" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:DeleteVolume" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "ec2:ResourceTag/kubernetes.io/created-for/pvc/name": "*" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:DeleteSnapshot" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "ec2:ResourceTag/CSIVolumeSnapshotName": "*" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:DeleteSnapshot" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "ec2:ResourceTag/ebs.csi.aws.com/cluster": "true" - } - } - } - ] -} \ No newline at end of file diff --git a/kubernetes-config/iam/aws-efs-csi-driver.json b/kubernetes-config/iam/aws-efs-csi-driver.json deleted file mode 100644 index 4ea88b6..0000000 --- a/kubernetes-config/iam/aws-efs-csi-driver.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "elasticfilesystem:DescribeAccessPoints", - "elasticfilesystem:DescribeFileSystems", - "elasticfilesystem:DescribeMountTargets", - "ec2:DescribeAvailabilityZones" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "elasticfilesystem:CreateAccessPoint" - ], - "Resource": "*", - "Condition": { - "StringLike": { - "aws:RequestTag/efs.csi.aws.com/cluster": "true" - } - } - }, - { - "Effect": "Allow", - "Action": "elasticfilesystem:DeleteAccessPoint", - "Resource": "*", - "Condition": { - "StringEquals": { - "aws:ResourceTag/efs.csi.aws.com/cluster": "true" - } - } - } - ] -} \ No newline at end of file diff --git a/kubernetes-config/iam/aws-load-balancer-controller.json b/kubernetes-config/iam/aws-load-balancer-controller.json deleted file mode 100644 index a9061bd..0000000 --- a/kubernetes-config/iam/aws-load-balancer-controller.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "iam:CreateServiceLinkedRole" - ], - "Resource": "*", - "Condition": { - "StringEquals": { - "iam:AWSServiceName": "elasticloadbalancing.amazonaws.com" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:DescribeAccountAttributes", - "ec2:DescribeAddresses", - "ec2:DescribeAvailabilityZones", - "ec2:DescribeInternetGateways", - "ec2:DescribeVpcs", - "ec2:DescribeVpcPeeringConnections", - "ec2:DescribeSubnets", - "ec2:DescribeSecurityGroups", - "ec2:DescribeInstances", - "ec2:DescribeNetworkInterfaces", - "ec2:DescribeTags", - "ec2:GetCoipPoolUsage", - "ec2:DescribeCoipPools", - "elasticloadbalancing:DescribeLoadBalancers", - "elasticloadbalancing:DescribeLoadBalancerAttributes", - "elasticloadbalancing:DescribeListeners", - "elasticloadbalancing:DescribeListenerCertificates", - "elasticloadbalancing:DescribeSSLPolicies", - "elasticloadbalancing:DescribeRules", - "elasticloadbalancing:DescribeTargetGroups", - "elasticloadbalancing:DescribeTargetGroupAttributes", - "elasticloadbalancing:DescribeTargetHealth", - "elasticloadbalancing:DescribeTags" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "cognito-idp:DescribeUserPoolClient", - "acm:ListCertificates", - "acm:DescribeCertificate", - "iam:ListServerCertificates", - "iam:GetServerCertificate", - "waf-regional:GetWebACL", - "waf-regional:GetWebACLForResource", - "waf-regional:AssociateWebACL", - "waf-regional:DisassociateWebACL", - "wafv2:GetWebACL", - "wafv2:GetWebACLForResource", - "wafv2:AssociateWebACL", - "wafv2:DisassociateWebACL", - "shield:GetSubscriptionState", - "shield:DescribeProtection", - "shield:CreateProtection", - "shield:DeleteProtection" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateSecurityGroup" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateTags" - ], - "Resource": "arn:aws:ec2:*:*:security-group/*", - "Condition": { - "StringEquals": { - "ec2:CreateAction": "CreateSecurityGroup" - }, - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateTags", - "ec2:DeleteTags" - ], - "Resource": "arn:aws:ec2:*:*:security-group/*", - "Condition": { - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "true", - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:DeleteSecurityGroup" - ], - "Resource": "*", - "Condition": { - "Null": { - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:CreateLoadBalancer", - "elasticloadbalancing:CreateTargetGroup" - ], - "Resource": "*", - "Condition": { - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:CreateListener", - "elasticloadbalancing:DeleteListener", - "elasticloadbalancing:CreateRule", - "elasticloadbalancing:DeleteRule" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:AddTags", - "elasticloadbalancing:RemoveTags" - ], - "Resource": [ - "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", - "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", - "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" - ], - "Condition": { - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "true", - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:AddTags", - "elasticloadbalancing:RemoveTags" - ], - "Resource": [ - "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*", - "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*", - "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*", - "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*" - ] - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:ModifyLoadBalancerAttributes", - "elasticloadbalancing:SetIpAddressType", - "elasticloadbalancing:SetSecurityGroups", - "elasticloadbalancing:SetSubnets", - "elasticloadbalancing:DeleteLoadBalancer", - "elasticloadbalancing:ModifyTargetGroup", - "elasticloadbalancing:ModifyTargetGroupAttributes", - "elasticloadbalancing:DeleteTargetGroup" - ], - "Resource": "*", - "Condition": { - "Null": { - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:RegisterTargets", - "elasticloadbalancing:DeregisterTargets" - ], - "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:SetWebAcl", - "elasticloadbalancing:ModifyListener", - "elasticloadbalancing:AddListenerCertificates", - "elasticloadbalancing:RemoveListenerCertificates", - "elasticloadbalancing:ModifyRule" - ], - "Resource": "*" - } - ] -} \ No newline at end of file diff --git a/kubernetes-config/iam/cluster-autoscaler.json b/kubernetes-config/iam/cluster-autoscaler.json deleted file mode 100644 index 4a7fdbb..0000000 --- a/kubernetes-config/iam/cluster-autoscaler.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "autoscaling:DescribeAutoScalingGroups", - "autoscaling:DescribeAutoScalingInstances", - "autoscaling:DescribeLaunchConfigurations", - "autoscaling:SetDesiredCapacity", - "autoscaling:DescribeTags", - "autoscaling:TerminateInstanceInAutoScalingGroup", - "ec2:DescribeInstanceTypes" - ], - "Resource": [ - "*" - ] - } - ] -} \ No newline at end of file diff --git a/kubernetes-config/iam/external-dns.json b/kubernetes-config/iam/external-dns.json deleted file mode 100644 index 8f4e2c8..0000000 --- a/kubernetes-config/iam/external-dns.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "route53:ChangeResourceRecordSets" - ], - "Resource": [ - "arn:aws:route53:::hostedzone/*" - ] - }, - { - "Effect": "Allow", - "Action": [ - "route53:ListHostedZones", - "route53:ListResourceRecordSets" - ], - "Resource": [ - "*" - ] - } - ] -} \ No newline at end of file diff --git a/kubernetes-config/local.tf b/kubernetes-config/local.tf deleted file mode 100644 index 02e04f7..0000000 --- a/kubernetes-config/local.tf +++ /dev/null @@ -1,3 +0,0 @@ -locals { - deployment_id = var.deployment_id -} \ No newline at end of file diff --git a/kubernetes-config/main.tf b/kubernetes-config/main.tf deleted file mode 100644 index 380046c..0000000 --- a/kubernetes-config/main.tf +++ /dev/null @@ -1,47 +0,0 @@ -provider "aws" { - region = var.aws_region - profile = var.aws_profile - - default_tags { - tags = { - Terraform = "true" - DeploymentID = var.deployment_id - Owner = var.owner - Environment = var.environment - } - } -} - -################################################################################ -# Kubernetes provider configuration -################################################################################ -data "aws_eks_cluster" "cluster" { - name = var.deployment_id -} - -data "aws_eks_cluster_auth" "cluster" { - name = var.deployment_id -} - -provider "kubernetes" { - host = data.aws_eks_cluster.cluster.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.cluster.token -} - -provider "helm" { - kubernetes { - host = data.aws_eks_cluster.cluster.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.cluster.token - } -} - -data "terraform_remote_state" "infra" { - backend = "s3" - config = { - bucket = var.s3_backend_infra_bucket - key = var.s3_backend_infra_remote_config_key - region = var.s3_backend_infra_bucket_region - } -} \ No newline at end of file diff --git a/kubernetes-config/output.tf b/kubernetes-config/output.tf deleted file mode 100644 index e69de29..0000000 diff --git a/kubernetes-config/s3_backend_configuration.conf b/kubernetes-config/s3_backend_configuration.conf deleted file mode 100644 index b5dc95a..0000000 --- a/kubernetes-config/s3_backend_configuration.conf +++ /dev/null @@ -1,3 +0,0 @@ -bucket="" -region="" -key="" \ No newline at end of file diff --git a/kubernetes-config/state.tf b/kubernetes-config/state.tf deleted file mode 100644 index f5c6d7c..0000000 --- a/kubernetes-config/state.tf +++ /dev/null @@ -1,6 +0,0 @@ - -terraform { - backend "s3" { - encrypt = true - } -} \ No newline at end of file diff --git a/kubernetes-config/variables.tf b/kubernetes-config/variables.tf deleted file mode 100644 index 4c1ceaf..0000000 --- a/kubernetes-config/variables.tf +++ /dev/null @@ -1,91 +0,0 @@ -# BACKEND CONFIG -variable "s3_backend_infra_bucket" { - type = string - description = "S3 name where the infra state is stored" - default = "" - validation { - condition = (length(var.s3_backend_infra_bucket) > 0) - error_message = "The s3_backend_infra_bucket variable is required." - } - nullable = false -} - -variable "s3_backend_infra_remote_config_key" { - description = "Path to the infra state file in the S3 Bucket" - type = string - default = "" - validation { - condition = (length(var.s3_backend_infra_remote_config_key) > 0) - error_message = "The s3_backend_infra_remote_config_key variable is required." - } - nullable = false -} - -variable "s3_backend_infra_bucket_region" { - description = "S3 backend bucket region" - type = string - default = "" - validation { - condition = (length(var.s3_backend_infra_bucket_region) > 0) - error_message = "The s3_backend_infra_bucket_region variable is required." - } - nullable = false -} - -# PROVIDERS -# AWS -variable "aws_region" { - type = string - description = "AWS region to use" -} -variable "aws_profile" { - description = "The aws profile used to run terraform." - type = string - nullable = false - - validation { - condition = (length(var.aws_profile) > 2) - error_message = "Must have at least 3 characters length." - } -} - -# METADATA VARIABLES -variable "environment" { - description = "the name of the environment. for example: production / dev / stanging/ QA and etc." - type = string - validation { - condition = (length(var.environment) > 0) - error_message = "The environment variable is required." - } - nullable = false -} - -variable "owner" { - description = "the name of the deployment owner. for example: ast-team" - type = string - validation { - condition = (length(var.owner) > 0) - error_message = "The owner variable is required." - } - nullable = false -} - -variable "deployment_id" { - description = "the id of the deployment. if not set will used \"{owner}_{environment}\". must be unique per AWS account" - type = string - nullable = false - validation { - condition = (length(var.deployment_id) > 0) - error_message = "The deployment_id is required." - } -} - -# Helm - -# external_DNS -variable "hosted_zone_id" { - description = "Route53 hosted Zone ID" - type = string - default = "" - nullable = false -} diff --git a/kubernetes-config/versions.tf b/kubernetes-config/versions.tf deleted file mode 100644 index 74d2805..0000000 --- a/kubernetes-config/versions.tf +++ /dev/null @@ -1,17 +0,0 @@ -terraform { - required_version = ">= 1.1.0" - required_providers { - aws = { - source = "hashicorp/aws" - version = "4.49.0" - } - kubernetes = { - source = "hashicorp/kubernetes" - version = "2.16.1" - } - helm = { - source = "hashicorp/helm" - version = "2.6.0" - } - } -} \ No newline at end of file From 2d57888af9655aed9005cfdce674e2981e66c523 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 10 Apr 2024 14:58:08 -0400 Subject: [PATCH 02/57] simplified wip --- examples/full/README.md | 136 +++++++ examples/full/main.tf | 235 +++++++++++ examples/full/network.tf | 82 ++++ examples/full/variables-cxone.tf | 605 +++++++++++++++++++++++++++++ examples/full/variables-example.tf | 110 ++++++ 5 files changed, 1168 insertions(+) create mode 100644 examples/full/README.md create mode 100644 examples/full/main.tf create mode 100644 examples/full/network.tf create mode 100644 examples/full/variables-cxone.tf create mode 100644 examples/full/variables-example.tf diff --git a/examples/full/README.md b/examples/full/README.md new file mode 100644 index 0000000..25ad975 --- /dev/null +++ b/examples/full/README.md @@ -0,0 +1,136 @@ +# Full Example + +This folder contains a full example for deploying [Checkmarx One](https://checkmarx.com/product/application-security-platform/) on [AWS](https://aws.amazon.com) using [Terraform](https://www.terraform.io). + +The project configures the VPC, KMS, SES, ACM, and other basic environment resources, and then invokes the `terraform-aws-cxone` module to deploy Checkmarx One infrastructure. + +Consult the [`example.auto.tfvars`](./example.auto.tfvars) for a full listing of what can be configured in this example, and the `terraform-aws-cxone module`. + + +# Module Documentation + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | +| [random](#provider\_random) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [acm](#module\_acm) | terraform-aws-modules/acm/aws | 5.0.1 | +| [checkmarx-one](#module\_checkmarx-one) | ../../ | n/a | +| [checkmarx-one-install](#module\_checkmarx-one-install) | ../../modules/cxone-install | n/a | +| [ses](#module\_ses) | cloudposse/ses/aws | 0.24.0 | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | 5.7.0 | +| [vpc\_endpoint\_security\_group](#module\_vpc\_endpoint\_security\_group) | terraform-aws-modules/security-group/aws | 5.1.2 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_group_policy.cxone_ses_group_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy) | resource | +| [aws_kms_key.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | +| [aws_vpc_endpoint.interface](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint.s3_gateway_private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [random_password.cxone_admin](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [random_password.db](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [random_password.elasticsearch](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [random_password.kots_admin](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_ebs\_csi\_driver\_version](#input\_aws\_ebs\_csi\_driver\_version) | The version of the EKS EBS CSI Addon. | `string` | n/a | yes | +| [coredns\_version](#input\_coredns\_version) | The version of the EKS Core DNS Addon. | `string` | n/a | yes | +| [create\_s3\_endpoint](#input\_create\_s3\_endpoint) | Enables creation of the s3 gateway VPC interface endpoint. | `bool` | `true` | no | +| [db\_allow\_major\_version\_upgrade](#input\_db\_allow\_major\_version\_upgrade) | Allows major version upgrades. | `bool` | `false` | no | +| [db\_apply\_immediately](#input\_db\_apply\_immediately) | Determines if changes will be applied immediately or wait until the next maintenance window. | `bool` | `false` | no | +| [db\_auto\_minor\_version\_upgrade](#input\_db\_auto\_minor\_version\_upgrade) | Automatically upgrade to latest minor version in maintenance window. | `bool` | `false` | no | +| [db\_autoscaling\_enabled](#input\_db\_autoscaling\_enabled) | Enables autoscaling of the aurora database. | `bool` | `true` | no | +| [db\_autoscaling\_max\_capacity](#input\_db\_autoscaling\_max\_capacity) | The maximum number of replicas via autoscaling. | `string` | `"3"` | no | +| [db\_autoscaling\_min\_capacity](#input\_db\_autoscaling\_min\_capacity) | The minimum number of replicas via autoscaling. | `string` | `"1"` | no | +| [db\_autoscaling\_scale\_in\_cooldown](#input\_db\_autoscaling\_scale\_in\_cooldown) | The database scale in cooldown period. | `number` | `300` | no | +| [db\_autoscaling\_scale\_out\_cooldown](#input\_db\_autoscaling\_scale\_out\_cooldown) | The database scale ou cooldown period. | `number` | `300` | no | +| [db\_autoscaling\_target\_cpu](#input\_db\_autoscaling\_target\_cpu) | The CPU utilization for autoscaling target tracking. | `number` | `70` | no | +| [db\_cluster\_db\_instance\_parameter\_group\_name](#input\_db\_cluster\_db\_instance\_parameter\_group\_name) | The name of the DB Cluster parameter group to use. | `string` | `null` | no | +| [db\_create](#input\_db\_create) | Controls creation of the Aurora postgres database. | `bool` | `true` | no | +| [db\_create\_rds\_proxy](#input\_db\_create\_rds\_proxy) | Enables an RDS proxy for the Aurora postgres database. | `bool` | `true` | no | +| [db\_deletion\_protection](#input\_db\_deletion\_protection) | Enables deletion protection to avoid accidental database deletion. | `bool` | `true` | no | +| [db\_engine\_version](#input\_db\_engine\_version) | The aurora postgres engine version. | `string` | `"13.8"` | no | +| [db\_final\_snapshot\_identifier](#input\_db\_final\_snapshot\_identifier) | Identifer for a final DB snapshot. Required when db\_skip\_final\_snapshot is false.. | `string` | `null` | no | +| [db\_instance\_class](#input\_db\_instance\_class) | The aurora postgres instance class. | `string` | `"db.r6g.xlarge"` | no | +| [db\_instances](#input\_db\_instances) | The DB instance configuration | `map(any)` |
{
"replica1": {},
"writer": {}
}
| no | +| [db\_master\_user\_password](#input\_db\_master\_user\_password) | The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it. | `string` | `null` | no | +| [db\_monitoring\_interval](#input\_db\_monitoring\_interval) | The aurora postgres engine version. | `string` | `"10"` | no | +| [db\_performance\_insights\_enabled](#input\_db\_performance\_insights\_enabled) | Enables database performance insights. | `bool` | `true` | no | +| [db\_performance\_insights\_retention\_period](#input\_db\_performance\_insights\_retention\_period) | Number of days to retain performance insights data. Free tier: 7 days. | `number` | `7` | no | +| [db\_port](#input\_db\_port) | The port on which the DB accepts connections. | `string` | `"5432"` | no | +| [db\_serverlessv2\_scaling\_configuration](#input\_db\_serverlessv2\_scaling\_configuration) | The serverless v2 scaling minimum and maximum. |
object({
min_capacity = number
max_capacity = number
})
|
{
"max_capacity": 32,
"min_capacity": 0.5
}
| no | +| [db\_skip\_final\_snapshot](#input\_db\_skip\_final\_snapshot) | Enables skipping the final snapshot upon deletion. | `bool` | `false` | no | +| [db\_snapshot\_identifer](#input\_db\_snapshot\_identifer) | The snapshot identifier to restore the database from. | `string` | `null` | no | +| [deployment\_id](#input\_deployment\_id) | The id of the deployment. Will be used to name resources like EKS cluster, etc. | `string` | n/a | yes | +| [ec2\_key\_name](#input\_ec2\_key\_name) | The name of the EC2 key pair to access servers. | `string` | `null` | no | +| [ec\_auto\_minor\_version\_upgrade](#input\_ec\_auto\_minor\_version\_upgrade) | Enables automatic minor version upgrades. Does not apply to serverless. | `bool` | `false` | no | +| [ec\_automatic\_failover\_enabled](#input\_ec\_automatic\_failover\_enabled) | Enables automatic failover. Does not apply to serverless. | `bool` | `true` | no | +| [ec\_create](#input\_ec\_create) | Enables the creation of elasticache resources. | `bool` | `true` | no | +| [ec\_enable\_serverless](#input\_ec\_enable\_serverless) | Enables the use of elasticache for redis serverless. | `bool` | `false` | no | +| [ec\_engine\_version](#input\_ec\_engine\_version) | The version of the elasticache cluster. Does not apply to serverless. | `string` | `"6.x"` | no | +| [ec\_multi\_az\_enabled](#input\_ec\_multi\_az\_enabled) | Enables automatic failover. Does not apply to serverless. | `bool` | `true` | no | +| [ec\_node\_type](#input\_ec\_node\_type) | The elasticache redis node type. Does not apply to serverless. | `string` | `"cache.m6g.large"` | no | +| [ec\_number\_of\_shards](#input\_ec\_number\_of\_shards) | The number of shards for redis. Does not apply to serverless. | `number` | `3` | no | +| [ec\_parameter\_group\_name](#input\_ec\_parameter\_group\_name) | The elasticache parameter group name. Does not apply to serverless. | `string` | `"default.redis6.x.cluster.on"` | no | +| [ec\_replicas\_per\_shard](#input\_ec\_replicas\_per\_shard) | The number of replicas per shard for redis. Does not apply to serverless. | `number` | `2` | no | +| [ec\_serverless\_max\_ecpu\_per\_second](#input\_ec\_serverless\_max\_ecpu\_per\_second) | The max eCPU per second for serverless elasticache for redis. | `number` | `5000` | no | +| [ec\_serverless\_max\_storage](#input\_ec\_serverless\_max\_storage) | The max storage, in GB, for serverless elasticache for redis. | `number` | `5` | no | +| [eks\_administrator\_principals](#input\_eks\_administrator\_principals) | The ARNs of the IAM roles for administrator access to EKS. |
list(object({
name = string
principal_arn = string
}))
| `[]` | no | +| [eks\_cluster\_endpoint\_public\_access\_cidrs](#input\_eks\_cluster\_endpoint\_public\_access\_cidrs) | List of CIDR blocks which can access the Amazon EKS public API server endpoint | `list(string)` |
[
"0.0.0.0/0"
]
| no | +| [eks\_create](#input\_eks\_create) | Enables the EKS resource creation | `bool` | `true` | no | +| [eks\_create\_cluster\_autoscaler\_irsa](#input\_eks\_create\_cluster\_autoscaler\_irsa) | Enables creation of cluster autoscaler IAM role. | `bool` | `true` | no | +| [eks\_create\_external\_dns\_irsa](#input\_eks\_create\_external\_dns\_irsa) | Enables creation of external dns IAM role. | `bool` | `true` | no | +| [eks\_create\_karpenter](#input\_eks\_create\_karpenter) | Enables creation of Karpenter resources. | `bool` | `false` | no | +| [eks\_create\_load\_balancer\_controller\_irsa](#input\_eks\_create\_load\_balancer\_controller\_irsa) | Enables creation of load balancer controller IAM role. | `bool` | `true` | no | +| [eks\_node\_additional\_security\_group\_ids](#input\_eks\_node\_additional\_security\_group\_ids) | Additional security group ids to attach to EKS nodes. | `list(string)` | `[]` | no | +| [eks\_node\_groups](#input\_eks\_node\_groups) | n/a |
list(object({
name = string
min_size = string
desired_size = string
max_size = string
volume_type = optional(string, "gp3")
disk_size = optional(number, 200)
disk_iops = optional(number, 3000)
disk_throughput = optional(number, 125)
device_name = optional(string, "/dev/xvda")
instance_types = list(string)
capacity_type = optional(string, "ON_DEMAND")
labels = optional(map(string), {})
taints = optional(map(object({ key = string, value = string, effect = string })), {})
}))
|
[
{
"desired_size": 3,
"instance_types": [
"c5.4xlarge"
],
"max_size": 9,
"min_size": 3,
"name": "ast-app"
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"sast-engine": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.4xlarge"
],
"labels": {
"sast-engine-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.2xlarge"
],
"labels": {
"sast-engine-extra-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-extra-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-extra-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.4xlarge"
],
"labels": {
"sast-engine-xxl": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-xxl",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-xxl",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"kics-engine": "true"
},
"max_size": 100,
"min_size": 1,
"name": "kics-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "kics-engine",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"repostore": "true"
},
"max_size": 100,
"min_size": 1,
"name": "repostore",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "repostore",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"service": "sca-source-resolver"
},
"max_size": 100,
"min_size": 0,
"name": "sca-source-resolver",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "service",
"value": "sca-source-resolver"
}
}
}
]
| no | +| [eks\_private\_endpoint\_enabled](#input\_eks\_private\_endpoint\_enabled) | Enables the EKS VPC private endpoint. | `bool` | `true` | no | +| [eks\_public\_endpoint\_enabled](#input\_eks\_public\_endpoint\_enabled) | Enables the EKS public endpoint. | `bool` | `false` | no | +| [eks\_version](#input\_eks\_version) | The version of the EKS Cluster (e.g. 1.27) | `string` | n/a | yes | +| [enable\_cluster\_creator\_admin\_permissions](#input\_enable\_cluster\_creator\_admin\_permissions) | Enables the identity used to create the EKS cluster to have administrator access to that EKS cluster. When enabled, do not specify the same principal arn for eks\_administrator\_principals. | `bool` | `true` | no | +| [es\_create](#input\_es\_create) | Enables creation of elasticsearch resources. | `bool` | `true` | no | +| [es\_instance\_count](#input\_es\_instance\_count) | The number of nodes in elasticsearch cluster | `number` | `2` | no | +| [es\_instance\_type](#input\_es\_instance\_type) | The instance type for elasticsearch nodes. | `string` | `"r6g.large.elasticsearch"` | no | +| [es\_tls\_security\_policy](#input\_es\_tls\_security\_policy) | n/a | `string` | `"Policy-Min-TLS-1-2-2019-07"` | no | +| [es\_username](#input\_es\_username) | The username for the elasticsearch user | `string` | `"ast"` | no | +| [es\_volume\_size](#input\_es\_volume\_size) | The size of volumes for nodes in elasticsearch cluster | `number` | `100` | no | +| [fqdn](#input\_fqdn) | The fully qualified domain name that will be used for the Checkmarx One deployment | `string` | n/a | yes | +| [interface\_vpc\_endpoints](#input\_interface\_vpc\_endpoints) | A list of services that vpc endpoints are created for. | `list(string)` |
[
"ec2",
"ec2messages",
"ssm",
"ssmmessages",
"ecr.api",
"ecr.dkr",
"kms",
"logs",
"sts",
"elasticloadbalancing",
"autoscaling"
]
| no | +| [kots\_admin\_email](#input\_kots\_admin\_email) | The email address of the Checkmarx One first admin user. | `string` | n/a | yes | +| [kots\_cxone\_version](#input\_kots\_cxone\_version) | The version of Checkmarx One to install | `string` | n/a | yes | +| [kots\_license\_file](#input\_kots\_license\_file) | The path to the kots license file to install Checkamrx One with. | `string` | n/a | yes | +| [kots\_release\_channel](#input\_kots\_release\_channel) | The release channel from which to install Checkmarx One | `string` | `"beta"` | no | +| [kube\_proxy\_version](#input\_kube\_proxy\_version) | The version of the EKS Kube Proxy Addon. | `string` | n/a | yes | +| [launch\_template\_tags](#input\_launch\_template\_tags) | Tags to associate with launch templates for node groups | `map(string)` | `null` | no | +| [object\_storage\_access\_key](#input\_object\_storage\_access\_key) | The S3 access key to use to access buckets | `string` | n/a | yes | +| [object\_storage\_endpoint](#input\_object\_storage\_endpoint) | The S3 endpoint to use to access buckets | `string` | n/a | yes | +| [object\_storage\_secret\_key](#input\_object\_storage\_secret\_key) | The S3 secret key to use to access buckets | `string` | n/a | yes | +| [route\_53\_hosted\_zone\_id](#input\_route\_53\_hosted\_zone\_id) | The hosted zone id for route 53 in which to create dns and certificates. | `string` | n/a | yes | +| [s3\_retention\_period](#input\_s3\_retention\_period) | The retention period, in days, to retain s3 objects. | `string` | `"90"` | no | +| [secondary\_vpc\_cidr](#input\_secondary\_vpc\_cidr) | The secondary VPC CIDR block to associate with the VPC. | `string` | `null` | no | +| [smtp\_port](#input\_smtp\_port) | The port of the SMTP server. | `number` | `587` | no | +| [vpc\_cidr](#input\_vpc\_cidr) | The primary VPC CIDR block to create the VPC with. | `string` | n/a | yes | +| [vpc\_cni\_version](#input\_vpc\_cni\_version) | The version of the EKS VPC CNI Addon. | `string` | n/a | yes | + +## Outputs + +No outputs. \ No newline at end of file diff --git a/examples/full/main.tf b/examples/full/main.tf new file mode 100644 index 0000000..73a4eff --- /dev/null +++ b/examples/full/main.tf @@ -0,0 +1,235 @@ +data "aws_region" "current" {} +data "aws_partition" "current" {} +data "aws_caller_identity" "current" {} +data "aws_availability_zones" "available" { + state = "available" +} + + +resource "aws_kms_key" "main" { + description = "KMS Key for the Checkmarx One deployment named ${var.deployment_id}" + deletion_window_in_days = 7 + enable_key_rotation = true + tags = { + Name = var.deployment_id + } +} + + +resource "random_password" "elasticsearch" { + length = 32 + special = false + override_special = "!*-_[]{}<>" + min_special = 1 + min_upper = 1 + min_lower = 1 + min_numeric = 1 +} + +resource "random_password" "db" { + length = 32 + special = false + override_special = "!*-_[]{}<>" + min_special = 1 + min_upper = 1 + min_lower = 1 + min_numeric = 1 +} + +resource "random_password" "kots_admin" { + length = 14 + special = false + override_special = "!*-_[]{}<>" + min_special = 1 + min_upper = 1 + min_lower = 1 + min_numeric = 1 +} + +resource "random_password" "cxone_admin" { + length = 14 + special = false + override_special = "!*-_[]{}<>" + min_special = 1 + min_upper = 1 + min_lower = 1 + min_numeric = 1 +} + + + + +module "acm" { + source = "terraform-aws-modules/acm/aws" + version = "5.0.1" + + domain_name = var.fqdn + zone_id = var.route_53_hosted_zone_id + + validation_method = "DNS" + create_certificate = true + create_route53_records = true + validate_certificate = true + wait_for_validation = true +} + +module "ses" { + source = "cloudposse/ses/aws" + version = "0.24.0" + zone_id = var.route_53_hosted_zone_id + domain = var.fqdn + verify_domain = true + verify_dkim = true + ses_group_enabled = true + ses_group_name = "${var.deployment_id}-ses-group" + ses_user_enabled = true + name = "CxOne-${var.deployment_id}" + environment = "dev" + enabled = true + + tags = { + Name = var.deployment_id + } +} + +resource "aws_iam_group_policy" "cxone_ses_group_policy" { + name = "${var.deployment_id}-ses-group-policy" + group = module.ses.ses_group_name + + depends_on = [module.ses] + + policy = jsonencode({ + Version : "2012-10-17" + Statement : [ + { + Effect : "Allow", + Action : [ + "ses:SendEmail", + "ses:SendRawEmail" + ], + Resource : "*" + } + ] + }) +} + + +module "checkmarx-one" { + source = "../../" + + # General Configuration + deployment_id = var.deployment_id + ec2_key_name = var.ec2_key_name + vpc_id = module.vpc.vpc_id + kms_key_arn = aws_kms_key.main.arn + s3_allowed_origins = [var.fqdn] + + # EKS Configuration + eks_create = var.eks_create + eks_subnets = module.vpc.private_subnets + eks_create_cluster_autoscaler_irsa = var.eks_create_cluster_autoscaler_irsa + eks_create_external_dns_irsa = var.eks_create_external_dns_irsa + eks_create_load_balancer_controller_irsa = var.eks_create_load_balancer_controller_irsa + eks_create_karpenter = var.eks_create_karpenter + eks_version = var.eks_version + coredns_version = var.coredns_version + kube_proxy_version = var.kube_proxy_version + vpc_cni_version = var.vpc_cni_version + aws_ebs_csi_driver_version = var.aws_ebs_csi_driver_version + eks_private_endpoint_enabled = var.eks_private_endpoint_enabled + eks_public_endpoint_enabled = var.eks_public_endpoint_enabled + eks_cluster_endpoint_public_access_cidrs = var.eks_cluster_endpoint_public_access_cidrs + enable_cluster_creator_admin_permissions = var.enable_cluster_creator_admin_permissions + launch_template_tags = var.launch_template_tags + + # RDS Configuration + db_subnets = module.vpc.private_subnets + db_engine_version = var.db_engine_version + db_allow_major_version_upgrade = var.db_allow_major_version_upgrade + db_auto_minor_version_upgrade = var.db_auto_minor_version_upgrade + db_apply_immediately = var.db_apply_immediately + db_deletion_protection = var.db_deletion_protection + db_snapshot_identifer = var.db_snapshot_identifer + db_skip_final_snapshot = var.db_skip_final_snapshot + db_final_snapshot_identifier = var.db_final_snapshot_identifier + db_instance_class = var.db_instance_class + db_monitoring_interval = var.db_monitoring_interval + # When enabling autoscaling, you may need to edit and save the autoscaling policy (no updates needed) + # to work around the issue described here: https://github.com/terraform-aws-modules/terraform-aws-rds-aurora/issues/432 + db_autoscaling_enabled = var.db_autoscaling_enabled + db_autoscaling_min_capacity = var.db_autoscaling_min_capacity + db_autoscaling_max_capacity = var.db_autoscaling_max_capacity + db_autoscaling_target_cpu = var.db_autoscaling_target_cpu + db_autoscaling_scale_out_cooldown = var.db_autoscaling_scale_out_cooldown + db_autoscaling_scale_in_cooldown = var.db_autoscaling_scale_in_cooldown + db_port = var.db_port + db_master_user_password = random_password.db.result + db_create_rds_proxy = var.db_create_rds_proxy + db_create = var.db_create + db_performance_insights_enabled = var.db_performance_insights_enabled + db_performance_insights_retention_period = var.db_performance_insights_retention_period + db_cluster_db_instance_parameter_group_name = var.db_cluster_db_instance_parameter_group_name + # Set individual instance properties. Reference https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_instance + db_instances = var.db_instances + db_serverlessv2_scaling_configuration = var.db_serverlessv2_scaling_configuration + + # Elasticache Configuration + ec_create = var.ec_create + ec_subnets = module.vpc.private_subnets + ec_enable_serverless = var.ec_enable_serverless + ec_serverless_max_storage = var.ec_serverless_max_storage + ec_serverless_max_ecpu_per_second = var.ec_serverless_max_ecpu_per_second + ec_engine_version = var.ec_engine_version + ec_parameter_group_name = var.ec_parameter_group_name + ec_automatic_failover_enabled = var.ec_automatic_failover_enabled + ec_multi_az_enabled = var.ec_multi_az_enabled + ec_node_type = var.ec_node_type # Production: cache.r7g.xlarge, Dev/Test: cache.r7g.large, Demo: cache.t4g.micro + ec_number_of_shards = var.ec_number_of_shards # Production 3, Dev/Test: 1, Demo: 1 + ec_replicas_per_shard = var.ec_replicas_per_shard # Production 2, Dev/Test: 1, Demo: 0 + ec_auto_minor_version_upgrade = var.ec_auto_minor_version_upgrade + + # Elasticsearch Configuration + es_create = var.es_create + es_subnets = module.vpc.private_subnets + es_instance_count = var.es_instance_count + es_instance_type = var.es_instance_type + es_volume_size = var.es_volume_size + es_tls_security_policy = var.es_tls_security_policy + es_password = random_password.elasticsearch.result +} + + +module "checkmarx-one-install" { + source = "../../modules/cxone-install" + + cxone_version = var.kots_cxone_version + release_channel = var.kots_release_channel + license_file = var.kots_license_file + kots_admin_password = random_password.kots_admin.result + + deployment_id = var.deployment_id + region = data.aws_region.current.name + admin_email = var.kots_admin_email + admin_password = random_password.cxone_admin.result + fqdn = var.fqdn + acm_certificate_arn = module.acm.acm_certificate_arn + bucket_suffix = module.checkmarx-one.s3_bucket_name_suffix + object_storage_endpoint = "s3.${data.aws_region.current.name}.amazonaws.com" + object_storage_access_key = var.object_storage_access_key + object_storage_secret_key = var.object_storage_secret_key + postgres_host = module.checkmarx-one.db_endpoint + postgres_database_name = module.checkmarx-one.db_database_name + postgres_user = module.checkmarx-one.db_master_username + postgres_password = module.checkmarx-one.db_master_password + redis_address = module.checkmarx-one.ec_endpoint + smtp_host = "email-smtp.${data.aws_region.current.name}.amazonaws.com" + smtp_port = var.smtp_port + smtp_password = module.ses.ses_smtp_password + smtp_user = module.ses.user_name + smtp_from_sender = "noreply@${var.fqdn}" + elasticsearch_host = module.checkmarx-one.es_endpoint + elasticsearch_password = random_password.elasticsearch.result + cluster_autoscaler_iam_role_arn = module.checkmarx-one.cluster_autoscaler_iam_role_arn + load_balancer_controller_iam_role_arn = module.checkmarx-one.load_balancer_controller_iam_role_arn + external_dns_iam_role_arn = module.checkmarx-one.external_dns_iam_role_arn +} diff --git a/examples/full/network.tf b/examples/full/network.tf new file mode 100644 index 0000000..87673a0 --- /dev/null +++ b/examples/full/network.tf @@ -0,0 +1,82 @@ +locals { + vpc_cidr_size = parseint(basename(var.vpc_cidr), 10) + aws_azs = slice(data.aws_availability_zones.available.names, 0, 3) + subnets_cidrs = cidrsubnets(var.vpc_cidr, (28 - local.vpc_cidr_size), (28 - local.vpc_cidr_size), (28 - local.vpc_cidr_size)) + private_subnets = slice(cidrsubnets(var.vpc_cidr, 2, 2, 2, 5, 5, 5, 6, 6, 6), 0, 3) # VPC=/16: /18 16,256; + public_subnets = slice(cidrsubnets(var.vpc_cidr, 2, 2, 2, 5, 5, 5, 6, 6, 6), 6, 9) # VPC=/16: /21 2,032; + database_subnets = slice(cidrsubnets(var.vpc_cidr, 2, 2, 2, 5, 5, 5, 6, 6, 6), 3, 6) # VPC=/16: /22 1,016; +} + +module "vpc" { + + source = "terraform-aws-modules/vpc/aws" + version = "5.7.0" + + name = var.deployment_id + cidr = var.vpc_cidr + secondary_cidr_blocks = var.secondary_vpc_cidr != null ? [var.secondary_vpc_cidr] : [] + + azs = local.aws_azs + + private_subnets = local.private_subnets + public_subnets = local.public_subnets + database_subnets = local.database_subnets + + enable_nat_gateway = true + single_nat_gateway = true + one_nat_gateway_per_az = false + + enable_dns_hostnames = true + enable_dns_support = true + + public_subnet_tags = { + "karpenter.sh/discovery" = "${var.deployment_id}" + "kubernetes.io/cluster/${var.deployment_id}" = "shared" + "kubernetes.io/role/elb" = "1" + } + + private_subnet_tags = { + "karpenter.sh/discovery" = "${var.deployment_id}" + "kubernetes.io/cluster/${var.deployment_id}" = "shared" + "kubernetes.io/role/internal-elb" = "1" + } + + enable_flow_log = true + create_flow_log_cloudwatch_iam_role = true + create_flow_log_cloudwatch_log_group = true + +} + +module "vpc_endpoint_security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "5.1.2" + name = "${var.deployment_id}-vpc-endpoints" + description = "VPC endpoint security group for Checkmarx One deployment named ${var.deployment_id}" + vpc_id = module.vpc.vpc_id + ingress_cidr_blocks = concat([module.vpc.vpc_cidr_block], module.vpc.vpc_secondary_cidr_blocks) + ingress_rules = ["https-443-tcp"] +} + + +resource "aws_vpc_endpoint" "interface" { + for_each = toset(var.interface_vpc_endpoints) + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + service_name = "com.amazonaws.${data.aws_region.current.name}.${each.key}" + vpc_endpoint_type = "Interface" + security_group_ids = [module.vpc_endpoint_security_group.security_group_id] + private_dns_enabled = true + tags = { + Name = "${var.deployment_id}-${each.key}-vpc-endpoint" + } +} + +resource "aws_vpc_endpoint" "s3_gateway_private" { + vpc_endpoint_type = "Gateway" + service_name = "com.amazonaws.${data.aws_region.current.name}.s3" + vpc_id = module.vpc.vpc_id + route_table_ids = module.vpc.private_route_table_ids + tags = { + Name = "${var.deployment_id}-s3-vpc-endpoint" + } +} diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf new file mode 100644 index 0000000..7173bae --- /dev/null +++ b/examples/full/variables-cxone.tf @@ -0,0 +1,605 @@ +# variables-cxone.tf contains pass thru variables defined by terraform-aws-cxone module. +# The variable definitions are duplicated here so that the example project can be used +# to control the module configuration. + + +#****************************************************************************** +# General Configuration +#****************************************************************************** + +variable "deployment_id" { + description = "The id of the deployment. Will be used to name resources like EKS cluster, etc." + type = string + nullable = false + validation { + condition = (length(var.deployment_id) >= 3) + error_message = "The deployment_id must be greater than 3 characters." + } +} + +# variable "kms_key_arn" { +# type = string +# description = "The ARN of the KMS key to use for encryption in AWS services" +# } + +# variable "vpc_id" { +# description = "The id of the vpc deploying into." +# type = string +# } + + +#****************************************************************************** +# S3 Configuration +#****************************************************************************** + +variable "s3_retention_period" { + description = "The retention period, in days, to retain s3 objects." + type = string + default = "90" +} + +# variable "s3_allowed_origins" { +# description = "The allowed orgins for S3 CORS rules." +# type = list(string) +# nullable = false +# } + +#****************************************************************************** +# EKS Configuration +#****************************************************************************** +variable "eks_create" { + type = bool + description = "Enables the EKS resource creation" + default = true +} + +# variable "eks_subnets" { +# description = "The subnets to deploy EKS into." +# type = list(string) +# } + +variable "eks_create_cluster_autoscaler_irsa" { + type = bool + description = "Enables creation of cluster autoscaler IAM role." + default = true +} + +variable "eks_create_external_dns_irsa" { + type = bool + description = "Enables creation of external dns IAM role." + default = true +} + +variable "eks_create_load_balancer_controller_irsa" { + type = bool + description = "Enables creation of load balancer controller IAM role." + default = true +} + +variable "eks_create_karpenter" { + type = bool + description = "Enables creation of Karpenter resources." + default = false +} + +variable "eks_version" { + type = string + description = "The version of the EKS Cluster (e.g. 1.27)" +} + +variable "eks_private_endpoint_enabled" { + type = bool + description = "Enables the EKS VPC private endpoint." + default = true +} + +variable "eks_public_endpoint_enabled" { + type = bool + description = "Enables the EKS public endpoint." + default = false +} + +variable "eks_cluster_endpoint_public_access_cidrs" { + type = list(string) + description = " List of CIDR blocks which can access the Amazon EKS public API server endpoint" + default = ["0.0.0.0/0"] +} + +variable "eks_node_additional_security_group_ids" { + description = "Additional security group ids to attach to EKS nodes." + type = list(string) + default = [] +} + +variable "coredns_version" { + type = string + description = "The version of the EKS Core DNS Addon." +} + +variable "kube_proxy_version" { + type = string + description = "The version of the EKS Kube Proxy Addon." +} + +variable "vpc_cni_version" { + type = string + description = "The version of the EKS VPC CNI Addon." +} + +variable "aws_ebs_csi_driver_version" { + type = string + description = "The version of the EKS EBS CSI Addon." +} + +variable "launch_template_tags" { + type = map(string) + description = "Tags to associate with launch templates for node groups" + default = null +} + +variable "enable_cluster_creator_admin_permissions" { + type = bool + description = "Enables the identity used to create the EKS cluster to have administrator access to that EKS cluster. When enabled, do not specify the same principal arn for eks_administrator_principals." + default = true +} + +variable "eks_administrator_principals" { + type = list(object({ + name = string + principal_arn = string + })) + description = "The ARNs of the IAM roles for administrator access to EKS." + default = [] +} + +variable "ec2_key_name" { + description = "The name of the EC2 key pair to access servers." + type = string + default = null +} + + +variable "eks_node_groups" { + type = list(object({ + name = string + min_size = string + desired_size = string + max_size = string + volume_type = optional(string, "gp3") + disk_size = optional(number, 200) + disk_iops = optional(number, 3000) + disk_throughput = optional(number, 125) + device_name = optional(string, "/dev/xvda") + instance_types = list(string) + capacity_type = optional(string, "ON_DEMAND") + labels = optional(map(string), {}) + taints = optional(map(object({ key = string, value = string, effect = string })), {}) + })) + default = [{ + name = "ast-app" + min_size = 3 + desired_size = 3 + max_size = 9 + instance_types = ["c5.4xlarge"] + }, + { + name = "sast-engine" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["m5.2xlarge"] + labels = { + "sast-engine" = "true" + } + taints = { + dedicated = { + key = "sast-engine" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sast-engine-large" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["m5.4xlarge"] + labels = { + "sast-engine-large" = "true" + } + taints = { + dedicated = { + key = "sast-engine-large" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sast-engine-extra-large" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["r5.2xlarge"] + labels = { + "sast-engine-extra-large" = "true" + } + taints = { + dedicated = { + key = "sast-engine-extra-large" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sast-engine-xxl" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["r5.4xlarge"] + labels = { + "sast-engine-xxl" = "true" + } + taints = { + dedicated = { + key = "sast-engine-xxl" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "kics-engine" + min_size = 1 + desired_size = 1 + max_size = 100 + instance_types = ["c5.2xlarge"] + labels = { + "kics-engine" = "true" + } + taints = { + dedicated = { + key = "kics-engine" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "repostore" + min_size = 1 + desired_size = 1 + max_size = 100 + instance_types = ["c5.2xlarge"] + labels = { + "repostore" = "true" + } + taints = { + dedicated = { + key = "repostore" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sca-source-resolver" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["m5.2xlarge"] + labels = { + "service" = "sca-source-resolver" + } + taints = { + dedicated = { + key = "service" + value = "sca-source-resolver" + effect = "NO_SCHEDULE" + } + } + } + ] +} + +#****************************************************************************** +# RDS Configuration +#****************************************************************************** + +variable "db_engine_version" { + description = "The aurora postgres engine version." + type = string + default = "13.8" +} + + +# variable "db_subnets" { +# description = "The subnets to deploy RDS into." +# type = list(string) +# } + + +variable "db_allow_major_version_upgrade" { + description = "Allows major version upgrades." + type = bool + default = false +} + +variable "db_auto_minor_version_upgrade" { + description = "Automatically upgrade to latest minor version in maintenance window." + type = bool + default = false +} + +variable "db_instance_class" { + description = "The aurora postgres instance class." + type = string + default = "db.r6g.xlarge" +} + +variable "db_monitoring_interval" { + description = "The aurora postgres engine version." + type = string + default = "10" +} + +variable "db_autoscaling_enabled" { + description = "Enables autoscaling of the aurora database." + type = bool + default = true +} + +variable "db_autoscaling_min_capacity" { + description = "The minimum number of replicas via autoscaling." + type = string + default = "1" +} + +variable "db_autoscaling_max_capacity" { + description = "The maximum number of replicas via autoscaling." + type = string + default = "3" +} + +variable "db_autoscaling_target_cpu" { + description = "The CPU utilization for autoscaling target tracking." + type = number + default = 70 +} + +variable "db_autoscaling_scale_in_cooldown" { + description = "The database scale in cooldown period." + type = number + default = 300 +} + +variable "db_autoscaling_scale_out_cooldown" { + description = "The database scale ou cooldown period." + type = number + default = 300 +} + +variable "db_snapshot_identifer" { + description = "The snapshot identifier to restore the database from." + type = string + default = null +} + +variable "db_port" { + description = "The port on which the DB accepts connections." + type = string + default = "5432" +} + +variable "db_master_user_password" { + description = "The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it." + type = string + default = null +} + +variable "db_create_rds_proxy" { + description = "Enables an RDS proxy for the Aurora postgres database." + type = bool + default = true +} + +variable "db_create" { + description = "Controls creation of the Aurora postgres database." + type = bool + default = true +} + +variable "db_skip_final_snapshot" { + description = "Enables skipping the final snapshot upon deletion." + type = bool + default = false +} + +variable "db_final_snapshot_identifier" { + description = "Identifer for a final DB snapshot. Required when db_skip_final_snapshot is false.." + type = string + default = null +} + +variable "db_deletion_protection" { + description = "Enables deletion protection to avoid accidental database deletion." + type = bool + default = true +} + +variable "db_instances" { + type = map(any) + description = "The DB instance configuration" + default = { + writer = {} + replica1 = {} + } +} + +variable "db_serverlessv2_scaling_configuration" { + description = "The serverless v2 scaling minimum and maximum." + type = object({ + min_capacity = number + max_capacity = number + }) + default = { + min_capacity = 0.5 + max_capacity = 32 + } +} + +variable "db_performance_insights_enabled" { + type = bool + default = true + description = "Enables database performance insights." +} + +variable "db_performance_insights_retention_period" { + type = number + default = 7 + description = "Number of days to retain performance insights data. Free tier: 7 days." +} + +variable "db_cluster_db_instance_parameter_group_name" { + type = string + default = null + description = "The name of the DB Cluster parameter group to use." +} + +variable "db_apply_immediately" { + type = bool + default = false + description = "Determines if changes will be applied immediately or wait until the next maintenance window." +} + + +#****************************************************************************** +# Elasticache Configuration +#****************************************************************************** +variable "ec_create" { + type = bool + default = true + description = "Enables the creation of elasticache resources." +} + +# variable "ec_subnets" { +# description = "The subnets to deploy Elasticache into." +# type = list(string) +# } + +variable "ec_enable_serverless" { + type = bool + default = false + description = "Enables the use of elasticache for redis serverless." +} + +variable "ec_serverless_max_storage" { + type = number + default = 5 + description = "The max storage, in GB, for serverless elasticache for redis." +} +variable "ec_serverless_max_ecpu_per_second" { + type = number + default = 5000 + description = "The max eCPU per second for serverless elasticache for redis." +} + +variable "ec_engine_version" { + type = string + description = "The version of the elasticache cluster. Does not apply to serverless." + default = "6.x" +} +variable "ec_parameter_group_name" { + type = string + description = "The elasticache parameter group name. Does not apply to serverless." + default = "default.redis6.x.cluster.on" +} + +variable "ec_node_type" { + type = string + description = "The elasticache redis node type. Does not apply to serverless." + default = "cache.m6g.large" +} + +variable "ec_number_of_shards" { + type = number + description = "The number of shards for redis. Does not apply to serverless." + default = 3 +} + +variable "ec_replicas_per_shard" { + type = number + description = "The number of replicas per shard for redis. Does not apply to serverless." + default = 2 +} + +variable "ec_auto_minor_version_upgrade" { + type = bool + description = "Enables automatic minor version upgrades. Does not apply to serverless." + default = false +} + +variable "ec_automatic_failover_enabled" { + type = bool + description = "Enables automatic failover. Does not apply to serverless." + default = true +} + +variable "ec_multi_az_enabled" { + type = bool + description = "Enables automatic failover. Does not apply to serverless." + default = true +} + +#****************************************************************************** +# Elasticsearch Configuration +#****************************************************************************** + +variable "es_create" { + type = bool + description = "Enables creation of elasticsearch resources." + default = true +} + +# variable "es_subnets" { +# description = "The subnets to deploy Elasticsearch into." +# type = list(string) +# } + +variable "es_instance_type" { + type = string + description = "The instance type for elasticsearch nodes." + default = "r6g.large.elasticsearch" +} + +variable "es_instance_count" { + type = number + description = "The number of nodes in elasticsearch cluster" + default = 2 +} + +variable "es_volume_size" { + type = number + description = "The size of volumes for nodes in elasticsearch cluster" + default = 100 +} + +variable "es_tls_security_policy" { + default = "Policy-Min-TLS-1-2-2019-07" + type = string +} + +variable "es_username" { + description = "The username for the elasticsearch user" + type = string + default = "ast" +} + +# variable "es_password" { +# description = "The password for the elasticsearch user" +# type = string +# sensitive = true +# } + diff --git a/examples/full/variables-example.tf b/examples/full/variables-example.tf new file mode 100644 index 0000000..40696fe --- /dev/null +++ b/examples/full/variables-example.tf @@ -0,0 +1,110 @@ +#****************************************************************************** +# Base Infrastructure Configuration - These variables are used by the example itself +#****************************************************************************** + +variable "vpc_cidr" { + type = string + description = "The primary VPC CIDR block to create the VPC with." +} + +variable "secondary_vpc_cidr" { + type = string + description = "The secondary VPC CIDR block to associate with the VPC." + default = null +} + +variable "interface_vpc_endpoints" { + type = list(string) + description = "A list of services that vpc endpoints are created for." + default = ["ec2", "ec2messages", "ssm", "ssmmessages", "ecr.api", "ecr.dkr", "kms", "logs", "sts", "elasticloadbalancing", "autoscaling"] +} + +variable "create_s3_endpoint" { + type = bool + description = "Enables creation of the s3 gateway VPC interface endpoint." + default = true +} + +variable "route_53_hosted_zone_id" { + type = string + description = "The hosted zone id for route 53 in which to create dns and certificates." + nullable = true +} + +variable "fqdn" { + type = string + description = "The fully qualified domain name that will be used for the Checkmarx One deployment" +} + + +#****************************************************************************** +# S3 Configuration +#****************************************************************************** + +variable "object_storage_endpoint" { + type = string + description = "The S3 endpoint to use to access buckets" +} + +variable "object_storage_access_key" { + type = string + description = "The S3 access key to use to access buckets" +} + +variable "object_storage_secret_key" { + type = string + description = "The S3 secret key to use to access buckets" +} + +#****************************************************************************** +# SMTP Configuration +#****************************************************************************** +# variable "smtp_host" { +# description = "The hostname of the SMTP server." +# type = string +# } + +variable "smtp_port" { + description = "The port of the SMTP server." + type = number + default = 587 +} + +# variable "smtp_user" { +# description = "The smtp user name." +# type = string +# } + +# variable "smtp_password" { +# description = "The smtp password." +# type = string +# } + +# variable "smtp_from_sender" { +# description = "The address to use in the from field when sending emails." +# type = string +# } + +#****************************************************************************** +# Kots & Installation Configuration +#****************************************************************************** +variable "kots_cxone_version" { + description = "The version of Checkmarx One to install" + type = string +} + +variable "kots_release_channel" { + description = "The release channel from which to install Checkmarx One" + type = string + default = "beta" +} + +variable "kots_license_file" { + description = "The path to the kots license file to install Checkamrx One with." + type = string +} + +variable "kots_admin_email" { + description = "The email address of the Checkmarx One first admin user." + type = string +} \ No newline at end of file From 097333d02fc97b89063354f191f6f034424ea3ff Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 10 Apr 2024 14:58:52 -0400 Subject: [PATCH 03/57] wip --- README.md | 239 +++-- data.tf | 6 + eks.tf | 322 +++++++ elasticache.tf | 85 ++ modules/cxone-install/install.sh.tftpl | 30 + .../kots.config.aws.reference.yaml.tftpl | 363 +++++++ modules/cxone-install/main.tf | 76 ++ modules/cxone-install/makefile.tftpl | 79 ++ modules/cxone-install/variables.tf | 173 ++++ opensearch.tf | 93 ++ outputs.tf | 4 + rds.tf | 150 +++ results.json | 888 ++++++++++++++++++ s3.tf | 162 ++++ variables.tf | 600 ++++++++++++ 15 files changed, 3188 insertions(+), 82 deletions(-) create mode 100644 data.tf create mode 100644 eks.tf create mode 100644 elasticache.tf create mode 100644 modules/cxone-install/install.sh.tftpl create mode 100644 modules/cxone-install/kots.config.aws.reference.yaml.tftpl create mode 100644 modules/cxone-install/main.tf create mode 100644 modules/cxone-install/makefile.tftpl create mode 100644 modules/cxone-install/variables.tf create mode 100644 opensearch.tf create mode 100644 outputs.tf create mode 100644 rds.tf create mode 100755 results.json create mode 100644 s3.tf create mode 100644 variables.tf diff --git a/README.md b/README.md index 08ca716..222f33f 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,157 @@ -# AWS Infrastructure - -The terraform code present in this repo will create the infrastructure necessary to run the AST single-tenant solution. - -# S3 Backend configuration - -[Terraform documentation](https://www.terraform.io/language/settings/backends/s3) - - - To enable remote state storage with S3, the first step is to create an S3 bucket. - -The terraform state file will be saved in an S3 bucket. To be sure that the state will be saved in the desired location, please change the file `s3_backend_configuration.conf` present in both directories (infrastructure and kubernetes-config). - -The file structure should follow this schema -``` -bucket="" -region="" -key="/terraform.tfstate" -``` - -For example: -``` -bucket="terraform-state-bucket" -region="eu-west-1" -key="infra/terraform.tfstate" -``` - - -## Using Makefile configuration -When running the command `make init` on the desired directory it will use this file to pass the parameters to the terraform backend configuration. - -## Using terraform command line -`terraform init -backend-config=s3_backend_configuration.conf` - -or you can use the full command: - -`terraform init -backend-config=bucket=BUCKET_NAME -backend-config=key=S3_KEY -backend-config=region=AWS_REGION` - -# Instalation order - -- The first terraform module that needs to be installed is `infrastructure` only after the instalation is complete you should move to the second one. - - -``` -cd infrastructure -make plan -make apply -``` - - -- When the infrastructure is ready, apply the module `kubernetes-config`. - -``` -cd kubernetes-config -make plan -make apply -``` - -Please, take a look on the `example.auto.tfvars` file to see the parameters that you need to inform. - - - -# TF Destroy - - -If you already installed the CxOne solution using Kots, before running `make destroy` it is recommended to uninstall the following HELM chart: -- ast (helm uninstall ast -n ast) - -This is recommended to avoid leaving the load balancer created by the traefik service behind. - -## Destroy the module kubernetes-config - -``` -cd kubernetes-config -make destroy -``` - -## Destroy the module infrastructure - -``` -cd infrastructure -make destroy -``` \ No newline at end of file +# terraform-aws-cxone + +This repo contains a module for deploying [Checkmarx One](https://checkmarx.com/product/application-security-platform/) on [AWS](https://aws.amazon.com) using [Terraform](https://www.terraform.io). Checkmarx One has everything you need to embed AppSec in every stage of the SDLC, provide an excellent developer experience, integrate with the technologies you use, and build a successful AppSec program. + + +# Module documentation + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | +| [random](#provider\_random) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [cluster\_autoscaler\_irsa](#module\_cluster\_autoscaler\_irsa) | terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks | 5.39.0 | +| [eks](#module\_eks) | terraform-aws-modules/eks/aws | 20.8.5 | +| [eks\_node\_iam\_role](#module\_eks\_node\_iam\_role) | terraform-aws-modules/iam/aws//modules/iam-assumable-role | 5.37.2 | +| [elasticache\_security\_group](#module\_elasticache\_security\_group) | terraform-aws-modules/security-group/aws | 5.1.2 | +| [elasticsearch\_security\_group](#module\_elasticsearch\_security\_group) | terraform-aws-modules/security-group/aws | 5.1.2 | +| [external\_dns\_irsa](#module\_external\_dns\_irsa) | terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks | 5.39.0 | +| [karpenter](#module\_karpenter) | terraform-aws-modules/eks/aws//modules/karpenter | 20.8.5 | +| [load\_balancer\_controller\_irsa](#module\_load\_balancer\_controller\_irsa) | terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks | 5.39.0 | +| [rds](#module\_rds) | terraform-aws-modules/rds-aurora/aws | 9.3.1 | +| [rds-proxy](#module\_rds-proxy) | terraform-aws-modules/rds-proxy/aws | 3.1.0 | +| [rds\_proxy\_sg](#module\_rds\_proxy\_sg) | terraform-aws-modules/security-group/aws | 5.1.2 | +| [s3\_bucket](#module\_s3\_bucket) | terraform-aws-modules/s3-bucket/aws | 4.1.1 | + +## Resources + +| Name | Type | +|------|------| +| [aws_autoscaling_group_tag.cluster_autoscaler_label](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group_tag) | resource | +| [aws_autoscaling_group_tag.cluster_autoscaler_taint](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group_tag) | resource | +| [aws_db_subnet_group.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_subnet_group) | resource | +| [aws_elasticache_replication_group.redis](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticache_replication_group) | resource | +| [aws_elasticache_serverless_cache.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticache_serverless_cache) | resource | +| [aws_elasticache_subnet_group.redis](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticache_subnet_group) | resource | +| [aws_elasticsearch_domain.es](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticsearch_domain) | resource | +| [aws_iam_policy.s3_bucket_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [random_string.random_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [aws_vpc.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aws\_ebs\_csi\_driver\_version](#input\_aws\_ebs\_csi\_driver\_version) | The version of the EKS EBS CSI Addon. | `string` | n/a | yes | +| [coredns\_version](#input\_coredns\_version) | The version of the EKS Core DNS Addon. | `string` | n/a | yes | +| [db\_allow\_major\_version\_upgrade](#input\_db\_allow\_major\_version\_upgrade) | Allows major version upgrades. | `bool` | `false` | no | +| [db\_apply\_immediately](#input\_db\_apply\_immediately) | Determines if changes will be applied immediately or wait until the next maintenance window. | `bool` | `false` | no | +| [db\_auto\_minor\_version\_upgrade](#input\_db\_auto\_minor\_version\_upgrade) | Automatically upgrade to latest minor version in maintenance window. | `bool` | `false` | no | +| [db\_autoscaling\_enabled](#input\_db\_autoscaling\_enabled) | Enables autoscaling of the aurora database. | `bool` | `true` | no | +| [db\_autoscaling\_max\_capacity](#input\_db\_autoscaling\_max\_capacity) | The maximum number of replicas via autoscaling. | `string` | `"3"` | no | +| [db\_autoscaling\_min\_capacity](#input\_db\_autoscaling\_min\_capacity) | The minimum number of replicas via autoscaling. | `string` | `"1"` | no | +| [db\_autoscaling\_scale\_in\_cooldown](#input\_db\_autoscaling\_scale\_in\_cooldown) | The database scale in cooldown period. | `number` | `300` | no | +| [db\_autoscaling\_scale\_out\_cooldown](#input\_db\_autoscaling\_scale\_out\_cooldown) | The database scale ou cooldown period. | `number` | `300` | no | +| [db\_autoscaling\_target\_cpu](#input\_db\_autoscaling\_target\_cpu) | The CPU utilization for autoscaling target tracking. | `number` | `70` | no | +| [db\_cluster\_db\_instance\_parameter\_group\_name](#input\_db\_cluster\_db\_instance\_parameter\_group\_name) | The name of the DB Cluster parameter group to use. | `string` | `null` | no | +| [db\_create](#input\_db\_create) | Controls creation of the Aurora postgres database. | `bool` | `true` | no | +| [db\_create\_rds\_proxy](#input\_db\_create\_rds\_proxy) | Enables an RDS proxy for the Aurora postgres database. | `bool` | `true` | no | +| [db\_deletion\_protection](#input\_db\_deletion\_protection) | Enables deletion protection to avoid accidental database deletion. | `bool` | `true` | no | +| [db\_engine\_version](#input\_db\_engine\_version) | The aurora postgres engine version. | `string` | `"13.8"` | no | +| [db\_final\_snapshot\_identifier](#input\_db\_final\_snapshot\_identifier) | Identifer for a final DB snapshot. Required when db\_skip\_final\_snapshot is false.. | `string` | `null` | no | +| [db\_instance\_class](#input\_db\_instance\_class) | The aurora postgres instance class. | `string` | `"db.r6g.xlarge"` | no | +| [db\_instances](#input\_db\_instances) | The DB instance configuration | `map(any)` |
{
"replica1": {},
"writer": {}
}
| no | +| [db\_master\_user\_password](#input\_db\_master\_user\_password) | The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it. | `string` | `null` | no | +| [db\_monitoring\_interval](#input\_db\_monitoring\_interval) | The aurora postgres engine version. | `string` | `"10"` | no | +| [db\_performance\_insights\_enabled](#input\_db\_performance\_insights\_enabled) | Enables database performance insights. | `bool` | `true` | no | +| [db\_performance\_insights\_retention\_period](#input\_db\_performance\_insights\_retention\_period) | Number of days to retain performance insights data. Free tier: 7 days. | `number` | `7` | no | +| [db\_port](#input\_db\_port) | The port on which the DB accepts connections. | `string` | `"5432"` | no | +| [db\_serverlessv2\_scaling\_configuration](#input\_db\_serverlessv2\_scaling\_configuration) | The serverless v2 scaling minimum and maximum. |
object({
min_capacity = number
max_capacity = number
})
|
{
"max_capacity": 32,
"min_capacity": 0.5
}
| no | +| [db\_skip\_final\_snapshot](#input\_db\_skip\_final\_snapshot) | Enables skipping the final snapshot upon deletion. | `bool` | `false` | no | +| [db\_snapshot\_identifer](#input\_db\_snapshot\_identifer) | The snapshot identifier to restore the database from. | `string` | `null` | no | +| [db\_subnets](#input\_db\_subnets) | The subnets to deploy RDS into. | `list(string)` | n/a | yes | +| [deployment\_id](#input\_deployment\_id) | The id of the deployment. Will be used to name resources like EKS cluster, etc. | `string` | n/a | yes | +| [ec2\_key\_name](#input\_ec2\_key\_name) | The name of the EC2 key pair to access servers. | `string` | `null` | no | +| [ec\_auto\_minor\_version\_upgrade](#input\_ec\_auto\_minor\_version\_upgrade) | Enables automatic minor version upgrades. Does not apply to serverless. | `bool` | `false` | no | +| [ec\_automatic\_failover\_enabled](#input\_ec\_automatic\_failover\_enabled) | Enables automatic failover. Does not apply to serverless. | `bool` | `true` | no | +| [ec\_create](#input\_ec\_create) | Enables the creation of elasticache resources. | `bool` | `true` | no | +| [ec\_enable\_serverless](#input\_ec\_enable\_serverless) | Enables the use of elasticache for redis serverless. | `bool` | `false` | no | +| [ec\_engine\_version](#input\_ec\_engine\_version) | The version of the elasticache cluster. Does not apply to serverless. | `string` | `"6.x"` | no | +| [ec\_multi\_az\_enabled](#input\_ec\_multi\_az\_enabled) | Enables automatic failover. Does not apply to serverless. | `bool` | `true` | no | +| [ec\_node\_type](#input\_ec\_node\_type) | The elasticache redis node type. Does not apply to serverless. | `string` | `"cache.m6g.large"` | no | +| [ec\_number\_of\_shards](#input\_ec\_number\_of\_shards) | The number of shards for redis. Does not apply to serverless. | `number` | `3` | no | +| [ec\_parameter\_group\_name](#input\_ec\_parameter\_group\_name) | The elasticache parameter group name. Does not apply to serverless. | `string` | `"default.redis6.x.cluster.on"` | no | +| [ec\_replicas\_per\_shard](#input\_ec\_replicas\_per\_shard) | The number of replicas per shard for redis. Does not apply to serverless. | `number` | `2` | no | +| [ec\_serverless\_max\_ecpu\_per\_second](#input\_ec\_serverless\_max\_ecpu\_per\_second) | The max eCPU per second for serverless elasticache for redis. | `number` | `5000` | no | +| [ec\_serverless\_max\_storage](#input\_ec\_serverless\_max\_storage) | The max storage, in GB, for serverless elasticache for redis. | `number` | `5` | no | +| [ec\_subnets](#input\_ec\_subnets) | The subnets to deploy Elasticache into. | `list(string)` | n/a | yes | +| [eks\_administrator\_principals](#input\_eks\_administrator\_principals) | The ARNs of the IAM roles for administrator access to EKS. |
list(object({
name = string
principal_arn = string
}))
| `[]` | no | +| [eks\_cluster\_endpoint\_public\_access\_cidrs](#input\_eks\_cluster\_endpoint\_public\_access\_cidrs) | List of CIDR blocks which can access the Amazon EKS public API server endpoint | `list(string)` |
[
"0.0.0.0/0"
]
| no | +| [eks\_create](#input\_eks\_create) | Enables the EKS resource creation | `bool` | `true` | no | +| [eks\_create\_cluster\_autoscaler\_irsa](#input\_eks\_create\_cluster\_autoscaler\_irsa) | Enables creation of cluster autoscaler IAM role. | `bool` | `true` | no | +| [eks\_create\_external\_dns\_irsa](#input\_eks\_create\_external\_dns\_irsa) | Enables creation of external dns IAM role. | `bool` | `true` | no | +| [eks\_create\_karpenter](#input\_eks\_create\_karpenter) | Enables creation of Karpenter resources. | `bool` | `false` | no | +| [eks\_create\_load\_balancer\_controller\_irsa](#input\_eks\_create\_load\_balancer\_controller\_irsa) | Enables creation of load balancer controller IAM role. | `bool` | `true` | no | +| [eks\_node\_additional\_security\_group\_ids](#input\_eks\_node\_additional\_security\_group\_ids) | Additional security group ids to attach to EKS nodes. | `list(string)` | `[]` | no | +| [eks\_node\_groups](#input\_eks\_node\_groups) | n/a |
list(object({
name = string
min_size = string
desired_size = string
max_size = string
volume_type = optional(string, "gp3")
disk_size = optional(number, 200)
disk_iops = optional(number, 3000)
disk_throughput = optional(number, 125)
device_name = optional(string, "/dev/xvda")
instance_types = list(string)
capacity_type = optional(string, "ON_DEMAND")
labels = optional(map(string), {})
taints = optional(map(object({ key = string, value = string, effect = string })), {})
}))
|
[
{
"desired_size": 3,
"instance_types": [
"c5.4xlarge"
],
"max_size": 9,
"min_size": 3,
"name": "ast-app"
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"sast-engine": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.4xlarge"
],
"labels": {
"sast-engine-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.2xlarge"
],
"labels": {
"sast-engine-extra-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-extra-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-extra-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.4xlarge"
],
"labels": {
"sast-engine-xxl": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-xxl",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-xxl",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"kics-engine": "true"
},
"max_size": 100,
"min_size": 1,
"name": "kics-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "kics-engine",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"repostore": "true"
},
"max_size": 100,
"min_size": 1,
"name": "repostore",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "repostore",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"service": "sca-source-resolver"
},
"max_size": 100,
"min_size": 1,
"name": "sca-source-resolver",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "service",
"value": "sca-source-resolver"
}
}
}
]
| no | +| [eks\_private\_endpoint\_enabled](#input\_eks\_private\_endpoint\_enabled) | Enables the EKS VPC private endpoint. | `bool` | `true` | no | +| [eks\_public\_endpoint\_enabled](#input\_eks\_public\_endpoint\_enabled) | Enables the EKS public endpoint. | `bool` | `false` | no | +| [eks\_subnets](#input\_eks\_subnets) | The subnets to deploy EKS into. | `list(string)` | n/a | yes | +| [eks\_version](#input\_eks\_version) | The version of the EKS Cluster (e.g. 1.27) | `string` | n/a | yes | +| [enable\_cluster\_creator\_admin\_permissions](#input\_enable\_cluster\_creator\_admin\_permissions) | Enables the identity used to create the EKS cluster to have administrator access to that EKS cluster. When enabled, do not specify the same principal arn for eks\_administrator\_principals. | `bool` | `true` | no | +| [es\_create](#input\_es\_create) | Enables creation of elasticsearch resources. | `bool` | `true` | no | +| [es\_instance\_count](#input\_es\_instance\_count) | The number of nodes in elasticsearch cluster | `number` | `2` | no | +| [es\_instance\_type](#input\_es\_instance\_type) | The instance type for elasticsearch nodes. | `string` | `"r6g.large.elasticsearch"` | no | +| [es\_password](#input\_es\_password) | The password for the elasticsearch user | `string` | n/a | yes | +| [es\_subnets](#input\_es\_subnets) | The subnets to deploy Elasticsearch into. | `list(string)` | n/a | yes | +| [es\_tls\_security\_policy](#input\_es\_tls\_security\_policy) | n/a | `string` | `"Policy-Min-TLS-1-2-2019-07"` | no | +| [es\_username](#input\_es\_username) | The username for the elasticsearch user | `string` | `"ast"` | no | +| [es\_volume\_size](#input\_es\_volume\_size) | The size of volumes for nodes in elasticsearch cluster | `number` | `100` | no | +| [kms\_key\_arn](#input\_kms\_key\_arn) | The ARN of the KMS key to use for encryption in AWS services | `string` | n/a | yes | +| [kube\_proxy\_version](#input\_kube\_proxy\_version) | The version of the EKS Kube Proxy Addon. | `string` | n/a | yes | +| [launch\_template\_tags](#input\_launch\_template\_tags) | Tags to associate with launch templates for node groups | `map(string)` | `null` | no | +| [s3\_allowed\_origins](#input\_s3\_allowed\_origins) | The allowed orgins for S3 CORS rules. | `list(string)` | n/a | yes | +| [s3\_retention\_period](#input\_s3\_retention\_period) | The retention period, in days, to retain s3 objects. | `string` | `"90"` | no | +| [vpc\_cni\_version](#input\_vpc\_cni\_version) | The version of the EKS VPC CNI Addon. | `string` | n/a | yes | +| [vpc\_id](#input\_vpc\_id) | The id of the vpc deploying into. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [bucket\_suffix](#output\_bucket\_suffix) | n/a | +| [cluster\_autoscaler\_iam\_role\_arn](#output\_cluster\_autoscaler\_iam\_role\_arn) | n/a | +| [db\_database\_name](#output\_db\_database\_name) | n/a | +| [db\_endpoint](#output\_db\_endpoint) | n/a | +| [db\_master\_password](#output\_db\_master\_password) | n/a | +| [db\_master\_username](#output\_db\_master\_username) | n/a | +| [db\_port](#output\_db\_port) | n/a | +| [db\_reader\_endpoint](#output\_db\_reader\_endpoint) | n/a | +| [ec\_endpoint](#output\_ec\_endpoint) | n/a | +| [ec\_port](#output\_ec\_port) | n/a | +| [eks](#output\_eks) | n/a | +| [es\_endpoint](#output\_es\_endpoint) | n/a | +| [es\_password](#output\_es\_password) | n/a | +| [es\_username](#output\_es\_username) | n/a | +| [external\_dns\_iam\_role\_arn](#output\_external\_dns\_iam\_role\_arn) | n/a | +| [karpenter\_iam\_role\_arn](#output\_karpenter\_iam\_role\_arn) | n/a | +| [load\_balancer\_controller\_iam\_role\_arn](#output\_load\_balancer\_controller\_iam\_role\_arn) | n/a | +| [s3\_bucket\_name\_suffix](#output\_s3\_bucket\_name\_suffix) | n/a | + + +# Regional Considerations + +## GovCloud + +* RDS Proxy is not available in AWS Gov Cloud regions, so `create_rds_proxy` must be set `false`. Monitor database for connection usage and scale accordingly. +* RDS's `ManageMasterUserPassword` capability is not supported. Specify a password via `db_master_user_password` +* Elasticache's `cache.r7g` instance class is not available. Consider using `cache.r6g`. diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..99b5bab --- /dev/null +++ b/data.tf @@ -0,0 +1,6 @@ +data "aws_region" "current" {} +data "aws_partition" "current" {} +data "aws_caller_identity" "current" {} +data "aws_vpc" "main" { + id = var.vpc_id +} \ No newline at end of file diff --git a/eks.tf b/eks.tf new file mode 100644 index 0000000..ebbed06 --- /dev/null +++ b/eks.tf @@ -0,0 +1,322 @@ +locals { + eks_nodegroups = { for node_group in var.eks_node_groups : node_group.name => { + name = "${var.deployment_id}-${node_group.name}" + launch_template_name = "${var.deployment_id}-${node_group.name}" + min_size = node_group.min_size + max_size = node_group.max_size + desired_size = node_group.desired_size + instance_types = node_group.instance_types + capacity_type = node_group.capacity_type + block_device_mappings = { + xvda = { + device_name = node_group.device_name + ebs = { + volume_size = node_group.disk_size + volume_type = node_group.volume_type + iops = node_group.disk_iops + throughput = node_group.disk_throughput + encrypted = true + delete_on_termination = true + } + } + } + labels = node_group.labels + taints = node_group.taints + tags = { + Name = "${var.deployment_id}-${node_group.name}" + } + lifecycle = { + ignore_changes = ["desired_capacity"] + } + } } + + admin_access_entries = { for entry in var.eks_administrator_principals : entry.name => { + principal_arn = entry.principal_arn + policy_associations = { + admin = { + policy_arn = "arn:${data.aws_partition.current.partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + access_scope = { + type = "cluster" + } + } + } + } } +} + +module "eks_node_iam_role" { + source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role" + version = "5.37.2" + trusted_role_services = [ + "ec2.amazonaws.com" + ] + create_role = true + role_name = "${var.deployment_id}-eks-nodes" + role_requires_mfa = false + custom_role_policy_arns = [ + "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy", + "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", + "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKS_CNI_Policy", + "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy", + "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore", + "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", + aws_iam_policy.s3_bucket_access.arn + ] +} + +resource "aws_iam_policy" "s3_bucket_access" { + name = "${var.deployment_id}-s3-access" + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : [ + "s3:*" + ], + "Effect" : "Allow", + "Resource" : [ + "arn:${data.aws_partition.current.partition}:s3:::${var.deployment_id}*", + "arn:${data.aws_partition.current.partition}:s3:::${var.deployment_id}*/*" + ] + } + ] + }) +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "20.8.5" + create = var.eks_create + + cluster_name = var.deployment_id + cluster_version = var.eks_version + + cluster_enabled_log_types = ["audit", "api", "authenticator", "scheduler"] + + cluster_endpoint_private_access = var.eks_private_endpoint_enabled + cluster_endpoint_public_access = var.eks_public_endpoint_enabled + + vpc_id = var.vpc_id + subnet_ids = var.eks_subnets + + create_cluster_security_group = true + #cluster_security_group_id = var.cluster_security_group_id + + create_node_security_group = true + #node_security_group_id = module.eks_nodes_security_group.security_group_id + + node_security_group_additional_rules = { + ingress_self_http80 = { + description = "Node to node ingress http/80" + protocol = "tcp" + from_port = 80 + to_port = 80 + type = "ingress" + self = true + } + ingress_self_regosync = { + description = "Node to node ingress http/81 (regosync service)" + protocol = "tcp" + from_port = 81 + to_port = 81 + type = "ingress" + self = true + } + ingress_self_feedback_kics = { + description = "Node to node ingress http/86-88 (feedback-mfe, kics, sast results service)" + protocol = "tcp" + from_port = 86 + to_port = 89 + type = "ingress" + self = true + } + ingress_self_sast_audit_queries = { + description = "Node to node ingress http/777-778 (sast audit queries service)" + protocol = "tcp" + from_port = 777 + to_port = 778 + type = "ingress" + self = true + } + } + + enable_irsa = true + + enable_cluster_creator_admin_permissions = var.enable_cluster_creator_admin_permissions + access_entries = local.admin_access_entries + + + cluster_addons = { + coredns = { + addon_version = var.coredns_version + } + kube-proxy = { + addon_version = var.kube_proxy_version + } + vpc-cni = { + addon_version = var.vpc_cni_version + # Todo: add configuration for secondary cidr here to vpc plugin + # before_compute = var.pod_custom_networking_subnets != null ? true : false + # configuration_values = jsonencode({ + # env = { + # AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = var.pod_custom_networking_subnets != null ? "true" : "false" + # ENI_CONFIG_LABEL_DEF = var.pod_custom_networking_subnets != null ? "topology.kubernetes.io/zone" : "" + # } }) + + } + aws-ebs-csi-driver = { + addon_version = var.aws_ebs_csi_driver_version + } + } + create_kms_key = false + cluster_encryption_config = { + "resources" = ["secrets"] + provider_key_arn = var.kms_key_arn + } + eks_managed_node_group_defaults = { + vpc_security_group_ids = var.eks_node_additional_security_group_ids + use_name_prefix = false + iam_role_use_name_prefix = false + launch_template_use_name_prefix = false + launch_template_tags = var.launch_template_tags + cluster_name = var.deployment_id + cluster_version = var.eks_version + subnet_ids = var.eks_subnets + create_iam_role = false + iam_role_arn = module.eks_node_iam_role.iam_role_arn + key_name = var.ec2_key_name + metadata_options = { + http_endpoint = "enabled" + http_tokens = "required" + instance_metadata_tags = "disabled" + http_put_response_hop_limit = "2" + } + } + + eks_managed_node_groups = local.eks_nodegroups +} + +resource "aws_autoscaling_group_tag" "cluster_autoscaler_label" { + for_each = { for node_group in var.eks_node_groups : node_group.name => node_group } + depends_on = [module.eks] + autoscaling_group_name = module.eks.eks_managed_node_groups["${each.value.name}"].node_group_autoscaling_group_names[0] + tag { + key = "k8s.io/cluster-autoscaler/node-template/label/${each.value.name}" + value = "true" + propagate_at_launch = true + } +} + +resource "aws_autoscaling_group_tag" "cluster_autoscaler_taint" { + for_each = { for node_group in var.eks_node_groups : node_group.name => node_group if length(node_group.taints) > 0 } + depends_on = [module.eks] + autoscaling_group_name = module.eks.eks_managed_node_groups["${each.value.name}"].node_group_autoscaling_group_names[0] + tag { + key = "k8s.io/cluster-autoscaler/node-template/taint/${each.value.taints.dedicated.key}" + value = "${each.value.taints.dedicated.value}:${each.value.taints.dedicated.effect}" + propagate_at_launch = true + } +} + + +module "cluster_autoscaler_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "5.39.0" + count = var.eks_create && var.eks_create_cluster_autoscaler_irsa ? 1 : 0 + + role_name = "cluster-autoscaler-${var.deployment_id}" + role_description = "IRSA role for cluster autoscaler" + attach_cluster_autoscaler_policy = true + + cluster_autoscaler_cluster_names = [module.eks.cluster_name] + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:cluster-autoscaler"] + } + } +} + + +module "external_dns_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "5.39.0" + count = var.eks_create && var.eks_create_external_dns_irsa ? 1 : 0 + + role_name = "external-dns-${var.deployment_id}" + role_description = "IRSA role for cluster external dns controller" + #external_dns_hosted_zone_arns = var.external_dns_hosted_zone_arns + # setting to false because we don't want to rely on exeternal policies + attach_external_dns_policy = true + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:external-dns"] + } + } +} + + +module "load_balancer_controller_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "5.39.0" + count = var.eks_create && var.eks_create_load_balancer_controller_irsa ? 1 : 0 + + role_name = "load_balancer_controller-${var.deployment_id}" + role_description = "IRSA role for cluster load balancer controller" + attach_load_balancer_controller_policy = true + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:aws-load-balancer-controller"] + } + } +} + + +module "karpenter" { + source = "terraform-aws-modules/eks/aws//modules/karpenter" + version = "20.8.5" + create = var.eks_create && var.eks_create_karpenter + + cluster_name = var.deployment_id + enable_irsa = true + irsa_oidc_provider_arn = module.eks.oidc_provider_arn + irsa_namespace_service_accounts = ["kube-system:karpenter"] + create_iam_role = true + iam_role_name = "KarpenterController-${var.deployment_id}" + iam_role_description = "IAM role for karpenter controller created by karpenter module" + create_node_iam_role = false + #node_iam_role_arn = module.eks.eks_managed_node_groups.nodegroup_iam_role_arn + create_access_entry = false + iam_policy_name = "KarpenterPolicy-${var.deployment_id}" + iam_policy_description = "Karpenter controller IAM policy created by karpenter module" + iam_role_use_name_prefix = false + node_iam_role_additional_policies = { + AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", + AmazonEBSCSIDriverPolicy = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" + } + enable_spot_termination = true + queue_name = "${var.deployment_id}-node-termination-handler" +} + + + +output "cluster_autoscaler_iam_role_arn" { + value = var.eks_create && var.eks_create_load_balancer_controller_irsa ? module.cluster_autoscaler_irsa[0].iam_role_arn : "" +} + +output "external_dns_iam_role_arn" { + value = var.eks_create && var.eks_create_load_balancer_controller_irsa ? module.external_dns_irsa[0].iam_role_arn : "" +} + +output "load_balancer_controller_iam_role_arn" { + value = var.eks_create && var.eks_create_load_balancer_controller_irsa ? module.load_balancer_controller_irsa[0].iam_role_arn : "" +} + +output "karpenter_iam_role_arn" { + value = var.eks_create && var.eks_create_karpenter ? module.karpenter.iam_role_arn : "" +} + +output "eks" { + value = module.eks.* +} diff --git a/elasticache.tf b/elasticache.tf new file mode 100644 index 0000000..8e775cb --- /dev/null +++ b/elasticache.tf @@ -0,0 +1,85 @@ +resource "aws_elasticache_serverless_cache" "main" { + engine = "redis" + count = var.ec_create && var.ec_enable_serverless ? 1 : 0 + name = "${var.deployment_id}-redis-serverless" + cache_usage_limits { + data_storage { + maximum = var.ec_serverless_max_storage + unit = "GB" + } + ecpu_per_second { + maximum = var.ec_serverless_max_ecpu_per_second + } + } + daily_snapshot_time = "09:00" + description = "Elasticache cluster for Checkmarx One deployment called: ${var.deployment_id}" + kms_key_id = var.kms_key_arn + major_engine_version = "7" + snapshot_retention_limit = 1 + security_group_ids = [module.elasticache_security_group.security_group_id] + subnet_ids = var.ec_subnets +} + + +resource "aws_elasticache_subnet_group" "redis" { + count = var.ec_create && var.ec_enable_serverless == false ? 1 : 0 + name = var.deployment_id + subnet_ids = var.ec_subnets +} + + +# tfsec:ignore:aws-elasticache-enable-in-transit-encryption +resource "aws_elasticache_replication_group" "redis" { + count = var.ec_create && var.ec_enable_serverless == false ? 1 : 0 + replication_group_id = var.deployment_id + description = "Redis cluster for AST application" + subnet_group_name = aws_elasticache_subnet_group.redis[0].name + security_group_ids = [module.elasticache_security_group.security_group_id] + node_type = var.ec_node_type + engine = "redis" + engine_version = var.ec_engine_version + port = 6379 + parameter_group_name = var.ec_parameter_group_name + snapshot_retention_limit = 2 + automatic_failover_enabled = var.ec_automatic_failover_enabled + multi_az_enabled = var.ec_multi_az_enabled + #num_cache_clusters = 2 + replicas_per_node_group = var.ec_replicas_per_shard + num_node_groups = var.ec_number_of_shards + + transit_encryption_enabled = false #var.redis_auth_token != "" ? true : false #BUG - AST can't work with TLS enabled + #auth_token = var.redis_auth_token != "" ? var.redis_auth_token : null + + # Per https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/at-rest-encryption.html + # "The default (service managed) encryption is the only option available in the GovCloud (US) Regions." + kms_key_id = data.aws_partition.current.partition == "aws-us-gov" ? null : var.kms_key_arn + at_rest_encryption_enabled = true + auto_minor_version_upgrade = var.ec_auto_minor_version_upgrade + apply_immediately = true +} + +module "elasticache_security_group" { + source = "terraform-aws-modules/security-group/aws" + create = var.ec_create + version = "5.1.2" + name = "${var.deployment_id}-elasticache" + description = "Elasticache security group for Checkmarx One deployment named ${var.deployment_id}" + vpc_id = var.vpc_id + ingress_cidr_blocks = data.aws_vpc.main.cidr_block_associations[*].cidr_block + ingress_rules = ["redis-tcp"] +} + +# output "redis" { +# value = { +# address = aws_elasticache_serverless_cache.main[0].endpoint[0].address +# port = aws_elasticache_serverless_cache.main[0].endpoint[0].port +# } +# } + +output "ec_endpoint" { + value = var.ec_enable_serverless ? aws_elasticache_serverless_cache.main[0].endpoint[0].address : aws_elasticache_replication_group.redis[0].configuration_endpoint_address +} + +output "ec_port" { + value = 6379 +} diff --git a/modules/cxone-install/install.sh.tftpl b/modules/cxone-install/install.sh.tftpl new file mode 100644 index 0000000..9ac0918 --- /dev/null +++ b/modules/cxone-install/install.sh.tftpl @@ -0,0 +1,30 @@ +#!/bin/bash + +# These environment variables are required for the kots commands that follow. +# Consider injecting them into your environment rather than hard coding in this script. +NAMESPACE="${namespace}" # The k8s namespace to deploy the application into. Suggested: ast +KOTS_CONFIG="${kots_config_file}" # The path to the kots configuration file +LICENSE="${license_file}" # The path to the Checkmarx One license file +SHARED_PASSWORD="${kots_admin_password}" # The shared password value for the Kots admin console +RELEASE_CHANNEL="${release_channel}" # The release channel that matches your license file. +APP_VERSION="${app_version}" # The application version to install + +# First, check if kotsadm and the ast application is installed yet. If the application is not installed, +# then install it and provide the license and shared password. We will *NOT* provide the configuration yet. +if ! kubectl kots get apps -n $NAMESPACE | grep ast > /dev/null; then + echo "ast app not found, installing..." + # + kubectl kots install ast/$RELEASE_CHANNEL -n $NAMESPACE --license-file $LICENSE --shared-password $SHARED_PASSWORD --app-version-label $APP_VERSION --no-port-forward +else + echo "ast app is already installed." +fi + +# Second, update the installed application with the desired kots configuration file and trigger a redeployment. +# This will provide the log output for any missing required configuration fields, and will allow the kots +# configuration to be version controlled and updated from time to time as needed. +echo "Updating kots configuration..." +kubectl kots set config ast -n $NAMESPACE --config-file $KOTS_CONFIG --deploy +KOTS_UPDATE_EXIT=$? +if [[ KOTS_UPDATE_EXIT -ne 0 ]]; then + echo "An error occurred updating the Kots configuration. Check the kots output above for clues." +fi diff --git a/modules/cxone-install/kots.config.aws.reference.yaml.tftpl b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl new file mode 100644 index 0000000..d4af69e --- /dev/null +++ b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl @@ -0,0 +1,363 @@ +apiVersion: kots.io/v1beta1 +kind: ConfigValues +metadata: + creationTimestamp: null +spec: + values: + #-------------------------------------------------------------------------- + # GENERAL + #-------------------------------------------------------------------------- + + # The default admin user is the Checkmarx One application admin user, aka root user. + # This user's credentials are injected into the system via Kots configuration, and then + # can be changed. The user will be forced to configure MFA upon first login. + default_admin_email: + value: ${admin_email} + # By convention, the first user is named admin + default_admin_user: + value: ${admin_username} + # default_admin_password must be > 14 characters in length. + default_admin_password: + value: ${admin_password} + + # Enable DAST component. + # bool - Valid values are "0" (false) and "1" true + # Recommendation: "1" if licensed for DAST, otherwise "0" + enable_dast_component: + value: "1" + + # Enable the local flow for SCA Global inventory + enable_sca_local_flow_for_global_inventory: + value: "0" + + # Enables using a dedicated node group just for SCA components. + # If enabled, the node group must exists with the correct labels and taint. + # bool - Valid values are "0" (false) and "1" true + # Recommended value: "0" + use_dedicated_sca_nodegroup: + value: "0" + + # Configures the SCA production environment for the Checkmarx Cloud + # Valid values are https://api-sca-.checkmarx.net and "https://eu.api-sca.checkmarx.net" + # Recommended value: match the region in your kots license file for cloudIamUrl + sca_prod_environment: + value: https://api-sca.checkmarx.net + + # environment_type is always production + environment_type: + value: development + + # deployment_type is always cloud + deployment_type: + value: cloud + + # The minimum microservice replicas. Used to control number of pods per service. + # Recommendation: production environments, at least 3. Non-prod can be as low as 1. + ms_replica_count: + value: "${ms_replica_count}" + + # cloud_provider is always AWS, when deploying to AWS. + cloud_provider: + value: AWS + + # The protocol the system will use in its url. Valid values are http and https (recommended) + # When https, you must provide an SSL certificate in networking settings. + PROTOCOL: + value: https + + # The domain name for the system. Used to configure traefik for listening for incoming requests. + # Example: checkmarx.example.com + DOMAIN: + value: ${fqdn} + + # Enable TLS for secure communication. Valid values are "0" and "1" + ENABLE_TLS: + default: "1" + + #-------------------------------------------------------------------------- + # SCA Global Inventory & Elasticsearch + #-------------------------------------------------------------------------- + + # Enables SCA Global Inventory + # bool - Valid values are "0" (false) and "1" true. + # When true, must provide elasticsearch configuration. + enable_sca_global_inventory: + value: "0" + + # Typically ES is provided via AWS OpenSearch service (must be Elasticsearch engine v 7.10) + sca_global_inventory_elasticsearch_host: + value: ${elasticsearch_host} + sca_global_inventory_elasticsearch_port: + value: "443" + sca_global_inventory_elasticsearch_username: + value: ast + sca_global_inventory_elasticsearch_password: + value: ${elasticsearch_password} + + #-------------------------------------------------------------------------- + # S3 Bucket Names + #-------------------------------------------------------------------------- + + # These are the names of the various buckets used by Checkmarx One components. + # The bucket names are passed to Checkmarx One rather than hard coding them. + + + apisec_s3_bucket_name: + value: ${deployment_id}-apisec-${bucket_name_suffix} + audit_s3_bucket_name: + value: ${deployment_id}-audit-${bucket_name_suffix} + configuration_s3_bucket_name: + value: ${deployment_id}-configuration-${bucket_name_suffix} + cxone_s3_bucket_name: + value: ${deployment_id}-cxone-${bucket_name_suffix} + engine_logs_s3_bucket_name: + value: ${deployment_id}-engine-logs-${bucket_name_suffix} + export_s3_bucket_name: + value: ${deployment_id}-export-${bucket_name_suffix} + imports_s3_bucket_name: + value: ${deployment_id}-imports-${bucket_name_suffix} + kics_worker_s3_bucket_name: + value: ${deployment_id}-kics-worker-${bucket_name_suffix} + logs_s3_bucket_name: + value: ${deployment_id}-logs-${bucket_name_suffix} + misc_s3_bucket_name: + value: ${deployment_id}-misc-${bucket_name_suffix} + queries_s3_bucket_name: + value: ${deployment_id}-queries-${bucket_name_suffix} + redis_shared_s3_bucket_name: + value: ${deployment_id}-redis-${bucket_name_suffix} + reports_s3_bucket_name: + value: ${deployment_id}-reports-${bucket_name_suffix} + report_templates_s3_bucket_name: + value: ${deployment_id}-report-templates-${bucket_name_suffix} + repostore_s3_bucket_name: + value: ${deployment_id}-repostore-${bucket_name_suffix} + sast_metadata_s3_bucket_name: + value: ${deployment_id}-sast-metadata-${bucket_name_suffix} + sast_worker_s3_bucket_name: + value: ${deployment_id}-sast-worker-${bucket_name_suffix} + scan_results_storage_s3_bucket_name: + value: ${deployment_id}-scan-results-storage-${bucket_name_suffix} + scans_s3_bucket_name: + value: ${deployment_id}-scans-${bucket_name_suffix} + sca_worker_s3_bucket_name: + value: ${deployment_id}-sca-worker-${bucket_name_suffix} + source_resolver_s3_bucket_name: + value: ${deployment_id}-source-resolver-${bucket_name_suffix} + uploads_s3_bucket_name: + value: ${deployment_id}-uploads-${bucket_name_suffix} + + #-------------------------------------------------------------------------- + # S3 Settings + #-------------------------------------------------------------------------- + + # The aws region e.g. us-east-1 or us-west-2, etc. + cloud_region: + value: ${aws_region} + + # The s3 endpoint. Typically will match your region. + # Example value: s3.us-west-2.amazonaws.com + object_storage_url: + value: ${object_storage_url} + + # The s3 endpoint, with scheme (protocol, aka http/https) + # Example value: https://s3.us-west-2.amazonaws.com + object_storage_schemeUrl: + value: https://${object_storage_url} + + # An AWS IAM user, with access key, and secret key, must be created out of band + # so that the credentials can be configured here. The user must have access to + # the s3 buckets for Checkmarx One. + object_storage_access_key: + value: ${object_storage_access_key} + object_storage_secret_key: + value: ${object_storage_secret_key} + + # Configures secure connections to s3. Can be "1" (true/enabled) or "0" (false/disabled) + # Recommended: "1" + object_storage_secure: + value: "1" + + # The SCA Host Type Settting valid values are 'ExeLocalServer' and 'S3' - controls + # wether SCA uses AWS SDK or Minio Client. + sca_host_type_setting: + value: "S3" + + #-------------------------------------------------------------------------- + # Database Settings + #-------------------------------------------------------------------------- + + # Always set to external_postgres. Other values used for development only. + postgres_type: + value: external_postgres + + # The host name to connect to postgres on + external_postgres_host: + value: ${postgres_host} + + # The postgres port. Typically "5432" + external_postgres_port: + value: "5432" + + # The postgres username. By convention, ast. + external_postgres_user: + value: ${postgres_user} + + # The password for the external_postgres_user. + external_postgres_password: + value: ${postgres_password} + + # The name of the CxOne database. By convention, ast. + external_postgres_db: + value: ast + + # Used to enforce SSL connections to postgres. Values are + # postgres_sslmode_require or postgres_sslmode_allow + external_postgres_sslmode: + value: postgres_sslmode_require + + #-------------------------------------------------------------------------- + # Metrics Analytics Settings + #-------------------------------------------------------------------------- + + # Enables analytics features, which requires an additional database. + enable_analytics: + value: "0" + + # The analaytics database connection information. required when enable_analytics is "1" + analytics_postgres_host: + value: + analytics_postgres_port: + value: "5432" + analytics_postgres_db_name: + value: analytics + analytics_postgres_user: + value: + analytics_postgres_password: + value: + # Can be either analytics_postgres_sslmode_require or analytics_postgres_sslmode_allow + analytics_postgres_sslmode_value: + value: analytics_postgres_sslmode_require + + #-------------------------------------------------------------------------- + # Redis Settings + #-------------------------------------------------------------------------- + + # Always set to external_redis. Other values used for development only. + redis_type: + value: external_redis + + # The host name to connect to redis on. + external_redis_address: + value: ${redis_address} + + # The port to connect to redis on. Typically 6379. + external_redis_port: + value: "6379" + + external_redis_password: {} + + external_redis_tls_enabled: + value: "0" + + external_redis_cluster_mode_enabled: + value: "1" + + external_redis_tls_skipverify: + value: "0" + + #-------------------------------------------------------------------------- + # SMTP Settings + #-------------------------------------------------------------------------- + + # The settings here configure how Checkmarx One will connect to your SMTP + # server for sending outbound emails. + + # Enables SMTP configuration. Other SMTP values are required when enabled. + # Valid values are "0" (false/disabled) and "1" (true/enabled) + enable_smtp: + value: "1" + + # The SMTP server host name that Checkmarx One will use for sending emails + smtp_host: + value: ${smtp_host} + + # The from address in outgoing emails + smtp_from_sender: + value: ${smtp_from_sender} + + # The port to which Checkmarx One will connect on the SMTP server. + # Typically 587 when using tls. + smtp_port: + value: "${smtp_port}" + + # Enables TLS connections to the SMTP server. When enabled, SMTP server must + # be configured with a TLS certificate from a well known Certificate Authority. + # Valid values are "0" (false/disabled) and "1" (true/enabled) + smtp_tls_enabled: + value: "1" + + # The username and password that Checkmarx One will use to authenticate to + # the SMTP server with, when smtp_auth_enabled = "1". Use "0" for no + # authentication (which your SMTP server must allow). + smtp_auth_enabled: + value: "1" + smtp_user: + value: ${smtp_user} + smtp_password: + value: ${smtp_password} + + + #-------------------------------------------------------------------------- + # Prometheus Settings + #-------------------------------------------------------------------------- + enable_prometheus_service_monitor_metrics: + value: "0" + prometheus_service_monitor_labels_release: + value: "" + + #-------------------------------------------------------------------------- + # Networking Settings + #-------------------------------------------------------------------------- + + # Networking settings are primarily settings that will drive configuration of the + # AWS Load Balancer Controller. Consult the LBC documentation for additional information + # on these settings https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.7/. + + # In Checkmarx One, these configuration values are injected into the Traefik service which will + # be connected to the load balancer according to how you specify your networking settings here. + + # Configures the type of load balancer to use. Valid values are: + # networking_type_load_balancer_AWS_NLB: AWS Network Load Balancer (recommended) + # networking_type_load_balancer_AWS_CLB: AWS Classic Load Balancer (not recommended) + # Reference: https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html + networking_type_aws: + value: networking_type_load_balancer_AWS_NLB + + # Configures the load balancer scheme. Valid values are: + # internet-facing: a load balancer with public IP addresses and public dns. Recommended when deploying CxOne for interenet access. + # internal: a load balancer that only has private ip addresses. Recommend when deploying CxOne privately. + # Reference https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html#load-balancer-scheme + network_load_balancer_scheme: + value: internet-facing + + # SSL Policy Name to be applied to the AWS Network Load Balancer. Reference https://docs.aws.amazon.com/elasticloadbalancing/latest/network/create-tls-listener.html#describe-ssl-policies. + network_load_balancer_ssl_negotiation_policy: + value: "ELBSecurityPolicy-TLS-1-2-Ext-2018-06" + + # Source CIDR ranges to be allowed access to the AWS Network Load Balancer. Reference https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.4/guide/service/annotations/#lb-source-ranges. + network_load_balancer_source_ranges: + value: "0.0.0.0/0" + + # Enable explicit NLB subnet assignments. Valid values are "0" (false) and "1" (true). + # Recommendation: only use when you want to explicitly control subnet placement for the NLB. Use auto discovery via tags instead. + network_load_balancer_subnets_enabled: + value: "0" + # When network_load_balancer_subnets_enabled is "1", uncomment this configuration item and set your subnets with list of subnet ids here + #network_load_balancer_subnets: + # value: subnet-xxxx, subnet-123123 + + # Specify the ARN to the SSL certificate in AWS ACM that will be used to configure load balancer listeners for TLS + # Valid values are ARNs e.g. arn:aws:acm:us-east-1:01234567890:certificate/d3f015a6-5b08-4c1e-8458-250220bf31e2 + nlb_tls_acm_arn: + value: ${nlb_tls_acm_arn} diff --git a/modules/cxone-install/main.tf b/modules/cxone-install/main.tf new file mode 100644 index 0000000..70673f0 --- /dev/null +++ b/modules/cxone-install/main.tf @@ -0,0 +1,76 @@ +# Look up the database password from secrets manager, as the Kots configuration requires it. +# data "aws_secretsmanager_secret" "rds_secret" { +# arn = var.postgres_password_secret_arn +# } +# data "aws_secretsmanager_secret_version" "rds_secret" { +# secret_id = data.aws_secretsmanager_secret.rds_secret.id +# } + +resource "local_file" "kots_config" { + content = templatefile("${path.module}/kots.config.aws.reference.yaml.tftpl", { + aws_region = var.region + admin_email = var.admin_email + admin_username = "admin" + admin_password = var.admin_password + + ms_replica_count = var.ms_replica_count + + fqdn = var.fqdn + nlb_tls_acm_arn = var.acm_certificate_arn + + # S3 buckets + bucket_name_suffix = var.bucket_suffix + deployment_id = var.deployment_id + object_storage_url = var.object_storage_endpoint + object_storage_access_key = var.object_storage_access_key + object_storage_secret_key = var.object_storage_secret_key + + # RDS + postgres_host = var.postgres_host + postgres_user = var.postgres_user + postgres_password = var.postgres_password #jsondecode(data.aws_secretsmanager_secret_version.rds_secret.secret_string)["password"] + postgres_db = var.postgres_database_name + + # Redis + redis_address = var.redis_address + + # SMTP + smtp_host = var.smtp_host + smtp_port = var.smtp_port + smtp_user = var.smtp_user + smtp_password = var.smtp_password + smtp_from_sender = var.smtp_from_sender + + # Elasticsearch + elasticsearch_host = var.elasticsearch_host + elasticsearch_password = var.elasticsearch_password + }) + filename = "kots.${var.deployment_id}.yaml" +} + + +resource "local_file" "makefile" { + content = templatefile("${path.module}/makefile.tftpl", { + tf_deployment_id = var.deployment_id + tf_deploy_region = var.region + tf_eks_cluster_name = var.deployment_id + tf_fqdn = var.fqdn + tf_cxone_version = var.cxone_version + tf_release_channel = var.release_channel + tf_kots_password = var.kots_admin_password + tf_namespace = "ast" + tf_license_file = var.license_file + tf_kots_config_file = "kots.${var.deployment_id}.yaml" + namespace = "ast" + kots_config_file = "kots.${var.deployment_id}.yaml" + license_file = var.license_file + release_channel = var.release_channel + app_version = var.cxone_version + cluster_autoscaler_iam_role_arn = var.cluster_autoscaler_iam_role_arn + load_balancer_controller_iam_role_arn = var.load_balancer_controller_iam_role_arn + + + }) + filename = "makefile" +} + diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/makefile.tftpl new file mode 100644 index 0000000..b06c361 --- /dev/null +++ b/modules/cxone-install/makefile.tftpl @@ -0,0 +1,79 @@ +DEPLOYMENT_ID = ${tf_deployment_id} +DEPLOY_REGION = ${tf_deploy_region} +EKS_CLUSTER_NAME = ${tf_eks_cluster_name} +FQDN = ${tf_fqdn} +CXONE_VERSION = ${tf_cxone_version} +RELEASE_CHANNEL = ${tf_release_channel} +KOTS_PASSWORD = ${tf_kots_password} +NAMESPACE = ${tf_namespace} +LICENSE_FILE = ${tf_license_file} +KOTS_CONFIG_FILE = ${tf_kots_config_file} + +.PHONY: update-kubeconfig +update-kubeconfig: + aws eks update-kubeconfig --name $${EKS_CLUSTER_NAME} + +.PHONY: kots-install +kots-install: + kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password $${KOTS_PASSWORD} --config-values $${KOTS_CONFIG_FILE} --app-version-label $${CXONE_VERSION} + +.PHONY: kots-set-config +kots-set-config: + kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy + +.PHONY: kots-get-config +kots-get-config: + kubectl kots get config -n $${NAMESPACE} --appslug ast + +.PHONY: kots-admin-console +kots-admin-console: + kubectl kots admin-console -n $${NAMESPACE} + +.PHONY: rollout-restart +rollout-restart: + kubectl -n $${NAMESPACE} rollout restart deploy + kubectl -n $${NAMESPACE} rollout restart statefulset + +.PHONY: install-cluster-autoscaler +install-cluster-autoscaler: + helm repo add autoscaler https://kubernetes.github.io/autoscaler; \ + helm repo update autoscaler; \ + helm install cluster-autoscaler autoscaler/cluster-autoscaler \ + --version 9.21.1 \ + -n kube-system \ + --set awsRegion=$${DEPLOY_REGION} \ + --set rbac.serviceAccount.create=true \ + --set rbac.serviceAccount.name=cluster-autoscaler \ + --set rbac.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${cluster_autoscaler_iam_role_arn}" \ + --set autoDiscovery.clusterName=$${EKS_CLUSTER_NAME} + +.PHONY: uninstall-cluster-autoscaler +uninstall-cluster-autoscaler: + helm uninstall cluster-autoscaler -n kube-system + +.PHONY: install-load-balancer-controller +install-load-balancer-controller: + helm repo add eks https://aws.github.io/eks-charts; \ + helm repo update eks; \ + helm install aws-load-balancer-controller eks/aws-load-balancer-controller \ + --version 1.7.1 \ + -n kube-system \ + --set region=$${DEPLOY_REGION}} \ + --set serviceAccount.create=true \ + --set serviceAccount.name=aws-load-balancer-controller \ + --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${load_balancer_controller_iam_role_arn}" \ + --set clusterName=$${EKS_CLUSTER_NAME}} \ + --set enableShield=false \ + --set enableWaf=false \ + --set enableWaafv2=false + + +.PHONY: uninstall-load-balancer-controller +uninstall-load-balancer-controller: + helm uninstall aws-load-balancer-controller -n kube-system + +.PHONY: clean-kots +clean-kots: + kubectl delete deployment kotsadm -n $${NAMESPACE} + kubectl delete statefulset kotsadm-minio -n $${NAMESPACE} + kubectl delete statefulset kotsadm-rqlite -n $${NAMESPACE} diff --git a/modules/cxone-install/variables.tf b/modules/cxone-install/variables.tf new file mode 100644 index 0000000..4dd233c --- /dev/null +++ b/modules/cxone-install/variables.tf @@ -0,0 +1,173 @@ +variable "region" { + type = string + description = "The AWS region e.g. us-east-1, us-west-2, etc." +} + +variable "admin_email" { + type = string + description = "The email of the first admin user." +} + +variable "admin_password" { + type = string + description = "The password for the first admin user. Must be > 14 characters." +} + +variable "fqdn" { + type = string + description = "The fully qualified domain name that will be used for the Checkmarx One deployment" +} + +variable "acm_certificate_arn" { + type = string + description = "The ARN for the ACM certificate to use to configure SSL with." +} + +variable "deployment_id" { + description = "The id of the deployment. Will be used to name resources like EKS cluster, etc." + type = string + nullable = false + validation { + condition = (length(var.deployment_id) >= 3) + error_message = "The deployment_id must be greater than 3 characters." + } +} + +variable "bucket_suffix" { + description = "The id of the deployment. Will be used to name resources like EKS cluster, etc." + type = string + nullable = false + validation { + condition = (length(var.bucket_suffix) > 3) + error_message = "The deployment_id must be greater than 3 characters." + } +} + +variable "cxone_version" { + type = string + description = "The version of CxOne to install" +} + +variable "release_channel" { + type = string + description = "The release channel to deploy from" +} + +variable "license_file" { + type = string + description = "The path to the license file to use" +} + +variable "kots_admin_password" { + type = string + description = "The Kots password to use" +} + +variable "ms_replica_count" { + type = number + description = "The microservices replica count (e.g. a minimum)" + default = 3 +} + +variable "cluster_autoscaler_iam_role_arn" { + type = string + nullable = true +} + +variable "external_dns_iam_role_arn" { + type = string + nullable = true +} + +variable "load_balancer_controller_iam_role_arn" { + type = string + nullable = true +} + +#****************************************************************************** +# S3 Access Configuration +#****************************************************************************** +variable "object_storage_endpoint" { + type = string + description = "The S3 endpoint to use to access buckets" +} + +variable "object_storage_access_key" { + type = string + description = "The S3 access key to use to access buckets" +} +variable "object_storage_secret_key" { + type = string + description = "The S3 secret key to use to access buckets" +} + +variable "postgres_host" { + type = string + description = "The endpoint for the main RDS server." +} + +variable "postgres_user" { + type = string + description = "The user name for the main RDS server." + default = "ast" +} + +variable "postgres_password" { + type = string + description = "The user name for the main RDS server." + default = "ast" +} + +variable "postgres_database_name" { + type = string + description = "The name of the main database." + default = "ast" +} + +variable "redis_address" { + type = string + description = "The redis endpoint." +} + + +#****************************************************************************** +# Elasticsearch Configuration +#****************************************************************************** + +variable "elasticsearch_host" { + type = string + default = "The elasticsearc host address." +} + +variable "elasticsearch_password" { + type = string + default = "The elasticsearch password." +} + +#****************************************************************************** +# SMTP Configuration +#****************************************************************************** +variable "smtp_host" { + description = "The hostname of the SMTP server." + type = string +} + +variable "smtp_port" { + description = "The port of the SMTP server." + type = number +} + +variable "smtp_user" { + description = "The smtp user name." + type = string +} + +variable "smtp_password" { + description = "The smtp password." + type = string +} + +variable "smtp_from_sender" { + description = "The address to use in the from field when sending emails." + type = string +} \ No newline at end of file diff --git a/opensearch.tf b/opensearch.tf new file mode 100644 index 0000000..7c28dee --- /dev/null +++ b/opensearch.tf @@ -0,0 +1,93 @@ + + +resource "aws_elasticsearch_domain" "es" { + count = var.es_create ? 1 : 0 + domain_name = var.deployment_id + elasticsearch_version = "7.10" + + cluster_config { + instance_type = var.es_instance_type + instance_count = var.es_instance_count + zone_awareness_enabled = true + zone_awareness_config { + availability_zone_count = min(length(var.es_subnets), var.es_instance_count) + } + } + vpc_options { + subnet_ids = slice(var.es_subnets, 0, var.es_instance_count) + security_group_ids = [module.elasticache_security_group.security_group_id] + } + snapshot_options { + automated_snapshot_start_hour = 06 + } + ebs_options { + ebs_enabled = true + volume_type = "gp3" + volume_size = var.es_volume_size + throughput = 125 + iops = 3000 + } + domain_endpoint_options { + enforce_https = true + tls_security_policy = var.es_tls_security_policy + } + advanced_options = { + "rest.action.multi.allow_explicit_index" = "true" + } + encrypt_at_rest { + enabled = true + } + node_to_node_encryption { + enabled = true + } + advanced_security_options { + enabled = true + internal_user_database_enabled = true + master_user_options { + master_user_name = var.es_username + master_user_password = var.es_password + } + } + access_policies = <= 3) + error_message = "The deployment_id must be greater than 3 characters." + } +} + +variable "kms_key_arn" { + type = string + description = "The ARN of the KMS key to use for encryption in AWS services" +} + +variable "vpc_id" { + description = "The id of the vpc deploying into." + type = string +} + + +#****************************************************************************** +# S3 Configuration +#****************************************************************************** + +variable "s3_retention_period" { + description = "The retention period, in days, to retain s3 objects." + type = string + default = "90" +} + +variable "s3_allowed_origins" { + description = "The allowed orgins for S3 CORS rules." + type = list(string) + nullable = false +} + +#****************************************************************************** +# EKS Configuration +#****************************************************************************** +variable "eks_create" { + type = bool + description = "Enables the EKS resource creation" + default = true +} + +variable "eks_subnets" { + description = "The subnets to deploy EKS into." + type = list(string) +} + +variable "eks_create_cluster_autoscaler_irsa" { + type = bool + description = "Enables creation of cluster autoscaler IAM role." + default = true +} + +variable "eks_create_external_dns_irsa" { + type = bool + description = "Enables creation of external dns IAM role." + default = true +} + +variable "eks_create_load_balancer_controller_irsa" { + type = bool + description = "Enables creation of load balancer controller IAM role." + default = true +} + +variable "eks_create_karpenter" { + type = bool + description = "Enables creation of Karpenter resources." + default = false +} + +variable "eks_version" { + type = string + description = "The version of the EKS Cluster (e.g. 1.27)" +} + +variable "eks_private_endpoint_enabled" { + type = bool + description = "Enables the EKS VPC private endpoint." + default = true +} + +variable "eks_public_endpoint_enabled" { + type = bool + description = "Enables the EKS public endpoint." + default = false +} + +variable "eks_cluster_endpoint_public_access_cidrs" { + type = list(string) + description = " List of CIDR blocks which can access the Amazon EKS public API server endpoint" + default = ["0.0.0.0/0"] +} + +variable "eks_node_additional_security_group_ids" { + description = "Additional security group ids to attach to EKS nodes." + type = list(string) + default = [] +} + +variable "coredns_version" { + type = string + description = "The version of the EKS Core DNS Addon." +} + +variable "kube_proxy_version" { + type = string + description = "The version of the EKS Kube Proxy Addon." +} + +variable "vpc_cni_version" { + type = string + description = "The version of the EKS VPC CNI Addon." +} + +variable "aws_ebs_csi_driver_version" { + type = string + description = "The version of the EKS EBS CSI Addon." +} + +variable "launch_template_tags" { + type = map(string) + description = "Tags to associate with launch templates for node groups" + default = null +} + +variable "enable_cluster_creator_admin_permissions" { + type = bool + description = "Enables the identity used to create the EKS cluster to have administrator access to that EKS cluster. When enabled, do not specify the same principal arn for eks_administrator_principals." + default = true +} + +variable "eks_administrator_principals" { + type = list(object({ + name = string + principal_arn = string + })) + description = "The ARNs of the IAM roles for administrator access to EKS." + default = [] +} + +variable "ec2_key_name" { + description = "The name of the EC2 key pair to access servers." + type = string + default = null +} + + +variable "eks_node_groups" { + type = list(object({ + name = string + min_size = string + desired_size = string + max_size = string + volume_type = optional(string, "gp3") + disk_size = optional(number, 200) + disk_iops = optional(number, 3000) + disk_throughput = optional(number, 125) + device_name = optional(string, "/dev/xvda") + instance_types = list(string) + capacity_type = optional(string, "ON_DEMAND") + labels = optional(map(string), {}) + taints = optional(map(object({ key = string, value = string, effect = string })), {}) + })) + default = [{ + name = "ast-app" + min_size = 3 + desired_size = 3 + max_size = 9 + instance_types = ["c5.4xlarge"] + }, + { + name = "sast-engine" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["m5.2xlarge"] + labels = { + "sast-engine" = "true" + } + taints = { + dedicated = { + key = "sast-engine" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sast-engine-large" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["m5.4xlarge"] + labels = { + "sast-engine-large" = "true" + } + taints = { + dedicated = { + key = "sast-engine-large" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sast-engine-extra-large" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["r5.2xlarge"] + labels = { + "sast-engine-extra-large" = "true" + } + taints = { + dedicated = { + key = "sast-engine-extra-large" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sast-engine-xxl" + min_size = 0 + desired_size = 0 + max_size = 100 + instance_types = ["r5.4xlarge"] + labels = { + "sast-engine-xxl" = "true" + } + taints = { + dedicated = { + key = "sast-engine-xxl" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "kics-engine" + min_size = 1 + desired_size = 1 + max_size = 100 + instance_types = ["c5.2xlarge"] + labels = { + "kics-engine" = "true" + } + taints = { + dedicated = { + key = "kics-engine" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "repostore" + min_size = 1 + desired_size = 1 + max_size = 100 + instance_types = ["c5.2xlarge"] + labels = { + "repostore" = "true" + } + taints = { + dedicated = { + key = "repostore" + value = "true" + effect = "NO_SCHEDULE" + } + } + }, + { + name = "sca-source-resolver" + min_size = 1 + desired_size = 1 + max_size = 100 + instance_types = ["m5.2xlarge"] + labels = { + "service" = "sca-source-resolver" + } + taints = { + dedicated = { + key = "service" + value = "sca-source-resolver" + effect = "NO_SCHEDULE" + } + } + } + ] +} + +#****************************************************************************** +# RDS Configuration +#****************************************************************************** + +variable "db_engine_version" { + description = "The aurora postgres engine version." + type = string + default = "13.8" +} + + +variable "db_subnets" { + description = "The subnets to deploy RDS into." + type = list(string) +} + + +variable "db_allow_major_version_upgrade" { + description = "Allows major version upgrades." + type = bool + default = false +} + +variable "db_auto_minor_version_upgrade" { + description = "Automatically upgrade to latest minor version in maintenance window." + type = bool + default = false +} + +variable "db_instance_class" { + description = "The aurora postgres instance class." + type = string + default = "db.r6g.xlarge" +} + +variable "db_monitoring_interval" { + description = "The aurora postgres engine version." + type = string + default = "10" +} + +variable "db_autoscaling_enabled" { + description = "Enables autoscaling of the aurora database." + type = bool + default = true +} + +variable "db_autoscaling_min_capacity" { + description = "The minimum number of replicas via autoscaling." + type = string + default = "1" +} + +variable "db_autoscaling_max_capacity" { + description = "The maximum number of replicas via autoscaling." + type = string + default = "3" +} + +variable "db_autoscaling_target_cpu" { + description = "The CPU utilization for autoscaling target tracking." + type = number + default = 70 +} + +variable "db_autoscaling_scale_in_cooldown" { + description = "The database scale in cooldown period." + type = number + default = 300 +} + +variable "db_autoscaling_scale_out_cooldown" { + description = "The database scale ou cooldown period." + type = number + default = 300 +} + +variable "db_snapshot_identifer" { + description = "The snapshot identifier to restore the database from." + type = string + default = null +} + +variable "db_port" { + description = "The port on which the DB accepts connections." + type = string + default = "5432" +} + +variable "db_master_user_password" { + description = "The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it." + type = string + default = null +} + +variable "db_create_rds_proxy" { + description = "Enables an RDS proxy for the Aurora postgres database." + type = bool + default = true +} + +variable "db_create" { + description = "Controls creation of the Aurora postgres database." + type = bool + default = true +} + +variable "db_skip_final_snapshot" { + description = "Enables skipping the final snapshot upon deletion." + type = bool + default = false +} + +variable "db_final_snapshot_identifier" { + description = "Identifer for a final DB snapshot. Required when db_skip_final_snapshot is false.." + type = string + default = null +} + +variable "db_deletion_protection" { + description = "Enables deletion protection to avoid accidental database deletion." + type = bool + default = true +} + +variable "db_instances" { + type = map(any) + description = "The DB instance configuration" + default = { + writer = {} + replica1 = {} + } +} + +variable "db_serverlessv2_scaling_configuration" { + description = "The serverless v2 scaling minimum and maximum." + type = object({ + min_capacity = number + max_capacity = number + }) + default = { + min_capacity = 0.5 + max_capacity = 32 + } +} + +variable "db_performance_insights_enabled" { + type = bool + default = true + description = "Enables database performance insights." +} + +variable "db_performance_insights_retention_period" { + type = number + default = 7 + description = "Number of days to retain performance insights data. Free tier: 7 days." +} + +variable "db_cluster_db_instance_parameter_group_name" { + type = string + default = null + description = "The name of the DB Cluster parameter group to use." +} + +variable "db_apply_immediately" { + type = bool + default = false + description = "Determines if changes will be applied immediately or wait until the next maintenance window." +} + + +#****************************************************************************** +# Elasticache Configuration +#****************************************************************************** +variable "ec_create" { + type = bool + default = true + description = "Enables the creation of elasticache resources." +} + +variable "ec_subnets" { + description = "The subnets to deploy Elasticache into." + type = list(string) +} + +variable "ec_enable_serverless" { + type = bool + default = false + description = "Enables the use of elasticache for redis serverless." +} + +variable "ec_serverless_max_storage" { + type = number + default = 5 + description = "The max storage, in GB, for serverless elasticache for redis." +} +variable "ec_serverless_max_ecpu_per_second" { + type = number + default = 5000 + description = "The max eCPU per second for serverless elasticache for redis." +} + +variable "ec_engine_version" { + type = string + description = "The version of the elasticache cluster. Does not apply to serverless." + default = "6.x" +} +variable "ec_parameter_group_name" { + type = string + description = "The elasticache parameter group name. Does not apply to serverless." + default = "default.redis6.x.cluster.on" +} + +variable "ec_node_type" { + type = string + description = "The elasticache redis node type. Does not apply to serverless." + default = "cache.m6g.large" +} + +variable "ec_number_of_shards" { + type = number + description = "The number of shards for redis. Does not apply to serverless." + default = 3 +} + +variable "ec_replicas_per_shard" { + type = number + description = "The number of replicas per shard for redis. Does not apply to serverless." + default = 2 +} + +variable "ec_auto_minor_version_upgrade" { + type = bool + description = "Enables automatic minor version upgrades. Does not apply to serverless." + default = false +} + +variable "ec_automatic_failover_enabled" { + type = bool + description = "Enables automatic failover. Does not apply to serverless." + default = true +} + +variable "ec_multi_az_enabled" { + type = bool + description = "Enables automatic failover. Does not apply to serverless." + default = true +} + +#****************************************************************************** +# Elasticsearch Configuration +#****************************************************************************** + +variable "es_create" { + type = bool + description = "Enables creation of elasticsearch resources." + default = true +} + +variable "es_subnets" { + description = "The subnets to deploy Elasticsearch into." + type = list(string) +} + +variable "es_instance_type" { + type = string + description = "The instance type for elasticsearch nodes." + default = "r6g.large.elasticsearch" +} + +variable "es_instance_count" { + type = number + description = "The number of nodes in elasticsearch cluster" + default = 2 +} + +variable "es_volume_size" { + type = number + description = "The size of volumes for nodes in elasticsearch cluster" + default = 100 +} + +variable "es_tls_security_policy" { + default = "Policy-Min-TLS-1-2-2019-07" + type = string +} + +variable "es_username" { + description = "The username for the elasticsearch user" + type = string + default = "ast" +} + +variable "es_password" { + description = "The password for the elasticsearch user" + type = string + sensitive = true +} + From b1ca74b6f1acda079165466197dedfb87be706e4 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 10 Apr 2024 15:56:15 -0400 Subject: [PATCH 04/57] Create examples.auto.tfvars --- examples/full/examples.auto.tfvars | 256 +++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 examples/full/examples.auto.tfvars diff --git a/examples/full/examples.auto.tfvars b/examples/full/examples.auto.tfvars new file mode 100644 index 0000000..d2bdefe --- /dev/null +++ b/examples/full/examples.auto.tfvars @@ -0,0 +1,256 @@ + +#****************************************************************************** +# Base Infrastructure Configuration +#****************************************************************************** +vpc_cidr = "10.77.0.0/16" +secondary_vpc_cidr = "100.64.0.0/16" +interface_vpc_endpoints = ["ec2", "ec2messages", "ssm", "ssmmessages", "ecr.api", "ecr.dkr", "kms", "logs", "sts", "elasticloadbalancing", "autoscaling"] +create_s3_endpoint = true +route_53_hosted_zone_id = " Date: Fri, 12 Apr 2024 15:59:02 -0400 Subject: [PATCH 05/57] Bugfix: cluster autoscaler and external dns were keyed off eks_create_load_balancer_controller_irsa erroneously. --- eks.tf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/eks.tf b/eks.tf index ebbed06..27a0eff 100644 --- a/eks.tf +++ b/eks.tf @@ -287,9 +287,9 @@ module "karpenter" { iam_role_description = "IAM role for karpenter controller created by karpenter module" create_node_iam_role = false #node_iam_role_arn = module.eks.eks_managed_node_groups.nodegroup_iam_role_arn - create_access_entry = false - iam_policy_name = "KarpenterPolicy-${var.deployment_id}" - iam_policy_description = "Karpenter controller IAM policy created by karpenter module" + create_access_entry = false + iam_policy_name = "KarpenterPolicy-${var.deployment_id}" + iam_policy_description = "Karpenter controller IAM policy created by karpenter module" iam_role_use_name_prefix = false node_iam_role_additional_policies = { AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", @@ -302,19 +302,19 @@ module "karpenter" { output "cluster_autoscaler_iam_role_arn" { - value = var.eks_create && var.eks_create_load_balancer_controller_irsa ? module.cluster_autoscaler_irsa[0].iam_role_arn : "" + value = var.eks_create && var.eks_create_cluster_autoscaler_irsa ? module.cluster_autoscaler_irsa[0].iam_role_arn : "" } output "external_dns_iam_role_arn" { - value = var.eks_create && var.eks_create_load_balancer_controller_irsa ? module.external_dns_irsa[0].iam_role_arn : "" + value = var.eks_create && var.eks_create_external_dns_irsa ? module.external_dns_irsa[0].iam_role_arn : "" } output "load_balancer_controller_iam_role_arn" { - value = var.eks_create && var.eks_create_load_balancer_controller_irsa ? module.load_balancer_controller_irsa[0].iam_role_arn : "" + value = var.eks_create && var.eks_create_load_balancer_controller_irsa ? module.load_balancer_controller_irsa[0].iam_role_arn : "" } output "karpenter_iam_role_arn" { - value = var.eks_create && var.eks_create_karpenter ? module.karpenter.iam_role_arn : "" + value = var.eks_create && var.eks_create_karpenter ? module.karpenter.iam_role_arn : "" } output "eks" { From 825672c2b32548a7e907f0e996503d1dc7baeb5c Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Sun, 21 Apr 2024 01:07:14 -0400 Subject: [PATCH 06/57] added inspection vpc --- modules/inspection-vpc/README.md | 178 +++++++++++++++++++ modules/inspection-vpc/docs/vpc.png | Bin 0 -> 368489 bytes modules/inspection-vpc/firewall-rules.tf | 162 +++++++++++++++++ modules/inspection-vpc/firewall.tf | 99 +++++++++++ modules/inspection-vpc/main.tf | 211 +++++++++++++++++++++++ modules/inspection-vpc/outputs.tf | 42 +++++ modules/inspection-vpc/variables.tf | 97 +++++++++++ modules/inspection-vpc/version.tf | 10 ++ modules/inspection-vpc/vpc-endpoints.tf | 30 ++++ 9 files changed, 829 insertions(+) create mode 100644 modules/inspection-vpc/README.md create mode 100644 modules/inspection-vpc/docs/vpc.png create mode 100644 modules/inspection-vpc/firewall-rules.tf create mode 100644 modules/inspection-vpc/firewall.tf create mode 100644 modules/inspection-vpc/main.tf create mode 100644 modules/inspection-vpc/outputs.tf create mode 100644 modules/inspection-vpc/variables.tf create mode 100644 modules/inspection-vpc/version.tf create mode 100644 modules/inspection-vpc/vpc-endpoints.tf diff --git a/modules/inspection-vpc/README.md b/modules/inspection-vpc/README.md new file mode 100644 index 0000000..613406f --- /dev/null +++ b/modules/inspection-vpc/README.md @@ -0,0 +1,178 @@ +# inspection-vpc + +This folder contains a [Terraform](https://www.terraform.io) module for deploying an [AWS Virtual Private Cloud (VPC)](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html). + +The VPC is designed for use in development environments and is not intended to be used in production. The module is optimized to reduce costs for non-prod VPCs at the expense of high availability, while also providing use of [AWS Network Firewall](https://aws.amazon.com/network-firewall/) and [NAT Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html. + +The VPC creates "Pod Subnets" in a secondary VPC CIDR attachment as a solution for ipv4 conservation as suggested in the AWS Blog Post [*Addressing IPv4 address exhaustion in Amazon EKS clusters using private NAT gateways*.](https://aws.amazon.com/blogs/containers/addressing-ipv4-address-exhaustion-in-amazon-eks-clusters-using-private-nat-gateways/) + +The VPC created looks like this: + +![VPC Diagram](docs/vpc.png) + +## Examples + +Minimal example: +``` +module "vpc" { + source = "../../terraform-aws-cxone/modules/inspection-vpc" + deployment_id = "bos-inspection-vpc" + primary_cidr_block = "10.77.0.0/16" +} +``` + +Full Example: + +``` +locals { + fqdn = "bos-govcloud.checkmarx-ps.com" +} + +module "vpc" { + source = "../../terraform-aws-cxone/modules/inspection-vpc" + deployment_id = "bos-inspection-vpc" + primary_cidr_block = "10.77.0.0/16" + secondary_cidr_block = "100.64.0.0/18" + interface_vpc_endpoints = ["ec2", "ec2messages", "ssm", "ssmmessages", "ecr.api", "ecr.dkr", "kms", "logs", "sts", "elasticloadbalancing", "autoscaling"] + create_interface_endpoints = true + create_s3_endpoint = true + enable_firewall = true + stateful_default_action = "aws:drop_established" + include_sca_rules = true + create_managed_rule_groups = false + managed_rule_groups = ["AbusedLegitMalwareDomainsStrictOrder", + "MalwareDomainsStrictOrder", + "AbusedLegitBotNetCommandAndControlDomainsStrictOrder", + "BotNetCommandAndControlDomainsStrictOrder", + "ThreatSignaturesBotnetStrictOrder", + "ThreatSignaturesBotnetWebStrictOrder", + "ThreatSignaturesBotnetWindowsStrictOrder", + "ThreatSignaturesIOCStrictOrder", + "ThreatSignaturesDoSStrictOrder", + "ThreatSignaturesEmergingEventsStrictOrder", + "ThreatSignaturesExploitsStrictOrder", + "ThreatSignaturesMalwareStrictOrder", + "ThreatSignaturesMalwareCoinminingStrictOrder", + "ThreatSignaturesMalwareMobileStrictOrder", + "ThreatSignaturesMalwareWebStrictOrder", + "ThreatSignaturesScannersStrictOrder", + "ThreatSignaturesSuspectStrictOrder", + "ThreatSignaturesWebAttacksStrictOrder" + ] + additional_suricata_rules = < $EXTERNAL_NET 443 (tls.sni; content:"${local.fqdn}"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:1; rev:1;) + +# https/tls protocol example.com exactly +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"example.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:1; rev:1;) + +# https/tls protocol specific subdomain of example.com +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"www.example.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:1; rev:1;) + +# https/tls protocol any subdomain of example.com +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:".example.com"; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:1; rev:1;) + +EOF +} +``` + +## AWS Network Firewall + +This module includes an option to deploy the AWS Network Firewall. When enabled, the firewall protects the private and pod subnets. The firewall stateful rules are executed in strict order. All rules are defined in [suricata format](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html). + +The firewall is enabled by the `enable_firewall` variable. When `enable_firewall` is false the VPC is deployed without the firewall components but still is a complete VPC with NAT Gateway for use with Checkmarx One deveopment/testing. + +The module contains default rules embedded in the module. You can view those rules in [firewall-rules.tf](./firewall-rules.tf). + +The embedded rules can be customized in these ways: + +| Approach | Instructions | +|---|---| +| Bring your own complete rules | Provide your rules in the `suricata_rules` variable. These rules will completely replace the embedded rules. | +| Append some custom rules | Provide your rules in `additional_suricata_rules` variable. These rules will be injected into the default rules. They will be executed after the embedded rules but before the default drop rule. Use a sequence ID of `YYMMDDNNN` (where NNN is a 3 digit sequence) to ensure no SID conflicts with embedded rules. | + +# Module documentation +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.6 | +| [aws](#requirement\_aws) | >= 5.46.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.46.0 | +| [template](#provider\_template) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [vpc\_endpoint\_security\_group](#module\_vpc\_endpoint\_security\_group) | terraform-aws-modules/security-group/aws | 5.1.2 | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.aws_nfw_alert](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_cloudwatch_log_group.aws_nfw_flow](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_eip.nat](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip) | resource | +| [aws_internet_gateway.igw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource | +| [aws_nat_gateway.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/nat_gateway) | resource | +| [aws_networkfirewall_firewall.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_firewall) | resource | +| [aws_networkfirewall_firewall_policy.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_firewall_policy) | resource | +| [aws_networkfirewall_logging_configuration.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_logging_configuration) | resource | +| [aws_networkfirewall_rule_group.cxone](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_rule_group) | resource | +| [aws_route_table.firewall](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) | resource | +| [aws_route_table.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) | resource | +| [aws_route_table.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) | resource | +| [aws_route_table_association.firewall](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource | +| [aws_route_table_association.pod](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource | +| [aws_route_table_association.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource | +| [aws_route_table_association.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource | +| [aws_subnet.database](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | +| [aws_subnet.firewall](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | +| [aws_subnet.pod](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | +| [aws_subnet.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | +| [aws_subnet.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | +| [aws_vpc.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) | resource | +| [aws_vpc_endpoint.interface](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_endpoint.s3_gateway_private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | +| [aws_vpc_ipv4_cidr_block_association.secondary_cidr_block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_ipv4_cidr_block_association) | resource | +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [template_file.default_suricata_rules](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_suricata\_rules](#input\_additional\_suricata\_rules) | Additional [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) rules to use in the network firewall. When provided these rules will be appended to the default rules prior to the default drop rule. | `string` | `""` | no | +| [create\_interface\_endpoints](#input\_create\_interface\_endpoints) | Enables creation of the [interface endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) specified in `interface_vpc_endpoints` | `bool` | `true` | no | +| [create\_managed\_rule\_groups](#input\_create\_managed\_rule\_groups) | Enables creation of the AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) provided in `managed_rule_groups` | `bool` | `true` | no | +| [create\_s3\_endpoint](#input\_create\_s3\_endpoint) | Enables creation of the [s3 gateway VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html) | `bool` | `true` | no | +| [deployment\_id](#input\_deployment\_id) | The deployment id for the VPC which is used to name resources | `string` | n/a | yes | +| [enable\_firewall](#input\_enable\_firewall) | Enables the use of the [AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/what-is-aws-network-firewall.html) to protect the private and pod subnets | `bool` | `true` | no | +| [include\_sca\_rules](#input\_include\_sca\_rules) | Enables inclusion of AWS Network Firewall rules used in SCA scanning. These rules may be overly permissive when not using SCA, so they are optional. These rules allow connectivity to various public package manager repositories like [Maven Central](https://mvnrepository.com/repos/central) and [npm](https://docs.npmjs.com/). | `bool` | `true` | no | +| [interface\_vpc\_endpoints](#input\_interface\_vpc\_endpoints) | A list of AWS services to create [VPC Private Endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) for. These endpoints are used for communication direct to AWS services without requiring connectivity and are useful for private EKS clusters. | `list(string)` |
[
"ec2",
"ec2messages",
"ssm",
"ssmmessages",
"ecr.api",
"ecr.dkr",
"kms",
"logs",
"sts",
"elasticloadbalancing",
"autoscaling"
]
| no | +| [managed\_rule\_groups](#input\_managed\_rule\_groups) | The AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) to include in the firewall policy. Must be strict order groups. | `list(string)` |
[
"AbusedLegitMalwareDomainsStrictOrder",
"MalwareDomainsStrictOrder",
"AbusedLegitBotNetCommandAndControlDomainsStrictOrder",
"BotNetCommandAndControlDomainsStrictOrder",
"ThreatSignaturesBotnetStrictOrder",
"ThreatSignaturesBotnetWebStrictOrder",
"ThreatSignaturesBotnetWindowsStrictOrder",
"ThreatSignaturesIOCStrictOrder",
"ThreatSignaturesDoSStrictOrder",
"ThreatSignaturesEmergingEventsStrictOrder",
"ThreatSignaturesExploitsStrictOrder",
"ThreatSignaturesMalwareStrictOrder",
"ThreatSignaturesMalwareCoinminingStrictOrder",
"ThreatSignaturesMalwareMobileStrictOrder",
"ThreatSignaturesMalwareWebStrictOrder",
"ThreatSignaturesScannersStrictOrder",
"ThreatSignaturesSuspectStrictOrder",
"ThreatSignaturesWebAttacksStrictOrder"
]
| no | +| [primary\_cidr\_block](#input\_primary\_cidr\_block) | The primary VPC CIDR block for the VPC. Must be at least a /19. | `string` | n/a | yes | +| [secondary\_cidr\_block](#input\_secondary\_cidr\_block) | The secondary VPC CIDR block for the EKS Pod [Custom Networking](https://aws.github.io/aws-eks-best-practices/networking/custom-networking/) configuration. Must be at least a /18. | `string` | `"100.64.0.0/18"` | no | +| [stateful\_default\_action](#input\_stateful\_default\_action) | The [default action](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order) for the AWS Network Firewall stateful rule group. Choose `aws:drop_established` or `aws:alert_established` | `string` | `"aws:drop_established"` | no | +| [suricata\_rules](#input\_suricata\_rules) | The [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) to use for the AWS Network Firewall. When provided, this variable completely overrides the embedded rules. Use this to bring your own rules. If you only need to provide some additional rules in addition to the bundled rules, then use `additional_suricata_rules` instead of `suricata_rules`. | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [database\_subnets](#output\_database\_subnets) | List of database subnet IDs in the VPC | +| [firewall\_subnets](#output\_firewall\_subnets) | List of firewall subnet IDs in the VPC | +| [pod\_subnet\_info](#output\_pod\_subnet\_info) | List of map of pod subnets including `subnet_id` and `availability_zone`. Useful for creating ENIConfigs for [EKS Custom Networking](https://docs.aws.amazon.com/eks/latest/userguide/cni-custom-network.html). | +| [pod\_subnets](#output\_pod\_subnets) | List of pod subnet IDs in the VPC | +| [private\_subnets](#output\_private\_subnets) | List of private subnet IDs in the VPC | +| [public\_subnets](#output\_public\_subnets) | List of public subnet IDs in the VPC | +| [vpc\_cidr\_blocks](#output\_vpc\_cidr\_blocks) | The VPC CIDR blocks of the VPC | +| [vpc\_id](#output\_vpc\_id) | The id of the VPC | diff --git a/modules/inspection-vpc/docs/vpc.png b/modules/inspection-vpc/docs/vpc.png new file mode 100644 index 0000000000000000000000000000000000000000..c9f478b00d8c952c2204bdfbf68c5c89708732e6 GIT binary patch literal 368489 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%0D*2^|JYmg!ds}R!ZYr`+1+i7)T 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

9ni@N{wW>(|+T z!QI7P+%K8RSC*Iw^xQ{((8Y0q>aqXGx{XElkNFjD6_~y6cI^2;RL~5R8LBQsvaHM> zevgiHhF~bSz7YH-GQRp!ons{_=Z!S1IR<^R=>siDyW{r+{~bHjBxLhhUay7&dCB>D zl72iVUU?-71Qe7@bRx2}O*Ysv*xA^hz6Y)ii$u6ktiM}lbNVH_-Yo*IXQ;cnK3kL? zje>B>hwDu3ol`28d^Dgi4Ct6)c!r7f7=sz&Y zkXgtPpEpJBr#>z1Js?`KFr_SN(G*Ap6^6QVM%Yr^+ z*#NPe0jL#}0tk#BEEpV9^$Y{8XS^G?VMkDZRwUvcmvzXWUKVm%g#Sa>d&g59{(s}n zu~#-Bd+(K*bnKO#b!?J7%ibcgD-J3gB*HOrjO@%pNE|CGN%qPt{H{}<&-eb`|NQR9 zqxHu*d5`P5Uh6rS4>A}WujPkurF2t*VfJp!Kysu?m(*^`#q;#Ma^oEU9+RLPV_RVv?G0p2<}1%Dg|VDz77Hd4h-gy7NuqGcwy z&-VQ@X%Ecpkn;>OvFnP7RqF+cA#eA;GUbny?Y-y6{dTqb9bgo0iYq3RVcYMC@rI8m zWh04U@-?J|v_z1yJ8z!$gJYS})||*XXr;^Q@l_Yq@$MTWR!e!FfW}1~p>@JYwx^rW z23^gZN9uLL6Voum!f9=&MnFRrug&Nad?2| z)BO?`MId428BES`mcj1Y=sj1iSp4XirhrSIHecR}u=NH7ioQ5YtlsXwQ zUT~gRc7JeL^}FmUi_J9X#3ym9w2s6tzEEDl@nP+$2DjiiZF){wKsi^2yO)>f5KkF! zG}`rrC?y+S_>L=qq&Ywl$z>pZ$%a-U6XTF$CvCO~`+(!NyD9UgUUtQkSdMMzS=|I?5%7TZ& zj%_g6rkNvCkFZ;sne6|hJMLG3Qr;JLRy)Ki_5eJdTJh@i=EHxNT1aCzoApMR;znR`p zOfF-q9$E^BlZz#kD@Upe3wQCcf8gK-P-k#TgjKEguaWIu@&4Lm^psbxTrmj+ucuy} z9H(riETYtNW%%YoRZK!fi~wt1u_+N>ea`-IsxD>Llp(%>cxy+Wgk|Eh)aONyuEA0` zhoH8$e~hE?{5H$2cX2hpYqEW#Q%qCp<`w5w#C^fx`Pp_Vg_1JS-ym%()&fiE+fU(a z+v{T{bnV zzLm>s`$xOPp3v(m^o9hA$0s}_D)U*Xh4lXa5Ser>@JQJR-~%@9_GCL2 zUvcEzD`ze0lUJtwcE45shp`n>+#SY?IJXEwlDGtU$VC%zhsk(-nY*A8Cr|BI?Vj_Bm`#DZAf zQZdQI<00dQC+MGsb32=18&b=~Yywo*95HOdYByUOMZxG4^4e&?)JD&rw!f8ZK zP>@~Beeff92b{F=EsNXlxuR#se1}DcD|)G{nkwIGHO1i;Fw3^%j|gJE3xT@D?&BU- z_!0b4@2vck6-b5dGkyEjp8hK_zH+jZgP(>lo=DnJ7hS!aAT|wKe`))YA%&|h=NHXi zq{#XX7vd@ykQtOwx-01MK)mpVlGFh}YTa7aw-(nswd=46ut<#=*cQLxy*vP;(69jQ z*n^PjpVz+@#gj7oy5Ur*pMQU5Furuz_ZL{%MW6#z zad4mGm0y-SP+uJ1a^NR){x)>y4_UQD;2uaxZEddMP)H8X_oh0^4bnSO6j92WFLmLV z2gp-Mnje0Pg;+nrK88Y^fleNpGTb8npQDB4MAqIAA9s+HupBKup9e&)vO&5GhHUvo zD8BWTn4|u?20&|w3#6jB)Wb>mLCZyWUeDt3<^OdU76n|N-lqufuK&3z#=`Dkoaz-|G*Ph+vmnui%Q=q9DUo9bItgVuh z8H2ga?GZA02SoJ7qXe%DEZh^ZLuQp%)7PKjp{d0|vxEgtM4<}^V{92w##5GRHC@z$ zRD9jtd=*cbl!90BNmD(ktd_F4is!eGU~4}cuf1=*S^TOqZDEbn9sH!(II!!QHp~XX)8;n+!C;1w&EPuPvZvaEWw5^Sx9WoN-Jz( z7(zq!`(4EEz!mv(K!7a)vXA8{^dfLM+rr{jJYs4sP)LK`x5+x)EydzsQwomM$y3S_ zA_sBmf%no)Se_%49%R(Ig1>MMRn_uAEfOL%H?g8PYCS>fBjG8%jxs|%CwZ6%7^vYb z57vql(Zfg4EUmMTn))Z&7bo62>t=feHSjfO;9A|z5cj~t_sR*7djWbbFlZnL031$y zq9`K!2J+A$cU)>U#*0Ta-9ezPNWR0Phz24rZ}!uMU&@;(0in5R5Lov`nPQJXQMqmz zNAZ0y+aR8Xy^6MaoJvsKYv%gn#6&{}V#-9y7|``KxBUbpq{=h!gSAFp%F}4Wx+r_r zRl+B$?d&2}t3=~yt`Zkbnzm66{OpgxB@`6%|6HVQ`bXR|zLZ{tg_FtFK9d2!TZFm5 zOiA!c$oAKF`&bpSU1D=^H-v#;Jk0v>2SQWXLo^DbkG&ZV1K29BfRFu@`e@e+IiGn& zy1FDiZOX@#-fmxBo6Z~bOeydY!9535{)ot!GBRiHK4!5?_Qn%VPID#sv$s=)k=8!`MtJ3#Z=5{RHZdrUl3fkLK~DLO@;l|iR2e9<$syqKSYk#iv?NRmRZ}f9p%#Xy{<)_8Wl6Xe-k)5vx!iI?z17-_^(eUG?2O zZ%}}I_Hw!HvV3p}6@@3aFN8GMs#rHM7c{wn_ARILK@Y3X?1w*R|BkD}LvmdbWdN(V zL&S;v$h{~Fsr&H++mAb9VsQo!aodp-h_C*|oIYCDLe9al5ec5+$iYqN2q?i%_tB?I zg06lt!rz=jUS^hZ#h+IG2R+wo`&v&K3+BTArTPaoG?S_JG%gX`lu`id zlfx)M_)m-zpE!rdRU|Q%_X%tYi$D9#0mH$v%djJ$7q1lSAH`x!jE6L#nExOpY}vjP zv15@Ev91agoRC3EaUuFSfZ+@1mQ+oD6es>KJ`zz7Jm0>BZIwkOTR@59SpVnl9?gG1 z_Hr-g|Ks6*{$D(N5eOK412EiHhEuoKzlOWVL*@6pSR`oN{v0&`>vAuG4`BnXPgv5a zHv2AUWa5tGNCUVF_=Oi84d87!dJa#(^+y6pheH~6Ha-wXV|+oV=M}&LoI*UK`2U5; zNBgeGtw=17>5ohzeg=$)`oA~Ag-0l$*KqA$&u^tUhXBx6L0q{#Yso-t5g%=NfNdq3 zg`vh4&=0XtD{Rsb@QY-S$P)WDKg1%JxEBfk-OJf$xA087+q{2sGwFA#mTm8_Ywx~Q z_l!1sD-n3|@4VwC<(xcJf!?>5>n|D zkX{|e&(tb;u=dKh84W=_j3-QoJxxqpG(EbM&aKx@ik10k^NB$?X3&Fww z6N|-SmZ*=>QGpwxGR7f0D^c+I!Gc@JfSTP!N-ZW?l_5&>|A0pDT+p@un$*~w{eMjX z|I?8Fe?p@H9ci#sulTk_)J1In>rvGW3Bf@ZK)?Dwt?faIJ4`oQbNBv>1<-eUxUgd+9-k87J*uW#w)hyz^k%z~dsh_z?X0B4j-zLCz>7&6|1B^-1^+L0Hwxg&mAW&)9}yoBV3h;|Wq!;J+aW!uG7nHi3Rr!dXY}Ujb$A^w z`~{#Y&6Ge;L;vqqu{^JGa?UikIK`zr>CI@1Io~A}v_w4^YlMAj#UjG*^=3Z*2Mdyg zT1pWI{|C)1ANB*y@wXYuiEU`$+Rbl;QLHR@H1e1hH9h5Y_}Xdy^A0W;75G{Ky2cfJ zj0*JPUs`&;L7~L*4ZC{g{Of3Me$8UzS{I~T$N)E_?UHoiLUI^SPh z;yNukl!~1FsSN?yxPd3B8tdPwNh+-7UM{zN0H~MDb~t8$gb7e(l<+!AA{m||kTAga z%8OmgboKakn;^GoC3D}p-AJ@T^=H~#u<^a~2#wu=Hosc3v8r&H>F{4k!ZY`6srynz zk^>8}zq(O>NxMUw6>>i?-_&`o^*78`I7ffR`~$C_WB*g!RUe|(*S|;RGIZXV zXlD;hJ8uu)mkd4k(=NE)z8`uKcGzIu>Sr*MH6oY*WE{m1*D~i^s)S-@&!?@8g>@yT z1I}(ScC2N7*MFgPZ(EM*4RwE(?a0a#&fKROUAUi5H*5698a&P1Y!vFwy!Y}C_TRs3 zds}5C|CpJtff~JRMQ6Igc{7~bcvL|d zZ__5yRxlzIpy8F(>P90#)waFsoQpop+;8Zrk}ciLbU7$NyzZ5HBaR<@*jF#S--Gk} z-UQXO|7Q8^$irIM!}IAdZbZ-1Y9>XLn!=1yv!z1_?|sR45tlK-3qeL_qDt&_vsoFa z{_255>HfPDv(9=fP5-*Z0HY;c|IAXJc?HAcVnJPaxr zt`;@-rGqb4CtxViw`9bGOX^B9n1k4~-f?V92pxlEAP7m^(YdQvrT9#MrZ>xruS!t4 zr9KS*F#})2%XfjA`OLMJG9Bj1%_Gyraqb1H2ZA_t7fKa)I|Y}+Dh+%sB;P<7k7O~n+7j=$hnN8E_5-P@UxzwtL;IoAmHW$&Xa;5#cRoFBPKY+gwzK|c zw!P~|^|YlO_Y*ik3!(*j?==v-19*+{$9 z&C=Ub;R7Xd4|rS)K0du2;^ER})MbRJ==f9e&Gz7xp76~d+X2D)K`)zmQG6FXQO75C zWN+&r0h&Ih^9v~-sri2|`lYf_&2qRBPDH^;?uB2g%8;-Of1Jpod+r&>SGi`U|CBG$ zmp0^l7&o_e8Qps4y6UTufOMK-MUolVlT_=f|DO?cw-vw}-@EiWd3yKE%Z&T}1L7HV z79H&$mkV;vL^H4UxVUZAgjeFhC@$%jU8v!F`Ab#m{&cQ~=GUG$=jjSrrCGNM9QJ!Y zi)YahD72Kc@?SILXgRx5iU%ZG&As}%2vE0fm6d&S8?oyU8a%Sepy!c%_iNO#4xYVs z`Jp*z;EnGiO*tF;521|H=nU#2B4_dSbnQ`<@M;BLqj*#HABW5EkW4R)4>9-2P*Ptb zR=u^BYiCzpJi@;d{P4+-ErrsqYd;#rcrL3W0w|tn{e8p##mQ*E2pNHVr{2H1?770D z-Z=%n^i?!u9Ebejg@v`~SXB4NzoIfz}A1 z_M{k1hXhrwoV8~d2u8FhhXgWTu!XHcSWWlp_p&oa!*&Gujq#pvwjJ}fY0+fNCu^Cw z710{Oo`Z;3|a{v7C3bsJxd z9|A&x>k+?f?_0CDdRR&BBXcXBS;98fBYVx-!QX4YA6YJQ940~U39`dHUY{8^@>8Zm z({MzRlQw>cn?pqAh$UZ>sciIW`kr4n;g_b+4dOcf9-e;;otV9hFDJrZX8vxj)O7r? zTimx>Tl_9P&2qE8o3UZrfQuTGwMjB^xQrkrL;Sq|{y_XvaUcHP@u!3)e=*4`Tji?e zyYGWaP3L}X@qCx`7De@@S-4u-z2MOtf8W4%hA^YI(Ei^r(XV z&*g@mwL5GJXU?cYe*gQ)Go>xx_l@u$i1E)!_7b2K|72y{9c(m-B$GE z;%i-2+Tx-6jcg9n7lZ4*@HZ}FMc(g+K0J}h9q512X~x!G?k!Y-xK-R793vac__*;| zz33C%Toc~M(hKu-LE~8+91crm7mmCE$vHQ^4bI${hoX-Jc}~?JMxB$il*q$+s;rBX zOlUq1x$RPZmRZ6%V2eio@) zLdPGK0+v+UWB`ov;(@I~iyF}XN(ujFs}?1HSMS~K1yOGL5jp8_o8*r za8BW#^>WL3y4Aqg*Ts^`+8`j^}jnMkD{x~_tvi{jNaVJ z4P)Es^kYZ=u z_{@r(>)FhA_E>!kzJJw6{UGx$cp(8=1Hz=J(jDe}NwyM}BA7Myjo`UPgtrrq_TB7k zX2{%L`~DH8D~rQA?0%{fIN`;L;0&(XK1tGJd=zm?Xw0XZ@}xdL{Yc(vLMAtgayBem z12O&?RX6lukTID3)0oPv?>2<&O+G``zH^P`-SWG5Ar@kySxn$Wp^&{+RjQ5d;@LSR*}O0HO`-2b zO7(Gr!f&1;gSXM;VK*+$2)WwNaKi`?$y7g*Fw^&GV*g)&oH{Xifeh?JgqG9`8yTJOmhObFc7s>O1+Wt$4ZbC~C*X*VsQhKm-To`%y zD}w7ijTS2{{@1eT?FiLqOzJ}y4O4If6;)x%YphNN(3z>Tz~x9HC^Y)E0mYp)OecSO zLZj$h#4DUJx$n*uRoL{Zsj&*Gry}PtKXI;?n1(kau-xB|V~J2V$GvzgWLjk{&oNQd zn`T(~hUuV)G>jy{Nh@vz`f6Iym`$nxzjG)WiWbdxG;SpnuqQ@0 zJXa_6vVI)dK&&kw*w`O+OH}Vev6gF|hI8xpt~u>vhT4`{vBSsEd}Q=?cNSP#;^m^A z4{^M{ep|OSY)%+s7DhM!7GB}8hFFObB1(YU8ken*SC_|e0pHDth24TD8Tm=jOA}bM zO#XC6tJ*KCbXJy>U&{vwZrFwhmvc)V6g_`4a@#w?Gog!Y(6iRD*Z0KtZi=loNaff` zPM(6~LPp~m<%_C~jddCir1V=F&xaJ}R}4Dd-;Tz#WLMyzg3=3Gtk}$_M3*yigeuYg zS_5-jBH)VWtN%T5NBvz55hxOM*W#WRq{{DME?*Z4Tz|6daqL)IFVX0_*?8k%-Kr!0 zS*5~#%0Wcz#SfEpa6bEt_^;(v^CQC+D3KBF@g?e*Xj5FNJ zoldbBL}zZqFrkjwoG29G{AFEN7nKVl*X_f+YO82W2>JONyt-(!E83@DgrBu1K|`^r z;|b3w6UNdUQYsWP{n?Eh#@y%#fo<%q$`S+#Xj8K z8_4KrJ1xl4U)To1SEe+0Y>z?GcoI}$ZG)9sR|}*wUOtcz}bmm zA^ad!z)Rb;=B*sYep56`J z0xO%Pc13(wFq2OL7CQC-hDk(9+6|0@Mw|Sc@Lw+Xr}qP-;pb8VHh|8=1qeR zS9t?@g*S~%6QTaDVo%PS= zD_0Yj?pH{?%qhj$0x;G@05f#}D7G9~pL~CaT<|Wtf^YonfQ5&0!SO9UzP)?tY-W`{_9yn4>BK5TU7jWhpSBJo#Ciz6&I_ z9l;G+)H=rdOWU`D(R^7ea3j?C_>{-Up%=e3Z#_|P(2P0i_m(V!i1Y8HLv>`t*C@0< zn=ip#)I=S@_g7yuX8CH;SE#|xh+eTKI^|-JoD<}#j zqagKqSZPBS=CS6uD5zTwX^3-Xd!~|tQGvs$)*Qu)L3v^xLq!XxA^VQ{Q5k z1c1yq5fPE0nFZp#T9oB$AiLd*16S$@+=8zJc8-VbQB29d^PwG`aWBp_>B){hGAg0h z(J_BtDWQKsRu4+rT#ufV=71=w7oQ2=n9%M@u}*;NjyJ-uv|Ys)Qq+`*>NQ zHQCY5Li(T611DfCI#7y;L<1FbbL2~a;szFR^lXtQg>T2l8WB-sq`)>#_lby=%*&f$ zX6K(2EPnvdYi2yCyp_KQ&nJ+{;+l6?cBq2j)oNlSnFuY4P8VGK0`p*$k6zYX7++NF z=qE`axZRgMl661E>mFw!{B@khJ#l8&)Eb*O^{(#~)$Qf>lE74TKt-1!TKgHVfH{0XbfxgyCaKCwjn1w5O@|!ks zkGNHTnc>P74u$sG2mPzyxnFzg-~Uqwv~7I;Za_<9J>Nye;do6$n&4pBGkRl}*99U? z0l#1`a$z1m3vSvxNPst}Tc?#kdQ>B^Ya z07rPe0NiA%Ssxg$-%*Q`UgZ);yDNpgSU(D%zk-f4iRCfbx49&(qkEBoIL+<1B}zjn z;<~RHYzVWrMC{TS7XHsEK_foCpUA&>h>gkN)UGt4M#`SDVT^UWg=WSsE_wAO(^O@L z1ju?!KMuV8A}C-CqyvqUvoRQ>4dFv6_VLlNTwGfM_8TUTwj~0bL!juYJI7JZVgGk1 zE5MzJ>#CFx#*7@SBC0Qm>)#Wm3moGI@#$97wMBN$6QP0Mp&4Qt>(;qKy#cZ>=FLyA z4gMjf!d799Oa>8gTa*-R-N5WW8!hCly)caJDM&ud|HfH)S&yK< z8@6$*feh&FdW^a~N2(JvdyelaVnhD(o;`8xu!hJkpAgCtRuMKI78(M&J;BY9V@gJy zbR;LVqi~hdHu~FEMSb)X7zM}JHp4gf7q4v<yrEZn?KD-<>`i<^r~X0Bo3ZuqWw zrn6sH+ZbJ0QqAmfY6_cHV#IZYDvYYQ2-~iCCBG}q-<|mch0Aki?Mgi3Q-?&Eg0p1F z1Xj^K*O_C{@b!u?UtX>uv)yb_eci@}coEMO{5sG*H@bC?UpVSENK3ew0CL2{W`{ZZ zXr2t6=K8)B4-R)G@37n;BE)oVq}ZW~y$%q6_8_PVOiRaU=JR(x!GqSBfl1K4ze`mc zSwHAMjY?J6q_Y~b#I-n;AFRs~F2v+3EHhh&-D)iL(y*+xbUp2r34L%^j;HV8;pD3@ zW%+Wh($IThk~F%^xO*qGZ4 z_`Y4c+j!AzW%p*}7WQNhCvbH-2ut;XTd?grD(RZzhZTON@_17v;QmWmbSjP9RitAI zoFoYlT!Ir+0D@`F)-Wwi0);lMFiQ;`V)6t}6-5z>AK=bdp?IXGW*-958}>4%?{v+0 zUr)Y`Gp$HQh3{@)FCo$c=$M5bH>w)n1LeNl@hiQe=;FVLDZQ&xvb+Zj)dushOlzq` zr(GkH5=JBJJQ2@^3s80A%Ez((50s@k97^iLF*sH}_;B4E>90hU^%6LfOyOIf#s7RD zb}p12kQw0Yg!5_K7Jpi;ih`iu&Tr+a6OSwt|JLZT5;I7Gw!euU!4XO5f6{G?IenJJ zPfh28X<|tccO8L91zEXm#d{s?1+O>;dky?J%z)Xc)tT*PT;Upyz^Z9`Ar(K`sbK^U#U7>+3rL04JtfCBg13>8vu zs-4T=G4!4gHU|Ogs=qN4*KP2vdk8bb!PJI^J$ZjP#x$YNbAoQR2dzYV?_AIyxUrTQ zmn2Oe%%IN##P~IDbnWFR1PFpdv4ZtbmTl%F+$D9y3D+p>)-hp@Gu!fh;mT?ieCHlm z$M|UQF4MN9p4!pws^LG_VOGLN6W_dgkG%8I29z(ljp6!~LYXsp|FCT~bNJ67X};8o z;4L?+aHA{MZuNUT_;=@c=ST-WD=}j%qf-4B-H}=RgioZ#PSi1`8*`Nb*bdQzo#rva zM=_G%w}5f^!~h~fIQ+x{UE9_yyEx7ogzNNnD%MR`93%0DsHZaF<9tJJP%X6Mr=WS& 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%0DQ63 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 literal 0 HcmV?d00001 diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf new file mode 100644 index 0000000..d6429fe --- /dev/null +++ b/modules/inspection-vpc/firewall-rules.tf @@ -0,0 +1,162 @@ + +locals { + sca_scanning_rules = var.include_sca_rules == false ? "" : < $EXTERNAL_NET 443 (tls.sni; content:"github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420065; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repo.maven.apache.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420066; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.npmjs.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420067; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"lscr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420068; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"index.docker.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420069; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"toolbox-data.anchore.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420070; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.hub.docker.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420071; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"hub.docker.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420072; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"ghcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420073; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy.golang.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420074; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sum.golang.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420075; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"pkg-containers.githubusercontent.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420076; rev:1;) + +EOF +} + +data "template_file" "default_suricata_rules" { + count = var.enable_firewall ? 1 : 0 + template = < any any (msg:"TLS 1.0 or 1.1"; ssl_version:tls1.0,tls1.1; sid:2023070518;) +drop ip $HOME_NET any -> $EXTERNAL_NET [1389,53,4444,445,135,139,389,3389] (msg:"Deny List High Risk Destination Ports"; sid:278670;) + +# Amazon Services - these must be allowed, or can be replaced by private VPC Endpoints (which have a charge https://aws.amazon.com/privatelink/pricing/) +# Note that these are AWS Region Dependent +# Reference https://docs.aws.amazon.com/eks/latest/userguide/private-clusters.html, https://eksctl.io/usage/eks-private-cluster/ +# These rules do not imply support for completely private clusters, but do help with private cluster deployments. +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"ssm.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190000; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"ec2.${data.aws_region.current.name}.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190001; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eks.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190002; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.ecr.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190003; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"dkr.ecr.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190004; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"ssmmessages.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190005; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"ec2messages.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190006; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sts.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190007; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"logs.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190008; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"route53.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190009; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cloudformation.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190010; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"elasticloadbalancing.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190011; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"autoscaling.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190012; rev:1;) + + +# Installation media for kube-system services pods like coredns, aws-node, ebs-csi-controller, ebs-csi-node, kube-proxy +# The *.dkr.ecr.*.amazonaws.com URLs are typically metadata repos that redirect to prod-$REGION-starport-layer-bucket.s3.$REGION.amazonaws.com to download packages +# References: +# 1. https://docs.aws.amazon.com/eks/latest/userguide/add-ons-images.html +# 2. https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html#ecr-setting-up-s3-gateway +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"877085696533.dkr.ecr.af-south-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420000; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"800184023465.dkr.ecr.ap-east-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420001; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.ap-northeast-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420002; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.ap-northeast-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420003; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.ap-northeast-3.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420004; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.ap-south-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420005; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"900889452093.dkr.ecr.ap-south-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420006; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.ap-southeast-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420007; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.ap-southeast-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420008; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"296578399912.dkr.ecr.ap-southeast-3.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420009; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"491585149902.dkr.ecr.ap-southeast-4.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420010; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.ca-central-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420011; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"761377655185.dkr.ecr.ca-west-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420012; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.eu-central-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420013; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"900612956339.dkr.ecr.eu-central-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420014; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.eu-north-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420015; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"590381155156.dkr.ecr.eu-south-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420016; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"455263428931.dkr.ecr.eu-south-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420017; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.eu-west-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420018; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.eu-west-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420019; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.eu-west-3.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420020; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"066635153087.dkr.ecr.il-central-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420021; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"558608220178.dkr.ecr.me-south-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420022; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"759879836304.dkr.ecr.me-central-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420023; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.sa-east-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420024; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.us-east-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420025; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.us-east-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420026; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"151742754352.dkr.ecr.us-gov-east-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420027; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"01004608.dkr.ecr.us-gov-west-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420028; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.us-west-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420029; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr.ecr.us-west-2.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420030; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"prod-${data.aws_region.current.name}-starport-layer-bucket.s3.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420031; rev:1;) + + +# Amazon Linux 2 managed node group updates - region dependent +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"amazonlinux-2-repos-${data.aws_region.current.name}.s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420032; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"al2023-repos-${data.aws_region.current.name}-de612dc2.s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420033; rev:1;) + + +# AWS Load Balancer Controller - public.ecr.aws (metadata) redirects to cloudfront (download) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"public.ecr.aws"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420034; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"d5l0dvt14r5h8.cloudfront.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420035; rev:1;) + +# Cluster Autoscaler - k8s.gcr.io (metadata) redirects to storage.googleapis.com (download) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"k8s.gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420036; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"storage.googleapis.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420037; rev:1;) + +# Kotsadm tools (minio, rqlite) come from docker.io and docker.com +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry-1.docker.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420038; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"auth.docker.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420039; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"production.cloudflare.docker.com"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420040; rev:1;) + +# Replicated APIs - used for license checking +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"replicated.app"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420041; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy.replicated.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420042; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy-auth.replicated.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420043; rev:1;) + +# Used by cxone images, and kube-rbac-proxy in CxOne operator +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240419044; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"checkmarx.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420044; rev:1;) + +# Liquibase schema files required for database migration execution +pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"www.liquibase.org"; startswith; endswith; msg:"Match liquidbase.com allowed"; flow:to_server, established; sid:240420062; rev:1;) + +# Feature Flags via Split.io +# Reference https://help.split.io/hc/en-us/articles/360006954331-How-do-I-allow-Split-to-work-in-my-environment +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sdk.split.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420045; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"auth.split.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420046; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"telemetry.split.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420047; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"events.split.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420048; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"streaming.split.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420049; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cdn.split.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420050; rev:1;) +# Fastly (a CDN), which is used for sdk.split.io https://api.fastly.com/public-ip-list. These are used sometimes w/o host name, so SNI cannot be used to filter. +pass tls $HOME_NET any -> [23.235.32.0/20,43.249.72.0/22,103.244.50.0/24,103.245.222.0/23,103.245.224.0/24,104.156.80.0/20,140.248.64.0/18,140.248.128.0/17,146.75.0.0/17,151.101.0.0/16,157.52.64.0/18,167.82.0.0/17,167.82.128.0/20,167.82.160.0/20,167.82.224.0/20,172.111.64.0/18,185.31.16.0/22,199.27.72.0/21,199.232.0.0/16] 443 (msg:"Fastly CDN"; flow:to_server, established; sid:240420051; rev:1;) + +# Allow access to s3 buckets for Checkmarx One. Buckets are typically created with a prefix of the deployment id which allows for regex matching +# Example bucket name and suffix: scan-results-bos-ap-southeast-1-lab-19205 +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420052; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420053; rev:1;) + +# Checkmarx One Scans will upload source to scan-results bucket with url path patterns like "https://s3.${data.aws_region.current.name}.amazonaws.com/scan-results-0aa15147e5f3/source-code/....." +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420054; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420055; rev:1;) + +# These are the checkmarx services for SCA scanning, cloud IAM (for Authentication to SCA), and codebashing +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"iam.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420056; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api-sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420057; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.iam.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420058; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.api-sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420059; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420060; rev:1;) +# Scan results buckets are used for SCA scan result syncing, and vary by the connected SCA region (e.g. NA, or EU) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prod-storage-1an26shc41yi3.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240420061; rev:1;) + +# These URLs are randomly generated, and used to discover the correct s3 API signature version to use when communicating with S3 buckets. +# They take two forms, where the long alphanumeric string is randomly generated. The buckets do not exist, but allow minio client +# to attempt to connect to S3 to discover the s3 signature version to use in subsequent requests to the actual buckets +# 1. probe-bucket-sign-vie4gezw1j6w.s3.dualstack.${data.aws_region.current.name}.amazonaws.com +# 2. probe-bsign-jmcvig40f29rwikvncljjtvohv4i4h.s3.dualstack.${data.aws_region.current.name}.amazonaws.com +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^probe-bucket-sign-[A-z0-9]{12}\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i"; flow: to_server; msg:"Minio client s3 signature version determination"; sid:240420063;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^probe-bsign-[A-z0-9]{30}\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i"; flow: to_server; msg:"Minio client s3 signature version determination"; sid:240420064;) + +${local.sca_scanning_rules} + +${var.additional_suricata_rules} + +# Drop other traffic +drop tls $HOME_NET any -> $EXTERNAL_NET any (msg:"not matching any TLS allowlisted FQDNs"; flow:to_server, established; sid:3; rev:1;) +reject tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"blocked uknown tcp"; flow:to_server, established; sid:4; rev:1;) + +EOF +} diff --git a/modules/inspection-vpc/firewall.tf b/modules/inspection-vpc/firewall.tf new file mode 100644 index 0000000..7284212 --- /dev/null +++ b/modules/inspection-vpc/firewall.tf @@ -0,0 +1,99 @@ +#****************************************************************************** +# Network Firewall +#****************************************************************************** + +resource "aws_networkfirewall_firewall" "main" { + count = var.enable_firewall ? 1 : 0 + name = "${var.deployment_id}-checkmarxone" + firewall_policy_arn = aws_networkfirewall_firewall_policy.main[0].arn + vpc_id = aws_vpc.main.id + timeouts { + create = "40m" + update = "50m" + delete = "1h" + } + subnet_mapping { + subnet_id = aws_subnet.firewall[0].id + } +} + +resource "aws_networkfirewall_rule_group" "cxone" { + count = var.enable_firewall ? 1 : 0 + capacity = 200 + name = "${var.deployment_id}-cxone" + type = "STATEFUL" + rule_group { + rules_source { + rules_string = var.suricata_rules != null ? var.suricata_rules : data.template_file.default_suricata_rules[0].rendered + } + stateful_rule_options { + rule_order = "STRICT_ORDER" + } + } +} + +resource "aws_networkfirewall_firewall_policy" "main" { + count = var.enable_firewall ? 1 : 0 + name = var.deployment_id + firewall_policy { + stateless_default_actions = ["aws:forward_to_sfe"] + stateless_fragment_default_actions = ["aws:forward_to_sfe"] + stateful_default_actions = [var.stateful_default_action] + stateful_engine_options { + rule_order = "STRICT_ORDER" + } + stateful_rule_group_reference { + priority = 1 + resource_arn = aws_networkfirewall_rule_group.cxone[0].arn + } + + dynamic "stateful_rule_group_reference" { + for_each = { for idx, rg in var.managed_rule_groups : rg => idx if var.create_managed_rule_groups } + content { + priority = stateful_rule_group_reference.value + 2 + resource_arn = "arn:${data.aws_partition.current.id}:network-firewall:${data.aws_region.current.name}:aws-managed:stateful-rulegroup/${stateful_rule_group_reference.key}" + } + } + + policy_variables { + rule_variables { + key = "HOME_NET" + ip_set { definition = [var.primary_cidr_block, var.secondary_cidr_block] } + } + } + } +} + +resource "aws_cloudwatch_log_group" "aws_nfw_alert" { + count = var.enable_firewall ? 1 : 0 + name = "${var.deployment_id}-aws-nfw-alert" + retention_in_days = 14 +} + +resource "aws_cloudwatch_log_group" "aws_nfw_flow" { + count = var.enable_firewall ? 1 : 0 + name = "${var.deployment_id}-aws-nfw-flow" + retention_in_days = 14 +} + + +resource "aws_networkfirewall_logging_configuration" "main" { + count = var.enable_firewall ? 1 : 0 + firewall_arn = aws_networkfirewall_firewall.main[0].arn + logging_configuration { + log_destination_config { + log_destination = { + logGroup = aws_cloudwatch_log_group.aws_nfw_alert[0].name + } + log_destination_type = "CloudWatchLogs" + log_type = "ALERT" + } + log_destination_config { + log_destination = { + logGroup = aws_cloudwatch_log_group.aws_nfw_flow[0].name + } + log_destination_type = "CloudWatchLogs" + log_type = "FLOW" + } + } +} diff --git a/modules/inspection-vpc/main.tf b/modules/inspection-vpc/main.tf new file mode 100644 index 0000000..2ef0da7 --- /dev/null +++ b/modules/inspection-vpc/main.tf @@ -0,0 +1,211 @@ +data "aws_partition" "current" {} +data "aws_region" "current" {} +data "aws_availability_zones" "available" { + state = "available" +} + +locals { + azs = slice(data.aws_availability_zones.available.names, 0, 2) + vpc_cidr_blocks = [var.primary_cidr_block, var.secondary_cidr_block] + + # Here we are calculating the CIDRs for the subnets from the given primary VPC CIDR block. + # This VPC should not be used for production. It is optimized to reduce AWS costs at the expense of availability. + # It is intended for dev/test workloads only. + # Cost optimizations: Single AZ NAT Gateway and AWS Network Firewall Endpoint for entire VPC + # Only two AZs for private and database subnets to reduce cross AZ data transfer + # Network Topology: + # Public Subnet: /27 (single AZ) - contains NLB, NAT Gateway + # Firewall Subnet: /28 (single AZ) - contains firewall endpoint + # Private Subnets: /21 /21 (two AZ) - for EKS deployment + # Database Subnets: /22 /22 (two AZ) - for RDS, Elasticache, Opensearch deployment + # Note about cidrsubnets newbits arguments: + # Subtracting the primary cidr size from the desired subnet size produces the 'newbits' value for the cidrsubnets + # function to consistently create subnets of the desired size, regardless of the user provided + # var.primary_cidr_block value. The primary_cidr_block must be at least a /19. + primary_cidr_size = split("/", var.primary_cidr_block)[1] + subnet_cidrs = cidrsubnets(var.primary_cidr_block, + (27 - local.primary_cidr_size), # Public Subnet + (28 - local.primary_cidr_size), # Firewall Subnet + (21 - local.primary_cidr_size), # Private Subnet 1 + (21 - local.primary_cidr_size), # Private Subnet 2 + (22 - local.primary_cidr_size), # Database Subnet 1 + (22 - local.primary_cidr_size)) # Database Subnet 2 + public_subnet_cidr = slice(local.subnet_cidrs, 0, 1)[0] + firewall_subnet_cidr = slice(local.subnet_cidrs, 1, 2)[0] + private_subnet_cidrs = slice(local.subnet_cidrs, 2, 4) + database_subnet_cidrs = slice(local.subnet_cidrs, 4, 6) + + # Calculate 2 /19s for the secondary cidr to use for pod custom networking. The secondary_cidr_block must be at least a /18. + secondary_cidr_size = split("/", var.secondary_cidr_block)[1] + pod_subnet_cidrs = cidrsubnets(var.secondary_cidr_block, (19 - local.secondary_cidr_size), (19 - local.secondary_cidr_size)) +} + +#****************************************************************************** +# VPC & Subnets +#****************************************************************************** +resource "aws_vpc" "main" { + cidr_block = var.primary_cidr_block + enable_dns_hostnames = true # Required for EKS + enable_dns_support = true # Required for EKS + + tags = { Name = "${var.deployment_id}" } +} + +resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr_block" { + vpc_id = aws_vpc.main.id + cidr_block = var.secondary_cidr_block +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = local.public_subnet_cidr + availability_zone = local.azs[0] + tags = { + Name = "${var.deployment_id} - public subnet ${local.azs[0]}" + "kubernetes.io/cluster/${var.deployment_id}" = "shared" + "kubernetes.io/role/elb" = "1" + "karpenter.sh/discovery" = "${var.deployment_id}" + } +} + +resource "aws_subnet" "firewall" { + count = var.enable_firewall ? 1 : 0 + vpc_id = aws_vpc.main.id + cidr_block = local.firewall_subnet_cidr + availability_zone = local.azs[0] + tags = { Name = "${var.deployment_id} - firewall subnet ${local.azs[0]}" } +} + +resource "aws_subnet" "private" { + for_each = { for idx, az in local.azs : az => idx } + vpc_id = aws_vpc.main.id + cidr_block = local.private_subnet_cidrs[each.value] + availability_zone = each.key + tags = { + Name = "${var.deployment_id} - private subnet ${each.key}" + "kubernetes.io/cluster/${var.deployment_id}" = "shared" + "kubernetes.io/role/internal-elb" = "1" + "karpenter.sh/discovery" = "${var.deployment_id}" + } +} + +resource "aws_subnet" "database" { + for_each = { for idx, az in local.azs : az => idx } + vpc_id = aws_vpc.main.id + cidr_block = local.database_subnet_cidrs[each.value] + availability_zone = each.key + tags = { Name = "${var.deployment_id} - database subnet ${each.key}" } +} + +resource "aws_subnet" "pod" { + for_each = { for idx, az in local.azs : az => idx if var.secondary_cidr_block != null } + vpc_id = aws_vpc.main.id + cidr_block = local.pod_subnet_cidrs[each.value] + availability_zone = each.key + tags = { + Name = "${var.deployment_id} - pod subnet ${each.key}" + "kubernetes.io/cluster/${var.deployment_id}" = "shared" + } + depends_on = [aws_vpc_ipv4_cidr_block_association.secondary_cidr_block] +} + +#****************************************************************************** +# IGW & NAT Gateway +#****************************************************************************** + +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.main.id + tags = { + Name = "${var.deployment_id}" + } +} + +resource "aws_eip" "nat" { + domain = "vpc" + depends_on = [aws_internet_gateway.igw] +} + +resource "aws_nat_gateway" "public" { + allocation_id = aws_eip.nat.id + subnet_id = aws_subnet.public.id + tags = { Name = "NAT Gateway - ${var.deployment_id}" } + depends_on = [aws_internet_gateway.igw] +} + +#****************************************************************************** +# Route Tables +#****************************************************************************** + +# Public Subnet Routing +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + tags = { "Name" = "${var.deployment_id}-public" } + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + dynamic "route" { + for_each = { for idx, az in local.azs : az => idx if var.enable_firewall } + content { + cidr_block = local.private_subnet_cidrs[route.value] + vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] + } + } + + dynamic "route" { + for_each = { for idx, az in local.azs : az => idx if var.enable_firewall } + content { + cidr_block = local.pod_subnet_cidrs[route.value] + vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] + } + } +} + +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} + + +# Firewall Subnet Routing +resource "aws_route_table" "firewall" { + count = var.enable_firewall ? 1 : 0 + vpc_id = aws_vpc.main.id + tags = { "Name" = "${var.deployment_id}-firewall" } + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.public.id + } +} + +resource "aws_route_table_association" "firewall" { + count = var.enable_firewall ? 1 : 0 + subnet_id = aws_subnet.firewall[0].id + route_table_id = aws_route_table.firewall[0].id +} + +# Private Subnets Routing +resource "aws_route_table" "private" { + vpc_id = aws_vpc.main.id + tags = { "Name" = "${var.deployment_id}-private" } + route { + cidr_block = "0.0.0.0/0" + vpc_endpoint_id = var.enable_firewall ? [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] : null + nat_gateway_id = var.enable_firewall ? null : aws_nat_gateway.public.id + } +} + +resource "aws_route_table_association" "private" { + for_each = { for idx, az in local.azs : az => idx } + subnet_id = aws_subnet.private[each.key].id + route_table_id = aws_route_table.private.id +} + +resource "aws_route_table_association" "pod" { + for_each = { for idx, az in local.azs : az => idx } + subnet_id = aws_subnet.pod[each.key].id + route_table_id = aws_route_table.private.id +} diff --git a/modules/inspection-vpc/outputs.tf b/modules/inspection-vpc/outputs.tf new file mode 100644 index 0000000..86d633f --- /dev/null +++ b/modules/inspection-vpc/outputs.tf @@ -0,0 +1,42 @@ +output "vpc_id" { + description = "The id of the VPC" + value = aws_vpc.main.id +} + +output "vpc_cidr_blocks" { + description = "The VPC CIDR blocks of the VPC" + value = local.vpc_cidr_blocks +} + +output "public_subnets" { + description = "List of public subnet IDs in the VPC" + value = [aws_subnet.public.id] +} + +output "firewall_subnets" { + description = "List of firewall subnet IDs in the VPC" + value = var.enable_firewall ? aws_subnet.firewall[0].id : null +} + +output "private_subnets" { + description = "List of private subnet IDs in the VPC" + value = [for s in aws_subnet.private : s.id] +} + +output "pod_subnets" { + description = "List of pod subnet IDs in the VPC" + value = [for s in aws_subnet.pod : s.id] +} + +output "database_subnets" { + description = "List of database subnet IDs in the VPC" + value = [for s in aws_subnet.database : s.id] +} + +output "pod_subnet_info" { + description = "List of map of pod subnets including `subnet_id` and `availability_zone`. Useful for creating ENIConfigs for [EKS Custom Networking](https://docs.aws.amazon.com/eks/latest/userguide/cni-custom-network.html)." + value = [for s in aws_subnet.pod : { + subnet_id = s.id + availability_zone = s.availability_zone + }] +} diff --git a/modules/inspection-vpc/variables.tf b/modules/inspection-vpc/variables.tf new file mode 100644 index 0000000..a600977 --- /dev/null +++ b/modules/inspection-vpc/variables.tf @@ -0,0 +1,97 @@ +variable "deployment_id" { + description = "The deployment id for the VPC which is used to name resources" + type = string + nullable = false +} + +variable "primary_cidr_block" { + description = "The primary VPC CIDR block for the VPC. Must be at least a /19." + type = string + nullable = false +} + +variable "secondary_cidr_block" { + description = "The secondary VPC CIDR block for the EKS Pod [Custom Networking](https://aws.github.io/aws-eks-best-practices/networking/custom-networking/) configuration. Must be at least a /18." + type = string + default = "100.64.0.0/18" + nullable = false +} + +variable "interface_vpc_endpoints" { + type = list(string) + description = "A list of AWS services to create [VPC Private Endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) for. These endpoints are used for communication direct to AWS services without requiring connectivity and are useful for private EKS clusters." + default = ["ec2", "ec2messages", "ssm", "ssmmessages", "ecr.api", "ecr.dkr", "kms", "logs", "sts", "elasticloadbalancing", "autoscaling"] +} + +variable "create_interface_endpoints" { + type = bool + description = "Enables creation of the [interface endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) specified in `interface_vpc_endpoints`" + default = true +} + +variable "create_s3_endpoint" { + type = bool + description = "Enables creation of the [s3 gateway VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)" + default = true +} + +variable "enable_firewall" { + description = "Enables the use of the [AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/what-is-aws-network-firewall.html) to protect the private and pod subnets" + type = bool + default = true +} + +variable "stateful_default_action" { + description = "The [default action](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order) for the AWS Network Firewall stateful rule group. Choose `aws:drop_established` or `aws:alert_established`" + type = string + default = "aws:drop_established" +} + +variable "suricata_rules" { + description = "The [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) to use for the AWS Network Firewall. When provided, this variable completely overrides the embedded rules. Use this to bring your own rules. If you only need to provide some additional rules in addition to the bundled rules, then use `additional_suricata_rules` instead of `suricata_rules`." + type = string + default = null +} + +variable "include_sca_rules" { + description = "Enables inclusion of AWS Network Firewall rules used in SCA scanning. These rules may be overly permissive when not using SCA, so they are optional. These rules allow connectivity to various public package manager repositories like [Maven Central](https://mvnrepository.com/repos/central) and [npm](https://docs.npmjs.com/)." + type = bool + default = true +} + +variable "additional_suricata_rules" { + description = "Additional [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) rules to use in the network firewall. When provided these rules will be appended to the default rules prior to the default drop rule." + type = string + default = "" +} + +variable "create_managed_rule_groups" { + type = bool + description = "Enables creation of the AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) provided in `managed_rule_groups`" + default = true +} + +variable "managed_rule_groups" { + description = "The AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) to include in the firewall policy. Must be strict order groups. " + type = list(string) + # ThreatSignaturesFUPStrictOrder and ThreatSignaturesPhishingStrictOrder are not included by default, as you can only have 20 stateful rule groups per FW policy. + default = ["AbusedLegitMalwareDomainsStrictOrder", + "MalwareDomainsStrictOrder", + "AbusedLegitBotNetCommandAndControlDomainsStrictOrder", + "BotNetCommandAndControlDomainsStrictOrder", + "ThreatSignaturesBotnetStrictOrder", + "ThreatSignaturesBotnetWebStrictOrder", + "ThreatSignaturesBotnetWindowsStrictOrder", + "ThreatSignaturesIOCStrictOrder", + "ThreatSignaturesDoSStrictOrder", + "ThreatSignaturesEmergingEventsStrictOrder", + "ThreatSignaturesExploitsStrictOrder", + "ThreatSignaturesMalwareStrictOrder", + "ThreatSignaturesMalwareCoinminingStrictOrder", + "ThreatSignaturesMalwareMobileStrictOrder", + "ThreatSignaturesMalwareWebStrictOrder", + "ThreatSignaturesScannersStrictOrder", + "ThreatSignaturesSuspectStrictOrder", + "ThreatSignaturesWebAttacksStrictOrder" + ] +} diff --git a/modules/inspection-vpc/version.tf b/modules/inspection-vpc/version.tf new file mode 100644 index 0000000..6e9b717 --- /dev/null +++ b/modules/inspection-vpc/version.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.46.0" + } + } +} \ No newline at end of file diff --git a/modules/inspection-vpc/vpc-endpoints.tf b/modules/inspection-vpc/vpc-endpoints.tf new file mode 100644 index 0000000..bec503f --- /dev/null +++ b/modules/inspection-vpc/vpc-endpoints.tf @@ -0,0 +1,30 @@ +module "vpc_endpoint_security_group" { + create = var.create_interface_endpoints + source = "terraform-aws-modules/security-group/aws" + version = "5.1.2" + name = "${var.deployment_id}-vpc-endpoints" + description = "VPC endpoint security group for Checkmarx One deployment named ${var.deployment_id}" + vpc_id = aws_vpc.main.id + ingress_cidr_blocks = local.vpc_cidr_blocks + ingress_rules = ["https-443-tcp"] +} + +resource "aws_vpc_endpoint" "interface" { + for_each = { for idx, endpoint in var.interface_vpc_endpoints : endpoint => idx if var.create_interface_endpoints } + vpc_id = aws_vpc.main.id + subnet_ids = [for s in aws_subnet.private : s.id] + service_name = "com.amazonaws.${data.aws_region.current.name}.${each.key}" + vpc_endpoint_type = "Interface" + security_group_ids = [module.vpc_endpoint_security_group.security_group_id] + private_dns_enabled = true + tags = { Name = "${var.deployment_id}-${each.key}-vpc-endpoint" } +} + +resource "aws_vpc_endpoint" "s3_gateway_private" { + count = var.create_s3_endpoint ? 1 : 0 + vpc_endpoint_type = "Gateway" + service_name = "com.amazonaws.${data.aws_region.current.name}.s3" + vpc_id = aws_vpc.main.id + route_table_ids = [aws_route_table.private.id, aws_route_table.public.id] + tags = { Name = "${var.deployment_id}-s3-vpc-endpoint" } +} From 50d24341b0b32cda30a13211a9c731dffc821cfe Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Sun, 21 Apr 2024 01:25:25 -0400 Subject: [PATCH 07/57] doc fix, add azs output --- modules/inspection-vpc/README.md | 2 +- modules/inspection-vpc/outputs.tf | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/inspection-vpc/README.md b/modules/inspection-vpc/README.md index 613406f..ee20208 100644 --- a/modules/inspection-vpc/README.md +++ b/modules/inspection-vpc/README.md @@ -2,7 +2,7 @@ This folder contains a [Terraform](https://www.terraform.io) module for deploying an [AWS Virtual Private Cloud (VPC)](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html). -The VPC is designed for use in development environments and is not intended to be used in production. The module is optimized to reduce costs for non-prod VPCs at the expense of high availability, while also providing use of [AWS Network Firewall](https://aws.amazon.com/network-firewall/) and [NAT Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html. +The VPC is designed for use in development environments and is not intended to be used in production. The module is optimized to reduce costs for non-prod VPCs at the expense of high availability, while also providing use of [AWS Network Firewall](https://aws.amazon.com/network-firewall/) and [NAT Gateway](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html). The VPC creates "Pod Subnets" in a secondary VPC CIDR attachment as a solution for ipv4 conservation as suggested in the AWS Blog Post [*Addressing IPv4 address exhaustion in Amazon EKS clusters using private NAT gateways*.](https://aws.amazon.com/blogs/containers/addressing-ipv4-address-exhaustion-in-amazon-eks-clusters-using-private-nat-gateways/) diff --git a/modules/inspection-vpc/outputs.tf b/modules/inspection-vpc/outputs.tf index 86d633f..3589fd6 100644 --- a/modules/inspection-vpc/outputs.tf +++ b/modules/inspection-vpc/outputs.tf @@ -40,3 +40,8 @@ output "pod_subnet_info" { availability_zone = s.availability_zone }] } + +output "azs" { + description = "The Availability Zones deployed into" + value = local.azs +} From 25d9c5968903bb2720f87b023d28bd60450519bc Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 23 Apr 2024 00:10:51 -0400 Subject: [PATCH 08/57] CxOne 3.10.22 --- README.md | 12 +- eks.tf | 119 ++- examples/full/README.md | 73 +- examples/full/examples.auto.tfvars | 126 ++- examples/full/main.tf | 112 +-- .../full/{network.tf => network.tfignore} | 2 +- examples/full/variables-cxone.tf | 12 + examples/full/variables-example.tf | 66 +- examples/full/variables-vpc.tf | 92 ++ .../apply-storageclass-config.sh.tftpl | 20 + .../karpenter.reference.yaml.tftpl | 165 ++++ .../kots.config.aws.reference.yaml.tftpl | 14 +- modules/cxone-install/main.tf | 31 +- modules/cxone-install/makefile.tftpl | 63 +- modules/cxone-install/variables.tf | 31 + modules/inspection-vpc/firewall-rules.tf | 7 +- modules/inspection-vpc/firewall.tf | 7 +- modules/inspection-vpc/main.tf | 13 + modules/inspection-vpc/outputs.tf | 14 + results.json | 888 ------------------ variables.tf | 18 + 21 files changed, 827 insertions(+), 1058 deletions(-) rename examples/full/{network.tf => network.tfignore} (97%) create mode 100644 examples/full/variables-vpc.tf create mode 100644 modules/cxone-install/apply-storageclass-config.sh.tftpl create mode 100644 modules/cxone-install/karpenter.reference.yaml.tftpl delete mode 100755 results.json diff --git a/README.md b/README.md index 222f33f..41d5af5 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ This repo contains a module for deploying [Checkmarx One](https://checkmarx.com/ # Module documentation +## Requirements + +No requirements. ## Providers @@ -43,6 +46,7 @@ This repo contains a module for deploying [Checkmarx One](https://checkmarx.com/ | [aws_iam_policy.s3_bucket_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [random_string.random_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_role.karpenter](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_role) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | | [aws_vpc.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source | @@ -101,8 +105,11 @@ This repo contains a module for deploying [Checkmarx One](https://checkmarx.com/ | [eks\_create\_external\_dns\_irsa](#input\_eks\_create\_external\_dns\_irsa) | Enables creation of external dns IAM role. | `bool` | `true` | no | | [eks\_create\_karpenter](#input\_eks\_create\_karpenter) | Enables creation of Karpenter resources. | `bool` | `false` | no | | [eks\_create\_load\_balancer\_controller\_irsa](#input\_eks\_create\_load\_balancer\_controller\_irsa) | Enables creation of load balancer controller IAM role. | `bool` | `true` | no | +| [eks\_enable\_externalsnat](#input\_eks\_enable\_externalsnat) | Enables [External SNAT](https://docs.aws.amazon.com/eks/latest/userguide/external-snat.html) for the EKS VPC CNI. When true, the EKS pods must have a route to a NAT Gateway for outbound communication. | `bool` | `false` | no | +| [eks\_enable\_fargate](#input\_eks\_enable\_fargate) | Enables Fargate profiles for the karpenter and kube-system namespaces. | `bool` | `false` | no | | [eks\_node\_additional\_security\_group\_ids](#input\_eks\_node\_additional\_security\_group\_ids) | Additional security group ids to attach to EKS nodes. | `list(string)` | `[]` | no | | [eks\_node\_groups](#input\_eks\_node\_groups) | n/a |

list(object({
name = string
min_size = string
desired_size = string
max_size = string
volume_type = optional(string, "gp3")
disk_size = optional(number, 200)
disk_iops = optional(number, 3000)
disk_throughput = optional(number, 125)
device_name = optional(string, "/dev/xvda")
instance_types = list(string)
capacity_type = optional(string, "ON_DEMAND")
labels = optional(map(string), {})
taints = optional(map(object({ key = string, value = string, effect = string })), {})
}))
|
[
{
"desired_size": 3,
"instance_types": [
"c5.4xlarge"
],
"max_size": 9,
"min_size": 3,
"name": "ast-app"
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"sast-engine": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.4xlarge"
],
"labels": {
"sast-engine-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.2xlarge"
],
"labels": {
"sast-engine-extra-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-extra-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-extra-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.4xlarge"
],
"labels": {
"sast-engine-xxl": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-xxl",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-xxl",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"kics-engine": "true"
},
"max_size": 100,
"min_size": 1,
"name": "kics-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "kics-engine",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"repostore": "true"
},
"max_size": 100,
"min_size": 1,
"name": "repostore",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "repostore",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"service": "sca-source-resolver"
},
"max_size": 100,
"min_size": 1,
"name": "sca-source-resolver",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "service",
"value": "sca-source-resolver"
}
}
}
]
| no | +| [eks\_pod\_subnets](#input\_eks\_pod\_subnets) | The subnets to use for EKS pods. When specified, custom networking configuration is applied to the EKS cluster. | `list(string)` | n/a | yes | | [eks\_private\_endpoint\_enabled](#input\_eks\_private\_endpoint\_enabled) | Enables the EKS VPC private endpoint. | `bool` | `true` | no | | [eks\_public\_endpoint\_enabled](#input\_eks\_public\_endpoint\_enabled) | Enables the EKS public endpoint. | `bool` | `false` | no | | [eks\_subnets](#input\_eks\_subnets) | The subnets to deploy EKS into. | `list(string)` | n/a | yes | @@ -130,6 +137,7 @@ This repo contains a module for deploying [Checkmarx One](https://checkmarx.com/ |------|-------------| | [bucket\_suffix](#output\_bucket\_suffix) | n/a | | [cluster\_autoscaler\_iam\_role\_arn](#output\_cluster\_autoscaler\_iam\_role\_arn) | n/a | +| [cluster\_endpoint](#output\_cluster\_endpoint) | n/a | | [db\_database\_name](#output\_db\_database\_name) | n/a | | [db\_endpoint](#output\_db\_endpoint) | n/a | | [db\_master\_password](#output\_db\_master\_password) | n/a | @@ -145,13 +153,13 @@ This repo contains a module for deploying [Checkmarx One](https://checkmarx.com/ | [external\_dns\_iam\_role\_arn](#output\_external\_dns\_iam\_role\_arn) | n/a | | [karpenter\_iam\_role\_arn](#output\_karpenter\_iam\_role\_arn) | n/a | | [load\_balancer\_controller\_iam\_role\_arn](#output\_load\_balancer\_controller\_iam\_role\_arn) | n/a | +| [nodegroup\_iam\_role\_name](#output\_nodegroup\_iam\_role\_name) | n/a | | [s3\_bucket\_name\_suffix](#output\_s3\_bucket\_name\_suffix) | n/a | - # Regional Considerations ## GovCloud * RDS Proxy is not available in AWS Gov Cloud regions, so `create_rds_proxy` must be set `false`. Monitor database for connection usage and scale accordingly. * RDS's `ManageMasterUserPassword` capability is not supported. Specify a password via `db_master_user_password` -* Elasticache's `cache.r7g` instance class is not available. Consider using `cache.r6g`. +* Elasticache's `cache.r7g` and `cache.tg4` instance class is not available. Consider using `cache.r6g` and `cache.t3` diff --git a/eks.tf b/eks.tf index 27a0eff..7124de3 100644 --- a/eks.tf +++ b/eks.tf @@ -1,4 +1,63 @@ locals { + fargate_profiles = { + karpenter = { + selectors = [ + { namespace = "karpenter" } + ] + } + kube-system = { + selectors = [ + { namespace = "kube-system" } + ] + } + } + core_dns_fargate_configuration_values = jsonencode({ + computeType = "Fargate" + # Ensure that we fully utilize the minimum amount of resources that are supplied by + # Fargate https://docs.aws.amazon.com/eks/latest/userguide/fargate-pod-configuration.html + # Fargate adds 256 MB to each pod's memory reservation for the required Kubernetes + # components (kubelet, kube-proxy, and containerd). Fargate rounds up to the following + # compute configuration that most closely matches the sum of vCPU and memory requests in + # order to ensure pods always have the resources that they need to run. + resources = { + limits = { + cpu = "0.25" + # We are targeting the smallest Task size of 512Mb, so we subtract 256Mb from the + # request/limit to ensure we can fit within that task + memory = "256M" + } + requests = { + cpu = "0.25" + # We are targeting the smallest Task size of 512Mb, so we subtract 256Mb from the + # request/limit to ensure we can fit within that task + memory = "256M" + } + } + }) + + # The EBS CSI Add On Controller pods can run on Fargate, but we must add a toleration to eks.amazonaws.com/compute-type=fargate + # in addition to the existing tolerations. Documentation for EBS CSI Driver configuration schema can be obtained from AWS CLI + # example: aws eks describe-addon-configuration --addon-name aws-ebs-csi-driver --addon-version v1.28.0-eksbuild.1 --query configurationSchema --output text + ebs_csi_fargate_configuration_values = jsonencode({ + controller = { + batching = false + tolerations = [ + { + key = "CriticalAddonsOnly" + operator = "Exists" + }, + { + effect = "NoExecute" + operator = "Exists" + tolerationSeconds = 300 + }, + { + key = "eks.amazonaws.com/compute-type" + operator = "Equal" + value = "fargate" + effect = "NoSchedule" + }] } }) + eks_nodegroups = { for node_group in var.eks_node_groups : node_group.name => { name = "${var.deployment_id}-${node_group.name}" launch_template_name = "${var.deployment_id}-${node_group.name}" @@ -104,6 +163,7 @@ module "eks" { create_node_security_group = true #node_security_group_id = module.eks_nodes_security_group.security_group_id + node_security_group_additional_rules = { ingress_self_http80 = { description = "Node to node ingress http/80" @@ -144,27 +204,35 @@ module "eks" { enable_cluster_creator_admin_permissions = var.enable_cluster_creator_admin_permissions access_entries = local.admin_access_entries + tags = { + # NOTE - if creating multiple security groups with this module, only tag the + # security group that Karpenter should utilize with the following tag + # (i.e. - at most, only one security group should have this tag in your account) + "karpenter.sh/discovery" = var.deployment_id + } cluster_addons = { coredns = { - addon_version = var.coredns_version + addon_version = var.coredns_version + configuration_values = var.eks_enable_fargate ? local.core_dns_fargate_configuration_values : null } kube-proxy = { addon_version = var.kube_proxy_version } vpc-cni = { - addon_version = var.vpc_cni_version - # Todo: add configuration for secondary cidr here to vpc plugin - # before_compute = var.pod_custom_networking_subnets != null ? true : false - # configuration_values = jsonencode({ - # env = { - # AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = var.pod_custom_networking_subnets != null ? "true" : "false" - # ENI_CONFIG_LABEL_DEF = var.pod_custom_networking_subnets != null ? "topology.kubernetes.io/zone" : "" - # } }) - + addon_version = var.vpc_cni_version + before_compute = var.eks_pod_subnets != null ? true : false + configuration_values = jsonencode({ + env = { + #AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = var.eks_pod_subnets != null ? "true" : "false" + #ENI_CONFIG_LABEL_DEF = var.eks_pod_subnets != null ? "topology.kubernetes.io/zone" : "" + AWS_VPC_K8S_CNI_EXTERNALSNAT = var.eks_enable_externalsnat ? "true" : "false" + } + }) } aws-ebs-csi-driver = { - addon_version = var.aws_ebs_csi_driver_version + addon_version = var.aws_ebs_csi_driver_version + configuration_values = var.eks_enable_fargate ? local.ebs_csi_fargate_configuration_values : null } } create_kms_key = false @@ -193,6 +261,11 @@ module "eks" { } eks_managed_node_groups = local.eks_nodegroups + + fargate_profile_defaults = { + subnet_ids = var.eks_pod_subnets != null ? var.eks_pod_subnets : null + } + fargate_profiles = var.eks_enable_fargate ? local.fargate_profiles : {} } resource "aws_autoscaling_group_tag" "cluster_autoscaler_label" { @@ -272,6 +345,10 @@ module "load_balancer_controller_irsa" { } } +data "aws_iam_role" "karpenter" { + name = "${var.deployment_id}-eks-nodes" + depends_on = [module.eks] +} module "karpenter" { source = "terraform-aws-modules/eks/aws//modules/karpenter" @@ -286,11 +363,11 @@ module "karpenter" { iam_role_name = "KarpenterController-${var.deployment_id}" iam_role_description = "IAM role for karpenter controller created by karpenter module" create_node_iam_role = false - #node_iam_role_arn = module.eks.eks_managed_node_groups.nodegroup_iam_role_arn - create_access_entry = false - iam_policy_name = "KarpenterPolicy-${var.deployment_id}" - iam_policy_description = "Karpenter controller IAM policy created by karpenter module" - iam_role_use_name_prefix = false + node_iam_role_arn = data.aws_iam_role.karpenter.arn + create_access_entry = false + iam_policy_name = "KarpenterPolicy-${var.deployment_id}" + iam_policy_description = "Karpenter controller IAM policy created by karpenter module" + iam_role_use_name_prefix = false node_iam_role_additional_policies = { AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", AmazonEBSCSIDriverPolicy = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" @@ -317,6 +394,14 @@ output "karpenter_iam_role_arn" { value = var.eks_create && var.eks_create_karpenter ? module.karpenter.iam_role_arn : "" } +output "cluster_endpoint" { + value = module.eks.cluster_endpoint +} + +output "nodegroup_iam_role_name" { + value = data.aws_iam_role.karpenter.name +} + output "eks" { - value = module.eks.* + value = module.eks } diff --git a/examples/full/README.md b/examples/full/README.md index 25ad975..8929673 100644 --- a/examples/full/README.md +++ b/examples/full/README.md @@ -2,13 +2,47 @@ This folder contains a full example for deploying [Checkmarx One](https://checkmarx.com/product/application-security-platform/) on [AWS](https://aws.amazon.com) using [Terraform](https://www.terraform.io). -The project configures the VPC, KMS, SES, ACM, and other basic environment resources, and then invokes the `terraform-aws-cxone` module to deploy Checkmarx One infrastructure. +The project configures the VPC, KMS, ACM, and other basic environment resources, and then invokes the `terraform-aws-cxone` module to deploy Checkmarx One infrastructure. The [`cxone-install`](../../modules/cxone-install) module is used to generate installation scripts for the application after Terraform deploys the infrastructure. Consult the [`example.auto.tfvars`](./example.auto.tfvars) for a full listing of what can be configured in this example, and the `terraform-aws-cxone module`. +# Installation +This example generates a Makefile in the project folder after Terraform finishes running. The Makefile has several targets that can help bootstrap your environment with the CxOne application. -# Module Documentation +The `kots.$DEPLOYMENT_ID.yaml` file is also automatically generated and can be reviewed & modified after Terraform finishes. + +Run these commands to bootstrap your cluster. + +Update your kubectl context: + +```sh +make update-kubeconfig +``` + +Update the EKS storage configuration to default to gp3: + +```sh +make apply-storageclass-config +``` +Install the cluster autoscaler: +```sh +make install-cluster-autoscaler +``` + +Install the load balancer controller (wait approx 1 minute after cluster autoscaler to avoid webhook issues): +```sh +make install-load-balancer-controller +``` + +Install the Checkmarx One application: +```sh +make kots-install +``` + +You can also build your own bootstrapping process using the Makefile as a reference. + +# Module Documentation ## Requirements No requirements. @@ -27,18 +61,13 @@ No requirements. | [acm](#module\_acm) | terraform-aws-modules/acm/aws | 5.0.1 | | [checkmarx-one](#module\_checkmarx-one) | ../../ | n/a | | [checkmarx-one-install](#module\_checkmarx-one-install) | ../../modules/cxone-install | n/a | -| [ses](#module\_ses) | cloudposse/ses/aws | 0.24.0 | -| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | 5.7.0 | -| [vpc\_endpoint\_security\_group](#module\_vpc\_endpoint\_security\_group) | terraform-aws-modules/security-group/aws | 5.1.2 | +| [vpc](#module\_vpc) | ../../modules/inspection-vpc | n/a | ## Resources | Name | Type | |------|------| -| [aws_iam_group_policy.cxone_ses_group_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy) | resource | | [aws_kms_key.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | -| [aws_vpc_endpoint.interface](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | -| [aws_vpc_endpoint.s3_gateway_private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint) | resource | | [random_password.cxone_admin](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [random_password.db](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [random_password.elasticsearch](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | @@ -52,9 +81,13 @@ No requirements. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [acm\_certificate\_arn](#input\_acm\_certificate\_arn) | The ARN to the SSL certificate in AWS ACM to use for securing the load balancer | `string` | `null` | no | +| [additional\_suricata\_rules](#input\_additional\_suricata\_rules) | Additional [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) rules to use in the network firewall. When provided these rules will be appended to the default rules prior to the default drop rule. | `string` | `""` | no | | [aws\_ebs\_csi\_driver\_version](#input\_aws\_ebs\_csi\_driver\_version) | The version of the EKS EBS CSI Addon. | `string` | n/a | yes | | [coredns\_version](#input\_coredns\_version) | The version of the EKS Core DNS Addon. | `string` | n/a | yes | -| [create\_s3\_endpoint](#input\_create\_s3\_endpoint) | Enables creation of the s3 gateway VPC interface endpoint. | `bool` | `true` | no | +| [create\_interface\_endpoints](#input\_create\_interface\_endpoints) | Enables creation of the [interface endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) specified in `interface_vpc_endpoints` | `bool` | `true` | no | +| [create\_managed\_rule\_groups](#input\_create\_managed\_rule\_groups) | Enables creation of the AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) provided in `managed_rule_groups` | `bool` | `true` | no | +| [create\_s3\_endpoint](#input\_create\_s3\_endpoint) | Enables creation of the [s3 gateway VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html) | `bool` | `true` | no | | [db\_allow\_major\_version\_upgrade](#input\_db\_allow\_major\_version\_upgrade) | Allows major version upgrades. | `bool` | `false` | no | | [db\_apply\_immediately](#input\_db\_apply\_immediately) | Determines if changes will be applied immediately or wait until the next maintenance window. | `bool` | `false` | no | | [db\_auto\_minor\_version\_upgrade](#input\_db\_auto\_minor\_version\_upgrade) | Automatically upgrade to latest minor version in maintenance window. | `bool` | `false` | no | @@ -101,12 +134,15 @@ No requirements. | [eks\_create\_external\_dns\_irsa](#input\_eks\_create\_external\_dns\_irsa) | Enables creation of external dns IAM role. | `bool` | `true` | no | | [eks\_create\_karpenter](#input\_eks\_create\_karpenter) | Enables creation of Karpenter resources. | `bool` | `false` | no | | [eks\_create\_load\_balancer\_controller\_irsa](#input\_eks\_create\_load\_balancer\_controller\_irsa) | Enables creation of load balancer controller IAM role. | `bool` | `true` | no | +| [eks\_enable\_externalsnat](#input\_eks\_enable\_externalsnat) | Enables [External SNAT](https://docs.aws.amazon.com/eks/latest/userguide/external-snat.html) for the EKS VPC CNI. When true, the EKS pods must have a route to a NAT Gateway for outbound communication. | `bool` | `false` | no | +| [eks\_enable\_fargate](#input\_eks\_enable\_fargate) | Enables Fargate profiles for the karpenter and kube-system namespaces. | `bool` | `false` | no | | [eks\_node\_additional\_security\_group\_ids](#input\_eks\_node\_additional\_security\_group\_ids) | Additional security group ids to attach to EKS nodes. | `list(string)` | `[]` | no | | [eks\_node\_groups](#input\_eks\_node\_groups) | n/a |
list(object({
name = string
min_size = string
desired_size = string
max_size = string
volume_type = optional(string, "gp3")
disk_size = optional(number, 200)
disk_iops = optional(number, 3000)
disk_throughput = optional(number, 125)
device_name = optional(string, "/dev/xvda")
instance_types = list(string)
capacity_type = optional(string, "ON_DEMAND")
labels = optional(map(string), {})
taints = optional(map(object({ key = string, value = string, effect = string })), {})
}))
|
[
{
"desired_size": 3,
"instance_types": [
"c5.4xlarge"
],
"max_size": 9,
"min_size": 3,
"name": "ast-app"
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"sast-engine": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.4xlarge"
],
"labels": {
"sast-engine-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.2xlarge"
],
"labels": {
"sast-engine-extra-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-extra-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-extra-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.4xlarge"
],
"labels": {
"sast-engine-xxl": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-xxl",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-xxl",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"kics-engine": "true"
},
"max_size": 100,
"min_size": 1,
"name": "kics-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "kics-engine",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"repostore": "true"
},
"max_size": 100,
"min_size": 1,
"name": "repostore",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "repostore",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"service": "sca-source-resolver"
},
"max_size": 100,
"min_size": 0,
"name": "sca-source-resolver",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "service",
"value": "sca-source-resolver"
}
}
}
]
| no | | [eks\_private\_endpoint\_enabled](#input\_eks\_private\_endpoint\_enabled) | Enables the EKS VPC private endpoint. | `bool` | `true` | no | | [eks\_public\_endpoint\_enabled](#input\_eks\_public\_endpoint\_enabled) | Enables the EKS public endpoint. | `bool` | `false` | no | | [eks\_version](#input\_eks\_version) | The version of the EKS Cluster (e.g. 1.27) | `string` | n/a | yes | | [enable\_cluster\_creator\_admin\_permissions](#input\_enable\_cluster\_creator\_admin\_permissions) | Enables the identity used to create the EKS cluster to have administrator access to that EKS cluster. When enabled, do not specify the same principal arn for eks\_administrator\_principals. | `bool` | `true` | no | +| [enable\_firewall](#input\_enable\_firewall) | Enables the use of the [AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/what-is-aws-network-firewall.html) to protect the private and pod subnets | `bool` | `true` | no | | [es\_create](#input\_es\_create) | Enables creation of elasticsearch resources. | `bool` | `true` | no | | [es\_instance\_count](#input\_es\_instance\_count) | The number of nodes in elasticsearch cluster | `number` | `2` | no | | [es\_instance\_type](#input\_es\_instance\_type) | The instance type for elasticsearch nodes. | `string` | `"r6g.large.elasticsearch"` | no | @@ -114,23 +150,34 @@ No requirements. | [es\_username](#input\_es\_username) | The username for the elasticsearch user | `string` | `"ast"` | no | | [es\_volume\_size](#input\_es\_volume\_size) | The size of volumes for nodes in elasticsearch cluster | `number` | `100` | no | | [fqdn](#input\_fqdn) | The fully qualified domain name that will be used for the Checkmarx One deployment | `string` | n/a | yes | -| [interface\_vpc\_endpoints](#input\_interface\_vpc\_endpoints) | A list of services that vpc endpoints are created for. | `list(string)` |
[
"ec2",
"ec2messages",
"ssm",
"ssmmessages",
"ecr.api",
"ecr.dkr",
"kms",
"logs",
"sts",
"elasticloadbalancing",
"autoscaling"
]
| no | +| [include\_sca\_rules](#input\_include\_sca\_rules) | Enables inclusion of AWS Network Firewall rules used in SCA scanning. These rules may be overly permissive when not using SCA, so they are optional. These rules allow connectivity to various public package manager repositories like [Maven Central](https://mvnrepository.com/repos/central) and [npm](https://docs.npmjs.com/). | `bool` | `true` | no | +| [interface\_vpc\_endpoints](#input\_interface\_vpc\_endpoints) | A list of AWS services to create [VPC Private Endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) for. These endpoints are used for communication direct to AWS services without requiring connectivity and are useful for private EKS clusters. | `list(string)` |
[
"ec2",
"ec2messages",
"ssm",
"ssmmessages",
"ecr.api",
"ecr.dkr",
"kms",
"logs",
"sts",
"elasticloadbalancing",
"autoscaling"
]
| no | | [kots\_admin\_email](#input\_kots\_admin\_email) | The email address of the Checkmarx One first admin user. | `string` | n/a | yes | | [kots\_cxone\_version](#input\_kots\_cxone\_version) | The version of Checkmarx One to install | `string` | n/a | yes | | [kots\_license\_file](#input\_kots\_license\_file) | The path to the kots license file to install Checkamrx One with. | `string` | n/a | yes | | [kots\_release\_channel](#input\_kots\_release\_channel) | The release channel from which to install Checkmarx One | `string` | `"beta"` | no | | [kube\_proxy\_version](#input\_kube\_proxy\_version) | The version of the EKS Kube Proxy Addon. | `string` | n/a | yes | | [launch\_template\_tags](#input\_launch\_template\_tags) | Tags to associate with launch templates for node groups | `map(string)` | `null` | no | +| [managed\_rule\_groups](#input\_managed\_rule\_groups) | The AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) to include in the firewall policy. Must be strict order groups. | `list(string)` |
[
"AbusedLegitMalwareDomainsStrictOrder",
"MalwareDomainsStrictOrder",
"AbusedLegitBotNetCommandAndControlDomainsStrictOrder",
"BotNetCommandAndControlDomainsStrictOrder",
"ThreatSignaturesBotnetStrictOrder",
"ThreatSignaturesBotnetWebStrictOrder",
"ThreatSignaturesBotnetWindowsStrictOrder",
"ThreatSignaturesIOCStrictOrder",
"ThreatSignaturesDoSStrictOrder",
"ThreatSignaturesEmergingEventsStrictOrder",
"ThreatSignaturesExploitsStrictOrder",
"ThreatSignaturesMalwareStrictOrder",
"ThreatSignaturesMalwareCoinminingStrictOrder",
"ThreatSignaturesMalwareMobileStrictOrder",
"ThreatSignaturesMalwareWebStrictOrder",
"ThreatSignaturesScannersStrictOrder",
"ThreatSignaturesSuspectStrictOrder",
"ThreatSignaturesWebAttacksStrictOrder"
]
| no | +| [ms\_replica\_count](#input\_ms\_replica\_count) | The microservices replica count (e.g. a minimum) | `number` | `3` | no | | [object\_storage\_access\_key](#input\_object\_storage\_access\_key) | The S3 access key to use to access buckets | `string` | n/a | yes | | [object\_storage\_endpoint](#input\_object\_storage\_endpoint) | The S3 endpoint to use to access buckets | `string` | n/a | yes | | [object\_storage\_secret\_key](#input\_object\_storage\_secret\_key) | The S3 secret key to use to access buckets | `string` | n/a | yes | +| [primary\_cidr\_block](#input\_primary\_cidr\_block) | The primary VPC CIDR block for the VPC. Must be at least a /19. | `string` | n/a | yes | | [route\_53\_hosted\_zone\_id](#input\_route\_53\_hosted\_zone\_id) | The hosted zone id for route 53 in which to create dns and certificates. | `string` | n/a | yes | | [s3\_retention\_period](#input\_s3\_retention\_period) | The retention period, in days, to retain s3 objects. | `string` | `"90"` | no | -| [secondary\_vpc\_cidr](#input\_secondary\_vpc\_cidr) | The secondary VPC CIDR block to associate with the VPC. | `string` | `null` | no | +| [secondary\_cidr\_block](#input\_secondary\_cidr\_block) | The secondary VPC CIDR block for the EKS Pod [Custom Networking](https://aws.github.io/aws-eks-best-practices/networking/custom-networking/) configuration. Must be at least a /18. | `string` | `"100.64.0.0/18"` | no | +| [smtp\_from\_sender](#input\_smtp\_from\_sender) | The address to use in the from field when sending emails. | `string` | n/a | yes | +| [smtp\_host](#input\_smtp\_host) | The hostname of the SMTP server. | `string` | n/a | yes | +| [smtp\_password](#input\_smtp\_password) | The smtp password. | `string` | n/a | yes | | [smtp\_port](#input\_smtp\_port) | The port of the SMTP server. | `number` | `587` | no | -| [vpc\_cidr](#input\_vpc\_cidr) | The primary VPC CIDR block to create the VPC with. | `string` | n/a | yes | +| [smtp\_user](#input\_smtp\_user) | The smtp user name. | `string` | n/a | yes | +| [stateful\_default\_action](#input\_stateful\_default\_action) | The [default action](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order) for the AWS Network Firewall stateful rule group. Choose `aws:drop_established` or `aws:alert_established` | `string` | `"aws:drop_established"` | no | +| [suricata\_rules](#input\_suricata\_rules) | The [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) to use for the AWS Network Firewall. When provided, this variable completely overrides the embedded rules. Use this to bring your own rules. If you only need to provide some additional rules in addition to the bundled rules, then use `additional_suricata_rules` instead of `suricata_rules`. | `string` | `null` | no | | [vpc\_cni\_version](#input\_vpc\_cni\_version) | The version of the EKS VPC CNI Addon. | `string` | n/a | yes | ## Outputs -No outputs. \ No newline at end of file +| Name | Description | +|------|-------------| +| [cxone1](#output\_cxone1) | n/a | \ No newline at end of file diff --git a/examples/full/examples.auto.tfvars b/examples/full/examples.auto.tfvars index d2bdefe..5cd6102 100644 --- a/examples/full/examples.auto.tfvars +++ b/examples/full/examples.auto.tfvars @@ -2,18 +2,85 @@ #****************************************************************************** # Base Infrastructure Configuration #****************************************************************************** -vpc_cidr = "10.77.0.0/16" -secondary_vpc_cidr = "100.64.0.0/16" -interface_vpc_endpoints = ["ec2", "ec2messages", "ssm", "ssmmessages", "ecr.api", "ecr.dkr", "kms", "logs", "sts", "elasticloadbalancing", "autoscaling"] -create_s3_endpoint = true +# Provide your hosted zone ID that corresponds to the domain used in the fqdn variable. +# The hosted zone is used for validating SSL certificates in ACM and for external DNS. +# If you are not using route 53 for DNS, then route_53_hosted_zone_id is not used, but you must +# also set eks_create_external_dns_irsa = false and provide your SSL certificate ARN in acm_certificate_arn. route_53_hosted_zone_id = " $EXTERNAL_NET 443 (tls.sni; content:"www.example.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240331001; rev:1;) +EOF + +#****************************************************************************** +# SMTP Configuration +#****************************************************************************** +# Enter your SMTP server information here +smtp_host = "smtp.example.com" +smtp_port = 587 +smtp_user = "" +smtp_password = "???" +smtp_from_sender = "noreply@example.com" #****************************************************************************** # S3 Configuration #****************************************************************************** +# Checkmarx One requires an IAM user with S3 access for connectivity to S3 buckets. +# Create the IAM user with S3 access policies, and enter the credentials here. object_storage_endpoint = "" object_storage_access_key = "" object_storage_secret_key = "" @@ -21,29 +88,35 @@ object_storage_secret_key = " $EXTERNAL_NET 443 (tls.sni; content:"${var.fqdn}"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240401001; rev:1;) + +${var.additional_suricata_rules} + +EOF +} + +module "vpc" { + source = "../../modules/inspection-vpc" + deployment_id = var.deployment_id + primary_cidr_block = var.primary_cidr_block + secondary_cidr_block = var.secondary_cidr_block + interface_vpc_endpoints = var.interface_vpc_endpoints + create_interface_endpoints = var.create_interface_endpoints + create_s3_endpoint = var.create_s3_endpoint + enable_firewall = var.enable_firewall + stateful_default_action = var.stateful_default_action + suricata_rules = var.suricata_rules + include_sca_rules = var.include_sca_rules + additional_suricata_rules = local.additional_suricata_rules + create_managed_rule_groups = var.create_managed_rule_groups + managed_rule_groups = var.managed_rule_groups +} + resource "aws_kms_key" "main" { description = "KMS Key for the Checkmarx One deployment named ${var.deployment_id}" @@ -19,7 +47,7 @@ resource "aws_kms_key" "main" { resource "random_password" "elasticsearch" { length = 32 special = false - override_special = "!*-_[]{}<>" + override_special = "!-_" min_special = 1 min_upper = 1 min_lower = 1 @@ -29,7 +57,7 @@ resource "random_password" "elasticsearch" { resource "random_password" "db" { length = 32 special = false - override_special = "!*-_[]{}<>" + override_special = "!-_" min_special = 1 min_upper = 1 min_lower = 1 @@ -39,7 +67,7 @@ resource "random_password" "db" { resource "random_password" "kots_admin" { length = 14 special = false - override_special = "!*-_[]{}<>" + override_special = "!-_" min_special = 1 min_upper = 1 min_lower = 1 @@ -49,19 +77,17 @@ resource "random_password" "kots_admin" { resource "random_password" "cxone_admin" { length = 14 special = false - override_special = "!*-_[]{}<>" + override_special = "!-_" min_special = 1 min_upper = 1 min_lower = 1 min_numeric = 1 } - - - module "acm" { source = "terraform-aws-modules/acm/aws" version = "5.0.1" + count = var.acm_certificate_arn != null ? 0 : 1 domain_name = var.fqdn zone_id = var.route_53_hosted_zone_id @@ -73,46 +99,6 @@ module "acm" { wait_for_validation = true } -module "ses" { - source = "cloudposse/ses/aws" - version = "0.24.0" - zone_id = var.route_53_hosted_zone_id - domain = var.fqdn - verify_domain = true - verify_dkim = true - ses_group_enabled = true - ses_group_name = "${var.deployment_id}-ses-group" - ses_user_enabled = true - name = "CxOne-${var.deployment_id}" - environment = "dev" - enabled = true - - tags = { - Name = var.deployment_id - } -} - -resource "aws_iam_group_policy" "cxone_ses_group_policy" { - name = "${var.deployment_id}-ses-group-policy" - group = module.ses.ses_group_name - - depends_on = [module.ses] - - policy = jsonencode({ - Version : "2012-10-17" - Statement : [ - { - Effect : "Allow", - Action : [ - "ses:SendEmail", - "ses:SendRawEmail" - ], - Resource : "*" - } - ] - }) -} - module "checkmarx-one" { source = "../../" @@ -127,6 +113,9 @@ module "checkmarx-one" { # EKS Configuration eks_create = var.eks_create eks_subnets = module.vpc.private_subnets + eks_pod_subnets = module.vpc.pod_subnets + eks_enable_externalsnat = var.eks_enable_externalsnat + eks_enable_fargate = var.eks_enable_fargate eks_create_cluster_autoscaler_irsa = var.eks_create_cluster_autoscaler_irsa eks_create_external_dns_irsa = var.eks_create_external_dns_irsa eks_create_load_balancer_controller_irsa = var.eks_create_load_balancer_controller_irsa @@ -141,9 +130,10 @@ module "checkmarx-one" { eks_cluster_endpoint_public_access_cidrs = var.eks_cluster_endpoint_public_access_cidrs enable_cluster_creator_admin_permissions = var.enable_cluster_creator_admin_permissions launch_template_tags = var.launch_template_tags + eks_node_groups = var.eks_node_groups # RDS Configuration - db_subnets = module.vpc.private_subnets + db_subnets = module.vpc.database_subnets db_engine_version = var.db_engine_version db_allow_major_version_upgrade = var.db_allow_major_version_upgrade db_auto_minor_version_upgrade = var.db_auto_minor_version_upgrade @@ -175,7 +165,7 @@ module "checkmarx-one" { # Elasticache Configuration ec_create = var.ec_create - ec_subnets = module.vpc.private_subnets + ec_subnets = module.vpc.database_subnets ec_enable_serverless = var.ec_enable_serverless ec_serverless_max_storage = var.ec_serverless_max_storage ec_serverless_max_ecpu_per_second = var.ec_serverless_max_ecpu_per_second @@ -190,7 +180,7 @@ module "checkmarx-one" { # Elasticsearch Configuration es_create = var.es_create - es_subnets = module.vpc.private_subnets + es_subnets = module.vpc.database_subnets es_instance_count = var.es_instance_count es_instance_type = var.es_instance_type es_volume_size = var.es_volume_size @@ -212,8 +202,9 @@ module "checkmarx-one-install" { admin_email = var.kots_admin_email admin_password = random_password.cxone_admin.result fqdn = var.fqdn - acm_certificate_arn = module.acm.acm_certificate_arn + acm_certificate_arn = var.acm_certificate_arn != null ? var.acm_certificate_arn : module.acm[0].acm_certificate_arn bucket_suffix = module.checkmarx-one.s3_bucket_name_suffix + ms_replica_count = var.ms_replica_count object_storage_endpoint = "s3.${data.aws_region.current.name}.amazonaws.com" object_storage_access_key = var.object_storage_access_key object_storage_secret_key = var.object_storage_secret_key @@ -222,14 +213,25 @@ module "checkmarx-one-install" { postgres_user = module.checkmarx-one.db_master_username postgres_password = module.checkmarx-one.db_master_password redis_address = module.checkmarx-one.ec_endpoint - smtp_host = "email-smtp.${data.aws_region.current.name}.amazonaws.com" + smtp_host = var.smtp_host smtp_port = var.smtp_port - smtp_password = module.ses.ses_smtp_password - smtp_user = module.ses.user_name - smtp_from_sender = "noreply@${var.fqdn}" + smtp_password = var.smtp_password + smtp_user = var.smtp_user + smtp_from_sender = var.smtp_from_sender elasticsearch_host = module.checkmarx-one.es_endpoint elasticsearch_password = random_password.elasticsearch.result cluster_autoscaler_iam_role_arn = module.checkmarx-one.cluster_autoscaler_iam_role_arn load_balancer_controller_iam_role_arn = module.checkmarx-one.load_balancer_controller_iam_role_arn external_dns_iam_role_arn = module.checkmarx-one.external_dns_iam_role_arn + karpenter_iam_role_arn = module.checkmarx-one.karpenter_iam_role_arn + cluster_endpoint = module.checkmarx-one.cluster_endpoint + nodegroup_iam_role_name = module.checkmarx-one.nodegroup_iam_role_name + availability_zones = module.vpc.azs + pod_eniconfig = module.vpc.ENIConfig + vpc_id = module.vpc.vpc_id } + + +output "cxone1" { + value = module.checkmarx-one.eks +} \ No newline at end of file diff --git a/examples/full/network.tf b/examples/full/network.tfignore similarity index 97% rename from examples/full/network.tf rename to examples/full/network.tfignore index 87673a0..b7fd5f0 100644 --- a/examples/full/network.tf +++ b/examples/full/network.tfignore @@ -30,7 +30,7 @@ module "vpc" { enable_dns_support = true public_subnet_tags = { - "karpenter.sh/discovery" = "${var.deployment_id}" + # "karpenter.sh/discovery" = "${var.deployment_id}" "kubernetes.io/cluster/${var.deployment_id}" = "shared" "kubernetes.io/role/elb" = "1" } diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf index 7173bae..ac304dd 100644 --- a/examples/full/variables-cxone.tf +++ b/examples/full/variables-cxone.tf @@ -58,6 +58,18 @@ variable "eks_create" { # type = list(string) # } +variable "eks_enable_externalsnat" { + type = bool + description = "Enables [External SNAT](https://docs.aws.amazon.com/eks/latest/userguide/external-snat.html) for the EKS VPC CNI. When true, the EKS pods must have a route to a NAT Gateway for outbound communication." + default = false +} + +variable "eks_enable_fargate" { + type = bool + description = "Enables Fargate profiles for the karpenter and kube-system namespaces." + default = false +} + variable "eks_create_cluster_autoscaler_irsa" { type = bool description = "Enables creation of cluster autoscaler IAM role." diff --git a/examples/full/variables-example.tf b/examples/full/variables-example.tf index 40696fe..6bcbd8e 100644 --- a/examples/full/variables-example.tf +++ b/examples/full/variables-example.tf @@ -2,29 +2,6 @@ # Base Infrastructure Configuration - These variables are used by the example itself #****************************************************************************** -variable "vpc_cidr" { - type = string - description = "The primary VPC CIDR block to create the VPC with." -} - -variable "secondary_vpc_cidr" { - type = string - description = "The secondary VPC CIDR block to associate with the VPC." - default = null -} - -variable "interface_vpc_endpoints" { - type = list(string) - description = "A list of services that vpc endpoints are created for." - default = ["ec2", "ec2messages", "ssm", "ssmmessages", "ecr.api", "ecr.dkr", "kms", "logs", "sts", "elasticloadbalancing", "autoscaling"] -} - -variable "create_s3_endpoint" { - type = bool - description = "Enables creation of the s3 gateway VPC interface endpoint." - default = true -} - variable "route_53_hosted_zone_id" { type = string description = "The hosted zone id for route 53 in which to create dns and certificates." @@ -36,6 +13,17 @@ variable "fqdn" { description = "The fully qualified domain name that will be used for the Checkmarx One deployment" } +variable "acm_certificate_arn" { + type = string + description = "The ARN to the SSL certificate in AWS ACM to use for securing the load balancer" + default = null +} + +variable "ms_replica_count" { + type = number + description = "The microservices replica count (e.g. a minimum)" + default = 3 +} #****************************************************************************** # S3 Configuration @@ -59,10 +47,10 @@ variable "object_storage_secret_key" { #****************************************************************************** # SMTP Configuration #****************************************************************************** -# variable "smtp_host" { -# description = "The hostname of the SMTP server." -# type = string -# } +variable "smtp_host" { + description = "The hostname of the SMTP server." + type = string +} variable "smtp_port" { description = "The port of the SMTP server." @@ -70,20 +58,20 @@ variable "smtp_port" { default = 587 } -# variable "smtp_user" { -# description = "The smtp user name." -# type = string -# } +variable "smtp_user" { + description = "The smtp user name." + type = string +} -# variable "smtp_password" { -# description = "The smtp password." -# type = string -# } +variable "smtp_password" { + description = "The smtp password." + type = string +} -# variable "smtp_from_sender" { -# description = "The address to use in the from field when sending emails." -# type = string -# } +variable "smtp_from_sender" { + description = "The address to use in the from field when sending emails." + type = string +} #****************************************************************************** # Kots & Installation Configuration diff --git a/examples/full/variables-vpc.tf b/examples/full/variables-vpc.tf new file mode 100644 index 0000000..5fea44c --- /dev/null +++ b/examples/full/variables-vpc.tf @@ -0,0 +1,92 @@ + +variable "primary_cidr_block" { + description = "The primary VPC CIDR block for the VPC. Must be at least a /19." + type = string + nullable = false +} + +variable "secondary_cidr_block" { + description = "The secondary VPC CIDR block for the EKS Pod [Custom Networking](https://aws.github.io/aws-eks-best-practices/networking/custom-networking/) configuration. Must be at least a /18." + type = string + default = "100.64.0.0/18" + nullable = false +} + +variable "interface_vpc_endpoints" { + type = list(string) + description = "A list of AWS services to create [VPC Private Endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) for. These endpoints are used for communication direct to AWS services without requiring connectivity and are useful for private EKS clusters." + default = ["ec2", "ec2messages", "ssm", "ssmmessages", "ecr.api", "ecr.dkr", "kms", "logs", "sts", "elasticloadbalancing", "autoscaling"] +} + +variable "create_interface_endpoints" { + type = bool + description = "Enables creation of the [interface endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) specified in `interface_vpc_endpoints`" + default = true +} + +variable "create_s3_endpoint" { + type = bool + description = "Enables creation of the [s3 gateway VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html)" + default = true +} + +variable "enable_firewall" { + description = "Enables the use of the [AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/what-is-aws-network-firewall.html) to protect the private and pod subnets" + type = bool + default = true +} + +variable "stateful_default_action" { + description = "The [default action](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order) for the AWS Network Firewall stateful rule group. Choose `aws:drop_established` or `aws:alert_established`" + type = string + default = "aws:drop_established" +} + +variable "suricata_rules" { + description = "The [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) to use for the AWS Network Firewall. When provided, this variable completely overrides the embedded rules. Use this to bring your own rules. If you only need to provide some additional rules in addition to the bundled rules, then use `additional_suricata_rules` instead of `suricata_rules`." + type = string + default = null +} + +variable "include_sca_rules" { + description = "Enables inclusion of AWS Network Firewall rules used in SCA scanning. These rules may be overly permissive when not using SCA, so they are optional. These rules allow connectivity to various public package manager repositories like [Maven Central](https://mvnrepository.com/repos/central) and [npm](https://docs.npmjs.com/)." + type = bool + default = true +} + +variable "additional_suricata_rules" { + description = "Additional [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) rules to use in the network firewall. When provided these rules will be appended to the default rules prior to the default drop rule." + type = string + default = "" +} + +variable "create_managed_rule_groups" { + type = bool + description = "Enables creation of the AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) provided in `managed_rule_groups`" + default = true +} + +variable "managed_rule_groups" { + description = "The AWS Network Firewall [managed rule groups](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-list.html) to include in the firewall policy. Must be strict order groups. " + type = list(string) + # ThreatSignaturesFUPStrictOrder and ThreatSignaturesPhishingStrictOrder are not included by default, as you can only have 20 stateful rule groups per FW policy. + default = ["AbusedLegitMalwareDomainsStrictOrder", + "MalwareDomainsStrictOrder", + "AbusedLegitBotNetCommandAndControlDomainsStrictOrder", + "BotNetCommandAndControlDomainsStrictOrder", + "ThreatSignaturesBotnetStrictOrder", + "ThreatSignaturesBotnetWebStrictOrder", + "ThreatSignaturesBotnetWindowsStrictOrder", + "ThreatSignaturesIOCStrictOrder", + "ThreatSignaturesDoSStrictOrder", + "ThreatSignaturesEmergingEventsStrictOrder", + "ThreatSignaturesExploitsStrictOrder", + "ThreatSignaturesMalwareStrictOrder", + "ThreatSignaturesMalwareCoinminingStrictOrder", + "ThreatSignaturesMalwareMobileStrictOrder", + "ThreatSignaturesMalwareWebStrictOrder", + "ThreatSignaturesScannersStrictOrder", + "ThreatSignaturesSuspectStrictOrder", + "ThreatSignaturesWebAttacksStrictOrder" + ] +} diff --git a/modules/cxone-install/apply-storageclass-config.sh.tftpl b/modules/cxone-install/apply-storageclass-config.sh.tftpl new file mode 100644 index 0000000..456f669 --- /dev/null +++ b/modules/cxone-install/apply-storageclass-config.sh.tftpl @@ -0,0 +1,20 @@ + +# Remove gp2 as the default storage class +kubectl patch storageclass gp2 -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' + +# Add gp3 as the default storage class +cat < 14 characters in length. default_admin_password: - value: ${admin_password} + value: "${admin_password}" # Enable DAST component. # bool - Valid values are "0" (false) and "1" true @@ -92,7 +92,7 @@ spec: sca_global_inventory_elasticsearch_username: value: ast sca_global_inventory_elasticsearch_password: - value: ${elasticsearch_password} + value: "${elasticsearch_password}" #-------------------------------------------------------------------------- # S3 Bucket Names @@ -169,9 +169,9 @@ spec: # so that the credentials can be configured here. The user must have access to # the s3 buckets for Checkmarx One. object_storage_access_key: - value: ${object_storage_access_key} + value: "${object_storage_access_key}" object_storage_secret_key: - value: ${object_storage_secret_key} + value: "${object_storage_secret_key}" # Configures secure connections to s3. Can be "1" (true/enabled) or "0" (false/disabled) # Recommended: "1" @@ -205,7 +205,7 @@ spec: # The password for the external_postgres_user. external_postgres_password: - value: ${postgres_password} + value: "${postgres_password}" # The name of the CxOne database. By convention, ast. external_postgres_db: @@ -234,7 +234,7 @@ spec: analytics_postgres_user: value: analytics_postgres_password: - value: + value: "" # Can be either analytics_postgres_sslmode_require or analytics_postgres_sslmode_allow analytics_postgres_sslmode_value: value: analytics_postgres_sslmode_require @@ -305,7 +305,7 @@ spec: smtp_user: value: ${smtp_user} smtp_password: - value: ${smtp_password} + value: "${smtp_password}" #-------------------------------------------------------------------------- diff --git a/modules/cxone-install/main.tf b/modules/cxone-install/main.tf index 70673f0..4500261 100644 --- a/modules/cxone-install/main.tf +++ b/modules/cxone-install/main.tf @@ -50,7 +50,7 @@ resource "local_file" "kots_config" { resource "local_file" "makefile" { - content = templatefile("${path.module}/makefile.tftpl", { + content = templatefile("${path.module}/Makefile.tftpl", { tf_deployment_id = var.deployment_id tf_deploy_region = var.region tf_eks_cluster_name = var.deployment_id @@ -68,9 +68,36 @@ resource "local_file" "makefile" { app_version = var.cxone_version cluster_autoscaler_iam_role_arn = var.cluster_autoscaler_iam_role_arn load_balancer_controller_iam_role_arn = var.load_balancer_controller_iam_role_arn + external_dns_iam_role_arn = var.external_dns_iam_role_arn + karpenter_iam_role_arn = var.karpenter_iam_role_arn + cluster_endpoint = var.cluster_endpoint + vpc_id = var.vpc_id + }) + filename = "Makefile" +} +resource "local_file" "karpenter" { + content = templatefile("${path.module}/karpenter.reference.yaml.tftpl", { + deployment_id = var.deployment_id + nodegroup_iam_role_name = var.nodegroup_iam_role_name + availability_zones = jsonencode(var.availability_zones) }) - filename = "makefile" + filename = "karpenter.${var.deployment_id}.yaml" } +resource "local_file" "storage_class" { + content = templatefile("${path.module}/apply-storageclass-config.sh.tftpl", { + deployment_id = var.deployment_id + nodegroup_iam_role_name = var.nodegroup_iam_role_name + availability_zones = jsonencode(var.availability_zones) + karpenter_iam_role_arn = var.karpenter_iam_role_arn + + }) + filename = "apply-storageclass-config.${var.deployment_id}.sh" +} + +resource "local_file" "ENIConfig" { + content = var.pod_eniconfig + filename = "custom-networking-config.${var.deployment_id}.yaml" +} diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/makefile.tftpl index b06c361..2b25302 100644 --- a/modules/cxone-install/makefile.tftpl +++ b/modules/cxone-install/makefile.tftpl @@ -8,6 +8,7 @@ KOTS_PASSWORD = ${tf_kots_password} NAMESPACE = ${tf_namespace} LICENSE_FILE = ${tf_license_file} KOTS_CONFIG_FILE = ${tf_kots_config_file} +TOTP = 123 .PHONY: update-kubeconfig update-kubeconfig: @@ -17,9 +18,9 @@ update-kubeconfig: kots-install: kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password $${KOTS_PASSWORD} --config-values $${KOTS_CONFIG_FILE} --app-version-label $${CXONE_VERSION} -.PHONY: kots-set-config -kots-set-config: - kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy +#.PHONY: kots-set-config +#kots-set-config: +# kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy .PHONY: kots-get-config kots-get-config: @@ -51,6 +52,21 @@ install-cluster-autoscaler: uninstall-cluster-autoscaler: helm uninstall cluster-autoscaler -n kube-system +.PHONY: install-external-dns +install-external-dns: + helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/; \ + helm upgrade --install external-dns external-dns/external-dns \ + -n kube-system \ + --version 1.11.0 \ + --set serviceAccount.create=true \ + --set serviceAccount.name=external-dns \ + --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${external_dns_iam_role_arn}" + +.PHONY: uninstall-external-dns +uninstall-external-dns: + helm uninstall external-dns -n kube-system + + .PHONY: install-load-balancer-controller install-load-balancer-controller: helm repo add eks https://aws.github.io/eks-charts; \ @@ -58,11 +74,12 @@ install-load-balancer-controller: helm install aws-load-balancer-controller eks/aws-load-balancer-controller \ --version 1.7.1 \ -n kube-system \ - --set region=$${DEPLOY_REGION}} \ + --set vpcId=${vpc_id} \ + --set region=$${DEPLOY_REGION} \ --set serviceAccount.create=true \ --set serviceAccount.name=aws-load-balancer-controller \ --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${load_balancer_controller_iam_role_arn}" \ - --set clusterName=$${EKS_CLUSTER_NAME}} \ + --set clusterName=$${EKS_CLUSTER_NAME} \ --set enableShield=false \ --set enableWaf=false \ --set enableWaafv2=false @@ -72,8 +89,44 @@ install-load-balancer-controller: uninstall-load-balancer-controller: helm uninstall aws-load-balancer-controller -n kube-system +.PHONY: install-karpenter +install-karpenter: + helm install karpenter oci://public.ecr.aws/karpenter/karpenter \ + --version 0.36.0 \ + -n kube-system \ + --create-namespace \ + --set serviceAccount.create=true \ + --set serviceAccount.name=karpenter \ + --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${karpenter_iam_role_arn}" \ + --set settings.clusterName=$${EKS_CLUSTER_NAME} \ + --set settings.clusterEndpoint=${cluster_endpoint} \ + --set settings.featureGates.spotToSpotConsolidation=true \ + --set settings.interruptionQueue=$${EKS_CLUSTER_NAME}-node-termination-handler \ + --set controller.resources.requests.cpu=1 \ + --set controller.resources.requests.memory=1Gi \ + --set controller.resources.limits.cpu=1 \ + --set controller.resources.limits.memory=1Gi + kubectl apply -f karpenter.$${DEPLOYMENT_ID}.yaml + +.PHONY: uninstall-karpenter +uninstall-karpenter: + helm uninstall karpenter -n kube-system + +.PHONY: view-firewall-logs +view-firewall-logs: + aws logs filter-log-events --start-time 1713475743 --log-group-name /aws/vendedlogs/$${DEPLOYMENT_ID}-aws-nfw-alert | jq -r ' .events[].message' | jq ' (.event.timestamp + " " + .event.alert.action + ": " + .event.src_ip + ":" + (.event.src_port|tostring) + " -> " + .event.proto + "/" + .event.app_proto + " " + .event.dest_ip + ":" + (.event.dest_port|tostring) + " " + .event.tls.sni + .event.http.hostname) + " " + .event.http.http_user_agent + " " + .event.http.http_method + " " + .event.http.url' + + +.PHONY: apply-storageclass-config +apply-storageclass-config: + ./apply-storageclass-config.$${DEPLOYMENT_ID}.sh + .PHONY: clean-kots clean-kots: kubectl delete deployment kotsadm -n $${NAMESPACE} kubectl delete statefulset kotsadm-minio -n $${NAMESPACE} kubectl delete statefulset kotsadm-rqlite -n $${NAMESPACE} + +.PHONY: totp +totp: + echo "${TOTP}" | totp-cli instant diff --git a/modules/cxone-install/variables.tf b/modules/cxone-install/variables.tf index 4dd233c..5234afc 100644 --- a/modules/cxone-install/variables.tf +++ b/modules/cxone-install/variables.tf @@ -33,6 +33,11 @@ variable "deployment_id" { } } +variable "vpc_id" { + description = "The VPC Id Checkmarx One is deployed into." + type = string +} + variable "bucket_suffix" { description = "The id of the deployment. Will be used to name resources like EKS cluster, etc." type = string @@ -84,6 +89,32 @@ variable "load_balancer_controller_iam_role_arn" { nullable = true } +variable "karpenter_iam_role_arn" { + type = string + nullable = true +} + +variable "cluster_endpoint" { + type = string + nullable = true +} + +variable "nodegroup_iam_role_name" { + type = string + nullable = true +} + +variable "availability_zones" { + type = list(string) + nullable = false +} + +variable "pod_eniconfig" { + description = "The ENIConfigs for EKS custom networking configuration." + type = string + nullable = true +} + #****************************************************************************** # S3 Access Configuration #****************************************************************************** diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index d6429fe..f988bee 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -141,6 +141,9 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.api-sca.checkm pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420060; rev:1;) # Scan results buckets are used for SCA scan result syncing, and vary by the connected SCA region (e.g. NA, or EU) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prod-storage-1an26shc41yi3.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240420061; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.stagecodebashing.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240422001; rev:1;) + + # These URLs are randomly generated, and used to discover the correct s3 API signature version to use when communicating with S3 buckets. # They take two forms, where the long alphanumeric string is randomly generated. The buckets do not exist, but allow minio client @@ -155,8 +158,8 @@ ${local.sca_scanning_rules} ${var.additional_suricata_rules} # Drop other traffic -drop tls $HOME_NET any -> $EXTERNAL_NET any (msg:"not matching any TLS allowlisted FQDNs"; flow:to_server, established; sid:3; rev:1;) -reject tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"blocked uknown tcp"; flow:to_server, established; sid:4; rev:1;) +#drop tls $HOME_NET any -> $EXTERNAL_NET any (msg:"not matching any TLS allowlisted FQDNs"; flow:to_server, established; sid:3; rev:1;) +#reject tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"blocked uknown tcp"; flow:to_server, established; sid:4; rev:1;) EOF } diff --git a/modules/inspection-vpc/firewall.tf b/modules/inspection-vpc/firewall.tf index 7284212..c9511ae 100644 --- a/modules/inspection-vpc/firewall.tf +++ b/modules/inspection-vpc/firewall.tf @@ -64,15 +64,18 @@ resource "aws_networkfirewall_firewall_policy" "main" { } } + +# Reference the policy document length of 5120 characters described at https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AWS-logs-and-resource-policy.html#AWS-logs-infrastructure-CWL +# and explains the solution of using /aws/vendedlogs prefix in log group names. resource "aws_cloudwatch_log_group" "aws_nfw_alert" { count = var.enable_firewall ? 1 : 0 - name = "${var.deployment_id}-aws-nfw-alert" + name = "/aws/vendedlogs/${var.deployment_id}-aws-nfw-alert" retention_in_days = 14 } resource "aws_cloudwatch_log_group" "aws_nfw_flow" { count = var.enable_firewall ? 1 : 0 - name = "${var.deployment_id}-aws-nfw-flow" + name = "/aws/vendedlogs/${var.deployment_id}-aws-nfw-flow" retention_in_days = 14 } diff --git a/modules/inspection-vpc/main.tf b/modules/inspection-vpc/main.tf index 2ef0da7..67d92d5 100644 --- a/modules/inspection-vpc/main.tf +++ b/modules/inspection-vpc/main.tf @@ -105,6 +105,7 @@ resource "aws_subnet" "pod" { tags = { Name = "${var.deployment_id} - pod subnet ${each.key}" "kubernetes.io/cluster/${var.deployment_id}" = "shared" + "kubernetes.io/role/cni" = "1" } depends_on = [aws_vpc_ipv4_cidr_block_association.secondary_cidr_block] } @@ -161,6 +162,7 @@ resource "aws_route_table" "public" { vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] } } + depends_on = [aws_networkfirewall_firewall.main] } resource "aws_route_table_association" "public" { @@ -196,6 +198,17 @@ resource "aws_route_table" "private" { vpc_endpoint_id = var.enable_firewall ? [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] : null nat_gateway_id = var.enable_firewall ? null : aws_nat_gateway.public.id } + + dynamic "route" { + # Route the traffic to public subnet, such as traffic from the load balancer, back through the firewall when firewall is enabled to preserve symetric routing. + for_each = var.enable_firewall ? ["apply"] : [] + content { + cidr_block = local.public_subnet_cidr + vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] + } + } + + depends_on = [aws_networkfirewall_firewall.main] } resource "aws_route_table_association" "private" { diff --git a/modules/inspection-vpc/outputs.tf b/modules/inspection-vpc/outputs.tf index 3589fd6..9d7827b 100644 --- a/modules/inspection-vpc/outputs.tf +++ b/modules/inspection-vpc/outputs.tf @@ -45,3 +45,17 @@ output "azs" { description = "The Availability Zones deployed into" value = local.azs } + +output "ENIConfig" { + description = "List of map of pod subnets including `subnet_id` and `availability_zone`. Useful for creating ENIConfigs for [EKS Custom Networking](https://docs.aws.amazon.com/eks/latest/userguide/cni-custom-network.html)." + value = join("", [for s in aws_subnet.pod : < Date: Tue, 23 Apr 2024 00:20:10 -0400 Subject: [PATCH 09/57] Make totp a make var, not terraform. --- modules/cxone-install/makefile.tftpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/makefile.tftpl index 2b25302..4757819 100644 --- a/modules/cxone-install/makefile.tftpl +++ b/modules/cxone-install/makefile.tftpl @@ -129,4 +129,4 @@ clean-kots: .PHONY: totp totp: - echo "${TOTP}" | totp-cli instant + echo "$${TOTP}" | totp-cli instant From 8b22b7a3414d59924cc30ab3e0af849ed0b6f01a Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 23 Apr 2024 01:23:50 -0400 Subject: [PATCH 10/57] Removing deprecated template_file --- modules/inspection-vpc/firewall-rules.tf | 28 +++++++++++++++--------- modules/inspection-vpc/firewall.tf | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index f988bee..9c71a21 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -16,14 +16,9 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sum.golang.org"; pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"pkg-containers.githubusercontent.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420076; rev:1;) EOF -} -data "template_file" "default_suricata_rules" { - count = var.enable_firewall ? 1 : 0 - template = < any any (msg:"TLS 1.0 or 1.1"; ssl_version:tls1.0,tls1.1; sid:2023070518;) -drop ip $HOME_NET any -> $EXTERNAL_NET [1389,53,4444,445,135,139,389,3389] (msg:"Deny List High Risk Destination Ports"; sid:278670;) # Amazon Services - these must be allowed, or can be replaced by private VPC Endpoints (which have a charge https://aws.amazon.com/privatelink/pricing/) # Note that these are AWS Region Dependent @@ -157,9 +152,22 @@ ${local.sca_scanning_rules} ${var.additional_suricata_rules} -# Drop other traffic -#drop tls $HOME_NET any -> $EXTERNAL_NET any (msg:"not matching any TLS allowlisted FQDNs"; flow:to_server, established; sid:3; rev:1;) -#reject tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"blocked uknown tcp"; flow:to_server, established; sid:4; rev:1;) - EOF } + + + +# data "template_file" "default_suricata_rules" { +# count = var.enable_firewall ? 1 : 0 +# template = < any any (msg:"TLS 1.0 or 1.1"; ssl_version:tls1.0,tls1.1; sid:2023070518;) +# drop ip $HOME_NET any -> $EXTERNAL_NET [1389,53,4444,445,135,139,389,3389] (msg:"Deny List High Risk Destination Ports"; sid:278670;) + + +# # Drop other traffic +# #drop tls $HOME_NET any -> $EXTERNAL_NET any (msg:"not matching any TLS allowlisted FQDNs"; flow:to_server, established; sid:3; rev:1;) +# #reject tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"blocked uknown tcp"; flow:to_server, established; sid:4; rev:1;) + +# EOF +# } diff --git a/modules/inspection-vpc/firewall.tf b/modules/inspection-vpc/firewall.tf index c9511ae..7a348a2 100644 --- a/modules/inspection-vpc/firewall.tf +++ b/modules/inspection-vpc/firewall.tf @@ -24,7 +24,7 @@ resource "aws_networkfirewall_rule_group" "cxone" { type = "STATEFUL" rule_group { rules_source { - rules_string = var.suricata_rules != null ? var.suricata_rules : data.template_file.default_suricata_rules[0].rendered + rules_string = var.suricata_rules != null ? var.suricata_rules : local.default_suricata_rules } stateful_rule_options { rule_order = "STRICT_ORDER" From fd0fc6a5df456a0a2531ca3cce5868c7f233f9e3 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 24 Apr 2024 23:21:22 -0400 Subject: [PATCH 11/57] userdata and cluster sg rule for vpc --- eks.tf | 2 ++ examples/full/examples.auto.tfvars | 2 ++ examples/full/main.tf | 12 ++++++++++++ examples/full/variables-cxone.tf | 12 ++++++++++++ variables.tf | 20 ++++++++++++++++++++ 5 files changed, 48 insertions(+) diff --git a/eks.tf b/eks.tf index 7124de3..6ca3e14 100644 --- a/eks.tf +++ b/eks.tf @@ -252,6 +252,8 @@ module "eks" { create_iam_role = false iam_role_arn = module.eks_node_iam_role.iam_role_arn key_name = var.ec2_key_name + post_bootstrap_user_data = var.eks_post_bootstrap_user_data + pre_bootstrap_user_data = var.eks_pre_bootstrap_user_data metadata_options = { http_endpoint = "enabled" http_tokens = "required" diff --git a/examples/full/examples.auto.tfvars b/examples/full/examples.auto.tfvars index 5cd6102..d0f6163 100644 --- a/examples/full/examples.auto.tfvars +++ b/examples/full/examples.auto.tfvars @@ -117,6 +117,8 @@ eks_public_endpoint_enabled = true eks_cluster_endpoint_public_access_cidrs = ["0.0.0.0/0"] # Use to lock down access to the public endpoint, when the public endpoint is enabled enable_cluster_creator_admin_permissions = true # the principal used to execute this terraform will be granted access to EKS eks_node_additional_security_group_ids = [] # pass arbitrary additional security groups to EKS nodes +eks_post_bootstrap_user_data = null +eks_pre_bootstrap_user_data = null # Uncomment the eks_administrator_principals to specify additional principal ARNs that should have admin # access to EKS. # eks_administrator_principals = [ diff --git a/examples/full/main.tf b/examples/full/main.tf index b1da625..f407667 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -120,6 +120,18 @@ module "checkmarx-one" { eks_create_external_dns_irsa = var.eks_create_external_dns_irsa eks_create_load_balancer_controller_irsa = var.eks_create_load_balancer_controller_irsa eks_create_karpenter = var.eks_create_karpenter + eks_pre_bootstrap_user_data = var.eks_pre_bootstrap_user_data + eks_post_bootstrap_user_data = var.eks_post_bootstrap_user_data + eks_cluster_security_group_additional_rules = { + egress_nodes_ephemeral_ports_tcp = { + description = "Ingress from VPC (management hosts)" + protocol = "tcp" + from_port = 443 + to_port = 443 + type = "ingress" + cidr_blocks = module.vpc.vpc_cidr_blocks + } + } eks_version = var.eks_version coredns_version = var.coredns_version kube_proxy_version = var.kube_proxy_version diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf index ac304dd..50c51c2 100644 --- a/examples/full/variables-cxone.tf +++ b/examples/full/variables-cxone.tf @@ -123,6 +123,18 @@ variable "eks_node_additional_security_group_ids" { default = [] } +variable "eks_post_bootstrap_user_data" { + type = string + description = "User data to insert after bootstrapping script." + default = "" +} + +variable "eks_pre_bootstrap_user_data" { + type = string + description = "User data to insert before bootstrapping script." + default = "" +} + variable "coredns_version" { type = string description = "The version of the EKS Core DNS Addon." diff --git a/variables.tf b/variables.tf index 2d65791..e1a7c9b 100644 --- a/variables.tf +++ b/variables.tf @@ -124,6 +124,26 @@ variable "eks_node_additional_security_group_ids" { default = [] } +variable "eks_cluster_security_group_additional_rules" { + description = "Additional security group rules for the EKS cluster" + type = any + default = {} + +} + +variable "eks_post_bootstrap_user_data" { + type = string + description = "User data to insert after bootstrapping script." + default = "" +} + +variable "eks_pre_bootstrap_user_data" { + type = string + description = "User data to insert before bootstrapping script." + default = "" +} + + variable "coredns_version" { type = string description = "The version of the EKS Core DNS Addon." From ed2fb47efe58ca1c13f7304b6a7456a5b7c66c9f Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 24 Apr 2024 23:21:53 -0400 Subject: [PATCH 12/57] quote password and clean up load balancer controller resdources target --- modules/cxone-install/makefile.tftpl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/makefile.tftpl index 4757819..c8d371e 100644 --- a/modules/cxone-install/makefile.tftpl +++ b/modules/cxone-install/makefile.tftpl @@ -16,7 +16,7 @@ update-kubeconfig: .PHONY: kots-install kots-install: - kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password $${KOTS_PASSWORD} --config-values $${KOTS_CONFIG_FILE} --app-version-label $${CXONE_VERSION} + kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password '$${KOTS_PASSWORD}' --config-values $${KOTS_CONFIG_FILE} --app-version-label $${CXONE_VERSION} #.PHONY: kots-set-config #kots-set-config: @@ -127,6 +127,13 @@ clean-kots: kubectl delete statefulset kotsadm-minio -n $${NAMESPACE} kubectl delete statefulset kotsadm-rqlite -n $${NAMESPACE} +.PHONY: destroy-load-balancer-controller +destroy-load-balancer-controller: + ELB_NAME=$(kubectl describe service ast-platform-traefik -n ast | grep "LoadBalancer Ingress" | awk '{print $3}' | cut -c 1-16); \ + aws elbv2 describe-load-balancers --query "LoadBalancers[?starts_with(LoadBalancerName,'$ELB_NAME')].LoadBalancerArn" --output text | tr "\t" "\n" | xargs -I{} aws elbv2 delete-load-balancer --load-balancer-arn {}; \ + aws ec2 describe-security-groups --filters Name=tag:elbv2.k8s.aws/cluster,Values=$${DEPLOYMENT_ID} --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n" | xargs -I{} aws ec2 delete-security-group --group-id {}; + + .PHONY: totp totp: echo "$${TOTP}" | totp-cli instant From 18d300282592ee07a1f4121ea40310ad50a5f43c Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 24 Apr 2024 23:22:16 -0400 Subject: [PATCH 13/57] fixing ingress --- modules/inspection-vpc/main.tf | 60 ++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/modules/inspection-vpc/main.tf b/modules/inspection-vpc/main.tf index 67d92d5..6f97680 100644 --- a/modules/inspection-vpc/main.tf +++ b/modules/inspection-vpc/main.tf @@ -137,14 +137,16 @@ resource "aws_nat_gateway" "public" { # Route Tables #****************************************************************************** -# Public Subnet Routing -resource "aws_route_table" "public" { +resource "aws_route_table" "igw" { vpc_id = aws_vpc.main.id - tags = { "Name" = "${var.deployment_id}-public" } + tags = { "Name" = "${var.deployment_id}-igw" } - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.igw.id + dynamic "route" { + for_each = var.enable_firewall ? ["apply"] : [] + content { + cidr_block = local.public_subnet_cidr + vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] + } } dynamic "route" { @@ -162,6 +164,34 @@ resource "aws_route_table" "public" { vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] } } + + dynamic "route" { + for_each = { for idx, az in local.azs : az => idx if var.enable_firewall } + content { + cidr_block = local.database_subnet_cidrs[route.value] + vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] + } + } + +} + +resource "aws_route_table_association" "igw" { + gateway_id = aws_internet_gateway.igw.id + route_table_id = aws_route_table.igw.id +} + + +# Public Subnet Routing +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + tags = { "Name" = "${var.deployment_id}-public" } + + route { + cidr_block = "0.0.0.0/0" + gateway_id = var.enable_firewall ? null : aws_internet_gateway.igw.id + vpc_endpoint_id = var.enable_firewall ? [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] : null + } + depends_on = [aws_networkfirewall_firewall.main] } @@ -178,8 +208,8 @@ resource "aws_route_table" "firewall" { tags = { "Name" = "${var.deployment_id}-firewall" } route { - cidr_block = "0.0.0.0/0" - nat_gateway_id = aws_nat_gateway.public.id + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id } } @@ -194,18 +224,8 @@ resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id tags = { "Name" = "${var.deployment_id}-private" } route { - cidr_block = "0.0.0.0/0" - vpc_endpoint_id = var.enable_firewall ? [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] : null - nat_gateway_id = var.enable_firewall ? null : aws_nat_gateway.public.id - } - - dynamic "route" { - # Route the traffic to public subnet, such as traffic from the load balancer, back through the firewall when firewall is enabled to preserve symetric routing. - for_each = var.enable_firewall ? ["apply"] : [] - content { - cidr_block = local.public_subnet_cidr - vpc_endpoint_id = [for ss in aws_networkfirewall_firewall.main[0].firewall_status[0].sync_states : ss.attachment[0].endpoint_id][0] - } + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.public.id } depends_on = [aws_networkfirewall_firewall.main] From ccd35197832cb0ee75253fffbbd0c61682ad6f26 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 25 Apr 2024 00:04:33 -0400 Subject: [PATCH 14/57] karpenter firewall and nodepool updates --- modules/cxone-install/karpenter.reference.yaml.tftpl | 6 ++++++ modules/inspection-vpc/firewall-rules.tf | 1 + 2 files changed, 7 insertions(+) diff --git a/modules/cxone-install/karpenter.reference.yaml.tftpl b/modules/cxone-install/karpenter.reference.yaml.tftpl index 63fa3cd..6c28b02 100644 --- a/modules/cxone-install/karpenter.reference.yaml.tftpl +++ b/modules/cxone-install/karpenter.reference.yaml.tftpl @@ -107,6 +107,12 @@ spec: - key: karpenter.k8s.aws/instance-category operator: NotIn values: ["t"] + - key: karpenter.k8s.aws/instance-cpu + operator: Gt + values: ["3"] + - key: karpenter.k8s.aws/instance-memory + operator: Gt + values: ["7"] - key: karpenter.k8s.aws/instance-hypervisor operator: In values: ["nitro"] diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index 9c71a21..ebc9ea9 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -37,6 +37,7 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"route53.amazonaws pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cloudformation.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190010; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"elasticloadbalancing.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190011; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"autoscaling.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190012; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sqs.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24240001; rev:1;) # Installation media for kube-system services pods like coredns, aws-node, ebs-csi-controller, ebs-csi-node, kube-proxy From ea1c6d7b30e4e153b74d8060732a713904a505dc Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 29 Apr 2024 15:53:20 -0400 Subject: [PATCH 15/57] custom-networking --- eks.tf | 6 ++-- modules/cxone-install/main.tf | 2 +- modules/cxone-install/makefile.tftpl | 6 ++-- modules/inspection-vpc/firewall-rules.tf | 39 +++++++++++------------- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/eks.tf b/eks.tf index 6ca3e14..4753978 100644 --- a/eks.tf +++ b/eks.tf @@ -224,9 +224,9 @@ module "eks" { before_compute = var.eks_pod_subnets != null ? true : false configuration_values = jsonencode({ env = { - #AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = var.eks_pod_subnets != null ? "true" : "false" - #ENI_CONFIG_LABEL_DEF = var.eks_pod_subnets != null ? "topology.kubernetes.io/zone" : "" - AWS_VPC_K8S_CNI_EXTERNALSNAT = var.eks_enable_externalsnat ? "true" : "false" + AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = var.eks_pod_subnets != null ? "true" : "false" + ENI_CONFIG_LABEL_DEF = var.eks_pod_subnets != null ? "topology.kubernetes.io/zone" : "" + AWS_VPC_K8S_CNI_EXTERNALSNAT = var.eks_enable_externalsnat ? "true" : "false" } }) } diff --git a/modules/cxone-install/main.tf b/modules/cxone-install/main.tf index 4500261..2ec9ca5 100644 --- a/modules/cxone-install/main.tf +++ b/modules/cxone-install/main.tf @@ -76,7 +76,7 @@ resource "local_file" "makefile" { filename = "Makefile" } -resource "local_file" "karpenter" { +resource "local_file" "karpenter_configuration" { content = templatefile("${path.module}/karpenter.reference.yaml.tftpl", { deployment_id = var.deployment_id nodegroup_iam_role_name = var.nodegroup_iam_role_name diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/makefile.tftpl index c8d371e..4ce7068 100644 --- a/modules/cxone-install/makefile.tftpl +++ b/modules/cxone-install/makefile.tftpl @@ -18,9 +18,9 @@ update-kubeconfig: kots-install: kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password '$${KOTS_PASSWORD}' --config-values $${KOTS_CONFIG_FILE} --app-version-label $${CXONE_VERSION} -#.PHONY: kots-set-config -#kots-set-config: -# kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy +.PHONY: kots-set-config +kots-set-config: + kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy .PHONY: kots-get-config kots-get-config: diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index ebc9ea9..873e24e 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -18,8 +18,6 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"pkg-containers.gi EOF default_suricata_rules = < $EXTERNAL_NET 443 (tls.sni; content:"route53.amazonaws pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cloudformation.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190010; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"elasticloadbalancing.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190011; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"autoscaling.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24190012; rev:1;) + + +# Karpenter +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"iam.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24250001; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.pricing.us-east-1.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24250002; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sqs.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24240001; rev:1;) @@ -84,31 +87,38 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"amazonlinux-2-rep pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"al2023-repos-${data.aws_region.current.name}-de612dc2.s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420033; rev:1;) -# AWS Load Balancer Controller - public.ecr.aws (metadata) redirects to cloudfront (download) +# AWS Load Balancer Controller & Karpenter - public.ecr.aws (metadata) redirects to cloudfront (download) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"public.ecr.aws"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420034; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"d5l0dvt14r5h8.cloudfront.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420035; rev:1;) + # Cluster Autoscaler - k8s.gcr.io (metadata) redirects to storage.googleapis.com (download) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"k8s.gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420036; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"storage.googleapis.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420037; rev:1;) + # Kotsadm tools (minio, rqlite) come from docker.io and docker.com pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry-1.docker.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420038; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"auth.docker.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420039; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"production.cloudflare.docker.com"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420040; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"subnet.min.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24250010; rev:1;) + # Replicated APIs - used for license checking pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"replicated.app"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420041; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy.replicated.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420042; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy-auth.replicated.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420043; rev:1;) + # Used by cxone images, and kube-rbac-proxy in CxOne operator pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240419044; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"checkmarx.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420044; rev:1;) + # Liquibase schema files required for database migration execution pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"www.liquibase.org"; startswith; endswith; msg:"Match liquidbase.com allowed"; flow:to_server, established; sid:240420062; rev:1;) + # Feature Flags via Split.io # Reference https://help.split.io/hc/en-us/articles/360006954331-How-do-I-allow-Split-to-work-in-my-environment pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sdk.split.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420045; rev:1;) @@ -120,15 +130,18 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cdn.split.io"; st # Fastly (a CDN), which is used for sdk.split.io https://api.fastly.com/public-ip-list. These are used sometimes w/o host name, so SNI cannot be used to filter. pass tls $HOME_NET any -> [23.235.32.0/20,43.249.72.0/22,103.244.50.0/24,103.245.222.0/23,103.245.224.0/24,104.156.80.0/20,140.248.64.0/18,140.248.128.0/17,146.75.0.0/17,151.101.0.0/16,157.52.64.0/18,167.82.0.0/17,167.82.128.0/20,167.82.160.0/20,167.82.224.0/20,172.111.64.0/18,185.31.16.0/22,199.27.72.0/21,199.232.0.0/16] 443 (msg:"Fastly CDN"; flow:to_server, established; sid:240420051; rev:1;) + # Allow access to s3 buckets for Checkmarx One. Buckets are typically created with a prefix of the deployment id which allows for regex matching # Example bucket name and suffix: scan-results-bos-ap-southeast-1-lab-19205 pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420052; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420053; rev:1;) + # Checkmarx One Scans will upload source to scan-results bucket with url path patterns like "https://s3.${data.aws_region.current.name}.amazonaws.com/scan-results-0aa15147e5f3/source-code/....." pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420054; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420055; rev:1;) + # These are the checkmarx services for SCA scanning, cloud IAM (for Authentication to SCA), and codebashing pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"iam.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420056; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api-sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420057; rev:1;) @@ -138,7 +151,8 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.sca.check # Scan results buckets are used for SCA scan result syncing, and vary by the connected SCA region (e.g. NA, or EU) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prod-storage-1an26shc41yi3.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240420061; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.stagecodebashing.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240422001; rev:1;) - +# upcoming features +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cx-sca-containers.es.us-east-1.aws.found.io"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:204290001; rev:1;) # These URLs are randomly generated, and used to discover the correct s3 API signature version to use when communicating with S3 buckets. @@ -155,20 +169,3 @@ ${var.additional_suricata_rules} EOF } - - - -# data "template_file" "default_suricata_rules" { -# count = var.enable_firewall ? 1 : 0 -# template = < any any (msg:"TLS 1.0 or 1.1"; ssl_version:tls1.0,tls1.1; sid:2023070518;) -# drop ip $HOME_NET any -> $EXTERNAL_NET [1389,53,4444,445,135,139,389,3389] (msg:"Deny List High Risk Destination Ports"; sid:278670;) - - -# # Drop other traffic -# #drop tls $HOME_NET any -> $EXTERNAL_NET any (msg:"not matching any TLS allowlisted FQDNs"; flow:to_server, established; sid:3; rev:1;) -# #reject tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"blocked uknown tcp"; flow:to_server, established; sid:4; rev:1;) - -# EOF -# } From 130fbfa855dff55accd4147ba800b63c3abe033a Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 30 Apr 2024 14:39:42 -0400 Subject: [PATCH 16/57] adding cluster security group rules --- eks.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eks.tf b/eks.tf index 4753978..4085ba4 100644 --- a/eks.tf +++ b/eks.tf @@ -262,7 +262,8 @@ module "eks" { } } - eks_managed_node_groups = local.eks_nodegroups + cluster_security_group_additional_rules = var.eks_cluster_security_group_additional_rules + eks_managed_node_groups = local.eks_nodegroups fargate_profile_defaults = { subnet_ids = var.eks_pod_subnets != null ? var.eks_pod_subnets : null From 54d3ae1f723b54b5fac2c76a557b5859e6ddb54d Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 30 Apr 2024 22:51:01 -0400 Subject: [PATCH 17/57] Adding explicit config for custom networking --- eks.tf | 8 ++++---- examples/full/examples.auto.tfvars | 1 + examples/full/variables-cxone.tf | 6 ++++++ variables.tf | 7 +++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/eks.tf b/eks.tf index 4085ba4..f466f0e 100644 --- a/eks.tf +++ b/eks.tf @@ -221,11 +221,11 @@ module "eks" { } vpc-cni = { addon_version = var.vpc_cni_version - before_compute = var.eks_pod_subnets != null ? true : false + before_compute = var.eks_enable_custom_networking configuration_values = jsonencode({ env = { - AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = var.eks_pod_subnets != null ? "true" : "false" - ENI_CONFIG_LABEL_DEF = var.eks_pod_subnets != null ? "topology.kubernetes.io/zone" : "" + AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = tostring(var.eks_enable_custom_networking) + ENI_CONFIG_LABEL_DEF = var.eks_enable_custom_networking ? "topology.kubernetes.io/zone" : "" AWS_VPC_K8S_CNI_EXTERNALSNAT = var.eks_enable_externalsnat ? "true" : "false" } }) @@ -266,7 +266,7 @@ module "eks" { eks_managed_node_groups = local.eks_nodegroups fargate_profile_defaults = { - subnet_ids = var.eks_pod_subnets != null ? var.eks_pod_subnets : null + subnet_ids = var.eks_enable_custom_networking ? var.eks_pod_subnets : null } fargate_profiles = var.eks_enable_fargate ? local.fargate_profiles : {} } diff --git a/examples/full/examples.auto.tfvars b/examples/full/examples.auto.tfvars index d0f6163..043a704 100644 --- a/examples/full/examples.auto.tfvars +++ b/examples/full/examples.auto.tfvars @@ -110,6 +110,7 @@ vpc_cni_version = "v1.18.0-eksbuild.1" aws_ebs_csi_driver_version = "v1.28.0-eksbuild.1" eks_enable_fargate = false # not yet working, do not enable eks_enable_externalsnat = false # leave false, unless working with external nat gateway +eks_enable_custom_networking = false eks_private_endpoint_enabled = true # When eks_public_endpoint_enabled = false, installation and management commands after Terraform must be run # from a system with connectivity to the private EKS endpoint, such as a bastion host. diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf index 50c51c2..001b96d 100644 --- a/examples/full/variables-cxone.tf +++ b/examples/full/variables-cxone.tf @@ -64,6 +64,12 @@ variable "eks_enable_externalsnat" { default = false } +variable "eks_enable_custom_networking" { + type = bool + description = "Enables custom networking for the EKS VPC CNI. When true, custom networking is enabled with `ENI_CONFIG_LABEL_DEF` = `topology.kubernetes.io/zone` and ENIConfig resources must be created." + default = false +} + variable "eks_enable_fargate" { type = bool description = "Enables Fargate profiles for the karpenter and kube-system namespaces." diff --git a/variables.tf b/variables.tf index e1a7c9b..735bffa 100644 --- a/variables.tf +++ b/variables.tf @@ -53,6 +53,13 @@ variable "eks_subnets" { type = list(string) } + +variable "eks_enable_custom_networking" { + type = bool + description = "Enables custom networking for the EKS VPC CNI. When true, custom networking is enabled with `ENI_CONFIG_LABEL_DEF` = `topology.kubernetes.io/zone` and ENIConfig resources must be created." + default = false +} + variable "eks_pod_subnets" { description = "The subnets to use for EKS pods. When specified, custom networking configuration is applied to the EKS cluster." type = list(string) From 3e0ce8caa29f00c972854fcc77f836a582f14233 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 8 May 2024 21:59:10 -0400 Subject: [PATCH 18/57] added https protocol to s3 allowed origins --- examples/full/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/full/main.tf b/examples/full/main.tf index f407667..fb1840b 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -108,7 +108,7 @@ module "checkmarx-one" { ec2_key_name = var.ec2_key_name vpc_id = module.vpc.vpc_id kms_key_arn = aws_kms_key.main.arn - s3_allowed_origins = [var.fqdn] + s3_allowed_origins = [var.fqdn, "https://${var.fqdn}"] # EKS Configuration eks_create = var.eks_create From edfa802c093d43ef2d22081ef9201d75d0b99495 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 14 May 2024 11:08:39 -0400 Subject: [PATCH 19/57] add airgap install stub to makefile --- modules/cxone-install/makefile.tftpl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/makefile.tftpl index 4ce7068..965ef56 100644 --- a/modules/cxone-install/makefile.tftpl +++ b/modules/cxone-install/makefile.tftpl @@ -8,6 +8,10 @@ KOTS_PASSWORD = ${tf_kots_password} NAMESPACE = ${tf_namespace} LICENSE_FILE = ${tf_license_file} KOTS_CONFIG_FILE = ${tf_kots_config_file} +AIRGAP_BUNDLE = ??? +KOTS_REGISTRY = ??? +REGISTRY_USERNAME = ??? +REGISTRY_PASSWORD = ??? TOTP = 123 .PHONY: update-kubeconfig @@ -18,6 +22,11 @@ update-kubeconfig: kots-install: kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password '$${KOTS_PASSWORD}' --config-values $${KOTS_CONFIG_FILE} --app-version-label $${CXONE_VERSION} +.PHONY: kots-install-from-airgap +kots-install-from-airgap: + kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password '$${KOTS_PASSWORD}' --config-values $${KOTS_CONFIG_FILE} \ + --airgap --airgap-bundle $${AIRGAP_BUNDLE} --kotsadm-registry $${KOTS_REGISTRY} --registry-username $${REGISTRY_USERNAME} --registry-password '$${REGISTRY_PASSWORD}' + .PHONY: kots-set-config kots-set-config: kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy From 4236ccec37c44a9c05468a810c5fe1bfd129c73e Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 14 May 2024 13:03:04 -0400 Subject: [PATCH 20/57] add destroy make target --- .../destroy-load-balancer.sh.tftpl | 23 ++++++++++++++++++ modules/cxone-install/main.tf | 7 ++++++ modules/cxone-install/makefile.tftpl | 24 +++++++++++++++---- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 modules/cxone-install/destroy-load-balancer.sh.tftpl diff --git a/modules/cxone-install/destroy-load-balancer.sh.tftpl b/modules/cxone-install/destroy-load-balancer.sh.tftpl new file mode 100644 index 0000000..84a0e2e --- /dev/null +++ b/modules/cxone-install/destroy-load-balancer.sh.tftpl @@ -0,0 +1,23 @@ +#!/bin/bash + +# The AWS Load Balancer Controller creates Load Balancers, Target Groups, and Security Groups outside +# of the Terraform project's scope. These resources will prevent terraform destroy from running successfully. +# This script should be run before terraform destroy to ensure these external resources are cleaned up and +# allows terraform to run successfully. + + +DEPLOYMENT_ID=${deployment_id} +# Clean up load balancer +ELB_ARN=$(aws elbv2 describe-load-balancers | jq -r '.LoadBalancers[].LoadBalancerArn' | xargs -I {} aws elbv2 describe-tags --resource-arns {} --query "TagDescriptions[?Tags[?Key=='elbv2.k8s.aws/cluster' &&Value=='$${DEPLOYMENT_ID}']].ResourceArn" --output text) +aws elbv2 delete-load-balancer --load-balancer-arn $${ELB_ARN} + +# Clean up target groups +aws elbv2 describe-target-groups | jq -r '.TargetGroups[].TargetGroupArn' | xargs -I {} aws elbv2 describe-tags --resource-arns {} --query "TagDescriptions[?Tags[?Key=='elbv2.k8s.aws/cluster' &&Value=='$${DEPLOYMENT_ID}']].ResourceArn" --output text | xargs -I {} aws elbv2 delete-target-group --target-group-arn {} + +# Clean up security groups +# The "Node" security group will have references to the ELB security groups, so remove all the rules to allow groups to be deleted successfully +NODE_SG_ID=$(aws ec2 describe-security-groups --filters Name=tag:Name,Values=$${DEPLOYMENT_ID}-node --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n") +aws ec2 revoke-security-group-ingress --group-id $${NODE_SG_ID} --ip-permissions "`aws ec2 describe-security-groups --output json --group-ids $${NODE_SG_ID} --query "SecurityGroups[0].IpPermissions"`" + +# Delete the ELB security groups +aws ec2 describe-security-groups --filters Name=tag:elbv2.k8s.aws/cluster,Values=$${DEPLOYMENT_ID} --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n" | xargs -I {} aws ec2 delete-security-group --group-id {} diff --git a/modules/cxone-install/main.tf b/modules/cxone-install/main.tf index 2ec9ca5..96c7e98 100644 --- a/modules/cxone-install/main.tf +++ b/modules/cxone-install/main.tf @@ -101,3 +101,10 @@ resource "local_file" "ENIConfig" { content = var.pod_eniconfig filename = "custom-networking-config.${var.deployment_id}.yaml" } + +resource "local_file" "destroy_load_balancer" { + content = templatefile("${path.module}/destroy-load-balancer.sh.tftpl", { + deployment_id = var.deployment_id + }) + filename = "destroy-load-balancer.${var.deployment_id}.sh" +} diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/makefile.tftpl index 965ef56..07c5f01 100644 --- a/modules/cxone-install/makefile.tftpl +++ b/modules/cxone-install/makefile.tftpl @@ -14,36 +14,44 @@ REGISTRY_USERNAME = ??? REGISTRY_PASSWORD = ??? TOTP = 123 + .PHONY: update-kubeconfig update-kubeconfig: aws eks update-kubeconfig --name $${EKS_CLUSTER_NAME} + .PHONY: kots-install kots-install: kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password '$${KOTS_PASSWORD}' --config-values $${KOTS_CONFIG_FILE} --app-version-label $${CXONE_VERSION} + .PHONY: kots-install-from-airgap kots-install-from-airgap: kubectl kots install ast/$${RELEASE_CHANNEL} -n $${NAMESPACE} --license-file $${LICENSE_FILE} --shared-password '$${KOTS_PASSWORD}' --config-values $${KOTS_CONFIG_FILE} \ --airgap --airgap-bundle $${AIRGAP_BUNDLE} --kotsadm-registry $${KOTS_REGISTRY} --registry-username $${REGISTRY_USERNAME} --registry-password '$${REGISTRY_PASSWORD}' + .PHONY: kots-set-config kots-set-config: kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy + .PHONY: kots-get-config kots-get-config: kubectl kots get config -n $${NAMESPACE} --appslug ast + .PHONY: kots-admin-console kots-admin-console: kubectl kots admin-console -n $${NAMESPACE} + .PHONY: rollout-restart rollout-restart: kubectl -n $${NAMESPACE} rollout restart deploy kubectl -n $${NAMESPACE} rollout restart statefulset + .PHONY: install-cluster-autoscaler install-cluster-autoscaler: helm repo add autoscaler https://kubernetes.github.io/autoscaler; \ @@ -57,10 +65,12 @@ install-cluster-autoscaler: --set rbac.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${cluster_autoscaler_iam_role_arn}" \ --set autoDiscovery.clusterName=$${EKS_CLUSTER_NAME} + .PHONY: uninstall-cluster-autoscaler uninstall-cluster-autoscaler: helm uninstall cluster-autoscaler -n kube-system + .PHONY: install-external-dns install-external-dns: helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/; \ @@ -71,6 +81,7 @@ install-external-dns: --set serviceAccount.name=external-dns \ --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${external_dns_iam_role_arn}" + .PHONY: uninstall-external-dns uninstall-external-dns: helm uninstall external-dns -n kube-system @@ -98,6 +109,7 @@ install-load-balancer-controller: uninstall-load-balancer-controller: helm uninstall aws-load-balancer-controller -n kube-system + .PHONY: install-karpenter install-karpenter: helm install karpenter oci://public.ecr.aws/karpenter/karpenter \ @@ -117,10 +129,12 @@ install-karpenter: --set controller.resources.limits.memory=1Gi kubectl apply -f karpenter.$${DEPLOYMENT_ID}.yaml + .PHONY: uninstall-karpenter uninstall-karpenter: helm uninstall karpenter -n kube-system + .PHONY: view-firewall-logs view-firewall-logs: aws logs filter-log-events --start-time 1713475743 --log-group-name /aws/vendedlogs/$${DEPLOYMENT_ID}-aws-nfw-alert | jq -r ' .events[].message' | jq ' (.event.timestamp + " " + .event.alert.action + ": " + .event.src_ip + ":" + (.event.src_port|tostring) + " -> " + .event.proto + "/" + .event.app_proto + " " + .event.dest_ip + ":" + (.event.dest_port|tostring) + " " + .event.tls.sni + .event.http.hostname) + " " + .event.http.http_user_agent + " " + .event.http.http_method + " " + .event.http.url' @@ -130,17 +144,17 @@ view-firewall-logs: apply-storageclass-config: ./apply-storageclass-config.$${DEPLOYMENT_ID}.sh + .PHONY: clean-kots clean-kots: kubectl delete deployment kotsadm -n $${NAMESPACE} kubectl delete statefulset kotsadm-minio -n $${NAMESPACE} kubectl delete statefulset kotsadm-rqlite -n $${NAMESPACE} -.PHONY: destroy-load-balancer-controller -destroy-load-balancer-controller: - ELB_NAME=$(kubectl describe service ast-platform-traefik -n ast | grep "LoadBalancer Ingress" | awk '{print $3}' | cut -c 1-16); \ - aws elbv2 describe-load-balancers --query "LoadBalancers[?starts_with(LoadBalancerName,'$ELB_NAME')].LoadBalancerArn" --output text | tr "\t" "\n" | xargs -I{} aws elbv2 delete-load-balancer --load-balancer-arn {}; \ - aws ec2 describe-security-groups --filters Name=tag:elbv2.k8s.aws/cluster,Values=$${DEPLOYMENT_ID} --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n" | xargs -I{} aws ec2 delete-security-group --group-id {}; + +.PHONY: destroy-load-balancer +destroy-load-balancer: + ./destroy-load-balancer.$${DEPLOYMENT_ID}.sh .PHONY: totp From 76d90d6af3560e3fe1cd1fc428dcc18e1262c1e9 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 16 May 2024 08:57:29 -0400 Subject: [PATCH 21/57] enable analytics, sca inventory, document type --- .../kots.config.aws.reference.yaml.tftpl | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/modules/cxone-install/kots.config.aws.reference.yaml.tftpl b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl index 313ce90..f379356 100644 --- a/modules/cxone-install/kots.config.aws.reference.yaml.tftpl +++ b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl @@ -43,9 +43,11 @@ spec: sca_prod_environment: value: https://api-sca.checkmarx.net - # environment_type is always production + # environment_type is either "production" or "development" + # This controls how strict the KOTS preflight checks are for memory, cpu, etc + # Recommended value: production environment_type: - value: development + value: production # deployment_type is always cloud deployment_type: @@ -82,7 +84,7 @@ spec: # bool - Valid values are "0" (false) and "1" true. # When true, must provide elasticsearch configuration. enable_sca_global_inventory: - value: "0" + value: "1" # Typically ES is provided via AWS OpenSearch service (must be Elasticsearch engine v 7.10) sca_global_inventory_elasticsearch_host: @@ -209,7 +211,7 @@ spec: # The name of the CxOne database. By convention, ast. external_postgres_db: - value: ast + value: ${postgres_db} # Used to enforce SSL connections to postgres. Values are # postgres_sslmode_require or postgres_sslmode_allow @@ -222,19 +224,19 @@ spec: # Enables analytics features, which requires an additional database. enable_analytics: - value: "0" + value: "1" # The analaytics database connection information. required when enable_analytics is "1" analytics_postgres_host: - value: + value: ${analytics_postgres_host} analytics_postgres_port: value: "5432" analytics_postgres_db_name: - value: analytics + value: ${analytics_postgres_db_name} analytics_postgres_user: - value: + value: ${analytics_postgres_user} analytics_postgres_password: - value: "" + value: "${analytics_postgres_password}" # Can be either analytics_postgres_sslmode_require or analytics_postgres_sslmode_allow analytics_postgres_sslmode_value: value: analytics_postgres_sslmode_require From ef1f8cc32b3357bea4130cc42997a34bb8288cc0 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 16 May 2024 09:00:20 -0400 Subject: [PATCH 22/57] Adding analytics database/enablement, and byor db management for v3.12 --- eks.tf | 8 ++ examples/full/examples.auto.tfvars | 16 +++ examples/full/main.tf | 44 +++++++ examples/full/variables-cxone.tf | 58 ++++++++++ helm/database-preparation/.helmignore | 23 ++++ helm/database-preparation/Chart.yaml | 6 + .../templates/_helpers.tpl | 62 ++++++++++ .../analytics-database-preparation.yaml | 37 ++++++ .../templates/byor-database-preparation.yaml | 37 ++++++ helm/database-preparation/values.yaml | 20 ++++ modules/cxone-install/main.tf | 6 + modules/cxone-install/variables.tf | 21 ++++ rds-analytics.tf | 107 ++++++++++++++++++ rds-databases.tf | 53 +++++++++ variables.tf | 56 +++++++++ 15 files changed, 554 insertions(+) create mode 100644 helm/database-preparation/.helmignore create mode 100644 helm/database-preparation/Chart.yaml create mode 100644 helm/database-preparation/templates/_helpers.tpl create mode 100644 helm/database-preparation/templates/analytics-database-preparation.yaml create mode 100644 helm/database-preparation/templates/byor-database-preparation.yaml create mode 100644 helm/database-preparation/values.yaml create mode 100644 rds-analytics.tf create mode 100644 rds-databases.tf diff --git a/eks.tf b/eks.tf index f466f0e..635c478 100644 --- a/eks.tf +++ b/eks.tf @@ -401,6 +401,14 @@ output "cluster_endpoint" { value = module.eks.cluster_endpoint } +output "cluster_name" { + value = module.eks.cluster_name +} + +output "cluster_certificate_authority_data" { + value = module.eks.cluster_certificate_authority_data +} + output "nodegroup_iam_role_name" { value = data.aws_iam_role.karpenter.name } diff --git a/examples/full/examples.auto.tfvars b/examples/full/examples.auto.tfvars index 043a704..d6e4b5e 100644 --- a/examples/full/examples.auto.tfvars +++ b/examples/full/examples.auto.tfvars @@ -308,6 +308,22 @@ db_serverlessv2_scaling_configuration = { } +#****************************************************************************** +# RDS - Analytics - Configuration +#****************************************************************************** +analytics_db_instance_class = "db.serverless" +analytics_db_final_snapshot_identifier = "your-final-analytics-snapshot-id" +analytics_db_snapshot_identifer = null +analytics_db_cluster_db_instance_parameter_group_name = "aurora-postgresql13-cluster-analytics" +analytics_db_instances = { + writer = {} +} +analytics_db_serverlessv2_scaling_configuration = { + min_capacity = 0.5 + max_capacity = 8 +} + + #****************************************************************************** # Elasticache Configuration #****************************************************************************** diff --git a/examples/full/main.tf b/examples/full/main.tf index fb1840b..935e319 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -64,6 +64,16 @@ resource "random_password" "db" { min_numeric = 1 } +resource "random_password" "analytics_db" { + length = 32 + special = false + override_special = "!-_" + min_special = 1 + min_upper = 1 + min_lower = 1 + min_numeric = 1 +} + resource "random_password" "kots_admin" { length = 14 special = false @@ -175,6 +185,14 @@ module "checkmarx-one" { db_instances = var.db_instances db_serverlessv2_scaling_configuration = var.db_serverlessv2_scaling_configuration + analytics_db_instance_class = var.analytics_db_instance_class + analytics_db_final_snapshot_identifier = var.analytics_db_final_snapshot_identifier + analytics_db_snapshot_identifer = var.analytics_db_snapshot_identifer + analytics_db_cluster_db_instance_parameter_group_name = var.analytics_db_cluster_db_instance_parameter_group_name + analytics_db_instances = var.analytics_db_instances + analytics_db_serverlessv2_scaling_configuration = var.analytics_db_serverlessv2_scaling_configuration + analytics_db_master_user_password = random_password.analytics_db.result + # Elasticache Configuration ec_create = var.ec_create ec_subnets = module.vpc.database_subnets @@ -224,6 +242,10 @@ module "checkmarx-one-install" { postgres_database_name = module.checkmarx-one.db_database_name postgres_user = module.checkmarx-one.db_master_username postgres_password = module.checkmarx-one.db_master_password + analytics_postgres_host = module.checkmarx-one.analytics_db_endpoint + analytics_postgres_database_name = module.checkmarx-one.analytics_db_database_name + analytics_postgres_user = module.checkmarx-one.analytics_db_master_username + analytics_postgres_password = module.checkmarx-one.analytics_db_master_password redis_address = module.checkmarx-one.ec_endpoint smtp_host = var.smtp_host smtp_port = var.smtp_port @@ -243,6 +265,28 @@ module "checkmarx-one-install" { vpc_id = module.vpc.vpc_id } +provider "kubernetes" { + host = module.checkmarx-one.cluster_endpoint + cluster_ca_certificate = base64decode(module.checkmarx-one.cluster_certificate_authority_data) + exec { + api_version = "client.authentication.k8s.io/v1beta1" + args = ["eks", "get-token", "--cluster-name", module.checkmarx-one.cluster_name] + command = "aws" + } +} + +provider "helm" { + kubernetes { + host = module.checkmarx-one.cluster_endpoint + cluster_ca_certificate = base64decode(module.checkmarx-one.cluster_certificate_authority_data) + exec { + api_version = "client.authentication.k8s.io/v1beta1" + args = ["eks", "get-token", "--cluster-name", module.checkmarx-one.cluster_name] + command = "aws" + } + } +} + output "cxone1" { value = module.checkmarx-one.eks diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf index 001b96d..c22d282 100644 --- a/examples/full/variables-cxone.tf +++ b/examples/full/variables-cxone.tf @@ -505,6 +505,64 @@ variable "db_apply_immediately" { } +#****************************************************************************** +# RDS - Analytics - Configuration +#****************************************************************************** + +variable "analytics_db_instance_class" { + description = "The aurora postgres instance class." + type = string + default = "db.r6g.xlarge" +} + +variable "analytics_db_final_snapshot_identifier" { + description = "Identifer for a final DB snapshot for the analytics database. Required when db_skip_final_snapshot is false.." + type = string + default = null +} + +variable "analytics_db_snapshot_identifer" { + description = "The snapshot identifier to restore the anatlytics database from." + type = string + default = null +} + +variable "analytics_db_cluster_db_instance_parameter_group_name" { + type = string + default = null + description = "The name of the DB Cluster parameter group to use." +} + +variable "analytics_db_master_user_password" { + description = "The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it." + type = string + default = null +} + +variable "analytics_db_instances" { + type = map(any) + description = "The DB instance configuration" + default = { + writer = {} + replica1 = {} + } +} + +variable "analytics_db_serverlessv2_scaling_configuration" { + description = "The serverless v2 scaling minimum and maximum." + type = object({ + min_capacity = number + max_capacity = number + }) + default = { + min_capacity = 0.5 + max_capacity = 32 + } +} + + + + #****************************************************************************** # Elasticache Configuration #****************************************************************************** diff --git a/helm/database-preparation/.helmignore b/helm/database-preparation/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/database-preparation/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/database-preparation/Chart.yaml b/helm/database-preparation/Chart.yaml new file mode 100644 index 0000000..d3d76b7 --- /dev/null +++ b/helm/database-preparation/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: database-preparation +description: A Helm chart for Kubernetes to prepare Analytics database +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/helm/database-preparation/templates/_helpers.tpl b/helm/database-preparation/templates/_helpers.tpl new file mode 100644 index 0000000..d7de703 --- /dev/null +++ b/helm/database-preparation/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "database-preparation.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "database-preparation.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "database-preparation.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "database-preparation.labels" -}} +helm.sh/chart: {{ include "database-preparation.chart" . }} +{{ include "database-preparation.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "database-preparation.selectorLabels" -}} +app.kubernetes.io/name: {{ include "database-preparation.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "database-preparation.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "database-preparation.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/database-preparation/templates/analytics-database-preparation.yaml b/helm/database-preparation/templates/analytics-database-preparation.yaml new file mode 100644 index 0000000..b378e56 --- /dev/null +++ b/helm/database-preparation/templates/analytics-database-preparation.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rds.analytics_enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }} + namespace: {{ .Release.Namespace }} +spec: + ttlSecondsAfterFinished: 100 + backoffLimit: 3 + template: + spec: + containers: + - name: analytics-database-preparation + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/bash", "-c"] + args: + - > + echo Creating analytics database...; + export PGPASSWORD={{ .Values.rds.masterPassword | quote }}; + echo "SELECT 'CREATE DATABASE $rds_analytics_db_name' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$rds_analytics_db_name')\gexec" | psql -h $rds_writer -d postgres -U $rds_master_username + env: + - name: rds_writer + value: {{ .Values.rds.writer_endpoint }} + - name: rds_master_username + value: {{ .Values.rds.masterUsername }} + - name: rds_app_user_password + value: {{ .Values.rds.appUserPassword | quote }} + - name: rds_analytics_db_name + value: {{ .Values.rds.analytics_db_name | quote }} + + {{- if .Values.imagePullSecrets }} + imagePullSecrets: + - name: {{ .Values.imagePullSecrets }} + {{- end }} + restartPolicy: Never +{{- end }} \ No newline at end of file diff --git a/helm/database-preparation/templates/byor-database-preparation.yaml b/helm/database-preparation/templates/byor-database-preparation.yaml new file mode 100644 index 0000000..0653638 --- /dev/null +++ b/helm/database-preparation/templates/byor-database-preparation.yaml @@ -0,0 +1,37 @@ +{{- if .Values.rds.byor_enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }} + namespace: {{ .Release.Namespace }} +spec: + ttlSecondsAfterFinished: 100 + backoffLimit: 3 + template: + spec: + containers: + - name: byor-database-preparation + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/bash", "-c"] + args: + - > + echo Creating byor database...; + export PGPASSWORD={{ .Values.rds.masterPassword | quote }}; + echo "SELECT 'CREATE DATABASE $rds_byor_db_name' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$rds_byor_db_name')\gexec" | psql -h $rds_writer -d postgres -U $rds_master_username + env: + - name: rds_writer + value: {{ .Values.rds.writer_endpoint }} + - name: rds_master_username + value: {{ .Values.rds.masterUsername }} + - name: rds_app_user_password + value: {{ .Values.rds.appUserPassword | quote }} + - name: rds_byor_db_name + value: {{ .Values.rds.byor_db_name | quote }} + + {{- if .Values.imagePullSecrets }} + imagePullSecrets: + - name: {{ .Values.imagePullSecrets }} + {{- end }} + restartPolicy: Never +{{- end }} diff --git a/helm/database-preparation/values.yaml b/helm/database-preparation/values.yaml new file mode 100644 index 0000000..4938899 --- /dev/null +++ b/helm/database-preparation/values.yaml @@ -0,0 +1,20 @@ +# Default values for database-preparation. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: postgres + pullPolicy: Always + tag: latest + imagePullSecrets: "" + +rds: + analytics_enabled: false + byor_enabled: false + writer_endpoint: "" + masterUsername: "" + masterPassword: "" + analytics_db_name: "analytics" + byor_db_name: "byor" diff --git a/modules/cxone-install/main.tf b/modules/cxone-install/main.tf index 96c7e98..0050c0f 100644 --- a/modules/cxone-install/main.tf +++ b/modules/cxone-install/main.tf @@ -31,6 +31,12 @@ resource "local_file" "kots_config" { postgres_password = var.postgres_password #jsondecode(data.aws_secretsmanager_secret_version.rds_secret.secret_string)["password"] postgres_db = var.postgres_database_name + # RDS - Analytics + analytics_postgres_host = var.analytics_postgres_host + analytics_postgres_user = var.analytics_postgres_user + analytics_postgres_password = var.analytics_postgres_password #jsondecode(data.aws_secretsmanager_secret_version.rds_secret.secret_string)["password"] + analytics_postgres_db_name = var.analytics_postgres_database_name + # Redis redis_address = var.redis_address diff --git a/modules/cxone-install/variables.tf b/modules/cxone-install/variables.tf index 5234afc..39f4a73 100644 --- a/modules/cxone-install/variables.tf +++ b/modules/cxone-install/variables.tf @@ -155,6 +155,27 @@ variable "postgres_database_name" { default = "ast" } +variable "analytics_postgres_host" { + type = string + description = "The endpoint for the analytics RDS server." +} + +variable "analytics_postgres_database_name" { + type = string + description = "The name of the analytics database." +} + +variable "analytics_postgres_user" { + type = string + description = "The user name for the analytics RDS server." + default = "ast" +} + +variable "analytics_postgres_password" { + type = string + description = "The user name for the analytics RDS server." +} + variable "redis_address" { type = string description = "The redis endpoint." diff --git a/rds-analytics.tf b/rds-analytics.tf new file mode 100644 index 0000000..09386ce --- /dev/null +++ b/rds-analytics.tf @@ -0,0 +1,107 @@ +module "rds-analytics" { + source = "terraform-aws-modules/rds-aurora/aws" + version = "9.3.1" + create = var.db_create + + name = "${var.deployment_id}-analytics" + engine = "aurora-postgresql" + engine_version = var.db_engine_version + allow_major_version_upgrade = var.db_allow_major_version_upgrade + engine_mode = "provisioned" + instance_class = var.analytics_db_instance_class + vpc_id = var.vpc_id + kms_key_id = var.kms_key_arn + db_subnet_group_name = aws_db_subnet_group.main.name + storage_encrypted = true + apply_immediately = var.db_apply_immediately + skip_final_snapshot = var.db_skip_final_snapshot + final_snapshot_identifier = var.analytics_db_final_snapshot_identifier + auto_minor_version_upgrade = var.db_auto_minor_version_upgrade + iam_database_authentication_enabled = false + snapshot_identifier = var.analytics_db_snapshot_identifer + monitoring_interval = var.db_monitoring_interval + performance_insights_enabled = var.db_performance_insights_enabled + performance_insights_retention_period = var.db_performance_insights_retention_period + db_cluster_db_instance_parameter_group_name = var.analytics_db_cluster_db_instance_parameter_group_name + master_username = "analytics" + database_name = "analytics" + master_password = var.analytics_db_master_user_password + manage_master_user_password = false + port = var.db_port + deletion_protection = var.db_deletion_protection + enabled_cloudwatch_logs_exports = ["postgresql"] + security_group_rules = { + ingress_from_vpc = { + cidr_blocks = data.aws_vpc.main.cidr_block_associations[*].cidr_block + } + } + serverlessv2_scaling_configuration = var.analytics_db_instance_class == "db.serverless" ? var.analytics_db_serverlessv2_scaling_configuration : {} + instances = var.analytics_db_instances +} + + +module "rds-proxy-analytics" { + source = "terraform-aws-modules/rds-proxy/aws" + version = "3.1.0" + create = var.db_create && var.db_create_rds_proxy + + name = var.deployment_id + vpc_subnet_ids = var.db_subnets + vpc_security_group_ids = [module.rds_proxy_sg.security_group_id] + endpoints = { + read_write = { + name = "read-write-endpoint" + vpc_subnet_ids = var.db_subnets + vpc_security_group_ids = [module.rds_proxy_sg.security_group_id] + }, + read_only = { + name = "read-only-endpoint" + vpc_subnet_ids = var.db_subnets + vpc_security_group_ids = [module.rds_proxy_sg.security_group_id] + target_role = "READ_ONLY" + } + } + + auth = { + "root" = { + description = "Cluster generated master user password" + secret_arn = var.db_create ? (var.analytics_db_master_user_password == null ? module.rds-analytics.cluster_master_user_secret[0].secret_arn : var.analytics_db_master_user_password) : null + auth_sceme = "SECRETS" + iam_auth = "DISABLED" + } + } + + engine_family = "POSTGRESQL" + debug_logging = true + + # Target Aurora cluster + target_db_cluster = true + db_cluster_identifier = module.rds-analytics.cluster_id + +} + + +output "analytics_db_endpoint" { + value = var.db_create_rds_proxy ? module.rds-proxy-analytics.db_proxy_endpoints.read_write.endpoint : module.rds-analytics.cluster_endpoint +} + +output "analytics_db_port" { + value = module.rds-analytics.cluster_port +} + +output "analytics_db_reader_endpoint" { + value = var.db_create_rds_proxy ? module.rds-proxy-analytics.db_proxy_endpoints.read_only.endpoint : module.rds-analytics.cluster_reader_endpoint +} + +output "analytics_db_database_name" { + value = module.rds-analytics.cluster_database_name +} + +output "analytics_db_master_username" { + value = module.rds-analytics.cluster_master_username +} + +output "analytics_db_master_password" { + value = module.rds-analytics.cluster_master_password + sensitive = true +} \ No newline at end of file diff --git a/rds-databases.tf b/rds-databases.tf new file mode 100644 index 0000000..b5e5028 --- /dev/null +++ b/rds-databases.tf @@ -0,0 +1,53 @@ +resource "helm_release" "analytics-rds-database-preparation" { + + depends_on = [ + module.rds, + module.rds-analytics, + module.eks + ] + + name = "database-preparation" + chart = "${path.module}/helm/database-preparation" + version = "0.1.0" + + # Set the namespace to install the release into + namespace = "ast" + create_namespace = true + set { + name = "rds.analytics_enabled" + value = "false" + } + + set { + name = "rds.byor_enabled" + value = "true" + } + + set { + name = "rds.writer_endpoint" + value = module.rds.cluster_endpoint + } + + set { + name = "rds.masterUsername" + value = "ast" + } + + set { + name = "rds.masterPassword" + value = var.db_master_user_password + } + + set { + name = "rds.analytics_db_name" + value = "analytics" + } + + set { + name = "rds.byor_db_name" + value = "byor" + } + + # Wait for the release to be deployed + wait = true +} \ No newline at end of file diff --git a/variables.tf b/variables.tf index 735bffa..dadb850 100644 --- a/variables.tf +++ b/variables.tf @@ -515,6 +515,62 @@ variable "db_apply_immediately" { } +#****************************************************************************** +# RDS - Analytics - Configuration +#****************************************************************************** + +variable "analytics_db_instance_class" { + description = "The aurora postgres instance class." + type = string + default = "db.r6g.xlarge" +} + +variable "analytics_db_final_snapshot_identifier" { + description = "Identifer for a final DB snapshot for the analytics database. Required when db_skip_final_snapshot is false.." + type = string + default = null +} + +variable "analytics_db_snapshot_identifer" { + description = "The snapshot identifier to restore the anatlytics database from." + type = string + default = null +} + +variable "analytics_db_cluster_db_instance_parameter_group_name" { + type = string + default = null + description = "The name of the DB Cluster parameter group to use." +} + +variable "analytics_db_master_user_password" { + description = "The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it." + type = string + default = null +} + +variable "analytics_db_instances" { + type = map(any) + description = "The DB instance configuration" + default = { + writer = {} + replica1 = {} + } +} + +variable "analytics_db_serverlessv2_scaling_configuration" { + description = "The serverless v2 scaling minimum and maximum." + type = object({ + min_capacity = number + max_capacity = number + }) + default = { + min_capacity = 0.5 + max_capacity = 32 + } +} + + #****************************************************************************** # Elasticache Configuration #****************************************************************************** From 3fc09270a38f1c2238d190b1b3890380aae66502 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 16 May 2024 09:33:11 -0400 Subject: [PATCH 23/57] doc updates for 3.12 --- README.md | 29 +++++++++++++++++++++++++++-- examples/full/README.md | 29 +++++++++++++++++++++++++---- examples/full/main.tf | 13 +++++++++++++ main.tf | 12 ++++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 main.tf diff --git a/README.md b/README.md index 41d5af5..63c6163 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ This repo contains a module for deploying [Checkmarx One](https://checkmarx.com/ # Module documentation ## Requirements -No requirements. +| Name | Version | +|------|---------| +| [helm](#requirement\_helm) | ~> 2.13.0 | +| [kubernetes](#requirement\_kubernetes) | ~> 2.30.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | n/a | +| [helm](#provider\_helm) | ~> 2.13.0 | | [random](#provider\_random) | n/a | ## Modules @@ -28,7 +32,9 @@ No requirements. | [karpenter](#module\_karpenter) | terraform-aws-modules/eks/aws//modules/karpenter | 20.8.5 | | [load\_balancer\_controller\_irsa](#module\_load\_balancer\_controller\_irsa) | terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks | 5.39.0 | | [rds](#module\_rds) | terraform-aws-modules/rds-aurora/aws | 9.3.1 | +| [rds-analytics](#module\_rds-analytics) | terraform-aws-modules/rds-aurora/aws | 9.3.1 | | [rds-proxy](#module\_rds-proxy) | terraform-aws-modules/rds-proxy/aws | 3.1.0 | +| [rds-proxy-analytics](#module\_rds-proxy-analytics) | terraform-aws-modules/rds-proxy/aws | 3.1.0 | | [rds\_proxy\_sg](#module\_rds\_proxy\_sg) | terraform-aws-modules/security-group/aws | 5.1.2 | | [s3\_bucket](#module\_s3\_bucket) | terraform-aws-modules/s3-bucket/aws | 4.1.1 | @@ -44,6 +50,7 @@ No requirements. | [aws_elasticache_subnet_group.redis](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticache_subnet_group) | resource | | [aws_elasticsearch_domain.es](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticsearch_domain) | resource | | [aws_iam_policy.s3_bucket_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| helm_release.analytics-rds-database-preparation | resource | | [random_string.random_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_role.karpenter](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_role) | data source | @@ -55,6 +62,13 @@ No requirements. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [analytics\_db\_cluster\_db\_instance\_parameter\_group\_name](#input\_analytics\_db\_cluster\_db\_instance\_parameter\_group\_name) | The name of the DB Cluster parameter group to use. | `string` | `null` | no | +| [analytics\_db\_final\_snapshot\_identifier](#input\_analytics\_db\_final\_snapshot\_identifier) | Identifer for a final DB snapshot for the analytics database. Required when db\_skip\_final\_snapshot is false.. | `string` | `null` | no | +| [analytics\_db\_instance\_class](#input\_analytics\_db\_instance\_class) | The aurora postgres instance class. | `string` | `"db.r6g.xlarge"` | no | +| [analytics\_db\_instances](#input\_analytics\_db\_instances) | The DB instance configuration | `map(any)` |
{
"replica1": {},
"writer": {}
}
| no | +| [analytics\_db\_master\_user\_password](#input\_analytics\_db\_master\_user\_password) | The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it. | `string` | `null` | no | +| [analytics\_db\_serverlessv2\_scaling\_configuration](#input\_analytics\_db\_serverlessv2\_scaling\_configuration) | The serverless v2 scaling minimum and maximum. |
object({
min_capacity = number
max_capacity = number
})
|
{
"max_capacity": 32,
"min_capacity": 0.5
}
| no | +| [analytics\_db\_snapshot\_identifer](#input\_analytics\_db\_snapshot\_identifer) | The snapshot identifier to restore the anatlytics database from. | `string` | `null` | no | | [aws\_ebs\_csi\_driver\_version](#input\_aws\_ebs\_csi\_driver\_version) | The version of the EKS EBS CSI Addon. | `string` | n/a | yes | | [coredns\_version](#input\_coredns\_version) | The version of the EKS Core DNS Addon. | `string` | n/a | yes | | [db\_allow\_major\_version\_upgrade](#input\_db\_allow\_major\_version\_upgrade) | Allows major version upgrades. | `bool` | `false` | no | @@ -100,16 +114,20 @@ No requirements. | [ec\_subnets](#input\_ec\_subnets) | The subnets to deploy Elasticache into. | `list(string)` | n/a | yes | | [eks\_administrator\_principals](#input\_eks\_administrator\_principals) | The ARNs of the IAM roles for administrator access to EKS. |
list(object({
name = string
principal_arn = string
}))
| `[]` | no | | [eks\_cluster\_endpoint\_public\_access\_cidrs](#input\_eks\_cluster\_endpoint\_public\_access\_cidrs) | List of CIDR blocks which can access the Amazon EKS public API server endpoint | `list(string)` |
[
"0.0.0.0/0"
]
| no | +| [eks\_cluster\_security\_group\_additional\_rules](#input\_eks\_cluster\_security\_group\_additional\_rules) | Additional security group rules for the EKS cluster | `any` | `{}` | no | | [eks\_create](#input\_eks\_create) | Enables the EKS resource creation | `bool` | `true` | no | | [eks\_create\_cluster\_autoscaler\_irsa](#input\_eks\_create\_cluster\_autoscaler\_irsa) | Enables creation of cluster autoscaler IAM role. | `bool` | `true` | no | | [eks\_create\_external\_dns\_irsa](#input\_eks\_create\_external\_dns\_irsa) | Enables creation of external dns IAM role. | `bool` | `true` | no | | [eks\_create\_karpenter](#input\_eks\_create\_karpenter) | Enables creation of Karpenter resources. | `bool` | `false` | no | | [eks\_create\_load\_balancer\_controller\_irsa](#input\_eks\_create\_load\_balancer\_controller\_irsa) | Enables creation of load balancer controller IAM role. | `bool` | `true` | no | +| [eks\_enable\_custom\_networking](#input\_eks\_enable\_custom\_networking) | Enables custom networking for the EKS VPC CNI. When true, custom networking is enabled with `ENI_CONFIG_LABEL_DEF` = `topology.kubernetes.io/zone` and ENIConfig resources must be created. | `bool` | `false` | no | | [eks\_enable\_externalsnat](#input\_eks\_enable\_externalsnat) | Enables [External SNAT](https://docs.aws.amazon.com/eks/latest/userguide/external-snat.html) for the EKS VPC CNI. When true, the EKS pods must have a route to a NAT Gateway for outbound communication. | `bool` | `false` | no | | [eks\_enable\_fargate](#input\_eks\_enable\_fargate) | Enables Fargate profiles for the karpenter and kube-system namespaces. | `bool` | `false` | no | | [eks\_node\_additional\_security\_group\_ids](#input\_eks\_node\_additional\_security\_group\_ids) | Additional security group ids to attach to EKS nodes. | `list(string)` | `[]` | no | | [eks\_node\_groups](#input\_eks\_node\_groups) | n/a |
list(object({
name = string
min_size = string
desired_size = string
max_size = string
volume_type = optional(string, "gp3")
disk_size = optional(number, 200)
disk_iops = optional(number, 3000)
disk_throughput = optional(number, 125)
device_name = optional(string, "/dev/xvda")
instance_types = list(string)
capacity_type = optional(string, "ON_DEMAND")
labels = optional(map(string), {})
taints = optional(map(object({ key = string, value = string, effect = string })), {})
}))
|
[
{
"desired_size": 3,
"instance_types": [
"c5.4xlarge"
],
"max_size": 9,
"min_size": 3,
"name": "ast-app"
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"sast-engine": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.4xlarge"
],
"labels": {
"sast-engine-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.2xlarge"
],
"labels": {
"sast-engine-extra-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-extra-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-extra-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.4xlarge"
],
"labels": {
"sast-engine-xxl": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-xxl",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-xxl",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"kics-engine": "true"
},
"max_size": 100,
"min_size": 1,
"name": "kics-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "kics-engine",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"repostore": "true"
},
"max_size": 100,
"min_size": 1,
"name": "repostore",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "repostore",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"service": "sca-source-resolver"
},
"max_size": 100,
"min_size": 1,
"name": "sca-source-resolver",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "service",
"value": "sca-source-resolver"
}
}
}
]
| no | | [eks\_pod\_subnets](#input\_eks\_pod\_subnets) | The subnets to use for EKS pods. When specified, custom networking configuration is applied to the EKS cluster. | `list(string)` | n/a | yes | +| [eks\_post\_bootstrap\_user\_data](#input\_eks\_post\_bootstrap\_user\_data) | User data to insert after bootstrapping script. | `string` | `""` | no | +| [eks\_pre\_bootstrap\_user\_data](#input\_eks\_pre\_bootstrap\_user\_data) | User data to insert before bootstrapping script. | `string` | `""` | no | | [eks\_private\_endpoint\_enabled](#input\_eks\_private\_endpoint\_enabled) | Enables the EKS VPC private endpoint. | `bool` | `true` | no | | [eks\_public\_endpoint\_enabled](#input\_eks\_public\_endpoint\_enabled) | Enables the EKS public endpoint. | `bool` | `false` | no | | [eks\_subnets](#input\_eks\_subnets) | The subnets to deploy EKS into. | `list(string)` | n/a | yes | @@ -135,9 +153,17 @@ No requirements. | Name | Description | |------|-------------| +| [analytics\_db\_database\_name](#output\_analytics\_db\_database\_name) | n/a | +| [analytics\_db\_endpoint](#output\_analytics\_db\_endpoint) | n/a | +| [analytics\_db\_master\_password](#output\_analytics\_db\_master\_password) | n/a | +| [analytics\_db\_master\_username](#output\_analytics\_db\_master\_username) | n/a | +| [analytics\_db\_port](#output\_analytics\_db\_port) | n/a | +| [analytics\_db\_reader\_endpoint](#output\_analytics\_db\_reader\_endpoint) | n/a | | [bucket\_suffix](#output\_bucket\_suffix) | n/a | | [cluster\_autoscaler\_iam\_role\_arn](#output\_cluster\_autoscaler\_iam\_role\_arn) | n/a | +| [cluster\_certificate\_authority\_data](#output\_cluster\_certificate\_authority\_data) | n/a | | [cluster\_endpoint](#output\_cluster\_endpoint) | n/a | +| [cluster\_name](#output\_cluster\_name) | n/a | | [db\_database\_name](#output\_db\_database\_name) | n/a | | [db\_endpoint](#output\_db\_endpoint) | n/a | | [db\_master\_password](#output\_db\_master\_password) | n/a | @@ -155,7 +181,6 @@ No requirements. | [load\_balancer\_controller\_iam\_role\_arn](#output\_load\_balancer\_controller\_iam\_role\_arn) | n/a | | [nodegroup\_iam\_role\_name](#output\_nodegroup\_iam\_role\_name) | n/a | | [s3\_bucket\_name\_suffix](#output\_s3\_bucket\_name\_suffix) | n/a | - # Regional Considerations ## GovCloud diff --git a/examples/full/README.md b/examples/full/README.md index 8929673..0a9df38 100644 --- a/examples/full/README.md +++ b/examples/full/README.md @@ -7,11 +7,11 @@ The project configures the VPC, KMS, ACM, and other basic environment resources, Consult the [`example.auto.tfvars`](./example.auto.tfvars) for a full listing of what can be configured in this example, and the `terraform-aws-cxone module`. # Installation -This example generates a Makefile in the project folder after Terraform finishes running. The Makefile has several targets that can help bootstrap your environment with the CxOne application. +This example generates a `Makefile` in the project folder after Terraform finishes running. The Makefile has several targets that can help bootstrap your environment with the CxOne application. The `kots.$DEPLOYMENT_ID.yaml` file is also automatically generated and can be reviewed & modified after Terraform finishes. -Run these commands to bootstrap your cluster. +Run these commands to bootstrap your cluster using the generated files. Update your kubectl context: @@ -35,17 +35,27 @@ Install the load balancer controller (wait approx 1 minute after cluster autosca make install-load-balancer-controller ``` +Install the external dns if you're using it (wait approx 1 minute after cluster autoscaler to avoid webhook issues): +```sh +make install-external-dns +``` + +Manually review your kots configuration file and make any adjustments, if needed. + Install the Checkmarx One application: ```sh make kots-install ``` -You can also build your own bootstrapping process using the Makefile as a reference. +You can also build your own installation process using your organization's tooling and techniques using the Makefile as a reference. # Module Documentation ## Requirements -No requirements. +| Name | Version | +|------|---------| +| [helm](#requirement\_helm) | ~> 2.13.0 | +| [kubernetes](#requirement\_kubernetes) | ~> 2.30.0 | ## Providers @@ -68,6 +78,7 @@ No requirements. | Name | Type | |------|------| | [aws_kms_key.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | +| [random_password.analytics_db](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [random_password.cxone_admin](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [random_password.db](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [random_password.elasticsearch](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | @@ -83,6 +94,13 @@ No requirements. |------|-------------|------|---------|:--------:| | [acm\_certificate\_arn](#input\_acm\_certificate\_arn) | The ARN to the SSL certificate in AWS ACM to use for securing the load balancer | `string` | `null` | no | | [additional\_suricata\_rules](#input\_additional\_suricata\_rules) | Additional [suricata rules](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html) rules to use in the network firewall. When provided these rules will be appended to the default rules prior to the default drop rule. | `string` | `""` | no | +| [analytics\_db\_cluster\_db\_instance\_parameter\_group\_name](#input\_analytics\_db\_cluster\_db\_instance\_parameter\_group\_name) | The name of the DB Cluster parameter group to use. | `string` | `null` | no | +| [analytics\_db\_final\_snapshot\_identifier](#input\_analytics\_db\_final\_snapshot\_identifier) | Identifer for a final DB snapshot for the analytics database. Required when db\_skip\_final\_snapshot is false.. | `string` | `null` | no | +| [analytics\_db\_instance\_class](#input\_analytics\_db\_instance\_class) | The aurora postgres instance class. | `string` | `"db.r6g.xlarge"` | no | +| [analytics\_db\_instances](#input\_analytics\_db\_instances) | The DB instance configuration | `map(any)` |
{
"replica1": {},
"writer": {}
}
| no | +| [analytics\_db\_master\_user\_password](#input\_analytics\_db\_master\_user\_password) | The master user password for RDS. Specify to explicitly set the password otherwise RDS will be allowed to manage it. | `string` | `null` | no | +| [analytics\_db\_serverlessv2\_scaling\_configuration](#input\_analytics\_db\_serverlessv2\_scaling\_configuration) | The serverless v2 scaling minimum and maximum. |
object({
min_capacity = number
max_capacity = number
})
|
{
"max_capacity": 32,
"min_capacity": 0.5
}
| no | +| [analytics\_db\_snapshot\_identifer](#input\_analytics\_db\_snapshot\_identifer) | The snapshot identifier to restore the anatlytics database from. | `string` | `null` | no | | [aws\_ebs\_csi\_driver\_version](#input\_aws\_ebs\_csi\_driver\_version) | The version of the EKS EBS CSI Addon. | `string` | n/a | yes | | [coredns\_version](#input\_coredns\_version) | The version of the EKS Core DNS Addon. | `string` | n/a | yes | | [create\_interface\_endpoints](#input\_create\_interface\_endpoints) | Enables creation of the [interface endpoints](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-aws-services.html) specified in `interface_vpc_endpoints` | `bool` | `true` | no | @@ -134,10 +152,13 @@ No requirements. | [eks\_create\_external\_dns\_irsa](#input\_eks\_create\_external\_dns\_irsa) | Enables creation of external dns IAM role. | `bool` | `true` | no | | [eks\_create\_karpenter](#input\_eks\_create\_karpenter) | Enables creation of Karpenter resources. | `bool` | `false` | no | | [eks\_create\_load\_balancer\_controller\_irsa](#input\_eks\_create\_load\_balancer\_controller\_irsa) | Enables creation of load balancer controller IAM role. | `bool` | `true` | no | +| [eks\_enable\_custom\_networking](#input\_eks\_enable\_custom\_networking) | Enables custom networking for the EKS VPC CNI. When true, custom networking is enabled with `ENI_CONFIG_LABEL_DEF` = `topology.kubernetes.io/zone` and ENIConfig resources must be created. | `bool` | `false` | no | | [eks\_enable\_externalsnat](#input\_eks\_enable\_externalsnat) | Enables [External SNAT](https://docs.aws.amazon.com/eks/latest/userguide/external-snat.html) for the EKS VPC CNI. When true, the EKS pods must have a route to a NAT Gateway for outbound communication. | `bool` | `false` | no | | [eks\_enable\_fargate](#input\_eks\_enable\_fargate) | Enables Fargate profiles for the karpenter and kube-system namespaces. | `bool` | `false` | no | | [eks\_node\_additional\_security\_group\_ids](#input\_eks\_node\_additional\_security\_group\_ids) | Additional security group ids to attach to EKS nodes. | `list(string)` | `[]` | no | | [eks\_node\_groups](#input\_eks\_node\_groups) | n/a |
list(object({
name = string
min_size = string
desired_size = string
max_size = string
volume_type = optional(string, "gp3")
disk_size = optional(number, 200)
disk_iops = optional(number, 3000)
disk_throughput = optional(number, 125)
device_name = optional(string, "/dev/xvda")
instance_types = list(string)
capacity_type = optional(string, "ON_DEMAND")
labels = optional(map(string), {})
taints = optional(map(object({ key = string, value = string, effect = string })), {})
}))
|
[
{
"desired_size": 3,
"instance_types": [
"c5.4xlarge"
],
"max_size": 9,
"min_size": 3,
"name": "ast-app"
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"sast-engine": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.4xlarge"
],
"labels": {
"sast-engine-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.2xlarge"
],
"labels": {
"sast-engine-extra-large": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-extra-large",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-extra-large",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"r5.4xlarge"
],
"labels": {
"sast-engine-xxl": "true"
},
"max_size": 100,
"min_size": 0,
"name": "sast-engine-xxl",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "sast-engine-xxl",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"kics-engine": "true"
},
"max_size": 100,
"min_size": 1,
"name": "kics-engine",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "kics-engine",
"value": "true"
}
}
},
{
"desired_size": 1,
"instance_types": [
"c5.2xlarge"
],
"labels": {
"repostore": "true"
},
"max_size": 100,
"min_size": 1,
"name": "repostore",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "repostore",
"value": "true"
}
}
},
{
"desired_size": 0,
"instance_types": [
"m5.2xlarge"
],
"labels": {
"service": "sca-source-resolver"
},
"max_size": 100,
"min_size": 0,
"name": "sca-source-resolver",
"taints": {
"dedicated": {
"effect": "NO_SCHEDULE",
"key": "service",
"value": "sca-source-resolver"
}
}
}
]
| no | +| [eks\_post\_bootstrap\_user\_data](#input\_eks\_post\_bootstrap\_user\_data) | User data to insert after bootstrapping script. | `string` | `""` | no | +| [eks\_pre\_bootstrap\_user\_data](#input\_eks\_pre\_bootstrap\_user\_data) | User data to insert before bootstrapping script. | `string` | `""` | no | | [eks\_private\_endpoint\_enabled](#input\_eks\_private\_endpoint\_enabled) | Enables the EKS VPC private endpoint. | `bool` | `true` | no | | [eks\_public\_endpoint\_enabled](#input\_eks\_public\_endpoint\_enabled) | Enables the EKS public endpoint. | `bool` | `false` | no | | [eks\_version](#input\_eks\_version) | The version of the EKS Cluster (e.g. 1.27) | `string` | n/a | yes | diff --git a/examples/full/main.tf b/examples/full/main.tf index 935e319..8ac2494 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -265,6 +265,19 @@ module "checkmarx-one-install" { vpc_id = module.vpc.vpc_id } +terraform { + required_providers { + helm = { + source = "registry.terraform.io/hashicorp/helm" + version = "~> 2.13.0" + } + kubernetes = { + source = "registry.terraform.io/hashicorp/kubernetes" + version = "~> 2.30.0" + } + } +} + provider "kubernetes" { host = module.checkmarx-one.cluster_endpoint cluster_ca_certificate = base64decode(module.checkmarx-one.cluster_certificate_authority_data) diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..c2d3065 --- /dev/null +++ b/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + helm = { + source = "registry.terraform.io/hashicorp/helm" + version = "~> 2.13.0" + } + kubernetes = { + source = "registry.terraform.io/hashicorp/kubernetes" + version = "~> 2.30.0" + } + } +} \ No newline at end of file From f519de19dd192645cd6be8d350bb5f4fe59c2870 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 6 Jun 2024 01:00:07 -0400 Subject: [PATCH 24/57] Fix SCA results processor errors --- modules/cxone-install/kots.config.aws.reference.yaml.tftpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/cxone-install/kots.config.aws.reference.yaml.tftpl b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl index f379356..c8325e4 100644 --- a/modules/cxone-install/kots.config.aws.reference.yaml.tftpl +++ b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl @@ -27,8 +27,9 @@ spec: value: "1" # Enable the local flow for SCA Global inventory + # Should always be "1", don't change unless advised by Cx enable_sca_local_flow_for_global_inventory: - value: "0" + value: "1" # Enables using a dedicated node group just for SCA components. # If enabled, the node group must exists with the correct labels and taint. From 4a84fd197e853b6f606370fc0d4e9174e911a609 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 6 Jun 2024 01:38:05 -0400 Subject: [PATCH 25/57] fix template names --- .../templates/analytics-database-preparation.yaml | 2 +- .../templates/byor-database-preparation.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/database-preparation/templates/analytics-database-preparation.yaml b/helm/database-preparation/templates/analytics-database-preparation.yaml index b378e56..85bead9 100644 --- a/helm/database-preparation/templates/analytics-database-preparation.yaml +++ b/helm/database-preparation/templates/analytics-database-preparation.yaml @@ -2,7 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: {{ .Release.Name }} + name: {{ printf "%s-%s" .Release.Name "analytics" }} namespace: {{ .Release.Namespace }} spec: ttlSecondsAfterFinished: 100 diff --git a/helm/database-preparation/templates/byor-database-preparation.yaml b/helm/database-preparation/templates/byor-database-preparation.yaml index 0653638..0ea8dfb 100644 --- a/helm/database-preparation/templates/byor-database-preparation.yaml +++ b/helm/database-preparation/templates/byor-database-preparation.yaml @@ -2,7 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: {{ .Release.Name }} + name: {{ printf "%s-%s" .Release.Name "byor" }} namespace: {{ .Release.Namespace }} spec: ttlSecondsAfterFinished: 100 From 0b669309680502993f5c42ba4d5ffa41bd08320c Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 6 Jun 2024 01:38:22 -0400 Subject: [PATCH 26/57] increase stability of destroying env --- modules/cxone-install/destroy-load-balancer.sh.tftpl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/cxone-install/destroy-load-balancer.sh.tftpl b/modules/cxone-install/destroy-load-balancer.sh.tftpl index 84a0e2e..0ae9b62 100644 --- a/modules/cxone-install/destroy-load-balancer.sh.tftpl +++ b/modules/cxone-install/destroy-load-balancer.sh.tftpl @@ -10,14 +10,18 @@ DEPLOYMENT_ID=${deployment_id} # Clean up load balancer ELB_ARN=$(aws elbv2 describe-load-balancers | jq -r '.LoadBalancers[].LoadBalancerArn' | xargs -I {} aws elbv2 describe-tags --resource-arns {} --query "TagDescriptions[?Tags[?Key=='elbv2.k8s.aws/cluster' &&Value=='$${DEPLOYMENT_ID}']].ResourceArn" --output text) aws elbv2 delete-load-balancer --load-balancer-arn $${ELB_ARN} +sleep 10 # Clean up target groups aws elbv2 describe-target-groups | jq -r '.TargetGroups[].TargetGroupArn' | xargs -I {} aws elbv2 describe-tags --resource-arns {} --query "TagDescriptions[?Tags[?Key=='elbv2.k8s.aws/cluster' &&Value=='$${DEPLOYMENT_ID}']].ResourceArn" --output text | xargs -I {} aws elbv2 delete-target-group --target-group-arn {} +sleep 10 # Clean up security groups # The "Node" security group will have references to the ELB security groups, so remove all the rules to allow groups to be deleted successfully NODE_SG_ID=$(aws ec2 describe-security-groups --filters Name=tag:Name,Values=$${DEPLOYMENT_ID}-node --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n") aws ec2 revoke-security-group-ingress --group-id $${NODE_SG_ID} --ip-permissions "`aws ec2 describe-security-groups --output json --group-ids $${NODE_SG_ID} --query "SecurityGroups[0].IpPermissions"`" +sleep 10 # Delete the ELB security groups aws ec2 describe-security-groups --filters Name=tag:elbv2.k8s.aws/cluster,Values=$${DEPLOYMENT_ID} --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n" | xargs -I {} aws ec2 delete-security-group --group-id {} +sleep 10 \ No newline at end of file From 6b8242ebefe14def154cd77efc3938677adf0385 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 6 Jun 2024 02:06:07 -0400 Subject: [PATCH 27/57] external config of keys and encrypted ebs pvc support --- examples/full/main.tf | 1 + .../apply-storageclass-config.sh.tftpl | 2 + modules/cxone-install/keys.tf | 66 +++++++++++++++++++ .../kots.config.aws.reference.yaml.tftpl | 24 +++++++ modules/cxone-install/main.tf | 21 ++++++ modules/cxone-install/variables.tf | 52 ++++++++++++++- 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 modules/cxone-install/keys.tf diff --git a/examples/full/main.tf b/examples/full/main.tf index 8ac2494..f16ecc9 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -263,6 +263,7 @@ module "checkmarx-one-install" { availability_zones = module.vpc.azs pod_eniconfig = module.vpc.ENIConfig vpc_id = module.vpc.vpc_id + kms_key_arn = aws_kms_key.main.arn } terraform { diff --git a/modules/cxone-install/apply-storageclass-config.sh.tftpl b/modules/cxone-install/apply-storageclass-config.sh.tftpl index 456f669..696dd1a 100644 --- a/modules/cxone-install/apply-storageclass-config.sh.tftpl +++ b/modules/cxone-install/apply-storageclass-config.sh.tftpl @@ -14,6 +14,8 @@ cat < Date: Thu, 6 Jun 2024 14:02:01 -0400 Subject: [PATCH 28/57] Move CSI Driver to IRSA --- eks.tf | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/eks.tf b/eks.tf index 635c478..fba4f74 100644 --- a/eks.tf +++ b/eks.tf @@ -112,7 +112,6 @@ module "eks_node_iam_role" { role_name = "${var.deployment_id}-eks-nodes" role_requires_mfa = false custom_role_policy_arns = [ - "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy", "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKS_CNI_Policy", "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy", @@ -141,6 +140,7 @@ resource "aws_iam_policy" "s3_bucket_access" { }) } + module "eks" { source = "terraform-aws-modules/eks/aws" version = "20.8.5" @@ -231,8 +231,9 @@ module "eks" { }) } aws-ebs-csi-driver = { - addon_version = var.aws_ebs_csi_driver_version - configuration_values = var.eks_enable_fargate ? local.ebs_csi_fargate_configuration_values : null + addon_version = var.aws_ebs_csi_driver_version + configuration_values = var.eks_enable_fargate ? local.ebs_csi_fargate_configuration_values : null + service_account_role_arn = module.ebs_csi_irsa[0].iam_role_arn } } create_kms_key = false @@ -294,6 +295,24 @@ resource "aws_autoscaling_group_tag" "cluster_autoscaler_taint" { } +module "ebs_csi_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "5.39.0" + count = var.eks_create ? 1 : 0 + + role_name = "ebs-csi-${var.deployment_id}" + role_description = "IRSA role for EBS CSI Driver" + attach_ebs_csi_policy = true + ebs_csi_kms_cmk_ids = [var.kms_key_arn] + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"] + } + } +} + + module "cluster_autoscaler_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" version = "5.39.0" From 8e1d0bacdcd1fd156ad5a406066fda12fd6906dc Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 6 Jun 2024 17:18:36 -0400 Subject: [PATCH 29/57] fix key generation --- modules/cxone-install/keys.tf | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/cxone-install/keys.tf b/modules/cxone-install/keys.tf index 8ed2496..9bd70f9 100644 --- a/modules/cxone-install/keys.tf +++ b/modules/cxone-install/keys.tf @@ -1,11 +1,13 @@ +# core_configuration_encryption_key is 64 character hex value resource "random_password" "core_configuration_encryption_key" { - count = var.core_configuration_encryption_key == null ? 1 : 0 - length = 64 - special = false - min_special = 0 - min_upper = 1 - min_lower = 1 - min_numeric = 1 + count = var.core_configuration_encryption_key == null ? 1 : 0 + length = 64 + special = true + override_special = "abcdef0123456789" + min_special = 64 + min_upper = 0 + min_lower = 0 + min_numeric = 0 } resource "random_password" "sca_client_secret" { @@ -57,7 +59,7 @@ resource "random_password" "integrations_repos_manager_github_tenant_key" { } resource "random_password" "integrations_repos_manager_gitlab_tenant_key" { count = var.integrations_repos_manager_gitlab_tenant_key == null ? 1 : 0 - length = 32 + length = 8 special = false min_special = 0 min_upper = 1 From acd5eff3567f8ff13b8624da229503af4b185cd6 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 11 Jun 2024 12:38:17 -0400 Subject: [PATCH 30/57] Update firewall rules for SCA EU region --- modules/inspection-vpc/firewall-rules.tf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index 873e24e..42bd103 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -148,10 +148,13 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api-sca.checkmarx pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.iam.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420058; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.api-sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420059; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420060; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.eu.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240611001; rev:1;) # Scan results buckets are used for SCA scan result syncing, and vary by the connected SCA region (e.g. NA, or EU) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prod-storage-1an26shc41yi3.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240420061; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prodeu-storage-1c25a060x93rl.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240611002; rev:1;) +# Codebashing pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.stagecodebashing.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240422001; rev:1;) -# upcoming features +# Upcoming features pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cx-sca-containers.es.us-east-1.aws.found.io"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:204290001; rev:1;) From 6cf863131c38503ef2786498b89d0851234caea9 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 11 Jun 2024 15:37:20 -0400 Subject: [PATCH 31/57] Temp rename file to fix case --- modules/cxone-install/{makefile.tftpl => Makefile2.tftpl} | 1 + 1 file changed, 1 insertion(+) rename modules/cxone-install/{makefile.tftpl => Makefile2.tftpl} (99%) diff --git a/modules/cxone-install/makefile.tftpl b/modules/cxone-install/Makefile2.tftpl similarity index 99% rename from modules/cxone-install/makefile.tftpl rename to modules/cxone-install/Makefile2.tftpl index 07c5f01..e6b759a 100644 --- a/modules/cxone-install/makefile.tftpl +++ b/modules/cxone-install/Makefile2.tftpl @@ -15,6 +15,7 @@ REGISTRY_PASSWORD = ??? TOTP = 123 + .PHONY: update-kubeconfig update-kubeconfig: aws eks update-kubeconfig --name $${EKS_CLUSTER_NAME} From 3797aa1f72d952aa372f8cd066397ccab651f1da Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 11 Jun 2024 15:38:05 -0400 Subject: [PATCH 32/57] Fix Makefile init casing --- modules/cxone-install/{Makefile2.tftpl => Makefile.tftpl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/cxone-install/{Makefile2.tftpl => Makefile.tftpl} (100%) diff --git a/modules/cxone-install/Makefile2.tftpl b/modules/cxone-install/Makefile.tftpl similarity index 100% rename from modules/cxone-install/Makefile2.tftpl rename to modules/cxone-install/Makefile.tftpl From 9694b3c7862d74d2fbd447a7e63379541f111c10 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Fri, 14 Jun 2024 12:08:47 -0400 Subject: [PATCH 33/57] set config on current version to avoid inadvertent upgrades. --- modules/cxone-install/Makefile.tftpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cxone-install/Makefile.tftpl b/modules/cxone-install/Makefile.tftpl index e6b759a..f01dd91 100644 --- a/modules/cxone-install/Makefile.tftpl +++ b/modules/cxone-install/Makefile.tftpl @@ -34,7 +34,7 @@ kots-install-from-airgap: .PHONY: kots-set-config kots-set-config: - kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy + kubectl kots set config ast -n $${NAMESPACE} --config-file $${KOTS_CONFIG_FILE} --deploy --current .PHONY: kots-get-config From 6c0c4e20bba8d04ead8e2e5462d1c5570021006c Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Fri, 14 Jun 2024 12:09:17 -0400 Subject: [PATCH 34/57] use single quotes on db password to avoid shell expansion on special characters --- .../templates/analytics-database-preparation.yaml | 2 +- .../templates/byor-database-preparation.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/database-preparation/templates/analytics-database-preparation.yaml b/helm/database-preparation/templates/analytics-database-preparation.yaml index 85bead9..3a2b2b1 100644 --- a/helm/database-preparation/templates/analytics-database-preparation.yaml +++ b/helm/database-preparation/templates/analytics-database-preparation.yaml @@ -17,7 +17,7 @@ spec: args: - > echo Creating analytics database...; - export PGPASSWORD={{ .Values.rds.masterPassword | quote }}; + export PGPASSWORD={{ .Values.rds.masterPassword | squote }}; echo "SELECT 'CREATE DATABASE $rds_analytics_db_name' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$rds_analytics_db_name')\gexec" | psql -h $rds_writer -d postgres -U $rds_master_username env: - name: rds_writer diff --git a/helm/database-preparation/templates/byor-database-preparation.yaml b/helm/database-preparation/templates/byor-database-preparation.yaml index 0ea8dfb..9fab220 100644 --- a/helm/database-preparation/templates/byor-database-preparation.yaml +++ b/helm/database-preparation/templates/byor-database-preparation.yaml @@ -17,7 +17,7 @@ spec: args: - > echo Creating byor database...; - export PGPASSWORD={{ .Values.rds.masterPassword | quote }}; + export PGPASSWORD={{ .Values.rds.masterPassword | squote }}; echo "SELECT 'CREATE DATABASE $rds_byor_db_name' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$rds_byor_db_name')\gexec" | psql -h $rds_writer -d postgres -U $rds_master_username env: - name: rds_writer From acbeab966f231728c2ef0a06f03af22193069a99 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 27 Jun 2024 09:51:32 -0400 Subject: [PATCH 35/57] Update default instance config Increased volume size to pass preflight checks. Increased instance size for repostore to ensure pod scheduling. --- variables.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variables.tf b/variables.tf index dadb850..a26a1ef 100644 --- a/variables.tf +++ b/variables.tf @@ -206,7 +206,7 @@ variable "eks_node_groups" { desired_size = string max_size = string volume_type = optional(string, "gp3") - disk_size = optional(number, 200) + disk_size = optional(number, 225) disk_iops = optional(number, 3000) disk_throughput = optional(number, 125) device_name = optional(string, "/dev/xvda") @@ -312,7 +312,7 @@ variable "eks_node_groups" { min_size = 1 desired_size = 1 max_size = 100 - instance_types = ["c5.2xlarge"] + instance_types = ["m5.2xlarge"] labels = { "repostore" = "true" } From 09490c7fc6d33d0b142bddfc2c796c893ff37392 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 27 Jun 2024 14:54:14 -0400 Subject: [PATCH 36/57] bump disk to 225gb to pass preflight checks --- examples/full/variables-cxone.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf index c22d282..e4e9a59 100644 --- a/examples/full/variables-cxone.tf +++ b/examples/full/variables-cxone.tf @@ -196,7 +196,7 @@ variable "eks_node_groups" { desired_size = string max_size = string volume_type = optional(string, "gp3") - disk_size = optional(number, 200) + disk_size = optional(number, 225) disk_iops = optional(number, 3000) disk_throughput = optional(number, 125) device_name = optional(string, "/dev/xvda") From ecc3d2cb1e2154d6bbede00b364503eb33523fc7 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Thu, 27 Jun 2024 14:55:05 -0400 Subject: [PATCH 37/57] Run VPC CNI with IRSA, remove --- eks.tf | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/eks.tf b/eks.tf index fba4f74..c1949d6 100644 --- a/eks.tf +++ b/eks.tf @@ -113,7 +113,6 @@ module "eks_node_iam_role" { role_requires_mfa = false custom_role_policy_arns = [ "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", - "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKS_CNI_Policy", "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy", "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore", "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", @@ -220,8 +219,9 @@ module "eks" { addon_version = var.kube_proxy_version } vpc-cni = { - addon_version = var.vpc_cni_version - before_compute = var.eks_enable_custom_networking + addon_version = var.vpc_cni_version + before_compute = var.eks_enable_custom_networking + service_account_role_arn = module.vpc_cni_irsa[0].iam_role_arn configuration_values = jsonencode({ env = { AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = tostring(var.eks_enable_custom_networking) @@ -312,6 +312,23 @@ module "ebs_csi_irsa" { } } +module "vpc_cni_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "5.39.0" + count = var.eks_create ? 1 : 0 + + role_name = "vpc-cni-${var.deployment_id}" + role_description = "IRSA role for VPC CNI" + attach_vpc_cni_policy = true + vpc_cni_enable_ipv4 = true + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:aws-node"] + } + } +} + module "cluster_autoscaler_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" From d73a588af8c41d376c59edfa1e63a9a840d003bd Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 6 Aug 2024 16:50:46 -0400 Subject: [PATCH 38/57] deprecate ENABLE_TLS --- modules/cxone-install/kots.config.aws.reference.yaml.tftpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/cxone-install/kots.config.aws.reference.yaml.tftpl b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl index ac44b74..a618a55 100644 --- a/modules/cxone-install/kots.config.aws.reference.yaml.tftpl +++ b/modules/cxone-install/kots.config.aws.reference.yaml.tftpl @@ -73,9 +73,9 @@ spec: DOMAIN: value: ${fqdn} - # Enable TLS for secure communication. Valid values are "0" and "1" + # Enable http to https redirect middleware in Traefik. Deprecated, should always be "0", and handle these redirects if needed outside of Traefik. ENABLE_TLS: - default: "1" + value: "0" #-------------------------------------------------------------------------- # Keys From 8b180e9649c410423be2220d501c7782e02785aa Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 14 Aug 2024 15:24:38 -0400 Subject: [PATCH 39/57] Added owner to external dns install to support multiple external dns usage in same domain or account --- modules/cxone-install/Makefile.tftpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/cxone-install/Makefile.tftpl b/modules/cxone-install/Makefile.tftpl index f01dd91..4eb3431 100644 --- a/modules/cxone-install/Makefile.tftpl +++ b/modules/cxone-install/Makefile.tftpl @@ -80,7 +80,8 @@ install-external-dns: --version 1.11.0 \ --set serviceAccount.create=true \ --set serviceAccount.name=external-dns \ - --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${external_dns_iam_role_arn}" + --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${external_dns_iam_role_arn}" \ + --set txtOwnerId=${EKS_CLUSTER_NAME} .PHONY: uninstall-external-dns From 28d24be682d83f99ea652e5941e73d7e738e379f Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Fri, 16 Aug 2024 11:01:52 -0400 Subject: [PATCH 40/57] fix make file var syntax --- modules/cxone-install/Makefile.tftpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cxone-install/Makefile.tftpl b/modules/cxone-install/Makefile.tftpl index 4eb3431..e1f90df 100644 --- a/modules/cxone-install/Makefile.tftpl +++ b/modules/cxone-install/Makefile.tftpl @@ -81,7 +81,7 @@ install-external-dns: --set serviceAccount.create=true \ --set serviceAccount.name=external-dns \ --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${external_dns_iam_role_arn}" \ - --set txtOwnerId=${EKS_CLUSTER_NAME} + --set txtOwnerId=$${EKS_CLUSTER_NAME} .PHONY: uninstall-external-dns From ec0fe83993dbb7dccd92b4187d9c9eded096060d Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 19 Aug 2024 12:04:22 -0400 Subject: [PATCH 41/57] exposing db back retention period --- examples/full/examples.auto.tfvars | 1 + examples/full/variables-cxone.tf | 6 ++++++ rds-analytics.tf | 1 + rds.tf | 1 + variables.tf | 7 +++++++ 5 files changed, 16 insertions(+) diff --git a/examples/full/examples.auto.tfvars b/examples/full/examples.auto.tfvars index d6e4b5e..e5b3885 100644 --- a/examples/full/examples.auto.tfvars +++ b/examples/full/examples.auto.tfvars @@ -306,6 +306,7 @@ db_serverlessv2_scaling_configuration = { min_capacity = 0.5 max_capacity = 8 } +db_backup_retention_period = 7 #****************************************************************************** diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf index e4e9a59..e7cc066 100644 --- a/examples/full/variables-cxone.tf +++ b/examples/full/variables-cxone.tf @@ -504,6 +504,12 @@ variable "db_apply_immediately" { description = "Determines if changes will be applied immediately or wait until the next maintenance window." } +variable "db_backup_retention_period" { + type = number + default = null + description = "The number of days to retain database backups for" +} + #****************************************************************************** # RDS - Analytics - Configuration diff --git a/rds-analytics.tf b/rds-analytics.tf index 09386ce..b083113 100644 --- a/rds-analytics.tf +++ b/rds-analytics.tf @@ -29,6 +29,7 @@ module "rds-analytics" { manage_master_user_password = false port = var.db_port deletion_protection = var.db_deletion_protection + backup_retention_period = var.db_backup_retention_period enabled_cloudwatch_logs_exports = ["postgresql"] security_group_rules = { ingress_from_vpc = { diff --git a/rds.tf b/rds.tf index 2323b98..cd40e81 100644 --- a/rds.tf +++ b/rds.tf @@ -35,6 +35,7 @@ module "rds" { manage_master_user_password = false port = var.db_port deletion_protection = var.db_deletion_protection + backup_retention_period = var.db_backup_retention_period enabled_cloudwatch_logs_exports = ["postgresql"] security_group_rules = { ingress_from_vpc = { diff --git a/variables.tf b/variables.tf index a26a1ef..6707814 100644 --- a/variables.tf +++ b/variables.tf @@ -514,6 +514,13 @@ variable "db_apply_immediately" { description = "Determines if changes will be applied immediately or wait until the next maintenance window." } +variable "db_backup_retention_period" { + type = number + default = null + description = "The number of days to retain database backups for" +} + + #****************************************************************************** # RDS - Analytics - Configuration From 61041156e6804491c8670055e595a96b21712867 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 19 Aug 2024 12:09:01 -0400 Subject: [PATCH 42/57] bump aurora module verions --- rds-analytics.tf | 2 +- rds.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rds-analytics.tf b/rds-analytics.tf index b083113..68dbe04 100644 --- a/rds-analytics.tf +++ b/rds-analytics.tf @@ -1,6 +1,6 @@ module "rds-analytics" { source = "terraform-aws-modules/rds-aurora/aws" - version = "9.3.1" + version = "9.9.0" create = var.db_create name = "${var.deployment_id}-analytics" diff --git a/rds.tf b/rds.tf index cd40e81..39fd57f 100644 --- a/rds.tf +++ b/rds.tf @@ -1,6 +1,6 @@ module "rds" { source = "terraform-aws-modules/rds-aurora/aws" - version = "9.3.1" + version = "9.9.0" create = var.db_create name = "${var.deployment_id}-main" From 5a014b813147d0293b8a734998802badc7cfdcc1 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 19 Aug 2024 12:12:22 -0400 Subject: [PATCH 43/57] bump eks module version --- eks.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eks.tf b/eks.tf index c1949d6..9f3c9b5 100644 --- a/eks.tf +++ b/eks.tf @@ -142,7 +142,7 @@ resource "aws_iam_policy" "s3_bucket_access" { module "eks" { source = "terraform-aws-modules/eks/aws" - version = "20.8.5" + version = "20.23.0" create = var.eks_create cluster_name = var.deployment_id From 0b046f342ae18dc9c9f3e1c952bd0506a6b39881 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 19 Aug 2024 12:13:03 -0400 Subject: [PATCH 44/57] bump irsa module version --- eks.tf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eks.tf b/eks.tf index 9f3c9b5..22d0ba5 100644 --- a/eks.tf +++ b/eks.tf @@ -297,7 +297,7 @@ resource "aws_autoscaling_group_tag" "cluster_autoscaler_taint" { module "ebs_csi_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.39.0" + version = "5.44.0" count = var.eks_create ? 1 : 0 role_name = "ebs-csi-${var.deployment_id}" @@ -314,7 +314,7 @@ module "ebs_csi_irsa" { module "vpc_cni_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.39.0" + version = "5.44.0" count = var.eks_create ? 1 : 0 role_name = "vpc-cni-${var.deployment_id}" @@ -332,7 +332,7 @@ module "vpc_cni_irsa" { module "cluster_autoscaler_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.39.0" + version = "5.44.0" count = var.eks_create && var.eks_create_cluster_autoscaler_irsa ? 1 : 0 role_name = "cluster-autoscaler-${var.deployment_id}" @@ -351,7 +351,7 @@ module "cluster_autoscaler_irsa" { module "external_dns_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.39.0" + version = "5.44.0" count = var.eks_create && var.eks_create_external_dns_irsa ? 1 : 0 role_name = "external-dns-${var.deployment_id}" @@ -370,7 +370,7 @@ module "external_dns_irsa" { module "load_balancer_controller_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - version = "5.39.0" + version = "5.44.0" count = var.eks_create && var.eks_create_load_balancer_controller_irsa ? 1 : 0 role_name = "load_balancer_controller-${var.deployment_id}" From 29123ee8c4bee996bc517d33bce25949fa2a71f4 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 19 Aug 2024 12:15:49 -0400 Subject: [PATCH 45/57] bump s3 module version --- s3.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s3.tf b/s3.tf index ac12b52..59cbb8b 100644 --- a/s3.tf +++ b/s3.tf @@ -103,7 +103,7 @@ locals { module "s3_bucket" { for_each = local.buckets source = "terraform-aws-modules/s3-bucket/aws" - version = "4.1.1" + version = "4.1.2" bucket = "${var.deployment_id}-${each.value.name}-${lower(local.s3_bucket_name_suffix)}" force_destroy = true From 63305935bff5029c1f73a51e8c8978ded72060e3 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 20 Aug 2024 10:55:20 -0400 Subject: [PATCH 46/57] Revert VPC CNI via IRSA --- eks.tf | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/eks.tf b/eks.tf index 22d0ba5..22ff30b 100644 --- a/eks.tf +++ b/eks.tf @@ -112,6 +112,7 @@ module "eks_node_iam_role" { role_name = "${var.deployment_id}-eks-nodes" role_requires_mfa = false custom_role_policy_arns = [ + "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKS_CNI_Policy", "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy", "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore", @@ -142,7 +143,7 @@ resource "aws_iam_policy" "s3_bucket_access" { module "eks" { source = "terraform-aws-modules/eks/aws" - version = "20.23.0" + version = "20.8.5" #"20.23.0" # create = var.eks_create cluster_name = var.deployment_id @@ -219,9 +220,9 @@ module "eks" { addon_version = var.kube_proxy_version } vpc-cni = { - addon_version = var.vpc_cni_version - before_compute = var.eks_enable_custom_networking - service_account_role_arn = module.vpc_cni_irsa[0].iam_role_arn + addon_version = var.vpc_cni_version + before_compute = var.eks_enable_custom_networking + #service_account_role_arn = var.eks_create ? module.vpc_cni_irsa[0].iam_role_arn : null configuration_values = jsonencode({ env = { AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG = tostring(var.eks_enable_custom_networking) @@ -233,7 +234,7 @@ module "eks" { aws-ebs-csi-driver = { addon_version = var.aws_ebs_csi_driver_version configuration_values = var.eks_enable_fargate ? local.ebs_csi_fargate_configuration_values : null - service_account_role_arn = module.ebs_csi_irsa[0].iam_role_arn + service_account_role_arn = var.eks_create ? module.ebs_csi_irsa[0].iam_role_arn : null } } create_kms_key = false From ee7e444e402ed752255d034d5faf989147289cc2 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 21 Aug 2024 20:31:55 -0400 Subject: [PATCH 47/57] Adding cloudwatch observibility addon --- eks.tf | 25 +++++++++++++++++++++++++ examples/full/main.tf | 1 + examples/full/variables-cxone.tf | 5 +++++ variables.tf | 5 +++++ 4 files changed, 36 insertions(+) diff --git a/eks.tf b/eks.tf index 22ff30b..1d56c47 100644 --- a/eks.tf +++ b/eks.tf @@ -273,6 +273,14 @@ module "eks" { fargate_profiles = var.eks_enable_fargate ? local.fargate_profiles : {} } +resource "aws_eks_addon" "amzn_cloudwatch_observability" { + count = var.aws_cloudwatch_observability_version != null ? 1 : 0 + cluster_name = module.eks.cluster_name + addon_name = "amazon-cloudwatch-observability" + addon_version = var.aws_cloudwatch_observability_version +} + + resource "aws_autoscaling_group_tag" "cluster_autoscaler_label" { for_each = { for node_group in var.eks_node_groups : node_group.name => node_group } depends_on = [module.eks] @@ -385,6 +393,23 @@ module "load_balancer_controller_irsa" { } } +module "aws_cloudwatch_observability_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "5.44.0" + count = var.eks_create && var.aws_cloudwatch_observability_version != null ? 1 : 0 + + role_name = "aws-cloudwatch-observability-${var.deployment_id}" + role_description = "IRSA role for AWS Cloudwatch Observability" + attach_cloudwatch_observability_policy = true + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["amazon-cloudwatch:cloudwatch-agent"] + } + } +} + + data "aws_iam_role" "karpenter" { name = "${var.deployment_id}-eks-nodes" depends_on = [module.eks] diff --git a/examples/full/main.tf b/examples/full/main.tf index f16ecc9..cda149c 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -147,6 +147,7 @@ module "checkmarx-one" { kube_proxy_version = var.kube_proxy_version vpc_cni_version = var.vpc_cni_version aws_ebs_csi_driver_version = var.aws_ebs_csi_driver_version + aws_cloudwatch_observability_version = var.aws_cloudwatch_observability_version eks_private_endpoint_enabled = var.eks_private_endpoint_enabled eks_public_endpoint_enabled = var.eks_public_endpoint_enabled eks_cluster_endpoint_public_access_cidrs = var.eks_cluster_endpoint_public_access_cidrs diff --git a/examples/full/variables-cxone.tf b/examples/full/variables-cxone.tf index e7cc066..78fb2da 100644 --- a/examples/full/variables-cxone.tf +++ b/examples/full/variables-cxone.tf @@ -161,6 +161,11 @@ variable "aws_ebs_csi_driver_version" { description = "The version of the EKS EBS CSI Addon." } +variable "aws_cloudwatch_observability_version" { + type = string + description = "The version of the AWS Cloudwatch Observability Addon. Specify a version to enable the addon, or leave blank to disable the addon." +} + variable "launch_template_tags" { type = map(string) description = "Tags to associate with launch templates for node groups" diff --git a/variables.tf b/variables.tf index 6707814..ceb61ad 100644 --- a/variables.tf +++ b/variables.tf @@ -171,6 +171,11 @@ variable "aws_ebs_csi_driver_version" { description = "The version of the EKS EBS CSI Addon." } +variable "aws_cloudwatch_observability_version" { + type = string + description = "The version of the AWS Cloudwatch Observability Addon. Specify a version to enable the addon, or leave blank to disable the addon." +} + variable "launch_template_tags" { type = map(string) description = "Tags to associate with launch templates for node groups" From cc23a19d2f25e27d3d37485837afca01e5e7b479 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 21 Aug 2024 20:37:56 -0400 Subject: [PATCH 48/57] Updating observability to use irsa --- eks.tf | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eks.tf b/eks.tf index 1d56c47..c3ad18d 100644 --- a/eks.tf +++ b/eks.tf @@ -274,10 +274,11 @@ module "eks" { } resource "aws_eks_addon" "amzn_cloudwatch_observability" { - count = var.aws_cloudwatch_observability_version != null ? 1 : 0 - cluster_name = module.eks.cluster_name - addon_name = "amazon-cloudwatch-observability" - addon_version = var.aws_cloudwatch_observability_version + count = var.aws_cloudwatch_observability_version != null ? 1 : 0 + cluster_name = module.eks.cluster_name + addon_name = "amazon-cloudwatch-observability" + addon_version = var.aws_cloudwatch_observability_version + service_account_role_arn = module.aws_cloudwatch_observability_irsa[0].iam_role_arn } From 18760ef46aa16dee4f80dfc96952340aac67f851 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Wed, 21 Aug 2024 20:38:09 -0400 Subject: [PATCH 49/57] Added metrics server --- modules/cxone-install/Makefile.tftpl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/cxone-install/Makefile.tftpl b/modules/cxone-install/Makefile.tftpl index e1f90df..bab1987 100644 --- a/modules/cxone-install/Makefile.tftpl +++ b/modules/cxone-install/Makefile.tftpl @@ -147,6 +147,11 @@ apply-storageclass-config: ./apply-storageclass-config.$${DEPLOYMENT_ID}.sh +.PHONY: install-metrics-server +install-metrics-server: + kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml + + .PHONY: clean-kots clean-kots: kubectl delete deployment kotsadm -n $${NAMESPACE} From 685c9920d821615fdd60b95dd37c04919de7468c Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 9 Sep 2024 16:18:47 -0400 Subject: [PATCH 50/57] increased destroy-load-balancer.sh reliability --- modules/cxone-install/destroy-load-balancer.sh.tftpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/cxone-install/destroy-load-balancer.sh.tftpl b/modules/cxone-install/destroy-load-balancer.sh.tftpl index 0ae9b62..e883329 100644 --- a/modules/cxone-install/destroy-load-balancer.sh.tftpl +++ b/modules/cxone-install/destroy-load-balancer.sh.tftpl @@ -20,7 +20,8 @@ sleep 10 # The "Node" security group will have references to the ELB security groups, so remove all the rules to allow groups to be deleted successfully NODE_SG_ID=$(aws ec2 describe-security-groups --filters Name=tag:Name,Values=$${DEPLOYMENT_ID}-node --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n") aws ec2 revoke-security-group-ingress --group-id $${NODE_SG_ID} --ip-permissions "`aws ec2 describe-security-groups --output json --group-ids $${NODE_SG_ID} --query "SecurityGroups[0].IpPermissions"`" -sleep 10 +echo "Waiting for rules to delete" +sleep 20 # Delete the ELB security groups aws ec2 describe-security-groups --filters Name=tag:elbv2.k8s.aws/cluster,Values=$${DEPLOYMENT_ID} --query "SecurityGroups[*].GroupId" --output text | tr "\t" "\n" | xargs -I {} aws ec2 delete-security-group --group-id {} From fbf46cefb1961aceadc094fd8524b567f95e0c20 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Mon, 9 Sep 2024 16:36:57 -0400 Subject: [PATCH 51/57] added bastion host --- examples/full/bastion-host.tf | 89 ++++++++++++++++++++++++++++++ examples/full/variables-example.tf | 33 +++++++++++ 2 files changed, 122 insertions(+) create mode 100644 examples/full/bastion-host.tf diff --git a/examples/full/bastion-host.tf b/examples/full/bastion-host.tf new file mode 100644 index 0000000..6844fe0 --- /dev/null +++ b/examples/full/bastion-host.tf @@ -0,0 +1,89 @@ + + +locals { + bastion_host_user_data_default = <<-EOT + #!/bin/bash + # Install kubectl + sudo curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.29.6/2024-07-12/bin/linux/amd64/kubectl + sudo curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.29.6/2024-07-12/bin/linux/amd64/kubectl.sha256 + #sha256sum -c kubectl.sha256 || exit 1 + sudo chmod +x ./kubectl + sudo cp ./kubectl /usr/local/bin/kubectl + + # Set kubecontext + aws eks update-kubeconfig --name ${var.deployment_id} + + #Install git + sudo dnf install git -y + + # Install kots + export REPL_USE_SUDO=y + export REPL_INSTALL_PATH=/usr/local/bin + sudo curl https://kots.io/install | bash + + # Install eksctl + ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" + PLATFORM=$(uname -s)_$ARCH + sudo curl -sLO "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_$PLATFORM.tar.gz" + tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm -f eksctl_$PLATFORM.tar.gz + sudo mv /tmp/eksctl /usr/local/bin + + # Install k9s + curl -sLO "https://github.com/derailed/k9s/releases/download/v0.32.5/k9s_linux_amd64.rpm" + sudo dnf install k9s_linux_amd64.rpm + EOT +} + +data "aws_ami" "amazon_linux_23" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["al2023-ami-2023*-x86_64"] + } +} + +module "ec2_instance" { + source = "terraform-aws-modules/ec2-instance/aws" + count = var.bastion_host_enabled ? 1 : 0 + + name = "${var.deployment_id}-bastion" + ami = data.aws_ami.amazon_linux_23.id + ignore_ami_changes = true + instance_type = var.bastion_host_instance_type + key_name = var.bastion_host_key_name + monitoring = false + vpc_security_group_ids = [module.bastion_security_group[0].security_group_id] + subnet_id = module.vpc.public_subnets[0] + associate_public_ip_address = true + user_data = var.bastion_host_user_data != null ? var.bastion_host_user_data : local.bastion_host_user_data_default + + create_iam_instance_profile = true + iam_role_description = "IAM role for EC2 instance" + iam_role_policies = { + AmazonSSMManagedInstanceCore = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore" + AdministratorAccess = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AdministratorAccess" + } + + tags = { + Terraform = "true" + Environment = "dev" + } +} + + +module "bastion_security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "~> 4.0" + count = var.bastion_host_enabled ? 1 : 0 + + name = "Bastion host for deployment ${var.deployment_id}" + description = "Security group for bastion host for deployment ${var.deployment_id}" + vpc_id = module.vpc.vpc_id + + ingress_cidr_blocks = concat(var.bastion_host_remote_management_cidrs, module.vpc.vpc_cidr_blocks) + ingress_rules = ["http-80-tcp", "all-icmp", "ssh-tcp"] + egress_rules = ["all-all"] + +} diff --git a/examples/full/variables-example.tf b/examples/full/variables-example.tf index 6bcbd8e..bcff8b0 100644 --- a/examples/full/variables-example.tf +++ b/examples/full/variables-example.tf @@ -95,4 +95,37 @@ variable "kots_license_file" { variable "kots_admin_email" { description = "The email address of the Checkmarx One first admin user." type = string +} + +#****************************************************************************** +# Bastion Host Configuration +#****************************************************************************** +variable "bastion_host_enabled" { + description = "Controls deployment of a bastion host to the VPC." + type = bool + default = false +} + +variable "bastion_host_instance_type" { + description = "The ec2 instance type for the bastion host." + type = string + default = "t3.large" +} + +variable "bastion_host_key_name" { + description = "The ec2 keypair name for the bastion host." + type = string + default = null +} + +variable "bastion_host_user_data" { + description = "User data for the bastion host. Default behavior is to install some basic tools." + type = string + default = null +} + +variable "bastion_host_remote_management_cidrs" { + description = "The list of CIDRs that need access to the bastion host" + type = list(string) + default = null } \ No newline at end of file From 5a30c08444787c0cddf0ed264784112044f20882 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 10 Sep 2024 10:31:23 -0400 Subject: [PATCH 52/57] adding multiple stateful action support --- examples/full/main.tf | 2 +- examples/full/variables-vpc.tf | 6 +++--- modules/inspection-vpc/firewall-rules.tf | 6 +++--- modules/inspection-vpc/firewall.tf | 2 +- modules/inspection-vpc/variables.tf | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/full/main.tf b/examples/full/main.tf index cda149c..eddfb65 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -25,7 +25,7 @@ module "vpc" { create_interface_endpoints = var.create_interface_endpoints create_s3_endpoint = var.create_s3_endpoint enable_firewall = var.enable_firewall - stateful_default_action = var.stateful_default_action + stateful_default_actions = var.stateful_default_actions suricata_rules = var.suricata_rules include_sca_rules = var.include_sca_rules additional_suricata_rules = local.additional_suricata_rules diff --git a/examples/full/variables-vpc.tf b/examples/full/variables-vpc.tf index 5fea44c..e2eaea4 100644 --- a/examples/full/variables-vpc.tf +++ b/examples/full/variables-vpc.tf @@ -36,10 +36,10 @@ variable "enable_firewall" { default = true } -variable "stateful_default_action" { +variable "stateful_default_actions" { description = "The [default action](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order) for the AWS Network Firewall stateful rule group. Choose `aws:drop_established` or `aws:alert_established`" - type = string - default = "aws:drop_established" + type = list(string) + default = ["aws:drop_established", "aws:alert_established"] } variable "suricata_rules" { diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index 42bd103..2b3723e 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -82,7 +82,7 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"602401143452.dkr. pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"prod-${data.aws_region.current.name}-starport-layer-bucket.s3.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420031; rev:1;) -# Amazon Linux 2 managed node group updates - region dependent +# Amazon Linux 2/2023 managed node group updates - region dependent pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"amazonlinux-2-repos-${data.aws_region.current.name}.s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420032; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"al2023-repos-${data.aws_region.current.name}-de612dc2.s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420033; rev:1;) @@ -112,7 +112,7 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy-auth.replic # Used by cxone images, and kube-rbac-proxy in CxOne operator pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240419044; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"checkmarx.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420044; rev:1;) +#pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"checkmarx.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420044; rev:1;) # Liquibase schema files required for database migration execution @@ -155,7 +155,7 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scan # Codebashing pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.stagecodebashing.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240422001; rev:1;) # Upcoming features -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cx-sca-containers.es.us-east-1.aws.found.io"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:204290001; rev:1;) +#pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cx-sca-containers.es.us-east-1.aws.found.io"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:204290001; rev:1;) # These URLs are randomly generated, and used to discover the correct s3 API signature version to use when communicating with S3 buckets. diff --git a/modules/inspection-vpc/firewall.tf b/modules/inspection-vpc/firewall.tf index 7a348a2..c1f1359 100644 --- a/modules/inspection-vpc/firewall.tf +++ b/modules/inspection-vpc/firewall.tf @@ -38,7 +38,7 @@ resource "aws_networkfirewall_firewall_policy" "main" { firewall_policy { stateless_default_actions = ["aws:forward_to_sfe"] stateless_fragment_default_actions = ["aws:forward_to_sfe"] - stateful_default_actions = [var.stateful_default_action] + stateful_default_actions = var.stateful_default_actions stateful_engine_options { rule_order = "STRICT_ORDER" } diff --git a/modules/inspection-vpc/variables.tf b/modules/inspection-vpc/variables.tf index a600977..aa6555e 100644 --- a/modules/inspection-vpc/variables.tf +++ b/modules/inspection-vpc/variables.tf @@ -41,10 +41,10 @@ variable "enable_firewall" { default = true } -variable "stateful_default_action" { +variable "stateful_default_actions" { description = "The [default action](https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order) for the AWS Network Firewall stateful rule group. Choose `aws:drop_established` or `aws:alert_established`" - type = string - default = "aws:drop_established" + type = list(string) + default = ["aws:drop_established", "aws:alert_established"] } variable "suricata_rules" { From 3ea00793411f88d9f7dfa367e67172b0b286ebd2 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Tue, 17 Sep 2024 10:03:07 -0400 Subject: [PATCH 53/57] 3.20 updates --- eks.tf | 3 +- examples/full/main.tf | 2 +- modules/cxone-install/Makefile.tftpl | 15 +-- modules/inspection-vpc/firewall-rules.tf | 115 +++++++++++++++++++---- 4 files changed, 108 insertions(+), 27 deletions(-) diff --git a/eks.tf b/eks.tf index c3ad18d..505421e 100644 --- a/eks.tf +++ b/eks.tf @@ -157,7 +157,8 @@ module "eks" { vpc_id = var.vpc_id subnet_ids = var.eks_subnets - create_cluster_security_group = true + create_cluster_primary_security_group_tags = false + create_cluster_security_group = true #cluster_security_group_id = var.cluster_security_group_id create_node_security_group = true diff --git a/examples/full/main.tf b/examples/full/main.tf index eddfb65..2b50e40 100644 --- a/examples/full/main.tf +++ b/examples/full/main.tf @@ -8,7 +8,7 @@ data "aws_availability_zones" "available" { locals { # Add the fqdn to the firewall rules additional_suricata_rules = < $EXTERNAL_NET 443 (tls.sni; content:"${var.fqdn}"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240401001; rev:1;) ${var.additional_suricata_rules} diff --git a/modules/cxone-install/Makefile.tftpl b/modules/cxone-install/Makefile.tftpl index bab1987..406c99c 100644 --- a/modules/cxone-install/Makefile.tftpl +++ b/modules/cxone-install/Makefile.tftpl @@ -57,7 +57,7 @@ rollout-restart: install-cluster-autoscaler: helm repo add autoscaler https://kubernetes.github.io/autoscaler; \ helm repo update autoscaler; \ - helm install cluster-autoscaler autoscaler/cluster-autoscaler \ + helm upgrade --install cluster-autoscaler autoscaler/cluster-autoscaler \ --version 9.21.1 \ -n kube-system \ --set awsRegion=$${DEPLOY_REGION} \ @@ -93,7 +93,7 @@ uninstall-external-dns: install-load-balancer-controller: helm repo add eks https://aws.github.io/eks-charts; \ helm repo update eks; \ - helm install aws-load-balancer-controller eks/aws-load-balancer-controller \ + helm upgrade --install aws-load-balancer-controller eks/aws-load-balancer-controller \ --version 1.7.1 \ -n kube-system \ --set vpcId=${vpc_id} \ @@ -114,15 +114,14 @@ uninstall-load-balancer-controller: .PHONY: install-karpenter install-karpenter: - helm install karpenter oci://public.ecr.aws/karpenter/karpenter \ - --version 0.36.0 \ + helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \ + --version 1.0.1 \ -n kube-system \ --create-namespace \ --set serviceAccount.create=true \ --set serviceAccount.name=karpenter \ --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"="${karpenter_iam_role_arn}" \ --set settings.clusterName=$${EKS_CLUSTER_NAME} \ - --set settings.clusterEndpoint=${cluster_endpoint} \ --set settings.featureGates.spotToSpotConsolidation=true \ --set settings.interruptionQueue=$${EKS_CLUSTER_NAME}-node-termination-handler \ --set controller.resources.requests.cpu=1 \ @@ -131,6 +130,8 @@ install-karpenter: --set controller.resources.limits.memory=1Gi kubectl apply -f karpenter.$${DEPLOYMENT_ID}.yaml + # --set settings.clusterEndpoint=${cluster_endpoint} \ + .PHONY: uninstall-karpenter uninstall-karpenter: @@ -154,9 +155,11 @@ install-metrics-server: .PHONY: clean-kots clean-kots: + kubectl delete statefulset kotsadm-rqlite -n $${NAMESPACE} kubectl delete deployment kotsadm -n $${NAMESPACE} kubectl delete statefulset kotsadm-minio -n $${NAMESPACE} - kubectl delete statefulset kotsadm-rqlite -n $${NAMESPACE} + + .PHONY: destroy-load-balancer diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index 2b3723e..abb3bf3 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -1,19 +1,68 @@ locals { sca_scanning_rules = var.include_sca_rules == false ? "" : < $EXTERNAL_NET 443 (tls.sni; content:"github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420065; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repo.maven.apache.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420066; rev:1;) +# These rules are used in SCA scaning for common dependency locations. +# These rules will vary based on what language/package manager/package repositories are used by the application being scanned with SCA. +# This list is non-exhaustive, and will vary depending on your usage. +# SCA scans will appear to hang for long periods of time when dependency resolution is blocked by a firewall. +# Firewalls should be monitored for dependency resolution connectivity needs of your organization and updated to allow scanning. + +# npm pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.npmjs.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420067; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"lscr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420068; rev:1;) + +# Yarn +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.yarnpkg.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910030; rev:1;) + +# Bower +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.bower.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910031; rev:1;) + +# PHP +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910032; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"codeload.github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910035; rev:1;) + +# Android and others +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"dl.google.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910039; rev:1;) + +# Go +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy.golang.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910033; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sum.golang.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420075; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420065; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"objects.githubusercontent.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910050; rev:1;) + +# Docker +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.hub.docker.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910034; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"index.docker.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420069; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"toolbox-data.anchore.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420070; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.hub.docker.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420071; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"hub.docker.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420072; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"ghcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420073; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy.golang.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420074; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sum.golang.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420075; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"pkg-containers.githubusercontent.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420076; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"ghcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420073; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"lscr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420068; rev:1;) + +# Gradle +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"services.gradle.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910036; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"plugins.gradle.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910037; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"jcenter.bintray.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910038; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repo.spring.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910040; rev:1;) + +# Maven +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"maven-central.storage-download.googleapis.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240912001; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repository.apache.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240912150; rev:1;) + +# Nuget +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.nuget.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910041; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"pkgs.dev.azure.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910045; rev:1;) + +# SBT/Scala +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repo.scala-sbt.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910042; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repo.typesafe.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910043; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"scala.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910044; rev:1;) + +# CRLs +pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; pcre:"/^crl\d\.digicert\.com$/i"; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240912200; rev:1;) +pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"ocsp.digicert.com"; startswith; endswith; msg:"Match liquidbase.com allowed"; flow:to_server, established; sid:240912205; rev:1;) +pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"ts-crl.ws.symantec.com"; startswith; endswith; msg:"Match liquidbase.com allowed"; flow:to_server, established; sid:240912201; rev:1;) +pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"s.symcb.com"; startswith; endswith; msg:"Match liquidbase.com allowed"; flow:to_server, established; sid:240912202; rev:1;) + EOF @@ -95,24 +144,43 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"d5l0dvt14r5h8.clo # Cluster Autoscaler - k8s.gcr.io (metadata) redirects to storage.googleapis.com (download) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"k8s.gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420036; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"storage.googleapis.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420037; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.k8s.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910001; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"us-west1-docker.pkg.dev"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910002; rev:1;) + +# External DNS +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"k8s.gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910003; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"us-west1-docker.pkg.dev"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240911001; rev:1;) +# Metrics Server +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.k8s.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910004; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"us-west1-docker.pkg.dev"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240911002; rev:1;) -# Kotsadm tools (minio, rqlite) come from docker.io and docker.com +# Kotsadm tools (minio, rqlite) come from docker.io and docker.com. +# Postgres:latest (for database preparation) also comes from dockerhub. pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry-1.docker.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420038; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"auth.docker.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420039; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"production.cloudflare.docker.com"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420040; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"subnet.min.io"; nocase; startswith; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:24250010; rev:1;) - -# Replicated APIs - used for license checking +# Replicated APIs - used for license and updates checking pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"replicated.app"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420041; rev:1;) + +# Replicated Image Proxy - used for image pulls for CxOne online installations pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy.replicated.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420042; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"proxy-auth.replicated.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420043; rev:1;) +# kube-rbac-proxy in the CxOne operator comes from gcr.io and storage.googleapis.com +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240911010; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"storage.googleapis.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240911011; rev:1;) # Used by cxone images, and kube-rbac-proxy in CxOne operator pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240419044; rev:1;) -#pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"checkmarx.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420044; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"checkmarx.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420044; rev:1;) + +# Unknown +#pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"accounts.google.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910005; rev:1;) +#pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"kics.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910021; rev:1;) +#pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"raw.githubusercontent.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240911012; rev:1;) # Liquibase schema files required for database migration execution @@ -131,12 +199,6 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cdn.split.io"; st pass tls $HOME_NET any -> [23.235.32.0/20,43.249.72.0/22,103.244.50.0/24,103.245.222.0/23,103.245.224.0/24,104.156.80.0/20,140.248.64.0/18,140.248.128.0/17,146.75.0.0/17,151.101.0.0/16,157.52.64.0/18,167.82.0.0/17,167.82.128.0/20,167.82.160.0/20,167.82.224.0/20,172.111.64.0/18,185.31.16.0/22,199.27.72.0/21,199.232.0.0/16] 443 (msg:"Fastly CDN"; flow:to_server, established; sid:240420051; rev:1;) -# Allow access to s3 buckets for Checkmarx One. Buckets are typically created with a prefix of the deployment id which allows for regex matching -# Example bucket name and suffix: scan-results-bos-ap-southeast-1-lab-19205 -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420052; rev:1;) -pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420053; rev:1;) - - # Checkmarx One Scans will upload source to scan-results bucket with url path patterns like "https://s3.${data.aws_region.current.name}.amazonaws.com/scan-results-0aa15147e5f3/source-code/....." pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.dualstack.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420054; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.${data.aws_region.current.name}.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420055; rev:1;) @@ -149,13 +211,21 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.iam.checkmarx. pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.api-sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420059; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420060; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.eu.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240611001; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.dusti.co"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910020; rev:1;) # Scan results buckets are used for SCA scan result syncing, and vary by the connected SCA region (e.g. NA, or EU) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prod-storage-1an26shc41yi3.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240420061; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prodeu-storage-1c25a060x93rl.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240611002; rev:1;) # Codebashing pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.stagecodebashing.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240422001; rev:1;) # Upcoming features -#pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cx-sca-containers.es.us-east-1.aws.found.io"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:204290001; rev:1;) +###pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"cx-sca-containers.es.us-east-1.aws.found.io"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:204290001; rev:1;) + + +# Allow access to s3 buckets for Checkmarx One. Buckets are typically created with a prefix of the deployment id which allows for regex matching +# Example bucket name and suffix: scan-results-bos-ap-southeast-1-lab-19205 +# These rules are required when using minio gateway, and may be otherwise required depending on your object storage configuration. +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420052; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_id}.*?\.s3\.${data.aws_region.current.name}\.amazonaws\.com$/i" msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420053; rev:1;) # These URLs are randomly generated, and used to discover the correct s3 API signature version to use when communicating with S3 buckets. @@ -163,9 +233,16 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.stagecodebash # to attempt to connect to S3 to discover the s3 signature version to use in subsequent requests to the actual buckets # 1. probe-bucket-sign-vie4gezw1j6w.s3.dualstack.${data.aws_region.current.name}.amazonaws.com # 2. probe-bsign-jmcvig40f29rwikvncljjtvohv4i4h.s3.dualstack.${data.aws_region.current.name}.amazonaws.com +# These rules are required when using minio gateway, and may be otherwise required depending on your object storage configuration. pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^probe-bucket-sign-[A-z0-9]{12}\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i"; flow: to_server; msg:"Minio client s3 signature version determination"; sid:240420063;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^probe-bsign-[A-z0-9]{30}\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i"; flow: to_server; msg:"Minio client s3 signature version determination"; sid:240420064;) +# Allow NTP +pass ntp $HOME_NET any -> $EXTERNAL_NET 123 (msg:"Allow ntp"; sid:240910006; rev:1;) + +# Allow incoming https +pass tls $EXTERNAL_NET any -> $HOME_NET 443 (msg:"Allow incoming https"; sid:240910008; rev:1;) + ${local.sca_scanning_rules} ${var.additional_suricata_rules} From b166f19f1730d3aaac65ee63009a3b7746e061da Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Sun, 20 Oct 2024 13:29:33 -0400 Subject: [PATCH 54/57] Add docker install to bastion server --- examples/full/bastion-host.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/full/bastion-host.tf b/examples/full/bastion-host.tf index 6844fe0..2ede7bd 100644 --- a/examples/full/bastion-host.tf +++ b/examples/full/bastion-host.tf @@ -16,6 +16,12 @@ locals { #Install git sudo dnf install git -y + # Set up docker + sudo dnf install docker -y + sudo systemctl start docker + sudo systemctl enable docker + sudo usermod -a -G docker $(whoami) + # Install kots export REPL_USE_SUDO=y export REPL_INSTALL_PATH=/usr/local/bin From 4c260a4b2509a1bd619625513035c35ff0a40f7c Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Sun, 20 Oct 2024 13:30:25 -0400 Subject: [PATCH 55/57] fix karpenter engines --- modules/cxone-install/karpenter.reference.yaml.tftpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cxone-install/karpenter.reference.yaml.tftpl b/modules/cxone-install/karpenter.reference.yaml.tftpl index 6c28b02..33abd53 100644 --- a/modules/cxone-install/karpenter.reference.yaml.tftpl +++ b/modules/cxone-install/karpenter.reference.yaml.tftpl @@ -134,7 +134,7 @@ spec: template: metadata: labels: - sast-engines: "true" + sast-engine: "true" sast-engine-medium: "true" sast-engine-large: "true" sast-engine-extra-large: "true" From 6c94ddbc3d538ef20e34dba25c08dcaf4c2b9526 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Sun, 20 Oct 2024 13:30:44 -0400 Subject: [PATCH 56/57] fix firewall default actions --- examples/full/examples.auto.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/full/examples.auto.tfvars b/examples/full/examples.auto.tfvars index e5b3885..a39c46c 100644 --- a/examples/full/examples.auto.tfvars +++ b/examples/full/examples.auto.tfvars @@ -39,7 +39,7 @@ create_s3_endpoint = true # Firewall only current works for egress filtering, and breaks ingress. Do not enable. enable_firewall = false -stateful_default_action = "aws:drop_established" +stateful_default_actions = ["aws:drop_established", "aws:alert_established"] include_sca_rules = true create_managed_rule_groups = false managed_rule_groups = ["AbusedLegitMalwareDomainsStrictOrder", From ba3f877273f7cbcd7f0b049d57418710999ea8c2 Mon Sep 17 00:00:00 2001 From: benjaminstokes Date: Sun, 20 Oct 2024 13:31:59 -0400 Subject: [PATCH 57/57] updates to firewall rules --- modules/inspection-vpc/firewall-rules.tf | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/inspection-vpc/firewall-rules.tf b/modules/inspection-vpc/firewall-rules.tf index abb3bf3..0aabf85 100644 --- a/modules/inspection-vpc/firewall-rules.tf +++ b/modules/inspection-vpc/firewall-rules.tf @@ -19,6 +19,7 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.bower.io # PHP pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910032; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"codeload.github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910035; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"packagist.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240919001; rev:1;) # Android and others pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"dl.google.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910039; rev:1;) @@ -29,7 +30,7 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"sum.golang.org"; pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"github.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420065; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"objects.githubusercontent.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910050; rev:1;) -# Docker +# Docker - also used by container scanning. pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"registry.hub.docker.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910034; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"index.docker.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420069; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"toolbox-data.anchore.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420070; rev:1;) @@ -49,8 +50,11 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"maven-central.sto pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repository.apache.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240912150; rev:1;) # Nuget +# See https://learn.microsoft.com/en-us/azure/devops/organizations/security/allow-list-ip-url?view=azure-devops&tabs=IP-V4 for additional common domains used with Nuget and Azure DevOps pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.nuget.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910041; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"pkgs.dev.azure.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910045; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:".vsassets.io"; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240919002; rev:1;) + # SBT/Scala pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"repo.scala-sbt.org"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910042; rev:1;) @@ -63,7 +67,6 @@ pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"ocsp.digicert.c pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"ts-crl.ws.symantec.com"; startswith; endswith; msg:"Match liquidbase.com allowed"; flow:to_server, established; sid:240912201; rev:1;) pass http $HOME_NET any -> $EXTERNAL_NET 80 (http.host; content:"s.symcb.com"; startswith; endswith; msg:"Match liquidbase.com allowed"; flow:to_server, established; sid:240912202; rev:1;) - EOF default_suricata_rules = < $EXTERNAL_NET 443 (tls.sni; content:"storage.googleapi # Used by cxone images, and kube-rbac-proxy in CxOne operator pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"gcr.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240419044; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"checkmarx.jfrog.io"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420044; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"jfrog-prod-euw1-shared-ireland-main.s3.amazonaws.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240925001; rev:1;) # Unknown #pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"accounts.google.com"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910005; rev:1;) @@ -212,6 +216,8 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"eu.api-sca.checkm pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240420060; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"uploads.eu.sca.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240611001; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"api.dusti.co"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:240910020; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"us.iam.checkmarx.net"; startswith; nocase; endswith; msg:"matching TLS allowlisted FQDNs"; flow:to_server, established; sid:241015001; rev:1;) + # Scan results buckets are used for SCA scan result syncing, and vary by the connected SCA region (e.g. NA, or EU) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prod-storage-1an26shc41yi3.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240420061; rev:1;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"microservice-scanresults-prodeu-storage-1c25a060x93rl.s3.amazonaws.com"; startswith; nocase; endswith; msg:"SCA NA region result sync bucket"; flow:to_server, established; sid:240611002; rev:1;) @@ -229,13 +235,19 @@ pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^${var.deployment_i # These URLs are randomly generated, and used to discover the correct s3 API signature version to use when communicating with S3 buckets. -# They take two forms, where the long alphanumeric string is randomly generated. The buckets do not exist, but allow minio client +# They take three forms, where the long alphanumeric string is randomly generated. The buckets do not exist, but allow minio client # to attempt to connect to S3 to discover the s3 signature version to use in subsequent requests to the actual buckets # 1. probe-bucket-sign-vie4gezw1j6w.s3.dualstack.${data.aws_region.current.name}.amazonaws.com # 2. probe-bsign-jmcvig40f29rwikvncljjtvohv4i4h.s3.dualstack.${data.aws_region.current.name}.amazonaws.com +# 3. s3.amazonaws.com/probe-bucket-sign-6n4nhxx1jt1j # These rules are required when using minio gateway, and may be otherwise required depending on your object storage configuration. pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^probe-bucket-sign-[A-z0-9]{12}\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i"; flow: to_server; msg:"Minio client s3 signature version determination"; sid:240420063;) pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; pcre:"/^probe-bsign-[A-z0-9]{30}\.s3\.dualstack\.${data.aws_region.current.name}\.amazonaws\.com$/i"; flow: to_server; msg:"Minio client s3 signature version determination"; sid:240420064;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.amazonaws.com"; startswith; nocase; endswith; msg:"Minio signature"; flow:to_server, established; sid:241015004; rev:1;) +pass tls $HOME_NET any -> $EXTERNAL_NET 443 (tls.sni; content:"s3.dualstack.us-east-1.amazonaws.com"; startswith; nocase; endswith; msg:"Minio signature"; flow:to_server, established; sid:241015005; rev:1;) + + + # Allow NTP pass ntp $HOME_NET any -> $EXTERNAL_NET 123 (msg:"Allow ntp"; sid:240910006; rev:1;)