Inside the AWS Onboarding Script (Terraform)
The AWS Terraform configuration automates onboarding of an AWS Account into AlgoSec Cloud Enterprise (ACE).
This Terraform template provisions the required AWS resources and IAM permissions, enabling AlgoSec to integrate with and monitor the environment. and invokes the ACE onboarding API to register the AWS Account.
The process is fully automated during 'terraform apply'.
There are two scripts available.
The following is a breakdown of the regular AWS flow (Terraform-based) sections and their purposes:
1. Terraform Configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.0"
}
time = {
source = "hashicorp/time"
version = "~> 0.10"
}
}
required_version = ">= 1.5.7"
}
Purpose: Defines the required Terraform CLI and AWS provider versions for the onboarding process.
2. Input Variables
variable "tenant_id" {
description = "TenantId - optional suffix for resources name"
type = string
default = ""
}
variable "client_id" {
description = "Client ID for AlgoSec authentication"
type = string
sensitive = true
}
variable "client_secret" {
description = "Client Secret for AlgoSec authentication"
type = string
sensitive = true
}
Purpose: Defines required onboarding inputs including tenant identifier and AlgoSec access credentials.
3. AWS Account Context
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
Purpose: Dynamically retrieves AWS account ID and region for ARN construction and permission scoping.
4. AlgoSec Role
resource "aws_iam_role" "algosec_role" {
name = "AlgosecRole${var.tenant_id}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = {
AWS = "<AlgoSec Account>"
}
Condition = {
StringEquals = {
"sts:ExternalId" = "algosec-xxx"
}
}
}
]
})
}
Purpose: Defines an IAM role that AlgoSec can assume to access AWS resources. Specifies the trusted entity (AlgoSec’s AWS account) and enforces secure cross-account access using an External ID.
5. SecurityAudit Policy Attachment
resource "aws_iam_role_policy_attachment" "security_audit" {
role = aws_iam_role.algosec_role.name
policy_arn = "arn:aws:iam::aws:policy/SecurityAudit"
}
Purpose Grants read-only access to AWS resources across the account. This allows Cloud App Analyzer to scan configurations, permissions, and logs without making changes, enabling a comprehensive security posture assessment.
6. Onboarding Role
resource "aws_iam_role" "function_role" {
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "function_role_basic_execution" {
role = aws_iam_role.function_role.id
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
Purpose: Defines an IAM role for the Lambda function, allowing it to be assumed by the Lambda service and granting basic execution permissions.
7. Lambda Function Source
resource "local_file" "notification_source" {
filename = "${path.module}/notification-function.js"
content = <<-EOF
Purpose: Begins the definition of the Lambda function, specifying zip file name and that the code will be provided inline
8. Modules
const https = require("https");
const {
SSMClient,
PutParameterCommand,
DeleteParameterCommand,
GetParameterCommand
} = require("@aws-sdk/client-ssm");
let cfnResponse;
try {
cfnResponse = require("cfn-response");
console.log("cfn-response module loaded");
} catch (e) {
console.log(
"cfn-response module not found, will use manual response for CloudFormation"
);
}
Purpose: Imports required modules.
9. Function Handler
exports.handler = async (event, context, callback) => {
console.log("Request Type:", event.RequestType);
const ssmClient = new SSMClient({
region: process.env.AWS_REGION
});
Purpose Log print and initiate SSMClient
10. Helper Function
const httpsRequest = (options, data) => {
return new Promise((resolve, reject) => {
const request = https.request(options, response => {
let responseBody = "";
response.on("data", chunk => {
responseBody += chunk.toString();
});
if (response.statusCode >= 200 && response.statusCode < 300) {
response.on("end", () => resolve(responseBody));
} else {
response.on("end", () => reject(responseBody));
}
});
request.on("error", err => reject(err));
request.write(data);
request.end();
});
};
Purpose: Defines a helper function for making HTTPS requests.
11. Access key
let clientId = "";
let clientSecret = "";
const getParameter = async (Name) => {
const { Parameter } = await ssmClient.send(
new GetParameterCommand({
Name,
WithDecryption: true
})
);
return Parameter.Value;
};
const clientIdParameterName = "/algosec/<algosec tenant id>/clientId";
const clientSecretParameterName = "/algosec/<algosec tenant id>/clientSecret";
if (event.RequestType === "Delete") {
// Retrieve ClientId and ClientSecret from SSM before deleting them
clientId = await getParameter(clientIdParameterName);
clientSecret = await getParameter(clientSecretParameterName);
console.log("Deleting SSM Parameters");
const parametersToDelete = [
clientIdParameterName,
clientSecretParameterName
];
for (const paramName of parametersToDelete) {
try {
await ssmClient.send(
new DeleteParameterCommand({ Name: paramName })
);
console.log("Successfully deleted: " + paramName);
} catch (e) {
console.log(
"Parameter " + paramName + " not found for deletion:",
e.message
);
}
}
} else {
// Retrieve ClientId and ClientSecret from parameters before creating/updating them
clientId = event.ResourceProperties.ClientId;
clientSecret = event.ResourceProperties.ClientSecret;
console.log("Creating/updating SSM Parameters");
const parameters = [
{
name: clientIdParameterName,
value: clientId,
description: "Algosec Client ID"
},
{
name: clientSecretParameterName,
value: clientSecret,
description: "Algosec Client Secret"
}
];
for (const param of parameters) {
if (!param.value) {
throw new Error(
"Parameter value is empty for " + param.name
);
}
await ssmClient.send(
new PutParameterCommand({
Name: param.name,
Value: param.value,
Type: "SecureString",
Description: param.description,
Overwrite: true
})
);
console.log("Successfully created/updated: " + param.name);
}
}
Purpose: Initializes variables and defines a helper function to retrieve parameters securely from AWS Systems Manager (SSM).
12. Save access key
} else {
// Retrieve ClientId and ClientSecret from parameters before creating/updating them
clientId = event.ResourceProperties.ClientId;
clientSecret = event.ResourceProperties.ClientSecret;
// Create/Update SSM Parameters
console.log("Creating/updating SSM Parameters");
const parameters = [
{
name: clientIdParameterName,
value: clientId,
description: "Algosec Client ID"
},
{
name: clientSecretParameterName,
value: clientSecret,
description: "Algosec Client Secret"
}
];
for (const param of parameters) {
if (!param.value) {
throw new Error(
"Parameter value is empty for " + param.name
);
}
await ssmClient.send(
new PutParameterCommand({
Name: param.name,
Value: param.value,
Type: "SecureString",
Description: param.description,
Overwrite: true
})
);
console.log(
"Successfully created/updated: " + param.name
);
}
}
Purpose: Retrieves the Client ID and Client Secret from the input event and securely stores or updates them in AWS Systems Manager (SSM) Parameter Store.
13. Login Access Keys
try {
const loginOptions = {
hostname: "us.app.algosec.com",
path: "/api/algosaas/auth/v1/access-keys/login",
method: "POST"
};
Purpose: Begins a try-catch block and sets up options for the login request.
14. Login Credentials
const loginPayload = {
tenantId: "5494786575842013",
clientId,
clientSecret
};
const loginData = JSON.stringify(loginPayload);
Purpose: Initiate login credentials.
15. Login Request
const loginResponse = JSON.parse(await httpsRequest(loginOptions, loginData));
Purpose: Sends the login request to AlgoSec and parses the authentication response.
16. Role Assignment
const onboardingOptions = {
hostname: "us.app.algosec.com",
path: "/api/algosaas/onboarding/v1/aws",
method: "POST",
headers: {
Authorization: "Bearer " + loginResponse.access_token
}
};
Purpose: Sets up options for the onboarding request, including the auth token.
17. Onboarding Request
const onboardingData = JSON.stringify({
event,
role_arn: process.env.algosecRole,
external_id: process.env.externalId,
supportChanges: false,
flowLogs: true
});
await httpsRequest(onboardingOptions, onboardingData);
Purpose: Sends the onboarding request.
18. Cloud Network Security Onboarding Call
if (cfnResponse) {
let physicalResourceId = event.PhysicalResourceId;
if (event.RequestType === "Create") {
const stackName = event.StackId.split("/")[1];
physicalResourceId =
"algosec-notification-" +
stackName +
"-" +
event.LogicalResourceId;
}
await cfnResponse.send(
event,
context,
cfnResponse.SUCCESS,
{
message:
"RequestType: " +
event.RequestType +
", physicalResourceId: " +
physicalResourceId
},
physicalResourceId
);
return;
}
return {
status: "SUCCESS",
message: "RequestType: " + event.RequestType
};
} catch (err) {
console.error(err);
if (cfnResponse) {
await cfnResponse.send(
event,
context,
cfnResponse.FAILED
);
return;
}
return {
status: "FAILED",
message: err
};
}
Purpose: Handles success and failure responses and returns a structured result to Terraform to indicate whether onboarding succeeded or failed.
19. Archive Code
data "archive_file" "notification_zip" {
type = "zip"
output_path = "${path.module}/notification_function.zip"
source {
content = local_file.notification_source.content
filename = local_file.notification_source.filename
}
}
Purpose: Create zip file with lambda notification code.
20. AlgoSec Notification Lambda
resource "aws_lambda_function" "algosec_notification" {
function_name = "AlgoSecOnboardingNotification${var.tenant_id}"
role = aws_iam_role.function_role.arn
handler = "notification-function.handler"
runtime = "nodejs22.x"
filename = data.archive_file.notification_zip.output_path
source_code_hash = data.archive_file.notification_zip.output_base64sha256
memory_size = 128
timeout = 10
environment {
variables = {
algosecRole = aws_iam_role.algosec_role.arn
externalId = "algosec-xxx"
}
}
}
Purpose: Deploys the Lambda function responsible for authenticating with AlgoSec and initiating AWS onboarding.
Configures the Lambda function's runtime settings and environment variables.
21. Lambda Invocation
Invoke create payload via data source (replaces null_resource invoke on create).
data "aws_lambda_invocation" "notification_create" {
function_name = aws_lambda_function.algosec_notification.function_name
input = jsonencode({
RequestType = "Create"
ResourceProperties = {
ClientId = var.client_id
ClientSecret = var.client_secret
Touch = "1771507784209"
}
})
depends_on = [
aws_lambda_function.algosec_notification,
]
}
Purpose: Invokes the onboarding Lambda automatically during Terraform apply.
22. Terraform Postcondition – Validate Onboarding Success
resource "terraform_data" "notification_create_ok" {
input = data.aws_lambda_invocation.notification_create.result
lifecycle {
postcondition {
condition = can(jsondecode(self.input)) && try(jsondecode(self.input).status, "") == "SUCCESS"
error_message = "Lambda notification_create failed: ${self.input}"
}
}
}
resource "local_file" "notification_delete_payload" {
filename = "${path.module}/notification-delete-payload.json"
content = jsonencode({
RequestType = "Delete"
})
}
Purpose: Create file with notification delete payload.
23. Lambda delete Invocation
# Replace data source with destroy-only invoke via null_resource
resource "null_resource" "invoke_notification_delete" {
triggers = {
function_name = aws_lambda_function.algosec_notification.function_name
payload_path = local_file.notification_delete_payload.filename
}
provisioner "local-exec" {
when = destroy
command = "aws lambda invoke --function-name ${self.triggers.function_name} --cli-binary-format raw-in-base64-out --payload file://${self.triggers.payload_path} -"
}
depends_on = [
aws_lambda_function.algosec_notification,
local_file.notification_delete_payload
Purpose: Invoke notification lambda during terraform destroy to offboard the account.
24. Read Only Policy
variable "cloudflow_base_actions" {
type = list(string)
default = [
# ... (list of allowed actions)
]
}
data "aws_iam_policy_document" "cloudflow_readonly" {
statement {
sid = "CloudFlowReadOnly"
effect = "Allow"
actions = var.cloudflow_base_actions
resources = ["*"]
}
}
resource "aws_iam_role_policy" "cloudflow_readonly" {
name = "CloudFlowReadOnly"
role = aws_iam_role.algosec_role.name
policy = data.aws_iam_policy_document.cloudflow_readonly.json
}
Purpose: Defines an IAM policy granting read-only access to various AWS services.
25. Flow Logs Policy
data "aws_iam_policy_document" "cloudflow_flow_logs" {
statement {
sid = "CloudFlowReadFlowLogs"
effect = "Allow"
actions = [
"s3:GetObject*"
]
resources = ["*"]
}
}
resource "aws_iam_role_policy" "cloudflow_read_flow_logs" {
name = "CloudFlowReadFlowLogs"
role = aws_iam_role.algosec_role.name
policy = data.aws_iam_policy_document.cloudflow_flow_logs.json
}
Purpose: Defines another IAM policy specifically for reading S3 objects (likely flow logs).
26. Write Policy
variable "cloudflow_write_actions" {
type = list(string)
default = [
"ec2:AuthorizeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:RevokeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress"
]
}
data "aws_iam_policy_document" "cloudflow_write" {
statement {
sid = "CloudFlowWrite"
effect = "Allow"
actions = var.cloudflow_write_actions
resources = ["*"]
}
}
resource "aws_iam_role_policy" "cloudflow_write" {
name = "CloudFlowWrite"
role = aws_iam_role.algosec_role.name
policy = data.aws_iam_policy_document.cloudflow_write.json
}
Purpose: Write Policy for Cloud Network security.
27. Prevasio Base Cloud Service Policy
variable "prevasio_base_actions" {
type = list(string)
default = [
"ses:DescribeActiveReceiptRuleSet",
"logs:DescribeLogGroups",
"logs:DescribeMetricFilters",
"dlm:GetLifecyclePolicies",
"kms:GetKeyRotationStatus",
"ecr-public:GetAuthorizationToken",
"ecr:GetAuthorizationToken",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:DeleteRepositoryPolicy",
"cloudwatch:GetMetricStatistics",
"sts:GetServiceBearerToken",
"inspector2:ListFindings",
"inspector2:ListCoverage",
"appflow:DescribeFlow",
"imagebuilder:List*",
"imagebuilder:Get*",
"wafv2:DescribeManagedRuleGroup",
"wafv2:GetRuleGroup",
"wafv2:ListManagedRuleSets",
"wafv2:ListResourcesForWebACL",
"apprunner:ListAssociatedServicesForWebAcl",
"cognito-idp:ListResourcesForWebACL",
"ec2:DescribeVerifiedAccessInstanceWebAclAssociations",
"wafv2:CheckCapacity",
"account:ListRegions",
"bedrock:GetAgent",
"bedrock:GetKnowledgeBase",
"bedrock:GetGuardrail"
]
}
data "aws_iam_policy_document" "prevasio_cspm" {
statement {
sid = "PrevasioCSPM"
effect = "Allow"
actions = var.prevasio_base_actions
resources = ["*"]
}
statement {
effect = "Allow"
actions = [
"s3:GetObject"
]
resources = [
"arn:aws:s3:::elasticbeanstalk*"
]
}
}
resource "aws_iam_role_policy" "prevasio_cspm" {
name = "PrevasioCSPM"
role = aws_iam_role.algosec_role.name
policy = data.aws_iam_policy_document.prevasio_cspm.json
}
Purpose: Creates the primary IAM policy that allows Cloud App Analyzer to perform cloud security scanning. Includes permissions required for comprehensive CSPM visibility across AWS services.
28. Cloud App Analyzer ECR Exclusion Role (CD-Mitigation)
variable "prevasio_cd_mitigation_actions" {
type = list(string)
default = [
"ecr:SetRepositoryPolicy"
]
}
data "aws_iam_policy_document" "prevasio_cd_mitigation" {
statement {
sid = "PrevasioCDMitigation"
effect = "Allow"
actions = var.prevasio_cd_mitigation_actions
resources = ["*"]
}
}
resource "aws_iam_role_policy" "prevasio_cd_mitigation" {
name = "PrevasioCDMitigation"
role = aws_iam_role.algosec_role.name
policy = data.aws_iam_policy_document.prevasio_cd_mitigation.json
}
resource "aws_iam_role" "prevasio_ecr_exclusions_role" {
name = "PrevasioECRExclusionsRole${var.tenant_id}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "prevasio_lambda_basic_execution" {
role = aws_iam_role.prevasio_ecr_exclusions_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
variable "prevasio_ecr_exclusions_actions" {
type = list(string)
default = [
"ecr:GetRepositoryPolicy",
"ecr:SetRepositoryPolicy",
"ecr:DescribeRepositories"
]
}
data "aws_iam_policy_document" "prevasio_ecr_exclusions_policy" {
statement {
effect = "Allow"
actions = var.prevasio_ecr_exclusions_actions
resources = ["*"]
}
}
resource "aws_iam_role_policy" "prevasio_ecr_exclusions_inline" {
name = "PrevasioECRExclusionsPolicy"
role = aws_iam_role.prevasio_ecr_exclusions_role.name
policy = data.aws_iam_policy_document.prevasio_ecr_exclusions_policy.json
}
Purpose: PrevasioECRExclusionsRole Defines an IAM role for Lambda functions with permissions to:
-
Execute basic Lambda operations
-
Assume the Prevasio secret role
-
Access and update the API connection secret
-
Interact with ECR repositories
The inline policy grants specific permissions for cross-account secret access and ECR repository management, which are crucial for the onboarding process.
29. Cloud App Analyzer Image Push Handler Function (CD-Mitigation)
resource "local_file" "prevasio_image_push_handler_function" {
filename = "${path.module}/index.py"
content = <<-EOF
# content...
}
data "archive_file" "prevasio_image_push_zip" {
type = "zip"
output_path = "${path.module}/prevasio_image_push_handler_function.zip"
source {
content = local_file.prevasio_image_push_handler_function.content
filename = local_file.prevasio_image_push_handler_function.filename
}
}
resource "aws_lambda_function" "prevasio_image_push_handler" {
function_name = "PrevasioImagePushHandlerFunction${var.tenant_id}"
filename = data.archive_file.prevasio_image_push_zip.output_path
source_code_hash = data.archive_file.prevasio_image_push_zip.output_base64sha256
handler = "index.handler"
runtime = "python3.11"
role = aws_iam_role.function_role.arn
memory_size = 128
timeout = 10
depends_on = [data.archive_file.prevasio_image_push_zip, aws_iam_role.function_role]
Purpose: A Lambda function that processes ECR image push events, the function sends container image push events to Cloud App Analyzer for potential container security scanning.
30. ImagePushedEventBridgeRule and InvokeImagePushHandlerFunctionPermission (CD-Mitigation)
resource "aws_cloudwatch_event_rule" "prevasio_image_pushed" {
name = "PrevasioImagePushedEventRule${var.tenant_id}"
event_pattern = jsonencode({
source = ["aws.ecr"]
"detail-type" = ["ECR Image Action"]
detail = {
result = ["SUCCESS"]
}
})
}
resource "aws_cloudwatch_event_target" "prevasio_image_push_target" {
rule = aws_cloudwatch_event_rule.prevasio_image_pushed.name
arn = aws_lambda_function.prevasio_image_push_handler.arn
target_id = "PrevasioImagePushHandlerFunction"
}
resource "aws_lambda_permission" "allow_eventbridge_invoke_image_push_handler" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.prevasio_image_push_handler.arn
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.prevasio_image_pushed.arn
}
Purpose: Sets up EventBridge to trigger the image push handler when images are pushed to ECR.
This enables automated notifications to Prevasio when new container images are available, facilitating continuous security scanning.
31. PrevasioECRExclusionsFunction
resource "local_file" "prevasio_ecr_exclusions_function" {
filename = "${path.module}/index.py"
content = <<-EOF
import json
import boto3
import os
def handler(event, context):
is_cf_event = 'ResponseURL' in event
if is_cf_event:
import cfnresponse
try:
account_id = boto3.client('sts').get_caller_identity().get('Account')
role_arn = os.environ['algosecRole']
assumed_role_arn = role_arn.replace(':iam::', ':sts::').replace(':role/', ':assumed-role/') + '/algosec'
aws_partition = role_arn.split(':')[1]
root_role = f'arn:{aws_partition}:iam::{account_id}:root'
ecr_client = boto3.client('ecr')
for repository in ecr_client.describe_repositories().get('repositories', []):
try:
policy = json.loads(ecr_client.get_repository_policy(repositoryName=repository['repositoryName']).get('policyText', '{}'))
statements = policy.get('Statement', [])
should_save_policy = False
for statement in statements:
if statement.get('Sid') == 'Prevasio Locker':
should_save_policy = True
not_principal = statement.get('NotPrincipal', {'AWS': []})
roles = not_principal.get('AWS', [])
if event['RequestType'] == 'Delete':
for role in [root_role, role_arn, assumed_role_arn]:
try:
if isinstance(roles, list):
roles.remove(role)
elif roles == role:
roles = []
except ValueError:
pass
if not roles:
not_principal = {}
else:
for role in [root_role, role_arn, assumed_role_arn]:
roles.append(role)
if not_principal:
statement.pop('Principal', None)
statement['NotPrincipal'] = not_principal
else:
statement.pop('NotPrincipal', None)
statement['Principal'] = {'AWS': '*'}
if should_save_policy:
ecr_client.set_repository_policy(repositoryName=repository['repositoryName'],
policyText=json.dumps(policy))
except ecr_client.exceptions.RepositoryPolicyNotFoundException:
pass
except Exception as e:
err_msg = f'Failed to update ECR repositories policies: {str(e)}'
print(err_msg)
if is_cf_event:
cfnresponse.send(event, context, cfnresponse.FAILED, {'Message': err_msg})
return
if is_cf_event:
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
EOF
}
data "archive_file" "prevasio_ecr_exclusions_zip" {
type = "zip"
output_path = "${path.module}/prevasio_ecr_exclusions_function.zip"
source {
content = local_file.prevasio_ecr_exclusions_function.content
filename = local_file.prevasio_ecr_exclusions_function.filename
}
}
resource "aws_lambda_function" "prevasio_ecr_exclusions" {
function_name = "PrevasioECRExclusionsFunction${var.tenant_id}"
filename = data.archive_file.prevasio_ecr_exclusions_zip.output_path
source_code_hash = data.archive_file.prevasio_ecr_exclusions_zip.output_base64sha256
handler = "index.handler"
runtime = "python3.11"
role = aws_iam_role.prevasio_ecr_exclusions_role.arn
memory_size = 128
timeout = 60
environment {
variables = {
algosecRole = aws_iam_role.algosec_role.arn
}
}
depends_on = [aws_iam_role.prevasio_ecr_exclusions_role, aws_iam_role_policy.prevasio_ecr_exclusions_inline]
}
Purpose: This Lambda function manages ECR repository policies to ensure or remove exclusion of specific IAM roles (e.g., root, AlgosecRole, and its assumed variant) under the "Prevasio Locker" statement. It's designed to run both as a CloudFormation Custom Resource (handling Create, Update, and Delete events) and as a standalone script.
-
On Create/Update, it adds the relevant roles to the NotPrincipal field of applicable ECR policies.
-
On Delete, it removes those roles and resets the policy if no exclusions remain.
32. PrevasioECRExclusionsFunctionInvoke
data "aws_lambda_invocation" "prevasio_ecr_exclusions_invoke" {
function_name = aws_lambda_function.prevasio_ecr_exclusions.function_name
input = jsonencode({})
depends_on = [
aws_lambda_function.prevasio_ecr_exclusions
]
}
Purpose: This custom resource triggers the PrevasioECRExclusionsFunction Lambda during CloudFormation stack operations (Create, Update, Delete). It ensures that the ECR exclusion logic is automatically executed as part of the deployment lifecycle.
-
Uses the Lambda’s ARN as a service token.
-
Ensures the function logic runs without manual invocation.
-
Typically used for policy enforcement or cleanup during provisioning.
33. Kubernetes Security
34. VM Scan
The Terraform configuration automates onboarding of an AWS Management Account into AlgoSec Cloud Enterprise (ACE).
The template provisions IAM roles, Lambda functions, SSM permissions, and triggers onboarding during terraform apply. Each of the following sections describes a specific component and its purpose within the infrastructure-as-code deployment.
1. Terraform and Provider Requirements
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.0"
}
}
required_version = ">= 1.5.7"
}
Purpose: Defines the Terraform CLI and AWS provider versions required for onboarding.
2. Input Variables
variable "tenant_id" {
type = string
default = ""
}
variable "client_id" {
type = string
default = ""
}
variable "client_secret" {
type = string
default = ""
}
Purpose: Defines required onboarding inputs including tenant identifier and AlgoSec access credentials.
3. AWS Identity and Region Data Sources
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
Purpose: Retrieves AWS account ID and region dynamically for constructing ARNs and scoping permissions.
4. Onboarding Management Lambda Role
resource "aws_iam_role" "onboarding_management_lambda_role" {
name = "OnboardingManagementLambdaRole${var.tenant_id}"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Service = [
"lambda.amazonaws.com"
]
},
Action = [
"sts:AssumeRole"
]
}
]
})
}
Purpose: Defines an IAM role for the management onboarding Lambda function, allowing it to be assumed by the Lambda service.
5. Lambda Basic Execution Policy Attachment
resource "aws_iam_role_policy_attachment" "function_role_basic_execution" {
role = aws_iam_role.onboarding_management_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
Purpose: Attaches the AWS managed policy AWSLambdaBasicExecutionRole to the Lambda execution role.
6. SSM Parameter Access Policy for the Lambda
resource "aws_iam_role_policy" "onboarding_ssm_parameter_access" {
name = "SSMParameterAccess"
role = aws_iam_role.onboarding_management_lambda_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParameter"
],
Resource = [
"arn:aws:ssm:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:parameter/algosec/5494786575842013/clientId",
"arn:aws:ssm:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:parameter/algosec/5494786575842013/clientSecret"
]
}
]
})
}
Purpose: Grants the onboarding Lambda permission to manage ACE credentials stored in AWS Systems Manager Parameter Store.
7. AlgoSec Management Role
resource "aws_iam_role" "algosec_management_role" {
name = "AlgosecManagementRole${var.tenant_id}"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"sts:AssumeRole"
],
Principal = {
AWS = "arn:aws:iam::084497542916:root"
}
}
]
})
}
resource "aws_iam_role_policy" "management_policy" {
name = "management-policy"
role = aws_iam_role.algosec_management_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = "organizations:ListAccounts",
Resource = "*"
},
{
Effect = "Allow",
Action = [
"organizations:ListParents",
"organizations:DescribeOrganizationalUnit"
],
Resource = [
"arn:aws:organizations::${data.aws_caller_identity.current.account_id}:account/o-*/*",
"arn:aws:organizations::${data.aws_caller_identity.current.account_id}:ou/o-*/*"
]
}
]
})
}
Purpose: Creates an IAM role that AlgoSec can assume to access the AWS Management Account. Defines trust with the AlgoSec AWS account and grants AWS Organizations permissions required for account discovery.
8. Onboarding Management Function
resource "aws_lambda_function" "onboard_management" {
function_name = "AlgosecOnboardingManagementNotification${var.tenant_id}"
filename = data.archive_file.onboard_management_zip.output_path
source_code_hash = data.archive_file.onboard_management_zip.output_base64sha256
handler = "index.handler"
runtime = "nodejs22.x"
role = aws_iam_role.onboarding_management_lambda_role.arn
memory_size = 128
timeout = 10
environment {
variables = {
algosecManagementRole = aws_iam_role.algosec_management_role.arn
externalId = "<external_id>"
}
}
depends_on = [
aws_iam_role_policy.onboarding_ssm_parameter_access
]
}
Purpose: Defines the Lambda function that performs the management-account onboarding to ACE. Uses the ZIP created earlier as code (onboard_management_function.zip). Configures the Lambda function's runtime settings and environment variables.
9. Lambda Function Source
resource "local_file" "onboard_management_function" {
filename = "${path.module}/index.js"
content = <<-EOF
...
EOF
}
Purpose: Begins the definition of the Lambda function, that the code will be provided inline.
10. Modules
const https = require('https');
const { SSMClient, PutParameterCommand, DeleteParameterCommand, GetParameterCommand } = require('@aws-sdk/client-ssm');
let cfnResponse;
try {
cfnResponse = require('cfn-response');
console.log('cfn-response module loaded');
} catch (e) {
console.log('cfn-response module not found, will use manual response for CloudFormation');
}
Purpose: Imports required Node.js modules. The cfn-response module is included for CloudFormation compatibility but is not required for Terraform execution.
11. Function Handler
exports.handler = async (event, context) => {
// Handler implementation
};
Purpose: Defines the Lambda function handler.
12. Helper Function
const httpsRequest = (options, data) => {
return new Promise((resolve, reject) => {
const request = https.request(options, response => {
let responseBody = '';
response.on('data', chunk => responseBody += chunk.toString());
if (response.statusCode >= 200 && response.statusCode < 300) {
response.on('end', () => resolve(responseBody));
} else {
response.on('end', () => reject(responseBody));
}
});
request.on('error', err => reject(err));
request.write(data);
request.end();
});
};
Purpose: Defines a helper function for making HTTPS requests.
13. Access Key
let clientId = '';
let clientSecret = '';
const getParameter = async (Name) => {
const { Parameter } = await ssmClient.send(new GetParameterCommand({
Name,
WithDecryption: true
}));
return Parameter.Value;
};
const clientIdParameterName = '/algosec/<algosec tenant id>/clientId';
const clientSecretParameterName = '/algosec/<algosec tenant id>/clientSecret';
Purpose: Initiate variables and getParameter function to get value from SSM.
14. Delete Access Key
if (event.RequestType === 'Delete') {
// Retrieve ClientId and ClientSecret from SSM before deleting them
clientId = await getParameter(clientIdParameterName);
clientSecret = await getParameter(clientSecretParameterName);
// Delete SSM Parameters
const parametersToDelete = [
clientIdParameterName,
clientSecretParameterName
];
for (const paramName of parametersToDelete) {
try {
await ssmClient.send(new DeleteParameterCommand({
Name: paramName
}));
} catch (e) {
// Parameter may not exist, ignore
}
}
}
Purpose: Retrieves credentials from SSM before deleting them during the destroy operation, ensuring proper offboarding.
15. Save Access Key
} else {
// Retrieve ClientId and ClientSecret from parameters before creating/updating them
clientId = event.ResourceProperties.ClientId;
clientSecret = event.ResourceProperties.ClientSecret;
// Create/Update SSM Parameters
const parameters = [
{
name: clientIdParameterName,
value: clientId,
description: 'Algosec Client ID'
},
{
name: clientSecretParameterName,
value: clientSecret,
description: 'Algosec Client Secret'
}
];
for (const param of parameters) {
if (!param.value) {
throw new Error('Parameter value is empty for ' + param.name);
}
await ssmClient.send(new PutParameterCommand({
Name: param.name,
Value: param.value,
Type: 'SecureString',
Description: param.description,
Overwrite: true
}));
}
}
Purpose: Retrieve ClientId and ClientSecret from parameters before creating/updating them.
16. Login Access Keys
const loginOptions = {
hostname: 'dev.app.algosec.com',
path: '/api/algosaas/auth/v1/access-keys/login',
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
Purpose: Begins a try-catch block and sets up options for the login request.
17. Login Credentials
const loginPayload = {
tenantId: '5494786575842013',
clientId,
clientSecret
};
const loginData = JSON.stringify(loginPayload);
Purpose: Initiate login credentials.
18. Login Request
const loginResponse = JSON.parse(await httpsRequest(loginOptions, loginData));
Purpose: Sends login request and parses the response.
19. Role Assignment
const onboardingManagementOptions = {
hostname: 'dev.app.algosec.com',
path: '/api/algosaas/onboarding/v1/aws/management-account',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + loginResponse.access_token
}
};
Purpose: Sets up options for the onboarding request, including the auth token.
20. Onboarding Request
const onboardingManagementData = JSON.stringify({
event,
role_arn: process.env.algosecManagementRole,
external_id: process.env.externalId
});
await httpsRequest(onboardingManagementOptions, onboardingManagementData);
Purpose: Sends the onboarding request.
21. Cloud Network Security Onboarding Call
let physicalResourceId = event.PhysicalResourceId;
if (cfnResponse) {
if (event.RequestType === 'Create') {
const stackName = event.StackId.split('/')[1];
physicalResourceId = 'algosec-management-notification-' + stackName + '-' + event.LogicalResourceId;
}
await cfnResponse.send(event, context, cfnResponse.SUCCESS, {
message: 'RequestType: ' + event.RequestType + ', physicalResourceId: ' + physicalResourceId
}, physicalResourceId);
return;
}
return {
status: 'SUCCESS',
message: 'RequestType: ' + event.RequestType + ', physicalResourceId: ' + physicalResourceId
};
} catch (err) {
console.error(err);
if (cfnResponse) {
await cfnResponse.send(event, context, cfnResponse.FAILED);
return;
}
return {
status: 'FAILED',
message: err
};
}
Purpose: Sends success and failure responses.
22. Archive Code
data "archive_file" "onboard_management_zip" {
type = "zip"
output_path = "${path.module}/onboard_management_function.zip"
source {
content = local_file.onboard_management_function.content
filename = local_file.onboard_management_function.filename
}
}
Purpose: Create zip file with Lambda notification code.
23. Lambda Invocation (Onboarding Trigger)
data "aws_lambda_invocation" "algosec_management_notification_invoke" {
function_name = aws_lambda_function.onboard_management.function_name
input = jsonencode({
RequestType = "Create"
ResourceProperties = {
ClientId = var.client_id
ClientSecret = var.client_secret
Touch = "<custom-resource>"
}
})
depends_on = [
aws_lambda_function.onboard_management
]
}
Purpose: Invokes the onboarding Lambda automatically during Terraform apply.
24. Terraform Postcondition – Validate Onboarding Success
resource "terraform_data" "management_create_ok" {
input = data.aws_lambda_invocation.algosec_management_notification_invoke.result
lifecycle {
postcondition {
condition = can(jsondecode(self.input)) && try(jsondecode(self.input).status, "") == "SUCCESS"
error_message = "Lambda algosec_management_notification_invoke failed: ${self.input}"
}
}
}
Purpose: Validates the Lambda execution result and causes Terraform to fail if onboarding does not return SUCCESS.