Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor fgt_asg module improvements #5

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion modules/fortigate/fgt_asg/fgt-asg-lambda-internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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":
Expand Down
96 changes: 94 additions & 2 deletions modules/fortigate/fgt_asg/fgt-asg-lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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 = "<from_internal_lambda_env>"

def main(self):
if self.detail_type == "EC2 Instance Launch Successful":
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -2027,14 +2100,33 @@ 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
}
}
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()
Expand Down
78 changes: 73 additions & 5 deletions modules/fortigate/fgt_asg/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}


Expand All @@ -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 }

Expand Down Expand Up @@ -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}"
},
]
})
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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
}
)
}
17 changes: 17 additions & 0 deletions modules/fortigate/fgt_asg/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down