Bläddra i källkod

Merge pull request #443 from mdr-engineering/feature/ftd_MSOCI-2141_HTTPS_for_Repo

Adds an NLB+ALB in front of the repo server for HTTPS
Frederick Damstra 3 år sedan
förälder
incheckning
a47941c64c

+ 37 - 0
base/repo_server/cert_private.tf

@@ -0,0 +1,37 @@
+#----------------------------------------------------------------------------
+# Private DNS Certificate
+#----------------------------------------------------------------------------
+resource "aws_acm_certificate" "cert_private" {
+  domain_name       = "reposerver.${var.dns_info["private"]["zone"]}"
+  validation_method = "DNS"
+
+  lifecycle {
+    create_before_destroy = true
+  }
+
+  tags = merge(var.standard_tags, var.tags)
+}
+
+resource "aws_acm_certificate_validation" "cert_private" {
+  certificate_arn         = aws_acm_certificate.cert_private.arn
+  validation_record_fqdns = [for record in aws_route53_record.cert_validation_private : record.fqdn]
+}
+
+resource "aws_route53_record" "cert_validation_private" {
+  provider = aws.mdr-common-services-commercial
+
+  for_each = {
+    for dvo in aws_acm_certificate.cert_private.domain_validation_options : dvo.domain_name => {
+      name   = dvo.resource_record_name
+      record = dvo.resource_record_value
+      type   = dvo.resource_record_type
+    }
+  }
+
+  allow_overwrite = true
+  name            = each.value.name
+  records         = [each.value.record]
+  ttl             = 60
+  type            = each.value.type
+  zone_id         = var.dns_info["public"]["zone_id"] # private zones sitll use public dns for validation
+}

+ 87 - 0
base/repo_server/lb.tf

@@ -0,0 +1,87 @@
+module "elb" {
+  source = "../../submodules/load_balancer/static_nlb_to_alb"
+
+  name                  = "reposerver"
+  target_ids            = [aws_instance.instance.id]
+  listener_port         = 443
+  target_port           = 80
+  target_protocol       = "HTTP"
+  target_security_group = aws_security_group.repo_server_security_group.id
+  allow_from_any        = false
+  redirect_80           = true
+
+  # We need extra security groups to overcome the rules per security group limit
+  extra_security_groups = 2
+
+  # WAF variables
+  waf_enabled = false # Disabled during testing
+  #excluded_rules_AWSManagedRulesCommonRuleSet = [ "SizeRestrictions_BODY" ]
+  #excluded_rules_AWSManagedRulesAmazonIpReputationList = []
+  #excluded_rules_AWSManagedRulesKnownBadInputsRuleSet = []
+  #excluded_rules_AWSManagedRulesSQLiRuleSet = []
+  #excluded_rules_AWSManagedRulesLinuxRuleSet = []
+  #excluded_rules_AWSManagedRulesUnixRuleSet = []
+  #additional_blocked_ips = []
+  #allowed_ips = []
+  #admin_ips = []
+
+  # Optional Variables
+  healthcheck_port     = 80
+  healthcheck_protocol = "HTTP"
+  healthcheck_path     = "/epel/7/repodata/repomd.xml"
+  healthcheck_matcher  = "200"
+  stickiness           = false
+
+  # Inherited Variables
+  tags           = merge(var.standard_tags, var.tags)
+  dns_info       = var.dns_info
+  public_subnets = var.public_subnets
+  environment    = var.environment
+  aws_partition  = var.aws_partition
+  aws_region     = var.aws_region
+  aws_account_id = var.aws_account_id
+  vpc_id         = var.vpc_id
+
+  providers = {
+    aws.mdr-common-services-commercial = aws.mdr-common-services-commercial
+    aws.c2                             = aws.c2
+  }
+}
+
+# module.elb.extra_security_groups
+resource "aws_security_group_rule" "alb-http-in-external-c2-users" {
+  # This deserves some explanation.  Terraform "for_each" expects to be
+  # getting as input a map of values to iterate over as part of the foreach.
+  # The keys of the map are used to name each of these objects created.  Looking
+  # in the terraform plan output of a for_each you'll see things like:
+  #
+  # aws_security_group_rule.resource_name["key-value-from-foreach"] will be created
+  #
+  # Our c2_services_external_ips is a list of maps, not a map of maps.  The for-expression
+  # makes a new thing that is a map of maps, where the key value is the description with
+  # blanks removed.
+  #
+  # We could have made the variable more natively-friendly to for_each but this seemed
+  # like a better solution for what we were trying to accomplish.
+  for_each = { for s in var.c2_services_external_ips : replace(s.description, "/\\s*/", "") => s }
+
+  description       = "For redirect from 80 to 443 - ${each.value.description}"
+  type              = "ingress"
+  from_port         = 80
+  to_port           = 80
+  protocol          = "tcp"
+  cidr_blocks       = each.value.cidr_blocks
+  security_group_id = module.elb.extra_security_group_ids[0]
+}
+
+resource "aws_security_group_rule" "https-in-external-c2-users" {
+  for_each = { for s in var.c2_services_external_ips : replace(s.description, "/\\s*/", "") => s }
+
+  description       = "inbound repository requests - ${each.value.description}"
+  type              = "ingress"
+  from_port         = 443
+  to_port           = 443
+  protocol          = "tcp"
+  cidr_blocks       = each.value.cidr_blocks
+  security_group_id = module.elb.extra_security_group_ids[1]
+}

