Surviving Azure Policies: Zero-Trust Hub & Spoke with Terraform
Surviving Azure Policies: Zero-Trust Hub & Spoke with Terraform
应对 Azure 策略:基于 Terraform 的零信任 Hub & Spoke 架构
Your Terraform pipeline is green. The deployment completes. You grab a coffee. Ten minutes later, Azure Policy has silently rewritten three of your resources. You run terraform plan. It detects drift. It tries to revert. Policy blocks the revert with a cryptic permission error. Your pipeline is now permanently broken — and nobody touched the code. This is Tuesday in an enterprise Azure tenant.
你的 Terraform 流水线显示运行成功,部署已完成。你起身去喝杯咖啡。十分钟后,Azure Policy 在后台静默修改了你的三个资源。当你再次运行 terraform plan 时,它检测到了配置漂移(Drift)并试图回滚,但 Azure Policy 却以晦涩的权限错误拦截了这次回滚。你的流水线现在彻底瘫痪了——而期间根本没人动过代码。这就是企业级 Azure 环境中司空见惯的“周二日常”。
The DINE Death Loop: DeployIfNotExists policies run continuously in the background. They inject tags like CreatedByPolicy=True or hidden-title into your resources for compliance tracking. Terraform sees these injected tags as drift. It plans to delete them. Azure Policy blocks the deletion. Your pipeline fails. This repeats on every run. Forever.
DINE 死循环:DeployIfNotExists (DINE) 策略在后台持续运行。为了合规追踪,它们会向你的资源注入诸如 CreatedByPolicy=True 或 hidden-title 之类的标签。Terraform 将这些注入的标签视为配置漂移,并计划将其删除,而 Azure Policy 又会拦截删除操作。流水线因此报错,且在每次运行时都会无限循环。
The fix is surgical — tell Terraform to ignore exactly these tags and nothing else: 解决办法非常精准——只需告诉 Terraform 忽略这些特定的标签,而不影响其他配置:
resource "azurerm_private_dns_zone" "enterprise_zones" {
for_each = toset(var.private_dns_zones)
name = each.key
resource_group_name = azurerm_resource_group.rg.name
lifecycle {
ignore_changes = [
tags["hidden-title"],
tags["CreatedByPolicy"]
]
}
}
Terraform now maintains the infrastructure. The compliance scanner gets its metadata. Nobody fights. No pipeline failures.
现在,Terraform 可以正常维护基础设施,合规扫描器也能获取所需的元数据。双方互不干扰,流水线也不会再报错。
Zero-Trust NSG Baseline
零信任 NSG 基准
A default Azure VNet allows unrestricted lateral movement and outbound internet access. For any ISO 27001 or KRITIS audit, this is an immediate finding. The fix: an NSG bound to Spoke subnets at creation — not as a follow-up ticket.
默认的 Azure VNet 允许不受限制的横向移动和出站互联网访问。对于任何 ISO 27001 或 KRITIS 审计来说,这都是一个严重的违规项。解决方法是:在创建 Spoke 子网时就绑定 NSG,而不是事后再通过工单补救。
resource "azurerm_network_security_group" "zero_trust" {
name = "nsg-zero-trust-${var.environment}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
security_rule {
name = "Allow-VNet-Inbound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
source_port_range = "*"
destination_port_range = "*"
}
security_rule {
name = "Deny-Internet-Inbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_address_prefix = "Internet"
destination_address_prefix = "*"
source_port_range = "*"
destination_port_range = "*"
}
}
# Critical: bind it immediately — an unbound NSG enforces nothing
# 关键点:立即绑定——未绑定的 NSG 无法执行任何规则
resource "azurerm_subnet_network_security_group_association" "spoke1_nsg" {
subnet_id = azurerm_subnet.spoke1_default.id
network_security_group_id = azurerm_network_security_group.zero_trust.id
}
The priority gap (100 → 4096) leaves room for hundreds of application-specific rules without renumbering the baseline.
优先级间隙(100 到 4096)留出了数百个应用特定规则的空间,无需重新编号基准规则。
Centralized Private DNS
集中式私有 DNS
Deploy DNS zones once in the Hub — Spokes resolve through peering automatically: 在 Hub 中部署一次 DNS 区域,Spoke 通过对等互联自动解析:
variable "private_dns_zones" {
default = [
"privatelink.blob.core.windows.net",
"privatelink.database.windows.net",
"privatelink.vaultcore.azure.net",
"privatelink.azurecr.io"
]
}
resource "azurerm_private_dns_zone" "enterprise_zones" {
for_each = toset(var.private_dns_zones)
name = each.key
resource_group_name = azurerm_resource_group.rg.name
lifecycle {
ignore_changes = [tags["hidden-title"], tags["CreatedByPolicy"]]
}
}
Four zones, one block, DINE-proof. No per-Spoke DNS configuration required. 四个区域,一个代码块,完美免疫 DINE 干扰。无需为每个 Spoke 单独配置 DNS。
The free base topology is on GitHub. The full article with complete DINE bypass logic, NSG associations, and VNet link protection is on my blog. 基础拓扑架构已开源至 GitHub。包含完整的 DINE 绕过逻辑、NSG 关联以及 VNet 链路保护的完整文章请见我的博客。