Enterprise Hub-and-Spoke with Zero Trust Edge
draft
Full article content will appear here once the pipeline processes this article.
The snippet
code
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.90.0"
}
}
}
provider "azurerm" {
features {}
}
variable "location" {
type = string
default = "eastus"
description = "Primary Azure region for hub network and zero-trust edge resources."
}
variable "resource_group_name" {
type = string
default = "rg-zerotrust-hub"
description = "Target resource group name."
}
variable "vnet_cidr" {
type = string
default = "10.100.0.0/16"
description = "CIDR block for the primary hub virtual network."
}
variable "subnet_fw_cidr" {
type = string
default = "10.100.0.0/26"
description = "Dedicated subnet for Azure Firewall (minimum /26 required)."
}
variable "subnet_gateway_cidr" {
type = string
default = "10.100.0.64/26"
description = "Dedicated GatewaySubnet for ExpressRoute circuit connection."
}
variable "subnet_workload_cidr" {
type = string
default = "10.100.1.0/24"
description = "Subnet hosting workload virtual machines."
}
variable "subnet_pe_cidr" {
type = string
default = "10.100.2.0/24"
description = "Dedicated subnet for Private Endpoints (RSV, Blob, Queue)."
}
# --- Resource Group ---
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
# --- Virtual Network & Subnets ---
resource "azurerm_virtual_network" "vnet" {
name = "vnet-hub"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = [var.vnet_cidr]
# Direct VNet DNS resolution through Firewall DNS Proxy to preserve private zone forwarding
dns_servers = [azurerm_firewall.fw.ip_configuration[0].private_ip_address]
}
resource "azurerm_subnet" "fw_subnet" {
name = "AzureFirewallSubnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.subnet_fw_cidr]
}
resource "azurerm_subnet" "gateway_subnet" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.subnet_gateway_cidr]
}
resource "azurerm_subnet" "workload_subnet" {
name = "snet-workload"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.subnet_workload_cidr]
}
resource "azurerm_subnet" "pe_subnet" {
name = "snet-private-endpoints"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.subnet_pe_cidr]
private_endpoint_network_policies = "Enabled"
}
# --- Azure Firewall & Policy (DNS Proxy Enabled) ---
resource "azurerm_public_ip" "fw_pip" {
name = "pip-afw-hub"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_firewall_policy" "fw_policy" {
name = "fwp-zerotrust-hub"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku = "Standard"
# DNS Proxy enabled so on-prem conditional forwarders and local VNets resolve via Firewall
dns {
proxy_enabled = true
}
}
resource "azurerm_firewall" "fw" {
name = "afw-hub"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
sku_name = "AZFW_VNet"
sku_tier = "Standard"
firewall_policy_id = azurerm_firewall_policy.fw_policy.id
ip_configuration {
name = "fw-ipconfig"
subnet_id = azurerm_subnet.fw_subnet.id
public_ip_address_id = azurerm_public_ip.fw_pip.id
}
}
# Rules matching explicit allow patterns in diagram
resource "azurerm_firewall_policy_rule_collection_group" "app_rules" {
name = "fw-app-rule-group"
firewall_policy_id = azurerm_firewall_policy.fw_policy.id
priority = 100
application_rule_collection {
name = "allow-zerotrust-endpoints"
priority = 110
action = "Allow"
rule {
name = "allow-azure-backup-storage-login"
source_addresses = ["*"]
target_fqdns = [
"*.backup.windowsazure.com",
"*.blob.core.windows.net",
"*.queue.core.windows.net",
"*.login.windows.net"
]
protocols {
type = "Https"
port = 443
}
}
}
}
# --- Route Table (Force Egress through Firewall) ---
resource "azurerm_route_table" "workload_rt" {
name = "rt-workload-to-fw"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
route {
name = "default-egress-to-fw"
address_prefix = "0.0.0.0/0"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = azurerm_firewall.fw.ip_configuration[0].private_ip_address
}
}
resource "azurerm_subnet_route_table_association" "workload_rt_assoc" {
subnet_id = azurerm_subnet.workload_subnet.id
route_table_id = azurerm_route_table.workload_rt.id
}
# --- ExpressRoute Gateway ---
resource "azurerm_public_ip" "gw_pip" {
name = "pip-ergw"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_virtual_network_gateway" "er_gw" {
name = "ergw-hub"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
type = "ExpressRoute"
sku = "Standard"
ip_configuration {
name = "ergw-config"
public_ip_address_id = azurerm_public_ip.gw_pip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.gateway_subnet.id
}
}
# --- Private DNS Zones & Virtual Network Links ---
resource "azurerm_private_dns_zone" "dns_blob" {
name = "privatelink.blob.core.windows.net"
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_private_dns_zone" "dns_queue" {
name = "privatelink.queue.core.windows.net"
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_private_dns_zone" "dns_vault" {
name = "privatelink.vaultcore.azure.net"
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_private_dns_zone_virtual_network_link" "link_blob" {
name = "link-dns-blob"
resource_group_name = azurerm_resource_group.rg.name
private_dns_zone_name = azurerm_private_dns_zone.dns_blob.name
virtual_network_id = azurerm_virtual_network.vnet.id
}
resource "azurerm_private_dns_zone_virtual_network_link" "link_queue" {
name = "link-dns-queue"
resource_group_name = azurerm_resource_group.rg.name
private_dns_zone_name = azurerm_private_dns_zone.dns_queue.name
virtual_network_id = azurerm_virtual_network.vnet.id
}
resource "azurerm_private_dns_zone_virtual_network_link" "link_vault" {
name = "link-dns-vault"
resource_group_name = azurerm_resource_group.rg.name
private_dns_zone_name = azurerm_private_dns_zone.dns_vault.name
virtual_network_id = azurerm_virtual_network.vnet.id
}
# --- Target Back-End Services & Private Endpoints (RSV_PE) ---
resource "azurerm_storage_account" "sa" {
name = "stzerotrusthub001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
public_network_access_enabled = false
}
resource "azurerm_recovery_services_vault" "rsv" {
name = "rsv-hub-backup"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
sku = "Standard"
soft_delete_enabled = true
}
resource "azurerm_private_endpoint" "pe_blob" {
name = "pe-storage-blob"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
subnet_id = azurerm_subnet.pe_subnet.id
private_service_connection {
name = "psc-blob"
private_connection_resource_id = azurerm_storage_account.sa.id
subresource_names = ["blob"]
is_manual_connection = false
}
private_dns_zone_group {
name = "dns-group-blob"
private_dns_zone_ids = [azurerm_private_dns_zone.dns_blob.id]
}
}
resource "azurerm_private_endpoint" "pe_queue" {
name = "pe-storage-queue"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
subnet_id = azurerm_subnet.pe_subnet.id
private_service_connection {
name = "psc-queue"
private_connection_resource_id = azurerm_storage_account.sa.id
subresource_names = ["queue"]
is_manual_connection = false
}
private_dns_zone_group {
name = "dns-group-queue"
private_dns_zone_ids = [azurerm_private_dns_zone.dns_queue.id]
}
}
resource "azurerm_private_endpoint" "pe_rsv" {
name = "pe-recovery-vault"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
subnet_id = azurerm_subnet.pe_subnet.id
private_service_connection {
name = "psc-rsv"
private_connection_resource_id = azurerm_recovery_services_vault.rsv.id
subresource_names = ["AzureBackup"]
is_manual_connection = false
}
private_dns_zone_group {
name = "dns-group-vault"
private_dns_zone_ids = [azurerm_private_dns_zone.dns_vault.id]
}
}
# --- Workload Machine ---
resource "azurerm_network_interface" "workload_nic" {
name = "nic-workload-vm"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.workload_subnet.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_linux_virtual_machine" "workload_vm" {
name = "vm-workload"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
size = "Standard_D2s_v5"
admin_username = "azureadmin"
disable_password_authentication = true
admin_ssh_key {
username = "azureadmin"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3..."
}
network_interface_ids = [
azurerm_network_interface.workload_nic.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "StandardSSD_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}
output "azure_firewall_dns_proxy_ip" {
value = azurerm_firewall.fw.ip_configuration[0].private_ip_address
description = "IP address to configure as target in on-premises DNS conditional forwarders."
}CAFWAFZero TrustLanding ZoneEnterprise
Newsletter
Stay ahead of the cloud curve.
Practical hybrid & multi-cloud insights, straight to your inbox. No spam — unsubscribe anytime.