Inside the Google Terraform Onboarding Template
The Terraform onboarding template automates onboarding of a GCP project into ACE. It provisions the required GCP resources and IAM permissions so AlgoSec can integrate with and monitor the environment. Optional modules support Cloud App Analyzer and related continuous-deployment mitigation workflows.
This page documents each major section of the template to help users understand its purpose and how it maps to Terraform resources.
There are two variants of the template.
-
For ACE with license for Cloud App Analyzer
-
For ACE without license for Cloud App Analyzer
terraform {
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 7.12.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 5.0.0"
}
http = {
source = "hashicorp/http"
version = ">= 3.4.1"
}
random = {
source = "hashicorp/random"
version = ">= 3.5.1"
}
time = {
source = "hashicorp/time"
version = "0.13.1"
}
local = {
source = "hashicorp/local"
version = ">= 2.4.0"
}
}
}
provider "google" {}
provider "google-beta" {}
variable "project_id" {
type = string
default = null
description = "GCP project ID where the service account and resources are created. If not set, the current provider project is used."
}
variable "algosec_client_id" {
type = string
description = "AlgoSec API client ID for authentication."
sensitive = true
default = ""
validation {
condition = length(var.algosec_client_id) > 0
error_message = "Set TF_VAR_algosec_client_id before running Terraform."
}
}
variable "algosec_client_secret" {
type = string
description = "AlgoSec API client secret for authentication."
sensitive = true
default = ""
validation {
condition = length(var.algosec_client_secret) > 0
error_message = "Set TF_VAR_algosec_client_secret before running Terraform."
}
}
data "google_client_config" "current" {}
locals {
algosec_tenant_id = "<ALGOSEC_TENANT_ID>"
algosec_cloud_host = "https://<HOST>"
algosec_cloud_login_url = "${local.algosec_cloud_host}/api/algosaas/auth/v1/access-keys/login"
algosec_cloud_onboarding_url = "${local.algosec_cloud_host}<ONBOARDING_PATH>"
env = "<ENVIRONMENT>"
# Define a working project where a service account would be created (use current project if not specified)
project_id = try(trimspace(var.project_id), "") != "" ? var.project_id : data.google_client_config.current.project
# Service account definitions
service_account_name="ace-sa-${local.algosec_tenant_id}"
service_account_display_name="AlgoSec Cloud Enterprise"
service_account_description="Service account for AlgoSec Cloud Enterprise"
service_account_email="${local.service_account_name}@${local.project_id}.iam.gserviceaccount.com"
}
data "google_project" "onboarded_project" {
project_id = local.project_id
}
data "http" "billing_accounts" {
url = "https://cloudbilling.googleapis.com/v1/billingAccounts"
request_headers = {
Authorization = "Bearer ${data.google_client_config.current.access_token}"
}
}
locals {
billing_accounts = try(
jsondecode(data.http.billing_accounts.response_body).billingAccounts,
[]
)
open_billing_accounts = [
for a in local.billing_accounts : a
if try(a.open, false) == true
]
}
resource "terraform_data" "verify_billing" {
lifecycle {
precondition {
condition = length(local.open_billing_accounts) > 0
error_message = "ERROR: No OPEN billing account found. Billing must be enabled for activation of some services."
}
}
}
data "http" "project_ancestry" {
url = "https://cloudresourcemanager.googleapis.com/v1/projects/${local.project_id}:getAncestry"
method = "POST"
request_headers = {
Authorization = "Bearer ${data.google_client_config.current.access_token}"
Content-Type = "application/json"
}
request_body = "{}"
}
locals {
ancestors = try(jsondecode(data.http.project_ancestry.response_body).ancestor, [])
organization_id = try(
[
for a in local.ancestors : a.resourceId.id
if a.resourceId.type == "organization"
][0],
null
)
}
# Stop execution if org id wasn't found (matches your script behavior)
resource "terraform_data" "verify_org_found" {
lifecycle {
precondition {
condition = local.organization_id != null
error_message = "Failed to retrieve organization id for access project id '${local.project_id}'. Make sure the project belongs to an Organization and you have permission to call getAncestry."
}
}
}
# Create or replace Service Account
resource "google_service_account" "algosec_sa" {
project = local.project_id
account_id = local.service_account_name
display_name = local.service_account_display_name
description = local.service_account_description
}
# IAM can fail briefly while the service account identity propagates; wait before binding roles.
resource "time_sleep" "after_algosec_sa" {
depends_on = [google_service_account.algosec_sa]
create_duration = "20s"
}
# Create service account key
resource "google_service_account_key" "algosec_sa_key" {
service_account_id = google_service_account.algosec_sa.name
}
variable "roles" {
type = list(string)
default = [
<SERVICE_ACCOUNT_ROLES>
]
}
resource "google_project_iam_member" "sa_roles" {
for_each = toset(var.roles)
project = local.project_id
role = "roles/${each.value}"
member = "serviceAccount:${local.service_account_email}"
depends_on = [time_sleep.after_algosec_sa]
}
#----------------------------------------------------
locals {
caa_role_id = "appAnalyzerOrgLevelViewer_${substr(md5(local.project_id), 0, 4)}${substr(md5(local.algosec_tenant_id), 0, 4)}"
caa_title = "Cloud App Analyzer Org Viewer"
caa_desc = "Org level viewer role for Algosec Cloud App Analyzer"
caa_permissions = [
"essentialcontacts.contacts.list",
"essentialcontacts.contacts.get",
]
}
resource "google_organization_iam_custom_role" "caa_role" {
org_id = local.organization_id
role_id = local.caa_role_id
title = local.caa_title
description = local.caa_desc
permissions = local.caa_permissions
}
resource "google_organization_iam_member" "assign_caa_role_sa" {
org_id = local.organization_id
role = google_organization_iam_custom_role.caa_role.name
member = "serviceAccount:${local.service_account_email}"
}
locals {
cns_role_id = "inheritedPolicyACViewer_${substr(md5(local.project_id), 0, 4)}${substr(md5(local.algosec_tenant_id), 0, 4)}"
cns_title = "Inherited Policy Viewer Algosec Cloud"
cns_desc = "Inherited Viewer Roles for Algosec Cloud"
cns_permissions = [
"compute.firewallPolicies.list",
"resourcemanager.folders.get",
"resourcemanager.organizations.get",
"storage.buckets.list",
]
}
resource "google_organization_iam_custom_role" "cns_role" {
org_id = local.organization_id
role_id = local.cns_role_id
title = local.cns_title
description = local.cns_desc
permissions = local.cns_permissions
}
resource "google_organization_iam_member" "assign_cns_role_sa" {
org_id = local.organization_id
role = google_organization_iam_custom_role.cns_role.name
member = "serviceAccount:${local.service_account_email}"
}
#----------------------------------------------------
#----------------------------------------------------
locals {
region="<GCP_REGION>"
prevasio_host="<PREVASIO_HOST>"
hash=substr(local.algosec_tenant_id, 0, 5)
note_id = "prevasio-${local.hash}-note"
attestor_id = "prevasio-${local.hash}-attestor"
keyring_name = "prevasio-attestor-keyring"
key_name = "prevasio-attestor-key"
additionals = base64encode(jsonencode({
tenantId = local.algosec_tenant_id
clientId = var.algosec_client_id
clientSecret = var.algosec_client_secret
}))
}
locals {
prevasio_mandatory_apis = toset([
"artifactregistry.googleapis.com",
"binaryauthorization.googleapis.com",
"cloudbuild.googleapis.com",
"cloudfunctions.googleapis.com",
"cloudkms.googleapis.com",
"cloudscheduler.googleapis.com",
"compute.googleapis.com",
"container.googleapis.com",
"containeranalysis.googleapis.com",
"pubsub.googleapis.com",
"run.googleapis.com",
"secretmanager.googleapis.com"
])
}
resource "google_project_service" "prevasio_apis" {
for_each = local.prevasio_mandatory_apis
project = local.project_id
service = each.value
disable_on_destroy = false
}
data "google_project" "target" {
project_id = local.project_id
}
data "google_service_account" "default_compute" {
project = data.google_project.target.project_id
account_id = "${data.google_project.target.number}-compute"
depends_on = [google_project_service.prevasio_apis]
}
# ------------------- Existing attestor and KMS resources -------------------
resource "google_container_analysis_note" "prevasio_note" {
project = local.project_id
name = local.note_id
attestation_authority {
hint {
human_readable_name = "Prevasio attestation authority"
}
}
}
resource "google_container_analysis_note_iam_member" "note_occurrences_viewer" {
project = local.project_id
note = local.note_id
role = "roles/containeranalysis.notes.occurrences.viewer"
member = "serviceAccount:service-${data.google_project.target.number}@gcp-sa-binaryauthorization.iam.gserviceaccount.com"
depends_on = [ google_container_analysis_note.prevasio_note ]
}
resource "google_kms_key_ring" "prevasio_attestor" {
project = local.project_id
name = local.keyring_name
location = "global"
}
resource "google_kms_crypto_key" "prevasio_attestor" {
name = local.key_name
key_ring = google_kms_key_ring.prevasio_attestor.id
purpose = "ASYMMETRIC_SIGN"
version_template {
algorithm = "EC_SIGN_P256_SHA256"
}
}
resource "google_kms_crypto_key_iam_member" "compute_can_sign" {
crypto_key_id = google_kms_crypto_key.prevasio_attestor.id
role = "roles/cloudkms.signer"
member = "serviceAccount:${data.google_service_account.default_compute.email}"
}
data "google_kms_crypto_key_version" "v1" {
crypto_key = google_kms_crypto_key.prevasio_attestor.id
version = "1"
}
resource "google_binary_authorization_attestor" "prevasio" {
project = local.project_id
name = local.attestor_id
attestation_authority_note {
note_reference = "projects/${local.project_id}/notes/${local.note_id}"
public_keys {
id = "kms-v1"
pkix_public_key {
public_key_pem = data.google_kms_crypto_key_version.v1.public_key[0].pem
signature_algorithm = "ECDSA_P256_SHA256"
}
}
}
depends_on = [
google_kms_crypto_key_iam_member.compute_can_sign
]
}
resource "null_resource" "binauthz_policy_add_prevasio_attestor" {
triggers = {
project_id = local.project_id
attestor_id = local.attestor_id
}
depends_on = [
google_binary_authorization_attestor.prevasio,
]
provisioner "local-exec" {
command = "bash ${path.module}/scripts/binauthz_policy_add_prevasio_attestor.sh \"${data.google_project.target.project_id}\" \"${local.attestor_id}\""
}
}
# ------------------- Secrets (org id, host, additionals, algosec cloud host) -------------------
resource "google_secret_manager_secret" "prevasio_org_id" {
project = local.project_id
secret_id = "prevasio-${local.hash}-org-id"
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "prevasio_org_id_v1" {
secret = google_secret_manager_secret.prevasio_org_id.id
secret_data = local.organization_id
}
resource "google_secret_manager_secret_iam_member" "prevasio_org_id_compute_accessor" {
secret_id = google_secret_manager_secret.prevasio_org_id.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${data.google_service_account.default_compute.email}"
}
resource "google_secret_manager_secret" "prevasio_host" {
project = local.project_id
secret_id = "prevasio-${local.hash}-host"
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "prevasio_host_v1" {
secret = google_secret_manager_secret.prevasio_host.id
secret_data = local.prevasio_host
}
resource "google_secret_manager_secret_iam_member" "prevasio_host_compute_accessor" {
secret_id = google_secret_manager_secret.prevasio_host.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${data.google_service_account.default_compute.email}"
}
resource "google_secret_manager_secret" "prevasio_additionals" {
project = local.project_id
secret_id = "prevasio-${local.hash}-additionals"
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "prevasio_additionals_v1" {
secret = google_secret_manager_secret.prevasio_additionals.id
secret_data = local.additionals
}
resource "google_secret_manager_secret_iam_member" "prevasio_additionals_compute_accessor" {
secret_id = google_secret_manager_secret.prevasio_additionals.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${data.google_service_account.default_compute.email}"
}
resource "google_secret_manager_secret" "algosec_cloud_host" {
project = local.project_id
secret_id = "prevasio-${local.hash}-algosec-cloud-host"
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "algosec_cloud_host_v1" {
secret = google_secret_manager_secret.algosec_cloud_host.id
secret_data = local.algosec_cloud_host
}
resource "google_secret_manager_secret_iam_member" "algosec_cloud_host_compute_accessor" {
secret_id = google_secret_manager_secret.algosec_cloud_host.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${data.google_service_account.default_compute.email}"
}
# ------------------- Pub/Sub topics -------------------
resource "google_pubsub_topic" "images_to_sign" {
project = local.project_id
name = "prevasio-${local.hash}-images-to-sign"
}
resource "google_pubsub_topic_iam_member" "images_to_sign_publisher" {
topic = google_pubsub_topic.images_to_sign.name
role = "roles/pubsub.publisher"
member = "serviceAccount:${data.google_service_account.default_compute.email}"
}
resource "google_pubsub_topic" "gcr" {
project = local.project_id
name = "gcr"
}
resource "google_storage_bucket" "functions_source" {
project = local.project_id
name = "${local.hash}-functions-source"
location = local.region
uniform_bucket_level_access = true
lifecycle_rule {
action {
type = "Delete"
}
condition {
age = 30
}
}
}
data "archive_file" "events_forwarder" {
type = "zip"
source_dir = "${path.module}/events_forwarder"
output_path = "${path.module}/events_forwarder.zip"
}
resource "google_storage_bucket_object" "events_forwarder_zip" {
name = "events_forwarder.zip"
bucket = google_storage_bucket.functions_source.name
source = data.archive_file.events_forwarder.output_path
}
resource "google_project_iam_member" "compute_sa_cloudbuild_builder" {
project = local.project_id
role = "roles/cloudbuild.builds.builder"
member = "serviceAccount:${data.google_service_account.default_compute.email}"
}
resource "time_sleep" "wait_after_compute_sa_cloudbuild_builder" {
depends_on = [google_project_iam_member.compute_sa_cloudbuild_builder]
create_duration = "60s"
}
resource "google_cloudfunctions2_function" "events_forwarder" {
project = local.project_id
name = "prevasio-${local.hash}-events-forwarder"
location = local.region
build_config {
runtime = "python310"
entry_point = "forward_func"
source {
storage_source {
bucket = google_storage_bucket.functions_source.name
object = google_storage_bucket_object.events_forwarder_zip.name
}
}
environment_variables = {
HASH = local.hash
}
}
service_config {
max_instance_count = 1
available_memory = "128Mi"
ingress_settings = "ALLOW_ALL"
service_account_email = data.google_service_account.default_compute.email
secret_environment_variables {
key = "PREVASIO_HOST"
secret = google_secret_manager_secret.prevasio_host.secret_id
version = "latest"
project_id = local.project_id
}
secret_environment_variables {
key = "PREVASIO_ADDITIONALS"
secret = google_secret_manager_secret.prevasio_additionals.secret_id
version = "latest"
project_id = local.project_id
}
secret_environment_variables {
key = "ORGANIZATION_ID"
secret = google_secret_manager_secret.prevasio_org_id.secret_id
version = "latest"
project_id = local.project_id
}
secret_environment_variables {
key = "ALGOSEC_CLOUD_HOST"
secret = google_secret_manager_secret.algosec_cloud_host.secret_id
version = "latest"
project_id = local.project_id
}
}
depends_on = [
google_project_iam_member.compute_sa_cloudbuild_builder,
time_sleep.wait_after_compute_sa_cloudbuild_builder,
]
}
data "archive_file" "cloud_run_scanner" {
type = "zip"
source_dir = "${path.module}/cloud_run_scanner"
output_path = "${path.module}/cloud_run_scanner.zip"
}
resource "google_storage_bucket_object" "cloud_run_scanner_zip" {
name = "cloud_run_scanner.zip"
bucket = google_storage_bucket.functions_source.name
source = data.archive_file.cloud_run_scanner.output_path
}
# Cloud Run scanner (HTTP trigger)
resource "google_cloudfunctions2_function" "cloud_run_scanner" {
project = local.project_id
name = "prevasio-${local.hash}-cloud-run-scanner"
location = local.region
build_config {
runtime = "python310"
entry_point = "scan_func"
source {
storage_source {
bucket = google_storage_bucket.functions_source.name
object = google_storage_bucket_object.cloud_run_scanner_zip.name
}
}
}
service_config {
max_instance_count = 1
available_memory = "256Mi"
ingress_settings = "ALLOW_ALL"
service_account_email = data.google_service_account.default_compute.email
timeout_seconds = 300
}
depends_on = [
google_project_iam_member.compute_sa_cloudbuild_builder,
time_sleep.wait_after_compute_sa_cloudbuild_builder,
]
}
data "archive_file" "image_attestation_creator" {
type = "zip"
source_dir = "${path.module}/image_attestation_creator"
output_path = "${path.module}/image_attestation_creator.zip"
}
resource "google_storage_bucket_object" "image_attestation_creator_zip" {
name = "image_attestation_creator.zip"
bucket = google_storage_bucket.functions_source.name
source = data.archive_file.image_attestation_creator.output_path
}
# Image attestation creator (HTTP push target)
resource "google_cloudfunctions2_function" "image_attestation_creator" {
project = local.project_id
name = "prevasio-${local.hash}-image-attestation-creator"
location = local.region
build_config {
runtime = "python310"
entry_point = "creator_func"
source {
storage_source {
bucket = google_storage_bucket.functions_source.name
object = google_storage_bucket_object.image_attestation_creator_zip.name
}
}
environment_variables = {
HASH = local.hash
}
}
service_config {
max_instance_count = 10
available_memory = "128Mi"
ingress_settings = "ALLOW_ALL"
service_account_email = data.google_service_account.default_compute.email
}
depends_on = [
google_project_iam_member.compute_sa_cloudbuild_builder,
time_sleep.wait_after_compute_sa_cloudbuild_builder,
]
}
# ------------------- Scheduler job for scanner -------------------
resource "google_cloud_scheduler_job" "cloud_run_scanner_cron" {
project = local.project_id
name = "prevasio-${local.hash}-cloud-run-scanner-scheduler"
region = local.region
schedule = "0 */6 * * *"
time_zone = "UTC"
http_target {
// Cloud Functions Gen2 HTTPS endpoint (regional)
uri = google_cloudfunctions2_function.cloud_run_scanner.service_config[0].uri
oidc_token {
service_account_email = data.google_service_account.default_compute.email
}
http_method = "POST"
}
}
data "google_service_account_id_token" "invoke_scanner" {
target_audience = google_cloudfunctions2_function.cloud_run_scanner.service_config[0].uri
}
data "http" "invoke_scanner_once" {
url = google_cloudfunctions2_function.cloud_run_scanner.service_config[0].uri
method = "POST"
# Provider default is 60s; regional Gen2 + cold start + scan work often exceeds that.
request_timeout_ms = 360000
retry {
attempts = 2
min_delay_ms = 5000
max_delay_ms = 60000
}
request_headers = {
Authorization = "Bearer ${data.google_service_account_id_token.invoke_scanner.id_token}"
Content-Type = "application/json"
}
request_body = "{}"
depends_on = [google_cloudfunctions2_function.cloud_run_scanner]
}
# ------------------- Pub/Sub subscription for image attestation creator -------------------
resource "google_pubsub_subscription" "image_attestation_creator_subscription" {
project = local.project_id
name = "prevasio-${local.hash}-image-attestation-creator-subscription"
topic = google_pubsub_topic.images_to_sign.id
ack_deadline_seconds = 65
message_retention_duration = "600s"
push_config {
push_endpoint = google_cloudfunctions2_function.image_attestation_creator.service_config[0].uri
oidc_token {
service_account_email = data.google_service_account.default_compute.email
}
}
}
resource "null_resource" "publish_existing_images_to_sign" {
triggers = {
project_id = local.project_id
topic = google_pubsub_topic.images_to_sign.name
}
depends_on = [
google_pubsub_topic.images_to_sign,
google_pubsub_subscription.image_attestation_creator_subscription,
]
provisioner "local-exec" {
command = "bash scripts/publish_existing_images_to_sign.sh ${local.project_id} prevasio-${local.hash}-images-to-sign"
}
}
# ------------------- Event subscription to gcr topic -> events forwarder -------------------
resource "google_pubsub_subscription" "event_subscription" {
project = local.project_id
name = "prevasio-${local.hash}-event-subscription"
topic = google_pubsub_topic.gcr.id
ack_deadline_seconds = 65
message_retention_duration = "600s"
push_config {
push_endpoint = google_cloudfunctions2_function.events_forwarder.service_config[0].uri
oidc_token {
service_account_email = data.google_service_account.default_compute.email
}
}
}
#----------------------------------------------------
#----------------------------------------------------
locals {
gke_role_id = "algosec.gke.custom.role_${substr(md5(local.project_id), 0, 4)}${substr(md5(local.algosec_tenant_id), 0, 4)}"
gke_role_title = "Algosec GKE Custom Role"
gke_permissions = [
"container.jobs.create",
"container.jobs.delete",
"container.namespaces.create",
"container.nodes.proxy",
"container.pods.getLogs",
]
}
// Create the custom role in the target project
resource "google_project_iam_custom_role" "algosec_gke_role" {
project = local.project_id
role_id = local.gke_role_id
title = local.gke_role_title
permissions = local.gke_permissions
}
// Bind the service account to the custom role in the project
resource "google_project_iam_member" "algosec_gke_role_binding" {
project = local.project_id
role = google_project_iam_custom_role.algosec_gke_role.name
member = "serviceAccount:${local.service_account_email}"
}
#----------------------------------------------------
locals {
onboard_project_body = jsonencode(merge(
jsondecode(base64decode(google_service_account_key.algosec_sa_key.private_key)),
{ organization_id = local.organization_id, name = data.google_project.onboarded_project.name }
))
}
resource "null_resource" "algosec_onboarding" {
triggers = {
project_name = data.google_project.onboarded_project.name
service_account_key_hash = nonsensitive(sha256(google_service_account_key.algosec_sa_key.private_key))
}
provisioner "local-exec" {
when = create
environment = {
LOGIN_URL = local.algosec_cloud_login_url
ONBOARD_URL = "${local.algosec_cloud_onboarding_url}?fromTerraform=true"
TENANT_ID = local.algosec_tenant_id
PROJECT_ID = local.project_id
PROJECT_NAME = data.google_project.onboarded_project.name
ONBOARD_BODY = nonsensitive(local.onboard_project_body)
}
command = "bash ${path.module}/scripts/algosec_gcp_onboard.sh"
quiet = false
}
depends_on = [
google_service_account_key.algosec_sa_key,
google_project_iam_member.sa_roles,
]
}
resource "null_resource" "algosec_offboarding" {
triggers = {
login_url = local.algosec_cloud_login_url
offboarding_url = "${local.algosec_cloud_onboarding_url}/${local.project_id}?fromTerraform=true"
algosec_tenant_id = local.algosec_tenant_id
project_id = local.project_id
}
provisioner "local-exec" {
when = destroy
on_failure = continue
environment = {
LOGIN_URL = self.triggers.login_url
OFFBOARD_URL = self.triggers.offboarding_url
TENANT_ID = self.triggers.algosec_tenant_id
PROJECT_ID = self.triggers.project_id
}
command = "bash ${path.module}/scripts/algosec_gcp_offboard.sh"
quiet = false
}
}
Script Sections
1. Terraform and provider requirements
terraform {
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 7.12.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 5.0.0"
}
http = {
source = "hashicorp/http"
version = ">= 3.4.1"
}
random = {
source = "hashicorp/random"
version = ">= 3.5.1"
}
time = {
source = "hashicorp/time"
version = "0.13.1"
}
local = {
source = "hashicorp/local"
version = ">= 2.4.0"
}
}
}
provider "google" {}
provider "google-beta" {}
Purpose: This section defines the Terraform version and required providers. The template relies on the Google and Google Beta providers to create GCP resources, the HTTP provider to call Google and AlgoSec APIs, and the Time and Local providers for orchestration support.
2. Input variables
variable "project_id" {
type = string
default = null
description = "GCP project ID where the service account and resources are created. If not set, the current provider project is used."
}
variable "algosec_client_id" {
type = string
description = "AlgoSec API client ID for authentication."
sensitive = true
default = ""
}
variable "algosec_client_secret" {
type = string
description = "AlgoSec API client secret for authentication."
sensitive = true
default = ""
}
Purpose: These variables control which project is onboarded and provide the AlgoSec API credentials used by the template. Validation ensures the AlgoSec credentials are supplied before Terraform runs.
3. Initialize local values
locals {
algosec_tenant_id = "<ALGOSEC_TENANT_ID>"
algosec_cloud_host = "https://<HOST>"
algosec_cloud_login_url = "${local.algosec_cloud_host}/api/algosaas/auth/v1/access-keys/login"
algosec_cloud_onboarding_url = "${local.algosec_cloud_host}<ONBOARDING_PATH>"
env = "<ENVIRONMENT>"
project_id = try(trimspace(var.project_id), "") != "" ? var.project_id : data.google_client_config.current.project
service_account_name = "ace-sa-${local.algosec_tenant_id}"
service_account_display_name = "AlgoSec Cloud Enterprise"
service_account_description = "Service account for AlgoSec Cloud Enterprise"
service_account_email = "${local.service_account_name}@${local.project_id}.iam.gserviceaccount.com"
}
Purpose: This section builds the main working values used throughout the template.
- ALGOSEC_TENANT_ID: AlgoSec tenant ID
- ALGOSEC_CLOUD_HOST: The host of AlgoSec's APIs
- algosec_cloud_login_url, algosec_cloud_onboarding_url: The URLs for AlgoSec's onboarding APIs
- project_id: The GCP project to be onboarded into ACE
- service_account_email: Derived email address of the service account that will be created
4. Read the current project context
data "google_client_config" "current" {}
data "google_project" "onboarded_project" {
project_id = local.project_id
}
Purpose: These data sources retrieve the active provider context and basic project metadata.
5. Verify that billing is enabled
data "http" "billing_accounts" {
url = "https://cloudbilling.googleapis.com/v1/billingAccounts"
request_headers = {
Authorization = "Bearer ${data.google_client_config.current.access_token}"
}
}
resource "terraform_data" "verify_billing" {
lifecycle {
precondition {
condition = length(local.open_billing_accounts) > 0
error_message = "ERROR: No OPEN billing account found. Billing must be enabled for activation of some services."
}
}
}
Purpose: Before creating resources, the template checks that at least one open billing account is available. This prevents onboarding from continuing when required GCP services cannot be activated.
6. Retrieve the organization ID from project ancestry
data "http" "project_ancestry" {
url = "https://cloudresourcemanager.googleapis.com/v1/projects/${local.project_id}:getAncestry"
method = "POST"
}
resource "terraform_data" "verify_org_found" {
lifecycle {
precondition {
condition = local.organization_id != null
error_message = "Failed to retrieve organization id for access project id '${local.project_id}'."
}
}
}
Purpose: The template resolves the organization ID that owns the project. This is required because the onboarding flow creates organization-level custom roles and bindings for inherited policy visibility and Cloud App Analyzer access.
7. Create the AlgoSec service account
resource "google_service_account" "algosec_sa" {
project = local.project_id
account_id = local.service_account_name
display_name = local.service_account_display_name
description = local.service_account_description
}
resource "time_sleep" "after_algosec_sa" {
depends_on = [google_service_account.algosec_sa]
create_duration = "20s"
}
resource "google_service_account_key" "algosec_sa_key" {
service_account_id = google_service_account.algosec_sa.name
}
Purpose: This section creates the service account used by ACE, waits briefly for IAM propagation, and then creates a key for API-based onboarding.
8. Grant required project roles
variable "roles" {
type = list(string)
default = [
<SERVICE_ACCOUNT_ROLES>
]
}
resource "google_project_iam_member" "sa_roles" {
for_each = toset(var.roles)
project = local.project_id
role = "roles/${each.value}"
member = "serviceAccount:${local.service_account_email}"
}
Purpose: This block assigns the required predefined project-level IAM roles to the service account.
9. Create and assign organization-level custom roles
Cloud App Analyzer organization role
locals {
caa_role_id = "appAnalyzerOrgLevelViewer_${substr(md5(local.project_id), 0, 4)}${substr(local.algosec_tenant_id, 0, 4)}"
caa_title = "Cloud App Analyzer Org Viewer"
caa_desc = "Org level viewer role for Algosec Cloud App Analyzer"
caa_permissions = [
"essentialcontacts.contacts.list",
"essentialcontacts.contacts.get",
]
}
resource "google_organization_iam_custom_role" "caa_role" {
org_id = local.organization_id
role_id = local.caa_role_id
title = local.caa_title
description = local.caa_desc
permissions = local.caa_permissions
}
resource "google_organization_iam_member" "assign_caa_role_sa" {
org_id = local.organization_id
role = google_organization_iam_custom_role.caa_role.name
member = "serviceAccount:${local.service_account_email}"
}
Inherited policy viewer role
locals {
cns_role_id = "inheritedPolicyACViewer_${substr(md5(local.project_id), 0, 4)}${substr(local.algosec_tenant_id, 0, 4)}"
cns_title = "Inherited Policy Viewer Algosec Cloud"
cns_desc = "Inherited Viewer Roles for Algosec Cloud"
cns_permissions = [
"compute.firewallPolicies.list",
"resourcemanager.folders.get",
"resourcemanager.organizations.get",
"storage.buckets.list",
]
}
resource "google_organization_iam_custom_role" "cns_role" {
org_id = local.organization_id
role_id = local.cns_role_id
title = local.cns_title
description = local.cns_desc
permissions = local.cns_permissions
}
resource "google_organization_iam_member" "assign_cns_role_sa" {
org_id = local.organization_id
role = google_organization_iam_custom_role.cns_role.name
member = "serviceAccount:${local.service_account_email}"
}
Purpose: These custom organization roles provide permissions that are not covered by the project-level roles alone. inheritedPolicyACViewer — Required by Cloud Network Security (CNS) to read hierarchical firewall policies, folders, organizations, and storage buckets. appAnalyzerOrgLevelViewer. Required by Cloud App Analyzer to read essential contacts at the organization level.
10. Initialize Cloud App Analyzer settings (Optional — Onboard CD Mitigation enabled)
locals {
region = "<GCP_REGION>"
prevasio_host = "<PREVASIO_HOST>"
hash = substr(local.algosec_tenant_id, 0, 5)
note_id = "prevasio-${local.hash}-note"
attestor_id = "prevasio-${local.hash}-attestor"
keyring_name = "prevasio-attestor-keyring"
key_name = "prevasio-attestor-key"
additionals = base64encode(jsonencode({
tenantId = local.algosec_tenant_id
clientId = var.algosec_client_id
clientSecret = var.algosec_client_secret
}))
}
Purpose: This section defines names and values used for the optional Cloud App Analyzer resources, including Binary Authorization, KMS, secrets, and supporting workloads.
- REGION — The GCP region where Cloud App Analyzer resources (Cloud Functions, schedulers, etc.) will be created
- PREVASIO_HOST — The host of AlgoSec's Cloud App Analyzer APIs
- HASH — A short unique identifier derived from the first characters of the tenant ID, used to namespace Cloud App Analyzer resources within a project to avoid naming conflicts
11. Enable required Google APIs (Optional — Onboard CD Mitigation enabled)
resource "google_project_service" "prevasio_apis" {
for_each = local.prevasio_mandatory_apis
project = local.project_id
service = each.value
disable_on_destroy = false
}
Purpose: The template enables the Google APIs required by the supporting resources, including Cloud Functions, KMS, Pub/Sub, Secret Manager, Binary Authorization, Cloud Run, Container Analysis, and related services.
12. Create attestation and KMS resources (Optional — Onboard CD Mitigation enabled)
resource "google_container_analysis_note" "prevasio_note" { ... }
resource "google_kms_key_ring" "prevasio_attestor" { ... }
resource "google_kms_crypto_key" "prevasio_attestor" { ... }
resource "google_binary_authorization_attestor" "prevasio" { ... }
Purpose: Sets up a Binary Authorization attestor in the specified GCP project, enabling cryptographic enforcement of image signing before deployment.
-
Create a Container Analysis note — A note is created via the Container Analysis REST API to serve as the attestation authority's backing resource.
-
Create the attestor — A Binary Authorization attestor is created referencing the note.
-
Grant IAM access — The Binary Authorization service account is granted containeranalysis.notes.occurrences.viewer on the note so it can verify attestations.
-
Create a KMS key — A Cloud KMS asymmetric signing key (ec-sign-p256-sha256) is created and its public key is registered with the attestor.
-
Update the Binary Authorization policy (conditional) — If IMAGE_LOCKING_ENABLED is true, the project's Binary Authorization policy is exported, the new attestor is appended to the requireAttestationsBy list, and the policy is reimported with enforcement mode set to ENFORCED_BLOCK_AND_AUDIT_LOG.
13. Store onboarding values in Secret Manager (Optional — Onboard CD Mitigation enabled)
resource "google_secret_manager_secret" "prevasio_org_id" { ... }
resource "google_secret_manager_secret" "prevasio_host" { ... }
resource "google_secret_manager_secret" "prevasio_additionals" { ... }
resource "google_secret_manager_secret" "algosec_cloud_host" { ... }
Purpose: The template stores runtime values in Secret Manager so the deployed functions can securely read them without hardcoding secrets into source files or function configuration.
14. Create Pub/Sub topics and storage bucket (Optional — Onboard CD Mitigation enabled)
resource "google_pubsub_topic" "images_to_sign" { ... }
resource "google_pubsub_topic" "gcr" { ... }
resource "google_storage_bucket" "functions_source" { ... }
Purpose: This block creates the storage bucket needed by the function-based workflow, the GCR topic, and a subscription that pushes Artifact Registry events to the events-forwarder function.
15. Package and deploy Cloud Functions (Optional — Onboard CD Mitigation enabled)
data "archive_file" "events_forwarder" { ... }
resource "google_cloudfunctions2_function" "events_forwarder" { ... }
data "archive_file" "cloud_run_scanner" { ... }
resource "google_cloudfunctions2_function" "cloud_run_scanner" { ... }
data "archive_file" "image_attestation_creator" { ... }
resource "google_cloudfunctions2_function" "image_attestation_creator" { ... }
Purpose: The template zips local source directories and deploys three Cloud Functions using gen2 runtime (python310):
- events_forwarder — Forwards Artifact Registry push events to AlgoSec
- cloud_run_scanner — Scans Cloud Run services; triggered on a schedule via Cloud Scheduler every 6 hours
- image_attestation_creator — Creates Binary Authorization attestations for signed images
16. Schedule and connect the event flow (Optional — Onboard CD Mitigation enabled)
resource "google_cloud_scheduler_job" "cloud_run_scanner_cron" { ... }
resource "google_pubsub_subscription" "image_attestation_creator_subscription" { ... }
resource "google_pubsub_subscription" "event_subscription" { ... }
Purpose: This section wires the system together by creating:
-
A scheduler job that triggers the scanner every 6 hours
-
A Pub/Sub subscription that pushes image-signing messages to the attestation function
-
A Pub/Sub subscription that pushes GCR-related events to the forwarding function
17. Apply GKE custom role to target projects (Optional — Cloud App Analyzer license)
locals {
gke_role_id = "algosec.gke.custom.role_${substr(md5(local.project_id), 0, 4)}${substr(md5(local.algosec_tenant_id), 0, 4)}"
gke_role_title = "Algosec GKE Custom Role"
gke_permissions = [
"container.jobs.create",
"container.jobs.delete",
"container.namespaces.create",
"container.nodes.proxy",
"container.pods.getLogs",
]
}
resource "google_project_iam_custom_role" "algosec_gke_role" {
project = local.project_id
role_id = local.gke_role_id
title = local.gke_role_title
permissions = local.gke_permissions
}
resource "google_project_iam_member" "algosec_gke_role_binding" {
project = local.project_id
role = google_project_iam_custom_role.algosec_gke_role.name
member = "serviceAccount:${local.service_account_email}"
}
Purpose: A dedicated post-deployment pass that creates and assigns the Algosec GKE custom IAM role to the ACE service account. This role grants the permissions required for AlgoSec's GKE integrations:
- container.jobs.create / container.jobs.delete
- container.namespaces.create
- container.nodes.proxy
- container.pods.getLogs
18. Build the onboarding payload
locals {
onboard_project_body = jsonencode(merge(
jsondecode(base64decode(google_service_account_key.algosec_sa_key.private_key)),
{ organization_id = local.organization_id, name = data.google_project.onboarded_project.name }
))
}
Purpose: This block decodes the generated service account key and combines it with the organization ID and project name to create the JSON payload that will be sent to the AlgoSec onboarding API.
19. Call the AlgoSec onboarding API
resource "null_resource" "algosec_onboarding" {
provisioner "local-exec" {
when = create
environment = {
LOGIN_URL = local.algosec_cloud_login_url
ONBOARD_URL = "${local.algosec_cloud_onboarding_url}?fromTerraform=true"
TENANT_ID = local.algosec_tenant_id
PROJECT_ID = local.project_id
PROJECT_NAME = data.google_project.onboarded_project.name
ONBOARD_BODY = nonsensitive(local.onboard_project_body)
}
command = "bash ${path.module}/scripts/algosec_gcp_onboard.sh"
}
}
Purpose: After all required resources and permissions are in place, Terraform runs a local script that authenticates to AlgoSec and sends the onboarding request.
20. Call the AlgoSec offboarding API on destroy
resource "null_resource" "algosec_offboarding" {
provisioner "local-exec" {
when = destroy
on_failure = continue
environment = {
LOGIN_URL = self.triggers.login_url
OFFBOARD_URL = self.triggers.offboarding_url
TENANT_ID = self.triggers.algosec_tenant_id
PROJECT_ID = self.triggers.project_id
}
command = "bash ${path.module}/scripts/algosec_gcp_offboard.sh"
}
}
Purpose: When the Terraform deployment is destroyed, this section calls the AlgoSec offboarding API so the project is removed cleanly from ACE.
1. Terraform and provider requirements
terraform {
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 7.12.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 5.0.0"
}
http = {
source = "hashicorp/http"
version = ">= 3.4.1"
}
random = {
source = "hashicorp/random"
version = ">= 3.5.1"
}
time = {
source = "hashicorp/time"
version = "0.13.1"
}
local = {
source = "hashicorp/local"
version = ">= 2.4.0"
}
}
}
provider "google" {}
provider "google-beta" {}
Purpose: This section defines the Terraform version and required providers. The template relies on the Google and Google Beta providers to create GCP resources, the HTTP provider to call Google and AlgoSec APIs, and the Time and Local providers for orchestration support.
2. Input variables
variable "project_id" {
type = string
default = null
description = "GCP project ID where the service account and resources are created. If not set, the current provider project is used."
}
variable "algosec_client_id" {
type = string
description = "AlgoSec API client ID for authentication."
sensitive = true
default = ""
}
variable "algosec_client_secret" {
type = string
description = "AlgoSec API client secret for authentication."
sensitive = true
default = ""
}
Purpose: These variables control which project is onboarded and provide the AlgoSec API credentials used by the template. Validation ensures the AlgoSec credentials are supplied before Terraform runs.
3. Initialize local values
locals {
algosec_tenant_id = "<ALGOSEC_TENANT_ID>"
algosec_cloud_host = "https://<HOST>"
algosec_cloud_login_url = "${local.algosec_cloud_host}/api/algosaas/auth/v1/access-keys/login"
algosec_cloud_onboarding_url = "${local.algosec_cloud_host}<ONBOARDING_PATH>"
env = "<ENVIRONMENT>"
project_id = try(trimspace(var.project_id), "") != "" ? var.project_id : data.google_client_config.current.project
service_account_name = "ace-sa-${local.algosec_tenant_id}"
service_account_display_name = "AlgoSec Cloud Enterprise"
service_account_description = "Service account for AlgoSec Cloud Enterprise"
service_account_email = "${local.service_account_name}@${local.project_id}.iam.gserviceaccount.com"
}
Purpose: This section builds the main working values used throughout the template.
- ALGOSEC_TENANT_ID — AlgoSec tenant ID
- ALGOSEC_CLOUD_HOST — The host of AlgoSec's APIs
- algosec_cloud_login_url, algosec_cloud_onboarding_url — The URLs for AlgoSec's onboarding APIs
- project_id — The GCP project to be onboarded into ACE
- service_account_email — Derived email address of the service account that will be created
4. Read the current project context
data "google_client_config" "current" {}
data "google_project" "onboarded_project" {
project_id = local.project_id
}
Purpose: These data sources retrieve the active provider context and basic project metadata.
5. Verify that billing is enabled
data "http" "billing_accounts" {
url = "https://cloudbilling.googleapis.com/v1/billingAccounts"
request_headers = {
Authorization = "Bearer ${data.google_client_config.current.access_token}"
}
}
resource "terraform_data" "verify_billing" {
lifecycle {
precondition {
condition = length(local.open_billing_accounts) > 0
error_message = "ERROR: No OPEN billing account found. Billing must be enabled for activation of some services."
}
}
}
Purpose: Before creating resources, the template checks that at least one open billing account is available. This prevents onboarding from continuing when required GCP services cannot be activated.
6. Retrieve the organization ID from project ancestry
data "http" "project_ancestry" {
url = "https://cloudresourcemanager.googleapis.com/v1/projects/${local.project_id}:getAncestry"
method = "POST"
}
resource "terraform_data" "verify_org_found" {
lifecycle {
precondition {
condition = local.organization_id != null
error_message = "Failed to retrieve organization id for access project id '${local.project_id}'."
}
}
}
Purpose: The template resolves the organization ID that owns the project. This is required because the onboarding flow creates organization-level custom roles and bindings for inherited policy visibility.
7. Create the AlgoSec service account
resource "google_service_account" "algosec_sa" {
project = local.project_id
account_id = local.service_account_name
display_name = local.service_account_display_name
description = local.service_account_description
}
resource "time_sleep" "after_algosec_sa" {
depends_on = [google_service_account.algosec_sa]
create_duration = "20s"
}
resource "google_service_account_key" "algosec_sa_key" {
service_account_id = google_service_account.algosec_sa.name
}
Purpose: This section creates the service account used by ACE, waits briefly for IAM propagation, and then creates a key for API-based onboarding.
8. Grant required project roles
variable "roles" {
type = list(string)
default = [
<SERVICE_ACCOUNT_ROLES>
]
}
resource "google_project_iam_member" "sa_roles" {
for_each = toset(var.roles)
project = local.project_id
role = "roles/${each.value}"
member = "serviceAccount:${local.service_account_email}"
}
Purpose: This block assigns the required predefined project-level IAM roles to the service account.
9. Create and assign organization-level custom roles
Inherited policy viewer role
locals {
cns_role_id = "inheritedPolicyACViewer_${substr(md5(local.project_id), 0, 4)}${substr(local.algosec_tenant_id, 0, 4)}"
cns_title = "Inherited Policy Viewer Algosec Cloud"
cns_desc = "Inherited Viewer Roles for Algosec Cloud"
cns_permissions = [
"compute.firewallPolicies.list",
"resourcemanager.folders.get",
"resourcemanager.organizations.get",
"storage.buckets.list",
]
}
resource "google_organization_iam_custom_role" "cns_role" {
org_id = local.organization_id
role_id = local.cns_role_id
title = local.cns_title
description = local.cns_desc
permissions = local.cns_permissions
}
resource "google_organization_iam_member" "assign_cns_role_sa" {
org_id = local.organization_id
role = google_organization_iam_custom_role.cns_role.name
member = "serviceAccount:${local.service_account_email}"
}
Purpose: These custom organization roles provide permissions that are not covered by the project-level roles alone. inheritedPolicyACViewer — Required by Cloud Network Security (CNS) to read hierarchical firewall policies, folders, organizations, and storage buckets.
10. Build the onboarding payload
locals {
onboard_project_body = jsonencode(merge(
jsondecode(base64decode(google_service_account_key.algosec_sa_key.private_key)),
{ organization_id = local.organization_id, name = data.google_project.onboarded_project.name }
))
}
Purpose: This block decodes the generated service account key and combines it with the organization ID and project name to create the JSON payload that will be sent to the AlgoSec onboarding API.
11. Call the AlgoSec onboarding API
resource "null_resource" "algosec_onboarding" {
provisioner "local-exec" {
when = create
environment = {
LOGIN_URL = local.algosec_cloud_login_url
ONBOARD_URL = "${local.algosec_cloud_onboarding_url}?fromTerraform=true"
TENANT_ID = local.algosec_tenant_id
PROJECT_ID = local.project_id
PROJECT_NAME = data.google_project.onboarded_project.name
ONBOARD_BODY = nonsensitive(local.onboard_project_body)
}
command = "bash ${path.module}/scripts/algosec_gcp_onboard.sh"
}
}
Purpose: After all required resources and permissions are in place, Terraform runs a local script that authenticates to AlgoSec and sends the onboarding request.
12. Call the AlgoSec offboarding API on destroy
resource "null_resource" "algosec_offboarding" {
provisioner "local-exec" {
when = destroy
on_failure = continue
environment = {
LOGIN_URL = self.triggers.login_url
OFFBOARD_URL = self.triggers.offboarding_url
TENANT_ID = self.triggers.algosec_tenant_id
PROJECT_ID = self.triggers.project_id
}
command = "bash ${path.module}/scripts/algosec_gcp_offboard.sh"
}
}
Purpose: When the Terraform deployment is destroyed, this section calls the AlgoSec offboarding API so the project is removed cleanly from ACE.