Skip to content

Day 3-4: Azure Firewall and Private Endpoints

File: terraform/01-foundation/firewall.tf

resource "azurerm_public_ip" "firewall" {
  name                = "${var.prefix}-fw-pip"
  resource_group_name = azurerm_resource_group.shared_services.name
  location            = azurerm_resource_group.shared_services.location
  allocation_method   = "Static"
  sku                 = "Standard"
  zones               = ["1", "2", "3"]
}

resource "azurerm_firewall" "main" {
  name                = "${var.prefix}-fw"
  resource_group_name = azurerm_resource_group.shared_services.name
  location            = azurerm_resource_group.shared_services.location
  sku_name            = "AZFW_VNet"
  sku_tier            = "Premium"
  zones               = ["1", "2", "3"]

  ip_configuration {
    name                 = "fw-ipconfig"
    subnet_id            = azurerm_subnet.azure_firewall.id
    public_ip_address_id = azurerm_public_ip.firewall.id
  }
}

resource "azurerm_firewall_policy" "main" {
  name                = "${var.prefix}-fw-policy"
  resource_group_name = azurerm_resource_group.shared_services.name
  location            = azurerm_resource_group.shared_services.location
  sku                 = "Premium"

  intrusion_detection {
    mode = "Alert"
  }

  dns {
    servers       = ["10.101.1.10", "10.101.1.11"]
    proxy_enabled = true
  }
}

resource "azurerm_firewall_policy_rule_collection_group" "network_rules" {
  name               = "network-rules"
  firewall_policy_id = azurerm_firewall_policy.main.id
  priority           = 100

  network_rule_collection {
    name     = "allow-branch-to-azure"
    priority = 100
    action   = "Allow"

    rule {
      name                  = "branch-to-sql"
      protocols             = ["TCP"]
      source_addresses      = ["192.168.0.0/16", "10.252.0.0/16"]
      destination_addresses = ["10.100.3.0/24"]
      destination_ports     = ["1433"]
    }

    rule {
      name                  = "branch-to-app-servers"
      protocols             = ["TCP"]
      source_addresses      = ["192.168.0.0/16", "10.252.0.0/16"]
      destination_addresses = ["10.100.2.0/24"]
      destination_ports     = ["443", "8080", "8443"]
    }

    rule {
      name                  = "azure-to-onprem"
      protocols             = ["Any"]
      source_addresses      = ["10.100.0.0/16", "10.101.0.0/16"]
      destination_addresses = ["10.252.0.0/16"]
      destination_ports     = ["*"]
    }
  }
}

File: terraform/01-foundation/private_link.tf

# Azure SQL Server (Public Access Disabled)

resource "azurerm_mssql_server" "main" {
  name                          = "${var.prefix}-sql"
  resource_group_name           = azurerm_resource_group.prod_workloads.name
  location                      = azurerm_resource_group.prod_workloads.location
  version                       = "12.0"
  administrator_login           = "sqladmin"
  administrator_login_password  = var.sql_admin_password
  public_network_access_enabled = false
}

## Azure Storage Account (Public Access Disabled)

resource "azurerm_storage_account" "main" {
  name                            = "${replace(var.prefix, "-", "")}storage"
  resource_group_name             = azurerm_resource_group.prod_workloads.name
  location                        = azurerm_resource_group.prod_workloads.location
  account_tier                    = "Standard"
  account_replication_type        = "GRS"
  public_network_access_enabled   = false
  allow_nested_items_to_be_public = false
  min_tls_version                 = "TLS1_2"

  network_rules {
    default_action = "Deny"
    bypass         = ["None"]
  }
}

## Azure Key Vault

resource "azurerm_key_vault" "main" {
  name                       = "${var.prefix}-kv"
  resource_group_name        = azurerm_resource_group.shared_services.name
  location                   = azurerm_resource_group.shared_services.location
  tenant_id                  = var.tenant_id
  sku_name                   = "premium"
  purge_protection_enabled   = true
  soft_delete_retention_days = 90

  network_acls {
    bypass         = ["None"]
    default_action = "Deny"
  }
}

## Private DNS Zones

resource "azurerm_private_dns_zone" "sql" {
  name                = "privatelink.database.windows.net"
  resource_group_name = azurerm_resource_group.networking.name
}

resource "azurerm_private_dns_zone" "storage_blob" {
  name                = "privatelink.blob.core.windows.net"
  resource_group_name = azurerm_resource_group.networking.name
}

resource "azurerm_private_dns_zone" "storage_file" {
  name                = "privatelink.file.core.windows.net"
  resource_group_name = azurerm_resource_group.networking.name
}

resource "azurerm_private_dns_zone" "keyvault" {
  name                = "privatelink.vaultcore.azure.net"
  resource_group_name = azurerm_resource_group.networking.name
}

## DNS Zone VNet Links