+ 143 - 0
base/repo_server/lb_internal.tf

@@ -0,0 +1,143 @@
+#----------------------------------------------------------------------------
+# INTERNAL LB
+#----------------------------------------------------------------------------
+resource "aws_lb" "internal" {
+  name_prefix        = substr(var.instance_name, 0, 6)
+  security_groups    = [aws_security_group.alb_internal.id]
+  internal           = true
+  subnets            = var.public_subnets
+  load_balancer_type = "application"
+
+  drop_invalid_header_fields = true
+
+  access_logs {
+    bucket  = "xdr-elb-${var.environment}"
+    enabled = true
+  }
+
+  idle_timeout = 1200
+
+  tags = merge(var.standard_tags, var.tags, { Name = "reposerver-internal-${var.environment}" })
+}
+
+# Create a new target group
+resource "aws_lb_target_group" "internal" {
+  name_prefix = substr(var.instance_name, 1, 6)
+  port        = 80
+  protocol    = "HTTP"
+  vpc_id      = var.vpc_id
+
+  health_check {
+    protocol            = "HTTP"
+    port                = "80"
+    path                = "/epel/7/repodata/repomd.xml"
+    matcher             = "200,302"
+    timeout             = "4"
+    interval            = "5"
+    unhealthy_threshold = 2
+    healthy_threshold   = 2
+  }
+
+  #stickiness {
+  #  type    = "lb_cookie"
+  #  enabled = false 
+  #}
+
+  tags = merge(var.standard_tags, var.tags, { Name = "reposerver-internal-${var.environment}" })
+}
+
+resource "aws_lb_target_group_attachment" "internal" {
+  target_group_arn = aws_lb_target_group.internal.arn
+  target_id        = aws_instance.instance.id
+  port             = 80
+}
+
+# Create a new alb listener
+resource "aws_lb_listener" "https_internal" {
+  load_balancer_arn = aws_lb.internal.arn
+  port              = "443"
+  protocol          = "HTTPS"
+  ssl_policy        = "ELBSecurityPolicy-FS-1-2-Res-2020-10" # PFS, TLS1.2, and GCM; most "restrictive" policy
+  certificate_arn   = aws_acm_certificate.cert_private.arn
+
+  default_action {
+    target_group_arn = aws_lb_target_group.internal.arn
+    type             = "forward"
+  }
+}
+
+resource "aws_lb_listener" "listener_http" {
+  load_balancer_arn = aws_lb.internal.arn
+  port              = "80"
+  protocol          = "HTTP"
+
+  default_action {
+    type = "redirect"
+
+    redirect {
+      port        = "443"
+      protocol    = "HTTPS"
+      status_code = "HTTP_301"
+    }
+  }
+}
+
+# #########################
+# # DNS Entry
+module "internal_lb_private_dns_record" {
+  source = "../../submodules/dns/private_CNAME_record"
+
+  name             = "reposerver"
+  target_dns_names = [aws_lb.internal.dns_name]
+  dns_info         = var.dns_info
+
+  providers = {
+    aws.c2 = aws.c2
+  }
+}
+
+#----------------------------------------------------------------------------
+# ALB Security Group
+#----------------------------------------------------------------------------
+resource "aws_security_group" "alb_internal" {
+  vpc_id      = var.vpc_id
+  name_prefix = "repo_alb"
+  description = "ALB for Repo Server Internal ALB"
+  tags        = merge(var.standard_tags, var.tags)
+}
+
+#----------------------------------------------------------------------------
+# INGRESS
+#----------------------------------------------------------------------------
+resource "aws_security_group_rule" "http_from_local" {
+  description       = "HTTP inbound from Private Servers"
+  type              = "ingress"
+  from_port         = "80"
+  to_port           = "80"
+  protocol          = "tcp"
+  cidr_blocks       = ["10.0.0.0/8"]
+  security_group_id = aws_security_group.alb_internal.id
+}
+
+resource "aws_security_group_rule" "https_from_local" {
+  description       = "HTTPS inbound from private servers"
+  type              = "ingress"
+  from_port         = "443"
+  to_port           = "443"
+  protocol          = "tcp"
+  cidr_blocks       = ["10.0.0.0/8"]
+  security_group_id = aws_security_group.alb_internal.id
+}
+
+#----------------------------------------------------------------------------
+# EGRESS
+#----------------------------------------------------------------------------
+resource "aws_security_group_rule" "alb_to_server" {
+  description              = "HTTP to the Server"
+  type                     = "egress"
+  from_port                = "80"
+  to_port                  = "80"
+  protocol                 = "tcp"
+  source_security_group_id = aws_security_group.repo_server_security_group.id
+  security_group_id        = aws_security_group.alb_internal.id
+}

