BC Gov Azure Landing Zone - digitalspace/eagle-demi GitHub Wiki
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.
- Canada Central and Canada East only; anything else is blocked by the
Resource-Locationsassignment. 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-httpenforces it on storage accounts.
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 useazapi_resourcerather thanazurerm_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_v2SKU, WAF in Prevention mode, with request-body inspection and bot protection on.
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.
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.
- 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.
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.
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.
- 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.netzone. 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-tokenhanded 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.