BC Gov Azure Landing Zone - digitalspace/eagle-demi GitHub Wiki

BC Gov Azure landing zone — the rules that bite

Compacted from bcgov/public-cloud-techdocs. Upstream is the source of truth, not this page — re-pull if a rule looks wrong: https://raw.githubusercontent.com/bcgov/public-cloud-techdocs/main/<path>. Live render: https://developer.gov.bc.ca/docs/default/component/public-cloud-techdocs/azure/.

Kept: constraints that block a deploy, change a design, or need a ticket. Dropped: tutorials, portal click-paths, AWS pages, and services DEMI will never run.

Policy beats RBAC. Owner does not get you past a Deny. Policy is assigned at the management group bcgov-managed-lz-live and its landing-zones child, above every c4b0a8 subscription.

Deployment restrictions

  • Canada Central and Canada East only; anything else is blocked by the Resource-Locations assignment. Networking works only in Canada Central — Canada East has no landing-zone connectivity, while Azure AI models are Canada-East-only. Both are true at once, so an AI model can sit in a region with no network. DEMI's Foundry account is in canadaeast for that reason.
  • A resource group must be in the same region as the resources in it (Deny-RSG-Locations).
  • A Deny policy lists resource types that cannot be created at all.
  • HTTPS everywhere, TLS 1.2 minimum. Deny-Storage-http enforces it on storage accounts.

Networking is mostly not yours

Cannot be created or modified, by policy: public IPs (standalone or on a NIC), VNets, VNet peering, VNet address space, VNet DNS settings, ExpressRoute circuits, VPN sites, VPN/NAT/local gateways, and route tables. The matching assignments are Deny-VNet-Creation, Deny-New-VNet-Peerings, Deny-VNet-DNS-Changes, Deny-Public-IP-On-NIC and Deny-Delete-Routes. A UDR needs a service request — SQL Managed Instance genuinely needs one, so ask before deploying. You also cannot delete setbypolicy diagnostic settings (Deny-Delete-Diagnostics), though you may add your own.

Other rules:

  • Every subnet needs an NSG (Deny-Subnet-Without-Nsg). The portal cannot create both at once, so use CLI or IaC. In Terraform use azapi_resource rather than azurerm_subnet — the latter cannot attach an NSG at creation, so policy rejects it.
  • Subnets need private endpoint network policies enabled.
  • Service endpoints are denied. Private endpoints are the sanctioned path.
  • All subnets are private, with no default outbound internet. Egress is centrally routed through vWAN and the firewall.
  • Spoke-to-spoke is off by default and needs a reviewed request.
  • Inbound 22 and 3389 from the internet are blocked (Deny-MgmtPorts-Internet). Use Bastion, and do not enable JIT VM Access — it is built for public-IP VMs and its NSG rules break Bastion.
  • Public exposure means HTTPS through Application Gateway only: WAF_v2 SKU, WAF in Prevention mode, with request-body inspection and bot protection on.

Private endpoints and DNS — the part that wastes days

Most PaaS is private-endpoint-only. Creating one with public access, or turning public access on later, is denied by the Deny-PublicPaaSEndpoints initiative.

That initiative is parameterised per service, so read the assignment before assuming a service is covered. Cosmos DB is set to Deny, which is why demi-cosmos-<env> cannot be public. Storage accounts are set to Disabled, which is why DEMI's $web static site can stay publicly reachable. AI Search is covered by the member policy Deny-CognitiveSearch-PublicEndpoint.

Private DNS zones are centrally owned in the Connectivity subscription, and Deny-Private-DNS-Zones blocks creating your own privatelink.* zone for a supported service. In the portal, set "Integrate with private DNS zone" to No: a DeployIfNotExists policy attaches a zone group named deployedByPolicy and writes the A-record for you.

The A-record appears about ten minutes after the private endpoint. Before that the name resolves to the service's public address, which policy has disabled, so it fails as a connection timeout that reads exactly like a missing DNS zone. Wait before diagnosing. Routing settles separately and well under a minute — two different not-ready states that look alike.

Linking your own private DNS zone to the VNet does not work. Every query goes to the central Private DNS Resolver, which never consults it. There is no self-service DNS fix. A genuinely missing zone (third-party services such as Confluent) is a support request, and the Public Cloud team attaches it to the central resolver.

