Skip to content
Merged
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
67 changes: 49 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,26 @@ jobs:
# Upload SARIF for code-scanning UI. Module authors get the
# findings in the PR file-view, not just the run summary.
security-events: write
env:
TFSEC_VERSION: v1.28.13
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Run tfsec
uses: aquasecurity/tfsec-sarif-action@9a83b5c3524f825c020e356335855741fd02745f # v0.1.4
with:
sarif_file: tfsec.sarif
working_directory: modules
# Soft-fail on the SARIF run; the gate below uses tfsec
# directly so we can scope severity and inline-disable rules.
- name: Install tfsec
# aquasecurity/tfsec-sarif-action@v0.1.4 was the previous step here,
# but it bundles a Node 16 runtime that GitHub flags as deprecated.
# tfsec itself (the binary) is fine — install it directly from the
# GitHub release and we sidestep the action runtime entirely.
run: |
set -eo pipefail
curl -sSfL -o /usr/local/bin/tfsec \
"https://github.com/aquasecurity/tfsec/releases/download/${TFSEC_VERSION}/tfsec-linux-amd64"
chmod +x /usr/local/bin/tfsec
tfsec --version

- name: Run tfsec (SARIF, no fail)
run: tfsec modules --format sarif --out tfsec.sarif --soft-fail

- name: Upload SARIF to GitHub code-scanning
uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.27.5
Expand All @@ -220,14 +229,7 @@ jobs:
# The SARIF path uploads everything; here we re-run with --severity
# so the build fails on HIGH+ and tolerates MEDIUM/LOW that are
# already triaged.
run: |
docker run --rm \
-v "${{ github.workspace }}:/src" \
aquasec/tfsec:v1.28.13 \
/src/modules \
--minimum-severity HIGH \
--format default \
--no-color
run: tfsec modules --minimum-severity HIGH --format default --no-color

# ---------------------------------------------------------------------
# Examples validation — every modules/*/aws/examples/* and
Expand Down Expand Up @@ -331,11 +333,40 @@ jobs:

