From 2a3878dde37018b3a1a561be68db0037c6949d89 Mon Sep 17 00:00:00 2001 From: "Michel Z. Santello" Date: Tue, 18 Jun 2024 23:03:55 -0300 Subject: [PATCH 1/4] chore(iam-policy): restrict S3 action to the module's bucket --- modules/fortigate/fgt_asg/main.tf | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/fortigate/fgt_asg/main.tf b/modules/fortigate/fgt_asg/main.tf index a4134aa..b1c8ab0 100644 --- a/modules/fortigate/fgt_asg/main.tf +++ b/modules/fortigate/fgt_asg/main.tf @@ -208,14 +208,25 @@ resource "aws_iam_role_policy" "iam_policy" { "ec2:CreateTags", "autoscaling:CompleteLifecycleAction", "autoscaling:DescribeAutoScalingGroups", - "s3:*", - "s3-object-lambda:*", + "lambda:InvokeFunction", "dynamodb:*" ], Effect = "Allow", Resource = "*" }, + { + Action = [ + "s3:*", + "s3-object-lambda:*", + ], + Effect = "Allow", + Resource = [ + "${aws_s3_bucket.fgt_lic[0].arn}", + "${aws_s3_bucket.fgt_lic[0].arn}/*" + ] + + }, { Action = [ "events:PutRule" From 855651e8cf3bcdfcd8589063bf5f8a00d8a4bb0f Mon Sep 17 00:00:00 2001 From: "Michel Z. Santello" Date: Tue, 18 Jun 2024 23:06:03 -0300 Subject: [PATCH 2/4] feat(launch-template): add detailed monitoring support --- modules/fortigate/fgt_asg/main.tf | 5 +++++ modules/fortigate/fgt_asg/variables.tf | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/modules/fortigate/fgt_asg/main.tf b/modules/fortigate/fgt_asg/main.tf index b1c8ab0..9338fac 100644 --- a/modules/fortigate/fgt_asg/main.tf +++ b/modules/fortigate/fgt_asg/main.tf @@ -41,6 +41,11 @@ resource "aws_launch_template" "fgt" { update_default_version = true user_data = base64encode(local.fgt_userdata) + monitoring { + enabled = var.detailed_monitoring + + } + dynamic "network_interfaces" { for_each = { for k, v in var.network_interfaces : k => v if v.device_index == 0 } diff --git a/modules/fortigate/fgt_asg/variables.tf b/modules/fortigate/fgt_asg/variables.tf index 25ed1f5..72ca77e 100644 --- a/modules/fortigate/fgt_asg/variables.tf +++ b/modules/fortigate/fgt_asg/variables.tf @@ -34,6 +34,12 @@ variable "instance_type" { type = string } +variable "detailed_monitoring" { + description = "If true, the launched EC2 instance will have detailed monitoring enabled." + default = false + type = bool +} + variable "license_type" { description = "Provide the license type for the FortiGate instances. Options: on_demand, byol. Default is on_demand." default = "on_demand" From c0c0890552355c70e10c9609103eb3a785b24ce1 Mon Sep 17 00:00:00 2001 From: "Michel Z. Santello" Date: Wed, 19 Jun 2024 16:56:51 -0300 Subject: [PATCH 3/4] feat(lambda): add support for admin password from AWS Secrets --- .../fgt_asg/fgt-asg-lambda-internal.py | 10 +++- modules/fortigate/fgt_asg/fgt-asg-lambda.py | 32 ++++++++++++- modules/fortigate/fgt_asg/main.tf | 47 +++++++++++++++++-- modules/fortigate/fgt_asg/variables.tf | 6 +++ 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/modules/fortigate/fgt_asg/fgt-asg-lambda-internal.py b/modules/fortigate/fgt_asg/fgt-asg-lambda-internal.py index 6dfc134..a4072d3 100644 --- a/modules/fortigate/fgt_asg/fgt-asg-lambda-internal.py +++ b/modules/fortigate/fgt_asg/fgt-asg-lambda-internal.py @@ -11,7 +11,6 @@ def __init__(self): self.logger = logging.getLogger("lambda") self.logger.setLevel(logging.INFO) self.cookie = {} - self.fgt_password = os.getenv("fgt_password") self.fgt_login_port = "" if os.getenv("fgt_login_port_number") == "" else ":" + os.getenv("fgt_login_port_number") self.return_json = { 'StatusCode': 200, @@ -22,6 +21,15 @@ def __init__(self): def main(self, event): self.logger.info(f"Start internal lambda function.") self.fgt_private_ip = event["private_ip"] + + fgt_password_from_secrets_manager = os.getenv("fgt_password_from_secrets_manager") + + if fgt_password_from_secrets_manager == "true": + self.logger.info(f"Using password from AWS Secrets Manager") + self.fgt_password = event["password"] + else: + self.fgt_password = os.getenv("fgt_password") + operation = event["operation"] parameters = event["parameters"] if operation == "change_password": diff --git a/modules/fortigate/fgt_asg/fgt-asg-lambda.py b/modules/fortigate/fgt_asg/fgt-asg-lambda.py index dbd6323..5cc325a 100644 --- a/modules/fortigate/fgt_asg/fgt-asg-lambda.py +++ b/modules/fortigate/fgt_asg/fgt-asg-lambda.py @@ -596,6 +596,8 @@ def __init__(self, event): self.s3_client = boto3.client("s3") self.dynamodb_client = boto3.client("dynamodb") self.lambda_client = boto3.client("lambda") + self.secrets_client = boto3.client("secretsmanager") + self.logger.info(f"Do FGT config.") self.logger.info(f"Event detail:: {event}") @@ -609,6 +611,12 @@ def __init__(self, event): self.fgt_login_port_number = os.getenv("fgt_login_port_number") self.internal_lambda_name = os.getenv("internal_lambda_name") self.asg_name = os.getenv("asg_name") + self.fgt_password_secret_name = os.getenv("fgt_password_secret_name") + + if os.getenv("fgt_password_from_secrets_manager") == "true": + self.fgt_password = self.get_secret() + else: + self.fgt_password = "" def main(self): if self.detail_type == "EC2 Instance Launch Successful": @@ -769,6 +777,7 @@ def upload_license(self, fgt_private_ip, fgt_vm_id): if license_type == "token": payload = { "private_ip" : fgt_private_ip, + "password" : self.fgt_password, "operation" : "upload_license", "parameters" : { "license_type": license_type, @@ -787,6 +796,7 @@ def upload_license(self, fgt_private_ip, fgt_vm_id): lic_file_content = self.get_lic_file_content(license_content) payload = { "private_ip" : fgt_private_ip, + "password" : self.fgt_password, "operation" : "upload_license", "parameters" : { "license_type": license_type, @@ -2002,6 +2012,7 @@ def upload_config(self, config_content, fgt_private_ip): self.logger.info("Upload configuration to FortiGate instance.") payload = { "private_ip" : fgt_private_ip, + "password" : self.fgt_password, "operation" : "upload_config", "parameters" : { "config_content": config_content @@ -2027,6 +2038,7 @@ def change_password(self, fgt_private_ip, fgt_vm_id): self.logger.info("Change password.") payload = { "private_ip" : fgt_private_ip, + "password" : self.fgt_password, "operation" : "change_password", "parameters" : { "fgt_vm_id": fgt_vm_id @@ -2034,7 +2046,25 @@ def change_password(self, fgt_private_ip, fgt_vm_id): } b_succ = self.invoke_lambda(payload) return b_succ - + + def get_secret(self): + + secret_name = self.fgt_password_secret_name + get_secret_value_response = "" + + try: + get_secret_value_response = self.secrets_client.get_secret_value( + SecretId=secret_name + ) + except ClientError as e: + # For a list of exceptions thrown, see + # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html + self.logger.error(f"Could not get password from AWS Secrets: {e}") + + secret = json.loads(get_secret_value_response['SecretString']) + + return secret['password'] + def lambda_handler(event, context): ## Network Interface operations intfObject = NetworkInterface() diff --git a/modules/fortigate/fgt_asg/main.tf b/modules/fortigate/fgt_asg/main.tf index 9338fac..70314c6 100644 --- a/modules/fortigate/fgt_asg/main.tf +++ b/modules/fortigate/fgt_asg/main.tf @@ -29,6 +29,7 @@ locals { fgt_login_port_number = var.fgt_login_port_number } fgt_userdata = templatefile("${path.module}/fgt-userdata.tftpl", local.vars) + secrets_manager_name = "/fgt_asg_admin/password" } @@ -238,7 +239,14 @@ resource "aws_iam_role_policy" "iam_policy" { ], Effect = "Allow", Resource = "arn:aws:events:*:*:rule/*" - } + }, + { + Action = [ + "secretsmanager:GetSecretValue" + ], + Effect = "Allow", + Resource = aws_secretsmanager_secret.fgt_asg_admin.arn + }, ] }) } @@ -362,6 +370,9 @@ resource "aws_lambda_function" "fgt_asg_lambda" { environment { variables = { + fgt_password_from_secrets_manager = var.fgt_password_from_secrets_manager + + fgt_password_secret_name = local.secrets_manager_name internal_lambda_name = "fgt-asg-lambda-internal_${var.asg_name}" asg_name = var.asg_name network_interfaces = jsonencode(var.network_interfaces) @@ -409,8 +420,9 @@ resource "aws_lambda_function" "fgt_asg_lambda_internal" { environment { variables = { - fgt_password = var.fgt_password - fgt_login_port_number = var.fgt_login_port_number + fgt_password = var.fgt_password + fgt_password_from_secrets_manager = var.fgt_password_from_secrets_manager + fgt_login_port_number = var.fgt_login_port_number } } @@ -480,4 +492,33 @@ resource "aws_cloudwatch_event_target" "fgt_asg_terminate" { rule = aws_cloudwatch_event_rule.fgt_asg_terminate.name target_id = "fgt_asg_terminate_target_${var.asg_name}" arn = aws_lambda_function.fgt_asg_lambda.arn +} + +# ------------------------------------------------------------------------------ +# SECRETS MANAGER +# ------------------------------------------------------------------------------ + +resource "random_password" "password" { + length = 32 + special = true + override_special = "@#_&!?" +} + +resource "aws_secretsmanager_secret" "fgt_asg_admin" { + name = local.secrets_manager_name + description = "Admin password for Fortigate ASG instances" + + tags = merge( + lookup(var.tags, "general", {}), + lookup(var.tags, "secretsmanager", {}) + ) +} + +resource "aws_secretsmanager_secret_version" "fgt_asg_admin_password" { + secret_id = aws_secretsmanager_secret.fgt_asg_admin.id + secret_string = jsonencode( + { + password = random_password.password.result + } + ) } \ No newline at end of file diff --git a/modules/fortigate/fgt_asg/variables.tf b/modules/fortigate/fgt_asg/variables.tf index 72ca77e..370dab1 100644 --- a/modules/fortigate/fgt_asg/variables.tf +++ b/modules/fortigate/fgt_asg/variables.tf @@ -320,6 +320,12 @@ variable "lambda_timeout" { default = 300 } +variable "fgt_password_from_secrets_manager" { + description = "Whether to use AWS Secrets Manager secret to retrieve FortiGate admin password." + type = bool + default = false +} + variable "lic_s3_name" { description = "AWS S3 bucket name that contains FortiGate license files or token json file." type = string From 70583a388609cd8f54ec6b8c7987ef592cceede6 Mon Sep 17 00:00:00 2001 From: "Michel Z. Santello" Date: Thu, 20 Jun 2024 21:51:19 -0300 Subject: [PATCH 4/4] feat: add support for DNS record creation for primary nodes --- modules/fortigate/fgt_asg/fgt-asg-lambda.py | 64 ++++++++++++++++++++- modules/fortigate/fgt_asg/main.tf | 13 ++++- modules/fortigate/fgt_asg/variables.tf | 5 ++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/modules/fortigate/fgt_asg/fgt-asg-lambda.py b/modules/fortigate/fgt_asg/fgt-asg-lambda.py index 5cc325a..44b3578 100644 --- a/modules/fortigate/fgt_asg/fgt-asg-lambda.py +++ b/modules/fortigate/fgt_asg/fgt-asg-lambda.py @@ -597,6 +597,7 @@ def __init__(self, event): self.dynamodb_client = boto3.client("dynamodb") self.lambda_client = boto3.client("lambda") self.secrets_client = boto3.client("secretsmanager") + self.route53_client = boto3.client('route53') self.logger.info(f"Do FGT config.") @@ -612,6 +613,7 @@ def __init__(self, event): self.internal_lambda_name = os.getenv("internal_lambda_name") self.asg_name = os.getenv("asg_name") self.fgt_password_secret_name = os.getenv("fgt_password_secret_name") + self.route53_zone_id = os.getenv("route53_zone_id") if os.getenv("fgt_password_from_secrets_manager") == "true": self.fgt_password = self.get_secret() @@ -670,7 +672,41 @@ def do_launch(self): config_content = self.gen_config_content(self.fgt_vm_id) b_succ = self.upload_config(config_content, fgt_private_ip) + + def update_route53_primary(self, record_name, record_value, zone_id, ttl=60): + self.logger.info(f"Updating record {record_name} with {record_value} on Route53.") + + if not zone_id: + self.logger.error(f"Error: route53_zone_id env var is undefined. Record update aborted") + return + + domain_name = self.route53_client.get_hosted_zone(Id=zone_id)['HostedZone']['Name'] + dns_record = record_name + '.' + domain_name + + # Prepare the change batch request + change_batch = { + 'Changes': [ + { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': dns_record, + 'Type': 'A', + 'TTL': ttl, + 'ResourceRecords': [{'Value': record_value}] + } + } + ] + } + # Update the record + try: + response = self.route53_client.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch=change_batch + ) + print(f"Change submitted. Status: {response['ChangeInfo']['Status']}") + except Exception as e: + print(f"Error updating the record: {e}") def do_terminate(self): need_license = os.getenv("need_license") @@ -723,6 +759,24 @@ def get_private_ip(self, instance): rst = cur_private_ip break return rst + + def get_public_ip(self, instance_id): + self.logger.info(f"Get public ip for current instance.") + rst = None + response = self.ec2_client.describe_instances(InstanceIds=[instance_id]) + instances = response['Reservations'][0]['Instances'] + + if instances: + for instance in instances: + network_interfaces = instance.get('NetworkInterfaces', []) + for interface in network_interfaces: + public_ip = interface.get('Association', {}).get('PublicIp') + if public_ip: + rst = public_ip + break + if not rst: + self.logger.error(f"Failed to get public interface address for {instance_id}.") + return rst def get_primary_ip(self, instance): self.logger.info(f"Get primary ip for current instance.") @@ -1797,7 +1851,15 @@ def update_primary(self, instance_id, primary_ip): }) b_succ = True except Exception as err: - self.logger.error(f"Could not update primary instance information: {err}") + self.logger.error(f"Could not update primary instance information: {err}") + + if instance_id: + # Create DNS records to indentify the primary instance + public_ip = self.get_public_ip(instance_id) + self.update_route53_primary('traffic-inspection-public', public_ip, self.route53_zone_id) + self.update_route53_primary('traffic-inspection-private', primary_ip, self.route53_zone_id) + + return b_succ def check_primary(self, fgt_vm_id): diff --git a/modules/fortigate/fgt_asg/main.tf b/modules/fortigate/fgt_asg/main.tf index 70314c6..dd8aa06 100644 --- a/modules/fortigate/fgt_asg/main.tf +++ b/modules/fortigate/fgt_asg/main.tf @@ -246,7 +246,17 @@ resource "aws_iam_role_policy" "iam_policy" { ], Effect = "Allow", Resource = aws_secretsmanager_secret.fgt_asg_admin.arn - }, + }, + { + Action = [ + "route53:ChangeResourceRecordSets", + "route53:GetChange", + "route53:GetHostedZone", + "route53:ListHostedZones", + ], + Effect = "Allow", + Resource = "arn:aws:route53:::hostedzone/${var.route53_zone_id}" + }, ] }) } @@ -392,6 +402,7 @@ resource "aws_lambda_function" "fgt_asg_lambda" { fortiflex_sn_list = jsonencode(var.fortiflex_sn_list) fortiflex_configid_list = jsonencode(var.fortiflex_configid_list) az_name_map = jsonencode(var.az_name_map) + route53_zone_id = var.route53_zone_id } } diff --git a/modules/fortigate/fgt_asg/variables.tf b/modules/fortigate/fgt_asg/variables.tf index 370dab1..eb8bc7b 100644 --- a/modules/fortigate/fgt_asg/variables.tf +++ b/modules/fortigate/fgt_asg/variables.tf @@ -320,6 +320,11 @@ variable "lambda_timeout" { default = 300 } +variable "route53_zone_id" { + description = "The ZoneID to be used for primary address DNS registration" + type = string +} + variable "fgt_password_from_secrets_manager" { description = "Whether to use AWS Secrets Manager secret to retrieve FortiGate admin password." type = bool