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 = []
+}