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..44b3578 100644 --- a/modules/fortigate/fgt_asg/fgt-asg-lambda.py +++ b/modules/fortigate/fgt_asg/fgt-asg-lambda.py @@ -596,6 +596,9 @@ 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.route53_client = boto3.client('route53') + self.logger.info(f"Do FGT config.") self.logger.info(f"Event detail:: {event}") @@ -609,6 +612,13 @@ 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") + self.route53_zone_id = os.getenv("route53_zone_id") + + 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": @@ -662,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") @@ -715,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.") @@ -769,6 +831,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 +850,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, @@ -1787,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): @@ -2002,6 +2074,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 +2100,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 +2108,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 a4134aa..dd8aa06 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" } @@ -41,6 +42,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 } @@ -208,21 +214,49 @@ 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" ], Effect = "Allow", Resource = "arn:aws:events:*:*:rule/*" - } + }, + { + Action = [ + "secretsmanager:GetSecretValue" + ], + 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}" + }, ] }) } @@ -346,6 +380,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) @@ -365,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 } } @@ -393,8 +431,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 } } @@ -464,4 +503,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 25ed1f5..eb8bc7b 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" @@ -314,6 +320,17 @@ 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 + default = false +} + variable "lic_s3_name" { description = "AWS S3 bucket name that contains FortiGate license files or token json file." type = string