From 30661fd98dc76e49e67f52d98fde5a84e0fe9311 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Wed, 10 Dec 2025 16:15:02 -0500 Subject: [PATCH 1/4] chore: private storage account endpoint (ENG-5444) [ENG-5444](https://stacklet.atlassian.net/browse/ENG-5444) What ---- - Disable public network access for the storage account. Why --- - It's insecure to allow public access and is unnecessary. Testing ------- - [ ] Deploy to Azure in sandbox subscription. Docs ---- Updated README --- README.md | 2 +- eventgrid.tf | 2 +- function.tf | 4 ++-- provider.tf | 8 ++++++++ storage.tf | 30 ++++++++++++++++++++++++++---- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f1f5c5d..2e5e8a5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The system works through a four-step process: - `Microsoft.Resources.ResourceDeleteSuccess` (resource deletions) ### 2. Event Storage (Azure Storage Queue) -- Events are queued in an **Azure Storage Queue** for reliable processing +- Events are queued in a private **Azure Storage Queue** for reliable processing - Uses CloudEvent schema v1.0 format for standardized event structure ### 3. Event Processing (Azure Function) diff --git a/eventgrid.tf b/eventgrid.tf index 2bdd2af..7221de1 100644 --- a/eventgrid.tf +++ b/eventgrid.tf @@ -45,7 +45,7 @@ resource "azurerm_eventgrid_system_topic_event_subscription" "azure_rm_event_sub storage_queue_endpoint { storage_account_id = azurerm_storage_account.stacklet.id - queue_name = azurerm_storage_queue.stacklet.name + queue_name = azapi_resource.stacklet_queue.name } included_event_types = var.event_names diff --git a/function.tf b/function.tf index 7324727..8537eec 100644 --- a/function.tf +++ b/function.tf @@ -40,7 +40,7 @@ resource "local_file" "function_json" { name = "msg" type = "queueTrigger" direction = "in" - queueName = azurerm_storage_queue.stacklet.name + queueName = azapi_resource.stacklet_queue.name connection = "AzureWebJobsStorage" } ] @@ -129,7 +129,7 @@ resource "azurerm_linux_function_app" "stacklet" { # Application configuration AZURE_CLIENT_ID = azurerm_user_assigned_identity.stacklet_identity.client_id AZURE_AUDIENCE = local.audience - AZURE_STORAGE_QUEUE_NAME = azurerm_storage_queue.stacklet.name + AZURE_STORAGE_QUEUE_NAME = azapi_resource.stacklet_queue.name AWS_TARGET_ACCOUNT = var.aws_target_account AWS_TARGET_REGION = var.aws_target_region AWS_TARGET_ROLE_NAME = var.aws_target_role_name diff --git a/provider.tf b/provider.tf index 63161fa..cb4b174 100644 --- a/provider.tf +++ b/provider.tf @@ -23,6 +23,10 @@ terraform { source = "hashicorp/azurerm" version = ">=4.35.0" } + azapi = { + source = "azure/azapi" + version = ">=2.8.0" + } } } @@ -35,3 +39,7 @@ provider "azurerm" { subscription_id = var.subscription_id } + +provider "azapi" { + subscription_id = var.subscription_id +} \ No newline at end of file diff --git a/storage.tf b/storage.tf index 0a009d0..c71b8ba 100644 --- a/storage.tf +++ b/storage.tf @@ -33,10 +33,32 @@ resource "azurerm_storage_account" "stacklet" { location = azurerm_resource_group.stacklet_rg.location account_tier = "Standard" account_replication_type = "LRS" - tags = local.tags + + # Disable public network access - only private endpoints allowed + public_network_access_enabled = false + + # Network rules to control access + network_rules { + default_action = "Deny" + # Allow Azure services (like Function Apps) to access + bypass = ["AzureServices"] + } + + tags = local.tags } -resource "azurerm_storage_queue" "stacklet" { - name = "${azurerm_storage_account.stacklet.name}-queue" - storage_account_name = azurerm_storage_account.stacklet.name +# Using azapi provider to create storage queue via ARM API (control plane) +# This avoids the need for Terraform to access storage data plane APIs +resource "azapi_resource" "stacklet_queue" { + type = "Microsoft.Storage/storageAccounts/queueServices/queues@2023-01-01" + name = "${azurerm_storage_account.stacklet.name}-queue" + parent_id = "${azurerm_storage_account.stacklet.id}/queueServices/default" + + body = { + properties = { + metadata = {} + } + } + + depends_on = [azurerm_storage_account.stacklet] } From 537ff11dc5453c28853e79c625968c582e90edb2 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Wed, 10 Dec 2025 16:22:46 -0500 Subject: [PATCH 2/4] Add missing EOF EOL --- provider.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider.tf b/provider.tf index cb4b174..9764f97 100644 --- a/provider.tf +++ b/provider.tf @@ -42,4 +42,4 @@ provider "azurerm" { provider "azapi" { subscription_id = var.subscription_id -} \ No newline at end of file +} From cf384305771c78df7d198d495b05cfb4d3a3528c Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Thu, 11 Dec 2025 11:01:34 -0500 Subject: [PATCH 3/4] Use a staged approach to make storage endpoint public until function code is uploaded then make it private --- storage.tf | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/storage.tf b/storage.tf index c71b8ba..228878e 100644 --- a/storage.tf +++ b/storage.tf @@ -34,14 +34,14 @@ resource "azurerm_storage_account" "stacklet" { account_tier = "Standard" account_replication_type = "LRS" - # Disable public network access - only private endpoints allowed - public_network_access_enabled = false - - # Network rules to control access + # Enable public network access temporarily to allow the function app to upload the function code. + # Note: This will always trigger a change on update because the azapi_update_resource will set it to false + # after the function app is deployed, but we need to set it to true again temporarily to allow the function + # app to upload any new function code. + public_network_access_enabled = true network_rules { - default_action = "Deny" - # Allow Azure services (like Function Apps) to access - bypass = ["AzureServices"] + default_action = "Allow" + bypass = null } tags = local.tags @@ -62,3 +62,35 @@ resource "azapi_resource" "stacklet_queue" { depends_on = [azurerm_storage_account.stacklet] } + +# Update storage account network settings to make it private after function app is deployed. +# This ensures the function app code can be uploaded to the storage account before we lock it +# down, and then the actual function run can still occur due to the bypass setting. +resource "azapi_update_resource" "stacklet_storage_network" { + type = "Microsoft.Storage/storageAccounts@2023-01-01" + resource_id = azurerm_storage_account.stacklet.id + + body = { + properties = { + # Disable public network access - only private endpoints allowed + publicNetworkAccess = "Disabled" + # Network rules to control access + networkAcls = { + defaultAction = "Deny" + # Allow Azure services (like Function Apps) to access + bypass = "AzureServices" + } + } + } + + depends_on = [azurerm_linux_function_app.stacklet] + + # Ensure that the update is applied if the public network access is enabled again (which will + # happen on every update because the azurerm_storage_account resource will always make it + # public to allow the function app to upload any new function code). + lifecycle { + replace_triggered_by = [ + azurerm_storage_account.stacklet.public_network_access_enabled + ] + } +} From ed5edd14d8edd49657a83544ba0ae7a3837b95e9 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Thu, 11 Dec 2025 13:00:40 -0500 Subject: [PATCH 4/4] Remove unnecessary network rules --- storage.tf | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/storage.tf b/storage.tf index 228878e..87bb170 100644 --- a/storage.tf +++ b/storage.tf @@ -39,10 +39,6 @@ resource "azurerm_storage_account" "stacklet" { # after the function app is deployed, but we need to set it to true again temporarily to allow the function # app to upload any new function code. public_network_access_enabled = true - network_rules { - default_action = "Allow" - bypass = null - } tags = local.tags } @@ -74,12 +70,6 @@ resource "azapi_update_resource" "stacklet_storage_network" { properties = { # Disable public network access - only private endpoints allowed publicNetworkAccess = "Disabled" - # Network rules to control access - networkAcls = { - defaultAction = "Deny" - # Allow Azure services (like Function Apps) to access - bypass = "AzureServices" - } } }