💪Deploy Fortinet Fortigate NextGen Firewall using Infrastructure-As-Code with Bicep Language

Learn how to quickly deploy a Fortinet Fortigate NextGen Firewall virtual machine using Azure Bicep Language.

Deploy Fortinet Fortigate NextGen Firewall using Infrastructure-As-Code with Bicep Language

This article will demonstrate how to deploy FortiGate NGFW using Bicep Language, a domain-specific language (DSL) that uses a declarative syntax to deploy Azure resources.

The FortiGate-VM on Microsoft Azure delivers next-generation firewall (NGFW) capabilities for organizations of all sizes, with the flexibility to be deployed as NGFW and/or VPN gateway. It protects against cyber threats with high performance, security efficacy, and deep visibility.

Prerequisites

Let’s get started!

Solution Overview

We will author a Bicep template that creates a Fortinet Fortigate NextGen Firewall virtual machine.

The solution will include the following files:

  • 📄 main.bicep: This is the Bicep template
  • 📄 azuredeploy.parameters.json: This parameter file contains the values to use for deploying your Bicep template.

This template also provisions a virtual network, a public IP address, two network interfaces, and a routing table.

Fortinet Fortigate NextGen Firewall using Infrastructure-As-Code with Bicep Language

Before we deploy this solution we have to accept the legal terms of the VM image. To read and accept legal terms, you can use PowerShell to accept the terms and conditions with the cmdlet below:

Get-AzMarketplaceTerms `
-Publisher 'fortinet' `
-Product 'fortinet_fortigate-vm_v5' `
-Name 'fortinet_fg-vm' |
Set-AzMarketplaceTerms -Accept

1. Azure Bicep Template — parameters

Create a new file in your working directory and name it ‘main.bicep’. We will define the following parameters:

@description('Username for the Virtual Machine')
param adminUsername string
@description('Password for the Virtual Machine')
@secure()
param adminPassword string
@description('Name for FortiGate virtual appliances (A & B will be appended to the end of each respectively)')
param fortiGateNamePrefix string
@description('Identifies whether to to use PAYG (on demand licensing) or BYOL license model (where license is purchased separately)')
@allowed([
'fortinet_fg-vm'
'fortinet_fg-vm_payg_20190624'
])
param fortiGateImageSKU string = 'fortinet_fg-vm'
@description('Only 6.0.0 has the A/P HA feature currently')
@allowed([
'6.4.7'
'6.4.8'
'7.0.2'
'7.0.3'
'latest'
])
param fortiGateImageVersion string = 'latest'
@description('Virtual Machine size selection - must be F4 or other instance that supports 4 NICs')
@allowed([
'Standard_F2s'
'Standard_F4s'
'Standard_F8s'
'Standard_DS3_v2'
'Standard_DS4_v2'
'Standard_DS5_v2'
'Standard_D2s_v3'
'Standard_D4s_v3'
])
param instanceType string = 'Standard_F2s'
@description('Accelerated Networking enables direct connection between the VM and network card. Only available on 2 CPU F/Fs and 4 CPU D/Dsv2, D/Dsv3, E/Esv3, Fsv2, Lsv2, Ms/Mms and Ms/Mmsv2')
@allowed([
'false'
'true'
])
param acceleratedNetworking string = 'false'
@description('Choose between an existing or new public IP for the External Azure Load Balancer')
@allowed([
'new'
'existing'
'none'
])
param publicIPNewOrExisting string = 'new'
@description('Name of Public IP address element')
param publicIPName string = ''
@description('Resource group to which the Public IP belongs')
param publicIPResourceGroup string = ''
@description('Type of public IP address')
@allowed([
'Dynamic'
'Static'
])
param publicIPAddressType string = 'Static'
@description('Identify whether to use a new or existing vnet')
@allowed([
'new'
'existing'
])
param vnetNewOrExisting string = 'new'
@description('Name of the Azure virtual network')
param vnetName string = ''
@description('Resource Group containing the existing virtual network (with new vnet the current resourcegroup is used)')
param vnetResourceGroup string = ''
@description('Virtual Network Address prefix')
param vnetAddressPrefix string = '172.16.136.0/22'
@description('External Subnet')
param subnet1Name string = 'ExternalSubnet'
@description('External Subnet Prefix')
param subnet1Prefix string = '172.16.136.0/26'
@description('Subnet 1 start address, 1 consecutive private IPs are required')
param subnet1StartAddress string = '172.16.136.4'
@description('Internal Subnet')
param subnet2Name string = 'TransitSubnet'
@description('Internal Subnet Prefix')
param subnet2Prefix string = '172.16.136.64/26'
@description('Subnet 2 start address, 2 consecutive private IPs are required')
param subnet2StartAddress string = '172.16.136.68'
@description('Protected A Subnet 5 Name')
param subnet3Name string = 'ProtectedASubnet'
@description('Protected A Subnet 3 Prefix')
param subnet3Prefix string = '172.16.137.0/24'
@description('Connect to FortiManager')
@allowed([
'yes'
'no'
])
param fortiManager string = 'no'
@description('FortiManager IP or DNS name to connect to on port TCP/541')
param fortiManagerIP string = ''
@description('FortiManager serial number to add the deployed FortiGate into the FortiManager')
param fortiManagerSerial string = ''
@description('FortiGate BYOL license content')
param fortiGateLicenseBYOL string = ''
@description('FortiGate BYOL Flex-VM license token')
param fortiGateLicenseFlexVM string = ''
@description('Location for all resources.')
param location string = resourceGroup().location
param fortinetTags object = {
publisher: 'Fortinet'
provider: '6EB3B02F-50E5-4A3E-8CB8-2E12925831VM'
}

2. Azure Bicep Template — variables

You can modify the variables depending on your environment needs. We will define the following variables:

var imagePublisher = 'fortinet'
var imageOffer = 'fortinet_fortigate-vm_v5'
var vnetName_var = ((vnetName == '') ? '${fortiGateNamePrefix}-VNET' : vnetName)
var subnet1Id = ((vnetNewOrExisting == 'new') ? resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName_var, subnet1Name) : resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', vnetName_var, subnet1Name))
var subnet2Id = ((vnetNewOrExisting == 'new') ? resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName_var, subnet2Name) : resourceId(vnetResourceGroup, 'Microsoft.Network/virtualNetworks/subnets', vnetName_var, subnet2Name))
var fgaVmName_var = '${fortiGateNamePrefix}-FGT-A'
var fmgCustomData = ((fortiManager == 'yes') ? '\nconfig system central-management\nset type fortimanager\n set fmg ${fortiManagerIP}\nset serial-number ${fortiManagerSerial}\nend\n config system interface\n edit port1\n append allowaccess fgfm\n end\n config system interface\n edit port2\n append allowaccess fgfm\n end\n' : '')
var customDataFlexVM = ((fortiGateLicenseFlexVM == '') ? '' : 'exec vm-license ${fortiGateLicenseFlexVM}\n')
var customDataHeader = 'Content-Type: multipart/mixed; boundary="12345"\nMIME-Version: 1.0\n--12345\nContent-Type: text/plain; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="config"\n\n'
var customDataBody = 'config system sdn-connector\nedit AzureSDN\nset type azure\nnext\nend\nconfig router static\nedit 1\nset gateway ${sn1GatewayIP}\nset device port1\nnext\nedit 2\nset dst ${vnetAddressPrefix}\nset gateway ${sn2GatewayIP}\nset device port2\nnext\nend\nconfig system interface\nedit port1\nset mode static\nset ip ${sn1IPfga}/${sn1CIDRmask}\nset description external\nset allowaccess ping ssh https\nnext\nedit port2\nset mode static\nset ip ${sn2IPfga}/${sn2CIDRmask}\nset description internal\nset allowaccess ping ssh https\nnext\nend\n${fmgCustomData}\n${customDataFlexVM}\n'
var customDataLicenseHeader = '--12345\nContent-Type: text/plain; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="fgtlicense"\n\n'
var customDataFooter = '--12345--\n'
var customDataCombined = concat(customDataHeader, customDataBody, customDataLicenseHeader, fortiGateLicenseBYOL, customDataFooter)
var fgaCustomData = base64(((fortiGateLicenseBYOL == '') ? customDataBody : customDataCombined))
var routeTableProtectedName_var = '${fortiGateNamePrefix}-RT-PROTECTED'
var routeTableProtectedId = routeTableProtectedName.id
var fgaNic1Name_var = '${fgaVmName_var}-Nic1'
var fgaNic1Id = fgaNic1Name.id
var fgaNic2Name_var = '${fgaVmName_var}-Nic2'
var fgaNic2Id = fgaNic2Name.id
var publicIPName_var = ((publicIPName == '') ? '${fortiGateNamePrefix}-FGT-PIP' : publicIPName)
var publicIPId = ((publicIPNewOrExisting == 'new') ? publicIPName_resource.id : resourceId(publicIPResourceGroup, 'Microsoft.Network/publicIPAddresses', publicIPName_var))
var publicIPAddressId = {
id: publicIPId
}
var NSGName_var = '${fortiGateNamePrefix}-${uniqueString(resourceGroup().id)}-NSG'
var NSGId = NSGName.id
var sn1IPArray = split(subnet1Prefix, '.')
var sn1IPArray2ndString = string(sn1IPArray[3])
var sn1IPArray2nd = split(sn1IPArray2ndString, '/')
var sn1CIDRmask = string(int(sn1IPArray2nd[1]))
var sn1IPArray3 = string((int(sn1IPArray2nd[0]) + 1))
var sn1IPArray2 = string(int(sn1IPArray[2]))
var sn1IPArray1 = string(int(sn1IPArray[1]))
var sn1IPArray0 = string(int(sn1IPArray[0]))
var sn1GatewayIP = '${sn1IPArray0}.${sn1IPArray1}.${sn1IPArray2}.${sn1IPArray3}'
var sn1IPStartAddress = split(subnet1StartAddress, '.')
var sn1IPfga = '${sn1IPArray0}.${sn1IPArray1}.${sn1IPArray2}.${int(sn1IPStartAddress[3])}'
var sn2IPArray = split(subnet2Prefix, '.')
var sn2IPArray2ndString = string(sn2IPArray[3])
var sn2IPArray2nd = split(sn2IPArray2ndString, '/')
var sn2CIDRmask = string(int(sn2IPArray2nd[1]))
var sn2IPArray3 = string((int(sn2IPArray2nd[0]) + 1))
var sn2IPArray2 = string(int(sn2IPArray[2]))
var sn2IPArray1 = string(int(sn2IPArray[1]))
var sn2IPArray0 = string(int(sn2IPArray[0]))
var sn2GatewayIP = '${sn2IPArray0}.${sn2IPArray1}.${sn2IPArray2}.${sn2IPArray3}'
var sn2IPStartAddress = split(subnet2StartAddress, '.')
var sn2IPfga = '${sn2IPArray0}.${sn2IPArray1}.${sn2IPArray2}.${int(sn2IPStartAddress[3])}'

3. Azure Bicep Template — resources

We will define the following resources:

resource routeTableProtectedName 'Microsoft.Network/routeTables@2020-04-01' = {
name: routeTableProtectedName_var
tags: {
provider: toUpper(fortinetTags.provider)
}
location: location
properties: {
routes: [
{
name: 'VirtualNetwork'
properties: {
addressPrefix: vnetAddressPrefix
nextHopType: 'VirtualAppliance'
nextHopIpAddress: sn2IPfga
}
}
{
name: 'Subnet'
properties: {
addressPrefix: subnet3Prefix
nextHopType: 'VnetLocal'
}
}
{
name: 'Default'
properties: {
addressPrefix: '0.0.0.0/0'
nextHopType: 'VirtualAppliance'
nextHopIpAddress: sn2IPfga
}
}
]
}
}
resource vnetName_resource 'Microsoft.Network/virtualNetworks@2020-04-01' = if (vnetNewOrExisting == 'new') {
name: vnetName_var
location: location
properties: {
addressSpace: {
addressPrefixes: [
vnetAddressPrefix
]
}
subnets: [
{
name: subnet1Name
properties: {
addressPrefix: subnet1Prefix
}
}
{
name: subnet2Name
properties: {
addressPrefix: subnet2Prefix
}
}
{
name: subnet3Name
properties: {
addressPrefix: subnet3Prefix
routeTable: {
id: routeTableProtectedId
}
}
}
]
}
dependsOn: [
routeTableProtectedName
]
}
resource NSGName 'Microsoft.Network/networkSecurityGroups@2020-04-01' = {
tags: {
provider: toUpper(fortinetTags.provider)
}
name: NSGName_var
location: location
properties: {
securityRules: [
{
name: 'AllowAllInbound'
properties: {
description: 'Allow all in'
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: 'Allow'
priority: 100
direction: 'Inbound'
}
}
{
name: 'AllowAllOutbound'
properties: {
description: 'Allow all out'
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
access: 'Allow'
priority: 105
direction: 'Outbound'
}
}
]
}
}
resource publicIPName_resource 'Microsoft.Network/publicIPAddresses@2020-04-01' = if (publicIPNewOrExisting == 'new') {
tags: {
provider: toUpper(fortinetTags.provider)
}
name: publicIPName_var
location: location
sku: {
name: 'Basic'
}
properties: {
publicIPAllocationMethod: publicIPAddressType
}
}
resource fgaNic1Name 'Microsoft.Network/networkInterfaces@2020-04-01' = {
tags: {
provider: toUpper(fortinetTags.provider)
}
name: fgaNic1Name_var
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAddress: sn1IPfga
privateIPAllocationMethod: 'Static'
publicIPAddress: ((publicIPNewOrExisting != 'none') ? publicIPAddressId : json('null'))
subnet: {
id: subnet1Id
}
}
}
]
enableIPForwarding: true
enableAcceleratedNetworking: false
networkSecurityGroup: {
id: NSGId
}
}
dependsOn: [
vnetName_resource
NSGName
]
}
resource fgaNic2Name 'Microsoft.Network/networkInterfaces@2020-04-01' = {
tags: {
provider: toUpper(fortinetTags.provider)
}
name: fgaNic2Name_var
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Static'
privateIPAddress: sn2IPfga
subnet: {
id: subnet2Id
}
}
}
]
enableIPForwarding: true
enableAcceleratedNetworking: acceleratedNetworking
networkSecurityGroup: {
id: NSGId
}
}
dependsOn: [
vnetName_resource
]
}
resource fgaVmName 'Microsoft.Compute/virtualMachines@2019-07-01' = {
name: fgaVmName_var
tags: {
provider: toUpper(fortinetTags.provider)
}
identity: {
type: 'SystemAssigned'
}
location: location
plan: {
name: fortiGateImageSKU
publisher: imagePublisher
product: imageOffer
}
properties: {
hardwareProfile: {
vmSize: instanceType
}
osProfile: {
computerName: fgaVmName_var
adminUsername: adminUsername
adminPassword: adminPassword
customData: fgaCustomData
}
storageProfile: {
imageReference: {
publisher: imagePublisher
offer: imageOffer
sku: fortiGateImageSKU
version: fortiGateImageVersion
}
osDisk: {
createOption: 'FromImage'
}
dataDisks: [
{
diskSizeGB: 30
lun: 0
createOption: 'Empty'
}
]
}
networkProfile: {
networkInterfaces: [
{
properties: {
primary: true
}
id: fgaNic1Id
}
{
properties: {
primary: false
}
id: fgaNic2Id
}
]
}
}
dependsOn: [
fgaNic1Name
fgaNic2Name
]
}

4. Parameters file

Create a new file named ‘azuredeploy.parameters.json’. The code below shows the definition of the parameters file:

{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"fortiGateNamePrefix": {
"value": "YOUR-FORTIGATE-NAME-PREFIX"
},
"fortiGateImageSKU": {
"value": "fortinet_fg-vm"
},
"fortiGateImageVersion": {
"value": "latest"
},
"adminUsername": {
"value": "YOUR-ADMIN-USERNAME"
},
"adminPassword": {
"value": "YOUR-PASSWORD"
},
"location": {
"value": "eastus"
},
"instanceType": {
"value": "Standard_F2s"
},
"acceleratedNetworking": {
"value": "true"
},
"publicIPNewOrExisting": {
"value": "new"
},
"publicIPName": {
"value": "FGTPublicIP"
},
"publicIPResourceGroup": {
"value": "YOUR-PUBLICIP-RESOURCE-GROUP"
},
"publicIPAddressType": {
"value": "Dynamic"
},
"vnetNewOrExisting": {
"value": "new"
},
"vnetName": {
"value": "FortiGate-VNET"
},
"vnetResourceGroup": {
"value": "YOUR-VNET-RESOURCE-GROUP"
},
"vnetAddressPrefix": {
"value": "10.0.0.0/22"
},
"subnet1Name": {
"value": "ExternalSubnet"
},
"subnet1Prefix": {
"value": "10.0.0.0/26"
},
"subnet1StartAddress": {
"value": "10.0.0.4"
},
"subnet2Name": {
"value": "InternalSubnet"
},
"subnet2Prefix": {
"value": "10.0.1.0/26"
},
"subnet2StartAddress": {
"value": "10.0.1.4"
},
"subnet3Name": {
"value": "ProtectedSubnet"
},
"subnet3Prefix": {
"value": "10.0.2.0/24"
},
"fortiManager": {
"value": "no"
},
"fortiManagerIP": {
"value": ""
},
"fortiManagerSerial": {
"value": ""
},
"fortiGateLicenseBYOL": {
"value": ""
},
"fortiGateLicenseFlexVM": {
"value": ""
}
}
}

5. Azure Bicep Template — Deployment

We will use the command below to deploy our Bicep template:

$date = Get-Date -Format "MM-dd-yyyy"
$deploymentName = "AzInsiderDeployment"+"$date"
New-AzResourceGroupDeployment -Name $deploymentName -ResourceGroupName azinsider_demo -TemplateFile .\main.bicep -TemplateParameterFile .\azuredeploy.parameters.json -c

The image below shows the preview of the deployment:

Fortinet Fortigate NextGen Firewall — Deployment preview

Then we will execute the deployment. The image below shows the deployment output:

Fortinet Fortigate NextGen Firewall — Deployment preview

Once the deployment is complete, you can access the Fortinet Fortigate NextGen Firewall using the public IP address and port 443:

Fortinet Fortigate NextGen Firewall

You can find the code of this solution in the following URL, feel free to contribute!

👉 Join the AzInsider email list here.

-Dave R.

--

--