Deploy an Azure Bastion into an existing Hub-Spoke network with Terraform

Because security is very important, Microsoft developed a PaaS Service “Azure Bastion” to connect secure to your virtual machines over port 22 and port 3389. With this solution your virtual machines don’t need a public ip address anymore.

In this blogpost I’ll show you how to deploy an Azure Bastion into an already existing Hub-Spoke Virtual Network with Terraform.

Existing resources that you need before deploying this code

  • Hub virtual network if you have it
  • Log Analytics Workspace
  • The AzureBastionSubnet in the Hub Vnet

Hub-Spoke Virtual Network

The first resource that you need is your vnet. In this case I’m deploying the Bastion in my Hub vnet. The code can be found on my Github. Feel free to adjust it to fit your ip addressing.

AzureBastionSubnet

As you can see in my screenshot I already deployed the “AzureBastionSubnet” in my Hub vnet. Note that recently the subnetmask for this subnet changed to /26.

Log analytics workspace

When we deploy new resources we need to be able to catch the diagnostic settings for these resources. For this a central Log Analytics Workspace can be used. Because this is a part of our Landing Zone, I import this resource into this Terraform script.

data "azurerm_log_analytics_workspace" "hub-law" {
  name = "law-jvn-hub-01"
  resource_group_name = "rg-jvn-law-hub-01"  
}

Now it’s time to take a look at different components for the Bastion deployment.

  • Azure Bastion
  • Network Security Group for Azure Bastion
  • Public IP address
  • Diagnostic Settings

Azure Bastion

Let’s have a look at the code for the Bastion itself. The first thing we need is the Resource Group for the Bastion. As you can see I’m only using 1 variable “prefix”.

resource "azurerm_resource_group" "bastion-rg" {
  name     = "rg-${var.prefix}-hub-bastion"
  location = "West Europe"
  tags = {
    "solution" = "bastion"
    "costcenter" = "it"
    "location" = "weu"
    "environment" = "hub"
  }
}

Now we can add the code for the Bastion. Very Important to know is that Azure Bastion has 2 sku’s. There is the Basic sku and Standard sku. If you don’t specify the sku in your script it will deploy the basic sku Bastion. You can upgrade your Bastion sku if needed from Basic to Standard but not the other way.

resource "azurerm_bastion_host" "bastion" {
  name                = "bas-${var.prefix}-hub"
  location            = azurerm_resource_group.bastion-rg.location
  resource_group_name = azurerm_resource_group.bastion-rg.name
  tags = {
    "solution" = "bastion"
    "costcenter" = "it"
    "location" = "weu"
    "environment" = "hub"
    "critical" = "yes"
  }

  ip_configuration {
    name                 = "ip-${var.prefix}-hub-bas"
    subnet_id            = data.azurerm_subnet.AzureBastionSubnet.id
    public_ip_address_id = azurerm_public_ip.bastion-pip.id
  }
}

The next very important step is to configure the Network Security Group for the Bastion. We need several inbound and outbound rules.

resource "azurerm_network_security_group" "bastion-nsg" {
  name = "nsg-${var.prefix}-hub-bastion"
  resource_group_name = azurerm_resource_group.bastion-rg.name
  location = azurerm_resource_group.bastion-rg.location
  security_rule {
    name                       = "Allow_TCP_443_Internet"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = 443
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }
  security_rule {
    name                       = "Allow_TCP_443_GatewayManager"
    priority                   = 110
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = 443
    source_address_prefix      = "GatewayManager"
    destination_address_prefix = "*"
  }
  security_rule {
    name                       = "Allow_TCP_4443_GatewayManager"
    priority                   = 120
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = 4443
    source_address_prefix      = "GatewayManager"
    destination_address_prefix = "*"
  }
  security_rule {
    name                       = "Allow_TCP_443_AzureLoadBalancer"
    priority                   = 130
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = 443
    source_address_prefix      = "AzureLoadBalancer"
    destination_address_prefix = "*"
  }
  security_rule {
    name                       = "Deny_any_other_traffic"
    priority                   = 900
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
  security_rule {
    name                       = "Allow_TCP_3389_VirtualNetwork"
    priority                   = 100
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "3389"
    source_address_prefix      = "*"
    destination_address_prefix = "VirtualNetwork"
  }
  security_rule {
    name                       = "Allow_TCP_22_VirtualNetwork"
    priority                   = 110
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "VirtualNetwork"
  }
  security_rule {
    name                       = "Allow_TCP_443_AzureCloud"
    priority                   = 120
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "*"
    destination_address_prefix = "VirtualNetwork"
  }
}

The Bastion also requires a public ip address.

resource "azurerm_public_ip" "bastion-pip" {
  name                = "pip-${var.prefix}-hub-bas"
  location            = azurerm_resource_group.bastion-rg.location
  resource_group_name = azurerm_resource_group.bastion-rg.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

Because monitoring is very important, the last step is to enable the diagnostic settings on all resources for the Bastion (Bastion, NSG, PIP).

resource "azurerm_monitor_diagnostic_setting" "bastion-diag" {
  name               = "bastion-diagsettings"
  target_resource_id = azurerm_bastion_host.bastion.id
  log_analytics_workspace_id = data.azurerm_log_analytics_workspace.hub-law.id
  depends_on = [azurerm_bastion_host.bastion]
  

  log {
    category = "BastionAuditLogs"
    enabled  = true

    retention_policy {
      enabled = true
    }
  }

  metric {
    category = "AllMetrics"

    retention_policy {
      enabled = true
    }
  }
}
resource "azurerm_monitor_diagnostic_setting" "nsg-diag" {
  name = "bastion-nsg-diag"
  target_resource_id = azurerm_network_security_group.bastion-nsg.id
  log_analytics_workspace_id = data.azurerm_log_analytics_workspace.hub-law.id
  log {
    category = "NetworkSecurityGroupEvent"
    enabled  = true

    retention_policy {
      enabled = true
    }
  }
  log {
    category = "NetworkSecurityGroupRuleCounter"
    enabled = true

    retention_policy {
      enabled =true
    }
  }
}
resource "azurerm_monitor_diagnostic_setting" "bastion-pip-diag" {
  name = "bastion-pip-diag"
  target_resource_id = azurerm_public_ip.bastion-pip.id
  log_analytics_workspace_id = data.azurerm_log_analytics_workspace.hub-law.id
  log {
    category = "DDoSProtectionNotifications"
    enabled  = true

    retention_policy {
      enabled = true
    }
   
  }
  log {
    category = "DDoSMitigationFlowLogs"
    enabled = true

    retention_policy {
      enabled = true
    }
  }
  log {
    category = "DDoSMitigationReports"
    enabled =true
  }

  metric {
    category = "AllMetrics"

    retention_policy {
      enabled = true
    }
  }
}

Now we have all the code it’s time to deploy the Bastion into our Hub Vnet.

Network Security Groups with All Inbound and Outbound rules

Public IP Adress

There we go, Azure Bastion with all components deployed using Terraform. If you have any questions or remarks feel free to contact me.

Leave a Reply

Your email address will not be published. Required fields are marked *