+ 28 - 77
base/repo_server/main.tf

@@ -17,8 +17,8 @@ data "aws_kms_key" "ebs-key" {
 }
 
 resource "aws_network_interface" "instance" {
-  subnet_id       = var.subnets[0]
-  security_groups = [data.aws_security_group.typical-host.id, aws_security_group.repo_server_security_group_80.id, aws_security_group.repo_server_security_group_443.id]
+  subnet_id       = var.public_subnets[0]
+  security_groups = [data.aws_security_group.typical-host.id, aws_security_group.repo_server_security_group.id]
   description     = var.instance_name
   tags            = merge(var.standard_tags, var.tags, { Name = var.instance_name })
 }
@@ -49,6 +49,12 @@ resource "aws_instance" "instance" {
   # that could be removed.
   lifecycle { ignore_changes = [ami, key_name, user_data, ebs_block_device] }
 
+  metadata_options {
+    http_endpoint = "enabled"
+    http_tokens   = "optional" # tfsec:ignore:aws-ec2-enforce-http-token-imds Breaks salt
+  }
+
+
   # These device definitions are optional, but added for clarity.
   root_block_device {
     volume_type = "gp3"
@@ -141,7 +147,7 @@ resource "aws_instance" "instance" {
 module "private_dns_record" {
   source = "../../submodules/dns/private_A_record"
 
-  name            = var.instance_name
+  name            = "${var.instance_name}-server"
   ip_addresses    = [aws_instance.instance.private_ip]
   dns_info        = var.dns_info
   reverse_enabled = var.reverse_enabled
@@ -151,18 +157,6 @@ module "private_dns_record" {
   }
 }
 
-module "public_dns_record" {
-  source = "../../submodules/dns/public_A_record"
-
-  name         = var.instance_name
-  ip_addresses = [aws_eip.instance.public_ip]
-  dns_info     = var.dns_info
-
-  providers = {
-    aws.mdr-common-services-commercial = aws.mdr-common-services-commercial
-  }
-}
-
 # Render a multi-part cloud-init config making use of the part
 # above, and other source files
 data "template_cloudinit_config" "cloud_init_config" {
@@ -193,78 +187,33 @@ data "template_cloudinit_config" "cloud_init_config" {
   }
 }
 
-resource "aws_security_group" "repo_server_security_group_80" {
-  name        = "repo_server_security_group_80"
+resource "aws_security_group" "repo_server_security_group" {
+  name        = "repo_server_security_group"
   description = "Security Group for the Repository Server(s) port 80"
   vpc_id      = var.vpc_id
   tags        = merge(var.standard_tags, var.tags)
 }
 
-resource "aws_security_group" "repo_server_security_group_443" {
-  name        = "repo_server_security_group_443"
-  description = "Security Group for the Repository Server(s) port 443"
-  vpc_id      = var.vpc_id
-  tags        = merge(var.standard_tags, var.tags)
-}
-
 resource "aws_security_group_rule" "http-in" {
-  description       = "inbound repository requests"
-  type              = "ingress"
-  from_port         = 80
-  to_port           = 80
-  protocol          = "tcp"
-  cidr_blocks       = toset(concat(["10.0.0.0/8"], var.repo_server_whitelist))
-  security_group_id = aws_security_group.repo_server_security_group_80.id
-}
-
-resource "aws_security_group_rule" "http-in-external-c2-users" {
-
-  # This deserves some explanation.  Terraform "for_each" expects to be
-  # getting as input a map of values to iterate over as part of the foreach.
-  # The keys of the map are used to name each of these objects created.  Looking
-  # in the terraform plan output of a for_each you'll see things like:
-  #
-  # aws_security_group_rule.resource_name["key-value-from-foreach"] will be created
-  #
-  # Our c2_services_external_ips is a list of maps, not a map of maps.  The for-expression
-  # makes a new thing that is a map of maps, where the key value is the description with
-  # blanks removed.
-  #
-  # We could have made the variable more natively-friendly to for_each but this seemed
-  # like a better solution for what we were trying to accomplish.
-  for_each = { for s in var.c2_services_external_ips : replace(s.description, "/\\s*/", "") => s }
-
-  description       = "inbound repository requests - ${each.value.description}"
-  type              = "ingress"
-  from_port         = 80
-  to_port           = 80
-  protocol          = "tcp"
-  cidr_blocks       = each.value.cidr_blocks
-  security_group_id = aws_security_group.repo_server_security_group_80.id
+  description              = "inbound repository requests"
+  type                     = "ingress"
+  from_port                = 80
+  to_port                  = 80
+  protocol                 = "tcp"
+  source_security_group_id = aws_security_group.alb_internal.id
+  security_group_id        = aws_security_group.repo_server_security_group.id
 }
 
-
-resource "aws_security_group_rule" "https-in" {
-  description       = "inbound repository requests"
-  type              = "ingress"
-  from_port         = 443
-  to_port           = 443
-  protocol          = "tcp"
-  cidr_blocks       = toset(concat(["10.0.0.0/8"], var.repo_server_whitelist))
-  security_group_id = aws_security_group.repo_server_security_group_443.id
+resource "aws_security_group_rule" "http-in-external" {
+  description              = "inbound repository requests from the alb"
+  type                     = "ingress"
+  from_port                = 80
+  to_port                  = 80
+  protocol                 = "tcp"
+  source_security_group_id = module.elb.security_group_id
+  security_group_id        = aws_security_group.repo_server_security_group.id
 }
 
-resource "aws_security_group_rule" "https-in-external-c2-users" {
-  for_each = { for s in var.c2_services_external_ips : replace(s.description, "/\\s*/", "") => s }
-
-  description       = "inbound repository requests - ${each.value.description}"
-  type              = "ingress"
-  from_port         = 443
-  to_port           = 443
-  protocol          = "tcp"
-  cidr_blocks       = each.value.cidr_blocks
-  security_group_id = aws_security_group.repo_server_security_group_443.id
-}
 
 
 # Repo server has an extra volume that is created separately, to keep it from being destroyed
@@ -273,6 +222,8 @@ resource "aws_ebs_volume" "repo_server_drive" {
   availability_zone = aws_instance.instance.availability_zone
   size              = local.repo_drive_size
   type              = "gp3" # consider moving to sc1 if this is ever > 500GB
+  encrypted         = true
+  kms_key_id        = data.aws_kms_key.ebs-key.arn
 
   #snapshot_id       = "${data.aws_ebs_snapshot.repo_snapshot.id}"
 

+ 3 - 4
base/repo_server/vars.tf

@@ -7,10 +7,6 @@ variable "azs" {
   type = list(string)
 }
 
-variable "subnets" {
-  type = list(string)
-}
-
 variable "vpc_id" {
   type = string
 }
@@ -58,3 +54,6 @@ variable "aws_partition_alias" { type = string }
 variable "aws_account_id" { type = string }
 variable "common_services_account" { type = string }
 variable "instance_termination_protection" { type = bool }
+variable "private_subnets" { type = list(string) }
+variable "public_subnets" { type = list(string) }
+

+ 1 - 1
submodules/load_balancer/static_nlb_to_alb/alb.tf

@@ -3,7 +3,7 @@
 #----------------------------------------------------------------------------
 resource "aws_lb" "external" {
   name_prefix                = substr("${var.name}-ext-lb", 0, 6)
-  security_groups            = [aws_security_group.lb_server_external.id]
+  security_groups            = concat([aws_security_group.lb_server_external.id], aws_security_group.extra_security_groups[*].id)
   internal                   = false #tfsec:ignore:aws-elb-alb-not-public
   subnets                    = var.public_subnets
   load_balancer_type         = "application"

+ 4 - 0
submodules/load_balancer/static_nlb_to_alb/outputs.tf

@@ -2,6 +2,10 @@ output "security_group_id" {
   value = aws_security_group.lb_server_external.id
 }
 
+output "extra_security_group_ids" {
+  value = aws_security_group.extra_security_groups[*].id
+}
+
 output "alb" {
   value = aws_lb.external
 }

+ 11 - 0
submodules/load_balancer/static_nlb_to_alb/security-groups.tf

@@ -59,3 +59,14 @@ resource "aws_security_group_rule" "alb_to_health" {
   description              = "${var.name} - Allows the ALB to talk to the Health check"
   security_group_id        = aws_security_group.lb_server_external.id
 }
+
+#----------------------------------------------------------------------------
+# Extra ALB Security Groups
+#----------------------------------------------------------------------------
+resource "aws_security_group" "extra_security_groups" {
+  count       = var.extra_security_groups
+  vpc_id      = var.vpc_id
+  name_prefix = "${var.name}-alb-sg-external-extra-${count.index}"
+  description = "${var.name} LB SG ${count.index}"
+  tags        = var.tags
+}

+ 6 - 0
submodules/load_balancer/static_nlb_to_alb/vars.tf

@@ -15,6 +15,12 @@ variable "redirect_80" {
   default     = false
 }
 
+variable "extra_security_groups" {
+  description = "Creates extra security groups for modification outside the module."
+  type        = number
+  default     = 0
+}
+
 variable "target_ids" {
   description = "List of targets to assign to the ALB"
   type        = set(string)