resource "azurerm_private_dns_zone_virtual_network_link" "sql_prod" {
  name                  = "sql-link-prod"
  resource_group_name   = azurerm_resource_group.networking.name
  private_dns_zone_name = azurerm_private_dns_zone.sql.name
  virtual_network_id    = azurerm_virtual_network.prod.id
  registration_enabled  = false
}

resource "azurerm_private_dns_zone_virtual_network_link" "blob_prod" {
  name                  = "blob-link-prod"
  resource_group_name   = azurerm_resource_group.networking.name
  private_dns_zone_name = azurerm_private_dns_zone.storage_blob.name
  virtual_network_id    = azurerm_virtual_network.prod.id
  registration_enabled  = false
}

resource "azurerm_private_dns_zone_virtual_network_link" "kv_prod" {
  name                  = "kv-link-prod"
  resource_group_name   = azurerm_resource_group.networking.name
  private_dns_zone_name = azurerm_private_dns_zone.keyvault.name
  virtual_network_id    = azurerm_virtual_network.prod.id
  registration_enabled  = false
}

## Private Endpoints

resource "azurerm_private_endpoint" "sql" {
  name                = "pe-${var.prefix}-sql"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location
  subnet_id           = azurerm_subnet.private_endpoints.id

  private_service_connection {
    name                           = "psc-sql"
    private_connection_resource_id = azurerm_mssql_server.main.id
    is_manual_connection           = false
    subresource_names              = ["sqlServer"]
  }

  private_dns_zone_group {
    name                 = "dns-group-sql"
    private_dns_zone_ids = [azurerm_private_dns_zone.sql.id]
  }
}

resource "azurerm_private_endpoint" "storage_blob" {
  name                = "pe-${var.prefix}-blob"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location
  subnet_id           = azurerm_subnet.private_endpoints.id

  private_service_connection {
    name                           = "psc-blob"
    private_connection_resource_id = azurerm_storage_account.main.id
    is_manual_connection           = false
    subresource_names              = ["blob"]
  }

  private_dns_zone_group {
    name                 = "dns-group-blob"
    private_dns_zone_ids = [azurerm_private_dns_zone.storage_blob.id]
  }
}

resource "azurerm_private_endpoint" "keyvault" {
  name                = "pe-${var.prefix}-kv"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location
  subnet_id           = azurerm_subnet.private_endpoints.id

  private_service_connection {
    name                           = "psc-kv"
    private_connection_resource_id = azurerm_key_vault.main.id
    is_manual_connection           = false
    subresource_names              = ["vault"]
  }

  private_dns_zone_group {
    name                 = "dns-group-kv"
    private_dns_zone_ids = [azurerm_private_dns_zone.keyvault.id]
  }
}
terraform plan -out=tfplan
terraform apply tfplan

## Verify private endpoints

az network private-endpoint list --resource-group abhavtech-networking --output table

## Verify DNS records auto-created

az network private-dns record-set a list \
  --resource-group abhavtech-networking \
  --zone-name privatelink.database.windows.net \
  --output table
## Expected: abhavtech-sql -> 10.100.10.10 (auto-assigned)

Week 2: ExpressRoute Deployment

Day 5: Private DNS Conditional Forwarding (On-Premise)

Windows DNS Server

## Run on Domain Controller / Windows DNS Server

## Required: DNS server must reach Azure DNS (168.63.129.16) via ExpressRoute


Import-Module DnsServer

$azureDnsResolver = "168.63.129.16"  # Azure's internal DNS resolver
$zones = @(
  "privatelink.database.windows.net",
  "privatelink.blob.core.windows.net",
  "privatelink.file.core.windows.net",
  "privatelink.vaultcore.azure.net"
)

foreach ($zone in $zones) {
## Check if zone already exists

  $existing = Get-DnsServerConditionalForwarderZone -Name $zone -ErrorAction SilentlyContinue
  if ($existing) {
    Write-Host "Zone already exists: $zone - updating"
    Set-DnsServerConditionalForwarderZone -Name $zone -MasterServers $azureDnsResolver
  } else {
    Write-Host "Creating conditional forwarder: $zone"
    Add-DnsServerConditionalForwarderZone -Name $zone -MasterServers $azureDnsResolver
  }
}

## Verify resolution from on-premise

Write-Host ""
Write-Host "=== DNS Resolution Verification ==="
$testHosts = @(
  "abhavtech-sql.database.windows.net",
  "abhavtechstorage.blob.core.windows.net",
  "abhavtech-kv.vault.azure.net"
)

foreach ($host in $testHosts) {
  $result = Resolve-DnsName $host -ErrorAction SilentlyContinue
  if ($result) {
    $ip = ($result | Where-Object Type -eq 'A').IPAddress
    if ($ip -like "10.100.10.*") {
      Write-Host "✅ $host -> $ip (private endpoint)"
    } else {
      Write-Host "❌ $host -> $ip (expected 10.100.10.x)"
    }
  } else {
    Write-Host "❌ $host -> No response"
  }
}

Week 4: SD-WAN Integration and Testing