From a64292424926c3f29e8170488cddbfc5aa6ff052 Mon Sep 17 00:00:00 2001 From: Christopher House Date: Wed, 17 Jun 2026 14:25:53 -0500 Subject: [PATCH] fix(iac): pin Cosmos ip_range_filter to allow Azure-datacenter traffic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each tofu apply during CD-dev was reverting the manual Cosmos firewall fix I'd been setting via az (ipRules = [{0.0.0.0}]). The indexer's change-feed listener kept 403'ing with: Request originated from IP 4.153.180.100 through public internet. This is blocked by your Cosmos DB account firewall settings. Root cause: the cosmos-account module never declared an ip_range_filter, so every apply emitted an empty ipRules set. When a Cosmos account has a private endpoint configured AND public_network_access_enabled = true, it enters a default 'restricted public' mode that drops public traffic unless explicitly allowed. Fix: - New cosmos-account module variable ip_range_filter (set of strings, default empty). When set, threads through to azurerm_cosmosdb_account. Docstring explains the magic value, the AAD-still-gates note, and the dependency on CAE vnet-integration that would let us remove the rule in a future spec. - dev composition passes ip_range_filter = ['0.0.0.0'] — Cosmos's magic value for 'Allow access from public Azure datacenters'. Narrower than '0.0.0.0/0' (the entire internet) and AAD/RBAC still gates every connection regardless of source IP. - terraform-docs regenerated for the module. No data plane impact. AAD-only auth remains in force (local_authentication_disabled = true). Only the network ACL changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- iac/environments/dev/main.tf | 11 +++++++++++ iac/modules/cosmos-account/README.md | 1 + iac/modules/cosmos-account/main.tf | 6 ++++++ iac/modules/cosmos-account/variables.tf | 16 ++++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/iac/environments/dev/main.tf b/iac/environments/dev/main.tf index 6665507..bdf8df6 100644 --- a/iac/environments/dev/main.tf +++ b/iac/environments/dev/main.tf @@ -559,6 +559,17 @@ module "cosmos_account" { private_endpoint_subnet_id = module.networking.subnet_private_endpoints_id private_dns_zone_id = module.networking.private_dns_zone_ids["privatelink.documents.azure.com"] + # Allow traffic originating from any Azure datacenter — the Container + # Apps Environment hosting the backend + indexer is not vnet-integrated + # (CAE vnetConfig: null), so its egress is a public Azure NAT IP. Cosmos + # in "PE + public-enabled" mode drops public traffic by default unless + # explicitly allowed via ipRules. `0.0.0.0` is Cosmos's magic value for + # "Allow access from public Azure datacenters" — narrower than allowing + # the entire internet (which would be `0.0.0.0/0`), and AAD/RBAC still + # gates every connection regardless. When the CAE is vnet-integrated by + # a future spec, this entry can be removed. + ip_range_filter = ["0.0.0.0"] + tags = local.shared_tags } diff --git a/iac/modules/cosmos-account/README.md b/iac/modules/cosmos-account/README.md index 0ee07ab..9c0032f 100644 --- a/iac/modules/cosmos-account/README.md +++ b/iac/modules/cosmos-account/README.md @@ -34,6 +34,7 @@ Spec 004 / Spec 005 / US1 — canonical resource store + change-event log. | [location](#input\_location) | Azure region for the Cosmos DB account. | `string` | n/a | yes | | [name](#input\_name) | Cosmos DB account name. Must be globally unique, 3-44 lowercase alphanumeric / hyphen chars. | `string` | n/a | yes | | [resource\_group\_name](#input\_resource\_group\_name) | Resource group hosting the Cosmos DB account. | `string` | n/a | yes | +| [ip\_range\_filter](#input\_ip\_range\_filter) | Cosmos `ipRules` set. When a private endpoint is configured AND
`public_network_access_enabled = true`, Cosmos enters a default
"restricted public" mode where public traffic is dropped unless
explicitly allowed via this set. The special magic value `0.0.0.0`
permits traffic originating from any Azure datacenter — used in
dev so the Container Apps Environment's egress NAT (a public
Azure-allocated IP) can reach Cosmos for the indexer's change-feed
listener. Empty set keeps the default-restrictive posture (PE-only
+ named IP allowlist). Bare IP literals or CIDRs are also accepted. | `set(string)` | `[]` | no | | [log\_analytics\_workspace\_id](#input\_log\_analytics\_workspace\_id) | Optional Log Analytics Workspace resource id for diagnostic settings. When
set, a diagnostic-setting routes allLogs + AllMetrics to the workspace
(Constitution §Operational Excellence — every Azure resource routes
diagnostic logs + AllMetrics to the LAW). Pass null to skip. | `string` | `null` | no | | [private\_dns\_zone\_id](#input\_private\_dns\_zone\_id) | Private DNS zone ID for `privatelink.documents.azure.com`. Required when private\_endpoint\_enabled = true. | `string` | `null` | no | | [private\_endpoint\_enabled](#input\_private\_endpoint\_enabled) | Plan-time bool toggling the conditional private-endpoint child module.
Required as a separate variable from `private_endpoint_subnet_id`
because the subnet ID is sourced from the networking module's output,
which is "known after apply" — using a nullable string in the `count`
expression breaks plan with "Invalid count argument: count value
depends on resource attributes that cannot be determined until apply".
The env composition passes a literal bool here (`var.private_endpoints_enabled`)
so plan can statically resolve the count. | `bool` | `false` | no | diff --git a/iac/modules/cosmos-account/main.tf b/iac/modules/cosmos-account/main.tf index a7935e2..82889c8 100644 --- a/iac/modules/cosmos-account/main.tf +++ b/iac/modules/cosmos-account/main.tf @@ -37,6 +37,12 @@ resource "azurerm_cosmosdb_account" "this" { # Spec 005 FR-031 — per-env public-network-access toggle (Q2c). public_network_access_enabled = var.public_network_access_enabled + # Cosmos `ipRules` — required when public access is enabled alongside a + # private endpoint, otherwise the account enters a default-restrictive + # mode that drops all public traffic. See variables.tf for the full + # rationale (and the `0.0.0.0` Azure-datacenter magic value). + ip_range_filter = var.ip_range_filter + # Automatic-failover off for dev — single-region serverless. AVM rejects multi-region # with EnableServerless anyway. automatic_failover_enabled = false diff --git a/iac/modules/cosmos-account/variables.tf b/iac/modules/cosmos-account/variables.tf index a8bff85..386f59e 100644 --- a/iac/modules/cosmos-account/variables.tf +++ b/iac/modules/cosmos-account/variables.tf @@ -71,3 +71,19 @@ variable "public_network_access_enabled" { type = bool default = true } + +variable "ip_range_filter" { + description = <<-EOT + Cosmos `ipRules` set. When a private endpoint is configured AND + `public_network_access_enabled = true`, Cosmos enters a default + "restricted public" mode where public traffic is dropped unless + explicitly allowed via this set. The special magic value `0.0.0.0` + permits traffic originating from any Azure datacenter — used in + dev so the Container Apps Environment's egress NAT (a public + Azure-allocated IP) can reach Cosmos for the indexer's change-feed + listener. Empty set keeps the default-restrictive posture (PE-only + + named IP allowlist). Bare IP literals or CIDRs are also accepted. + EOT + type = set(string) + default = [] +}