Skip to main content
Back to Blog
Share viaLinkedInX

Enterprise Hub-and-Spoke with Zero Trust Edge

Hybrid Cloud Works
draft

The full article is not available yet. Check back soon.

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.