The hub resolver is 10.53.244.4. Anything VNet-integrated has to be pointed at it explicitly or privatelink names resolve to public addresses; for App Service that is the WEBSITE_DNS_SERVER app setting, covered in Environment-Reality-and-Operational-Gotchas.

Two known traps: Azure OpenAI A-records are sometimes not created, which is a platform bug and needs a ticket; and multiple private endpoints on one resource is a footgun, because the policy automation rewrites the single A-record to the newest endpoint, so deleting that endpoint can remove the record while another endpoint still exists and DNS then resolves to nothing.

In Terraform, the policy adds a privateDnsZoneGroups/deployedByPolicy block that Terraform wants to destroy. Add lifecycle { ignore_changes = [private_dns_zone_group] }.

Private-only also means portal tools that call the data plane from your browser stop working — SQL Query Editor, Data Factory Studio. Reach them from a VM inside the VNet via Bastion.

Azure AI services

Provisioning of Azure AI services is managed through the AI Services Hub. Request access at https://bcgov.github.io/ai-hub-tracking/ and describe what you are building. The same page handles enhancement requests, bugs and architecture guidance.

AI Search outbound traffic — indexer data sources and skillsets — needs a shared private link, which is distinct from the inbound private endpoint. This is the documented, expected pattern. The link is created in Pending and has to be approved on the target resource.

Cognitive Services, OpenAI and ML attract extra Enterprise Scale guardrails covering permitted SKUs, managed-identity auth, storage configuration and outbound network. Configure at maximum security from the start; retrofitting fails deploys.

Private-only AI services are typically administered from a VM in the VNet through Bastion, minimum SKU Developer, which needs no dedicated subnet.

Identity, secrets, data

  • Managed identity first. An app registration is only for human sign-in, multi-tenant, or M365 integration, and must be requested through My Service Centre — self-registration is off.
  • Client secrets and certificates must live in Key Vault with rotation. Hard-coded credentials in source or pipeline variables are prohibited.
  • Key Vault: soft delete and purge protection on, firewall set to Deny All, RBAC only (access policies are denied), and secrets and keys need an expiry (secrets default to a 90-day maximum).
  • Customer-managed keys are required for many services, including Storage, Cosmos DB, SQL TDE, AKS disks, Cognitive Services, Data Factory, Service Bus and Synapse.
  • Storage: no custom domains, no SFTP, no local users.
  • Access is via Entra groups named DO_PuC_Azure_Live_{LicensePlate}_{Role}. The Product Owner manages membership and can delegate.
  • Control plane is not data plane. ARM RBAC grants no data access; those are separate roles.

Cost and tagging

The platform auto-applies account_coding, billing_group and ministry_name from subscription tags. Your own tags are additive, and only tag usage from the moment they are applied — never retroactively. Policies audit unattached disks, unused public IPs and empty App Service plans.

Defender for Cloud plan tiers are set by the platform, not by the project team, and show up on the subscription bill regardless of what the resource group holds. Test storage accounts carry a per-account override that turns Defender for Storage off. See Azure-Environments for what that costs DEMI.

Getting help

The Jira Service Management portal is preferred. Quote the six-character license plate — the management group or subscription name under BCGov-Managed-LZ > BCGov-Managed-LZ-Live > Landing Zones.

There is also the Teams channel CloudAzure-howto, and Microsoft Enterprise Support for vendor issues (not available on third-party vendor landing zones). Policy exceptions may be granted but need formal review and justification.

What this constrains in DEMI

  • AI Search has to be created with publicNetworkAccess: 'Disabled' plus a private endpoint from the start. There is no start-public-then-lock-down path.
  • The ten-minute A-record delay after creating a private endpoint reads exactly like a missing privatelink.search.windows.net zone. Wait it out before opening a ticket or redesigning around a gateway.
  • Debugging a private-only service from a workstation is impossible by design. What works is auth minted outside and network from inside: a token from az account get-access-token handed to a request issued from the App Service container.
  • Fabric capacity is blocked by Deny-Fabric-Capacity, and Power BI licensing is blocked separately on the tenant. Both are covered in Environment-Reality-and-Operational-Gotchas.
⚠️ **GitHub.com Fallback** ⚠️