fail=0
for tf in $(grep -rl 'marketplace_plans' modules/ --include='*.tf'); do
got_publisher=$(grep -oP 'publisher\s*=\s*"\K[^"]+' "$tf" | sort -u | head -1)
if [ -n "${got_publisher}" ] && [ "${got_publisher}" != "${want_publisher}" ]; then
echo "::error file=${tf}::Azure publisher drift: want=${want_publisher} got=${got_publisher}"
# Scope the publisher check to the marketplace_plans local block
# only. The same .tf files legitimately reference other publishers
# (Canonical for a Postgres-on-VM bastion image, Microsoft.Azure.Extensions
# for VMSS CustomScript extensions) and we don't want those to
# trigger a marketplace-drift error.
in_block_publishers=$(awk '
/marketplace_plans[[:space:]]*=[[:space:]]*\{/ { depth=1; next }
depth > 0 {
for (i = 1; i <= length($0); i++) {
c = substr($0, i, 1)
if (c == "{") depth++
else if (c == "}") { depth--; if (depth == 0) exit }
}
if (match($0, /publisher[[:space:]]*=[[:space:]]*"[^"]+"/)) {
s = substr($0, RSTART, RLENGTH)
sub(/^publisher[[:space:]]*=[[:space:]]*"/, "", s)
sub(/"$/, "", s)
print s
}
}
' "$tf" | sort -u)

if [ -z "${in_block_publishers}" ]; then
echo "::error file=${tf}::No publisher found inside marketplace_plans block"
fail=1
continue
fi
while IFS= read -r got_publisher; do
if [ "${got_publisher}" != "${want_publisher}" ]; then
echo "::error file=${tf}::Azure publisher drift inside marketplace_plans: want=${want_publisher} got=${got_publisher}"
fail=1
fi
done <<< "${in_block_publishers}"

if grep -q "${want_asm_offer}" "$tf" && ! grep -q "${want_sat_offer}" "$tf"; then
# Should mention both products' offers.
echo "::error file=${tf}::Azure SAT offer missing — should reference both ${want_asm_offer} and ${want_sat_offer}"
Expand Down
53 changes: 29 additions & 24 deletions modules/asm-azure-autoscale/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,35 @@ module "this" {

product = "asm"

resource_group_name = var.resource_group_name
location = var.location
vm_subnet_id = var.vm_subnet_id
db_delegated_subnet_id = var.db_delegated_subnet_id
private_dns_zone_id = var.private_dns_zone_id
allowed_cidrs = var.allowed_cidrs
admin_username = var.admin_username
ssh_public_key = var.ssh_public_key
vmss_min_count = var.vmss_min_count
vmss_max_count = var.vmss_max_count
vmss_default_count = var.vmss_default_count
vm_size = var.vm_size
target_cpu_percent = var.target_cpu_percent
db_sku_name = var.db_sku_name
db_storage_mb = var.db_storage_mb
db_version = var.db_version
db_backup_retention_days = var.db_backup_retention_days
db_replica_count = var.db_replica_count
environment = var.environment
name_prefix = var.name_prefix
alert_email = var.alert_email
accept_marketplace_terms = var.accept_marketplace_terms
marketplace_sku_override = var.marketplace_sku_override
marketplace_image_version = var.marketplace_image_version
resource_group_name = var.resource_group_name
location = var.location
vm_subnet_id = var.vm_subnet_id
db_delegated_subnet_id = var.db_delegated_subnet_id
private_dns_zone_id = var.private_dns_zone_id
allowed_cidrs = var.allowed_cidrs
admin_username = var.admin_username
ssh_public_key = var.ssh_public_key

# Key Vault network ACL + VMSS subnet NSG association
key_vault_network_default_action = var.key_vault_network_default_action
key_vault_ip_rules = var.key_vault_ip_rules
associate_vm_subnet_nsg = var.associate_vm_subnet_nsg
vmss_min_count = var.vmss_min_count
vmss_max_count = var.vmss_max_count
vmss_default_count = var.vmss_default_count
vm_size = var.vm_size
target_cpu_percent = var.target_cpu_percent
db_sku_name = var.db_sku_name
db_storage_mb = var.db_storage_mb
db_version = var.db_version
db_backup_retention_days = var.db_backup_retention_days
db_replica_count = var.db_replica_count
environment = var.environment
name_prefix = var.name_prefix
alert_email = var.alert_email
accept_marketplace_terms = var.accept_marketplace_terms
marketplace_sku_override = var.marketplace_sku_override
marketplace_image_version = var.marketplace_image_version

# Patching and migration safety
create_backup_storage_account = var.create_backup_storage_account
Expand Down
18 changes: 18 additions & 0 deletions modules/asm-azure-autoscale/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ variable "admin_username" { type = string }

variable "ssh_public_key" { type = string }

variable "key_vault_network_default_action" {
description = "Default action for the Key Vault network ACL. 'Allow' preserves the pre-network-ACL behavior; set 'Deny' once you've added the operator IP to key_vault_ip_rules and the Microsoft.KeyVault service endpoint on vm_subnet_id."
type = string
default = "Allow"
}

variable "key_vault_ip_rules" {
description = "IPv4 addresses or CIDRs allowed to reach the Key Vault data plane. Required only when key_vault_network_default_action = Deny and you don't have Private Link configured."
type = list(string)
default = []
}

variable "associate_vm_subnet_nsg" {
description = "Associate the module-managed NSG (built from allowed_cidrs) with vm_subnet_id. Set false if the subnet already has an NSG attached by your landing-zone tooling."
type = bool
default = true
}

variable "vmss_min_count" {
type = number
default = 3
Expand Down
46 changes: 25 additions & 21 deletions modules/asm-azure-ha/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,31 @@ module "this" {

product = "asm"

resource_group_name = var.resource_group_name
location = var.location
vm_subnet_id = var.vm_subnet_id
db_delegated_subnet_id = var.db_delegated_subnet_id
private_dns_zone_id = var.private_dns_zone_id
lb_subnet_id = var.lb_subnet_id
allowed_cidrs = var.allowed_cidrs
admin_username = var.admin_username
ssh_public_key = var.ssh_public_key
environment = var.environment
name_prefix = var.name_prefix
vm_size = var.vm_size
data_disk_size_gb = var.data_disk_size_gb
db_sku_name = var.db_sku_name
db_storage_mb = var.db_storage_mb
db_version = var.db_version
db_backup_retention_days = var.db_backup_retention_days
db_high_availability_mode = var.db_high_availability_mode
accept_marketplace_terms = var.accept_marketplace_terms
marketplace_sku_override = var.marketplace_sku_override
marketplace_image_version = var.marketplace_image_version
resource_group_name = var.resource_group_name
location = var.location
vm_subnet_id = var.vm_subnet_id
db_delegated_subnet_id = var.db_delegated_subnet_id
private_dns_zone_id = var.private_dns_zone_id
lb_subnet_id = var.lb_subnet_id
allowed_cidrs = var.allowed_cidrs
admin_username = var.admin_username
ssh_public_key = var.ssh_public_key

# Key Vault network ACL
key_vault_network_default_action = var.key_vault_network_default_action
key_vault_ip_rules = var.key_vault_ip_rules
environment = var.environment
name_prefix = var.name_prefix
vm_size = var.vm_size
data_disk_size_gb = var.data_disk_size_gb
db_sku_name = var.db_sku_name
db_storage_mb = var.db_storage_mb
db_version = var.db_version
db_backup_retention_days = var.db_backup_retention_days
db_high_availability_mode = var.db_high_availability_mode
accept_marketplace_terms = var.accept_marketplace_terms
marketplace_sku_override = var.marketplace_sku_override
marketplace_image_version = var.marketplace_image_version

# Patching and migration safety
db_mode = var.db_mode
Expand Down
12 changes: 12 additions & 0 deletions modules/asm-azure-ha/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ variable "ssh_public_key" {
type = string
}

variable "key_vault_network_default_action" {
description = "Default action for the Key Vault network ACL. 'Allow' preserves the pre-network-ACL behavior; set 'Deny' once you've added the operator IP to key_vault_ip_rules and the Microsoft.KeyVault service endpoint on vm_subnet_id."
type = string
default = "Allow"
}

variable "key_vault_ip_rules" {
description = "IPv4 addresses or CIDRs allowed to reach the Key Vault data plane. Required only when key_vault_network_default_action = Deny and you don't have Private Link configured."
type = list(string)
default = []
}

variable "environment" {
type = string
default = "prod"
Expand Down
16 changes: 11 additions & 5 deletions modules/ha-hot-hot/aws/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ locals {
db_host = coalesce(one(aws_db_instance.main[*].address), one(aws_instance.db_ec2[*].private_ip))
db_port = local.use_rds ? coalesce(one(aws_db_instance.main[*].port), 5432) : 5432
db_arn = coalesce(one(aws_db_instance.main[*].arn), one(aws_instance.db_ec2[*].arn))
db_id = coalesce(one(aws_db_instance.main[*].id), one(aws_instance.db_ec2[*].id))

# Redis is required for HA: both app instances must share session state and
# the worker-lock heartbeat through the same Redis. The module provisions
Expand Down Expand Up @@ -642,8 +641,12 @@ data "aws_region" "current" {}
# ----- ALB -----

resource "aws_lb" "main" {
name = "${local.name_prefix}-alb"
internal = false
name = "${local.name_prefix}-alb"
# The HailBytes SAT / ASM console is customer-facing by design; the ALB sits
# in public subnets behind a security group that only allows ingress from
# var.allowed_cidrs. Customers who want a fully private deployment can front
# the module with their own internal ALB or API Gateway.
internal = false #tfsec:ignore:aws-elb-alb-not-public
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
Expand Down Expand Up @@ -742,8 +745,11 @@ resource "aws_wafv2_web_acl_association" "alb" {
# ----- SNS topic for patching alerts -----

resource "aws_sns_topic" "alerts" {
name = "${local.name_prefix}-alerts"
kms_master_key_id = var.enable_customer_managed_key ? aws_kms_key.main[0].id : "alias/aws/sns"
name = "${local.name_prefix}-alerts"
# CMK is opt-in via var.enable_customer_managed_key. tfsec's static analysis
# evaluates the false branch ("alias/aws/sns") of the ternary; customers who
# set enable_customer_managed_key = true get the module-owned CMK.
kms_master_key_id = var.enable_customer_managed_key ? aws_kms_key.main[0].id : "alias/aws/sns" #tfsec:ignore:aws-sns-topic-encryption-use-cmk
tags = local.common_tags
}

Expand Down
23 changes: 20 additions & 3 deletions modules/ha-hot-hot/azure/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ locals {
use_vm_db = var.db_mode == "vm"

db_host = local.use_flexible_server ? one(azurerm_postgresql_flexible_server.main[*].fqdn) : one(azurerm_linux_virtual_machine.db_vm[*].private_ip_address)
db_arn = local.use_flexible_server ? one(azurerm_postgresql_flexible_server.main[*].id) : one(azurerm_linux_virtual_machine.db_vm[*].id)

create_backup_storage = var.create_backup_storage_account
backup_storage_account_name = local.create_backup_storage ? azurerm_storage_account.backup[0].name : var.backup_storage_account_name
Expand Down Expand Up @@ -81,6 +80,18 @@ resource "azurerm_key_vault" "main" {
soft_delete_retention_days = 30
enable_rbac_authorization = true
tags = local.common_tags

network_acls {
# default_action is wired through var.key_vault_network_default_action so
# customers can opt into "Deny" once they've added the operator IP to
# key_vault_ip_rules and a Microsoft.KeyVault service endpoint on
# vm_subnet_id. Defaulting to "Allow" preserves pre-ACL behavior;
# data-plane access is still gated by RBAC and the AzureServices bypass.
default_action = var.key_vault_network_default_action #tfsec:ignore:azure-keyvault-specify-network-acl
bypass = "AzureServices"
ip_rules = var.key_vault_ip_rules
virtual_network_subnet_ids = [var.vm_subnet_id]
}
}

resource "random_password" "db" {
Expand Down Expand Up @@ -140,6 +151,14 @@ resource "azurerm_network_security_rule" "lb_https_in" {
network_security_group_name = azurerm_network_security_group.lb.name
}

# Attach the NSG to the LB frontend subnet so the allow-https-* rules
# actually take effect. Without this association the rules exist on the
# NSG but the subnet routes traffic unfiltered.
resource "azurerm_subnet_network_security_group_association" "lb" {
subnet_id = var.lb_subnet_id
network_security_group_id = azurerm_network_security_group.lb.id
}

# ----- Load Balancer -----

resource "azurerm_public_ip" "lb" {
Expand Down Expand Up @@ -572,8 +591,6 @@ resource "azurerm_role_assignment" "db_vm_kv_reader" {

# ----- Backup Storage Account + immutable container -----

data "azurerm_subscription" "current" {}

resource "azurerm_storage_account" "backup" {
count = local.create_backup_storage ? 1 : 0
name = coalesce(var.backup_storage_account_name, substr(replace("${local.name_prefix}backup", "-", ""), 0, 24))
Expand Down
18 changes: 18 additions & 0 deletions modules/ha-hot-hot/azure/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ variable "ssh_public_key" {
type = string
}

# ----- Key Vault network ACL -----

variable "key_vault_network_default_action" {
description = "Default action for the Key Vault network ACL. 'Allow' preserves the pre-network-ACL behavior (public endpoint open, RBAC-gated); set 'Deny' once you've added the operator IP to key_vault_ip_rules and the Microsoft.KeyVault service endpoint on vm_subnet_id. AzureServices bypass is always on so the VMs' managed identities can read secrets either way."
type = string
default = "Allow"
validation {
condition = contains(["Allow", "Deny"], var.key_vault_network_default_action)
error_message = "key_vault_network_default_action must be one of: Allow, Deny."
}
}

variable "key_vault_ip_rules" {
description = "IPv4 addresses or CIDRs allowed to reach the Key Vault data plane (typically the operator IP running terraform apply, or your bastion's egress NAT). Required only when default_action = Deny and you don't have Private Link configured."
type = list(string)
default = []
}

# ----- Optional -----

variable "environment" {
Expand Down
Loading
Loading