Oh boy, as Network Engineers we have been learning so many new things lately, it's like we're in a never-ending loop. First, we had to learn Python then, we had to master Ansible. And now, on top of that, we're being told we need to learn Terraform too.
Overview
Terraform is an open-source infrastructure as code (IaC) tool that enables you to create, manage and version your infrastructure declaratively. It allows you to define your infrastructure as code using a high-level configuration language and then automates deploying and updating that infrastructure across various cloud providers, such as AWS, Azure, and Google Cloud Platform.
In addition to cloud resources, Terraform can also be used to automate network devices such as firewalls, routers, and switches. Terraform can interact with network devices' APIs and automate their configuration and deployment in a repeatable and consistent manner. This makes managing network infrastructure at scale easier, reducing manual errors and streamlining operations.
In this blog post, we will explore how Terraform can be used to automate Palo Alto firewalls, providing a scalable and repeatable way to manage security policies and configurations.
Prerequisites
Before we dive into automating Palo Alto firewalls with Terraform, it's important to note that this blog post assumes that you have prior knowledge of Palo Alto firewalls. Additionally, make sure that you have installed Terraform on your machine, and that you are familiar with its basic usage. With that out of the way, let's dive in.
Get the API Key
Terraform interact with the Firewalls/Panorama using the API key. If you don't have the key yet, you can generate one using the following command. Replace the firewall IP, username and password.
suresh@mac:~|⇒ curl -k -X GET 'https://<FIREWALL_IP>/api/?type=keygen&user=<USERNAME>&password=<PASSWORD>'
<response status = 'success'><result><key>LUFRPT1ueFpaNXFCUGM2ckxXNGFIeVRscVkrfgty5ldDN1pQMUxMN0FDRCtIa0tUczBSbHhlTDZtZWFUb1F4d3FhSnpRTVVwS3VsYXlxblODJFS2xWczUwL3FlRQ==</key></result></response>
Take note of the API Key you receive, it usually looks like the following.
LUFRPT1ueFpaNXFCUGM2ckxXNGFIeVRscVkrfgty5ldDN1pQMUxMN0FDRCtIa0tUczBSbHhlTDZtZWFUb1F4d3FhSnpRTVVwS3VsYXbg67DJFS2xWczUwL3FlRQ==
Working Directory and Files
To set up the environment, we will start by creating a directory called panos_terraform
(can be any name).
Inside this directory, initially, we will create two files, provider.tf
and panos-creds.json
. The provider.tf
file is used to specify the provider and its configuration details.
We will also define the credentials required to authenticate with the firewall in the panos-creds.json
file. This file will contain the firewall's IP address and the API key, which Terraform will use to authenticate and interact with the firewall.
What are we configuring?
For this example, I'm going to configure two interfaces, two zones, some address objects and two security policies. Let's put some data into a variable file called variables.tf
Variable File
The variables.tf
file is used in Terraform to define input variables that can be used across multiple files within the same Terraform configuration. By defining variables in a separate file, you can easily modify and update them without changing your main configuration code.
This variables.tf
file defines two input variables - interfaces
and address_objects
.
The interfaces
variable is a map type variable that contains the configuration details for the firewall interfaces and zones. It has a default value which is a map containing two key-value pairs. Each key represents the name of the interface and the value is another map that defines the configuration details for that interface. In this case, the ethernet1/1
interface is configured with an IP address of 10.10.1.1/24
, assigned to the USERS
zone and labelled with a comment of "user traffic". The ethernet1/10
interface is configured with an IP address of 185.10.10.1/30
, assigned to the OUTSIDE
zone and labelled with a comment of "internet traffic".
The address_objects
variable is also a map type variable that contains the configuration details for the firewall address objects. It has a default value that is a map containing two key-value pairs. Each key represents the name of the address object, and the value is the IP address or subnet associated with that address object. In this case, there are two address objects defined - user_subnet
with an IP address of 192.168.10.0/24
and dns_server
with an IP address of 8.8.8.8/32
. These address objects will be used to simplify the configuration of security policies in the next step.
Configuration File
In Terraform, main.tf
is the main configuration file where you define the resources and their configurations that you want to create or manage. It is the entry point for your Terraform configuration, and it contains the core infrastructure as code logic. In this file, you specify the desired state of your infrastructure and the resources you want to provision, as well as any dependencies or variables that are required.
In short, main.tf
is where you define your desired infrastructure and how you want Terraform to manage it.
template
and device_group
parameters from the below file.Let's break down each resource block.
panos_device_group
- Creates a device group on the Palo Alto firewall with the nametest_lab_dg
and descriptionTest Lab
.panos_panorama_template
- Creates a Panorama template with the nametest_lab_tp
and descriptionTest Lab
.panos_panorama_ethernet_interface
- Creates a layer 3 ethernet interface for each key-value pair in theinterfaces
variable defined invariables.tf
. Thetemplate
parameter specifies the Panorama template to use, andfor_each
parameter is used to loop over each interface in theinterfaces
variable. Thestatic_ips
parameter sets the IP address for each interface, and thecomment
parameter sets the interface description.panos_zone
- Creates a zone for each unique zone value in theinterfaces
variable. Thetemplate
parameter specifies the Panorama template to use, and thefor_each
parameter is used to loop over each zone in theinterfaces
variable. Theinterfaces
parameter associates the interface names with each zone, based on thezone
value in theinterfaces
variable. This ensures that each interface is assigned to the correct zone.panos_address_object
- Creates address objects for each key-value pair in theaddress_objects
variable defined invariables.tf
. These address objects can be used to simplify the configuration of security policies.panos_security_policy
- creates two security policies to allow traffic from theuser_subnet
to the internet and allow DNS traffic to thedns_server
. Therule
parameter specifies the security policy rule and its parameters. Thedepends_on
parameter ensures that thepanos_address_object
resources are created before the security policies, as they are required to set the source and destination addresses in the policy rules.- The
lifecycle
parameter is used to set thecreate_before_destroy
parameter totrue
, ensuring that new security policies are created before the old ones are destroyed, and minimizing downtime during updates.
Resource Creation
There are four main Terraform commands that you need to know about that are terraform init
, terraform plan
, terraform apply
and terraform destroy
. These commands are the core building blocks of the Terraform workflow and are used to initialize, plan, apply and destroy infrastructure changes respectively.
terraform init
is used to set up the Terraform environment, including downloading provider plugins and setting up the backend configuration.
terraform plan
is used to preview the changes that will be made to the infrastructure based on the current Terraform configuration, while terraform apply
is used to apply those changes and create, modify or delete resources.
Finally, terraform destroy
is used to remove all the resources that were created by Terraform, ensuring that the infrastructure is returned to its original state. Together, these commands provide a powerful way to manage infrastructure as code, allowing you to automate your infrastructure deployments and make them more scalable, reliable, and maintainable.
For brevity, I will just show you the output of terraform apply
and terraform destroy
.
Terraform apply
Terraform apply says it is going to create 10 resources as shown below.
suresh@mac:~/Documents/panos_terraform|blog⚡ ⇒ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# panos_address_object.objects["dns_server"] will be created
+ resource "panos_address_object" "objects" {
+ device_group = "test_lab_dg"
+ id = (known after apply)
+ name = "dns_server"
+ type = "ip-netmask"
+ value = "8.8.8.8/32"
+ vsys = "vsys1"
}
# panos_address_object.objects["user_subnet"] will be created
+ resource "panos_address_object" "objects" {
+ device_group = "test_lab_dg"
+ id = (known after apply)
+ name = "user_subnet"
+ type = "ip-netmask"
+ value = "192.168.10.0/24"
+ vsys = "vsys1"
}
# panos_device_group.dg will be created
+ resource "panos_device_group" "dg" {
+ description = "Test Lab"
+ id = (known after apply)
+ name = "test_lab_dg"
}
# panos_panorama_ethernet_interface.ports["ethernet1/1"] will be created
+ resource "panos_panorama_ethernet_interface" "ports" {
+ comment = "user traffic"
+ id = (known after apply)
+ mode = "layer3"
+ name = "ethernet1/1"
+ static_ips = [
+ "10.10.1.1/24",
]
+ template = "test_lab_tp"
+ vsys = "vsys1"
}
# panos_panorama_ethernet_interface.ports["ethernet1/10"] will be created
+ resource "panos_panorama_ethernet_interface" "ports" {
+ comment = "internet traffic"
+ id = (known after apply)
+ mode = "layer3"
+ name = "ethernet1/10"
+ static_ips = [
+ "185.10.10.1/30",
]
+ template = "test_lab_tp"
+ vsys = "vsys1"
}
# panos_panorama_template.tp will be created
+ resource "panos_panorama_template" "tp" {
+ default_vsys = (known after apply)
+ description = "Test Lab"
+ id = (known after apply)
+ name = "test_lab_tp"
}
# panos_security_policy.rule1 will be created
+ resource "panos_security_policy" "rule1" {
+ device_group = "test_lab_dg"
+ id = (known after apply)
+ rulebase = "pre-rulebase"
+ vsys = "vsys1"
+ rule {
+ action = "allow"
+ applications = [
+ "ssl",
]
+ categories = [
+ "any",
]
+ destination_addresses = [
+ "any",
]
+ destination_zones = [
+ "OUTSIDE",
]
+ log_end = true
+ name = "users-to-internet"
+ services = [
+ "application-default",
]
+ source_addresses = [
+ "user_subnet",
]
+ source_users = [
+ "any",
]
+ source_zones = [
+ "USERS",
]
+ type = "universal"
+ uuid = (known after apply)
}
}
# panos_security_policy.rule2 will be created
+ resource "panos_security_policy" "rule2" {
+ device_group = "test_lab_dg"
+ id = (known after apply)
+ rulebase = "pre-rulebase"
+ vsys = "vsys1"
+ rule {
+ action = "allow"
+ applications = [
+ "dns",
]
+ categories = [
+ "any",
]
+ destination_addresses = [
+ "dns_server",
]
+ destination_zones = [
+ "OUTSIDE",
]
+ log_end = true
+ name = "Allow-DNS"
+ services = [
+ "application-default",
]
+ source_addresses = [
+ "user_subnet",
]
+ source_users = [
+ "any",
]
+ source_zones = [
+ "USERS",
]
+ type = "universal"
+ uuid = (known after apply)
}
}
# panos_zone.zones["ethernet1/1"] will be created
+ resource "panos_zone" "zones" {
+ id = (known after apply)
+ interfaces = [
+ "ethernet1/1",
]
+ mode = "layer3"
+ name = "USERS"
+ template = "test_lab_tp"
+ vsys = "vsys1"
}
# panos_zone.zones["ethernet1/10"] will be created
+ resource "panos_zone" "zones" {
+ id = (known after apply)
+ interfaces = [
+ "ethernet1/10",
]
+ mode = "layer3"
+ name = "OUTSIDE"
+ template = "test_lab_tp"
+ vsys = "vsys1"
}
Plan: 10 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
panos_panorama_template.tp: Creating...
panos_device_group.dg: Creating...
panos_device_group.dg: Creation complete after 1s [id=test_lab_dg]
panos_address_object.objects["user_subnet"]: Creating...
panos_address_object.objects["dns_server"]: Creating...
panos_panorama_template.tp: Creation complete after 1s [id=test_lab_tp]
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Creating...
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Creating...
panos_address_object.objects["user_subnet"]: Creation complete after 1s [id=test_lab_dg:user_subnet]
panos_address_object.objects["dns_server"]: Creation complete after 1s [id=test_lab_dg:dns_server]
panos_security_policy.rule1: Creating...
panos_security_policy.rule2: Creating...
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Creation complete after 2s [id=test_lab_tp::vsys1:ethernet1/1]
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Creation complete after 2s [id=test_lab_tp::vsys1:ethernet1/10]
panos_zone.zones["ethernet1/1"]: Creating...
panos_zone.zones["ethernet1/10"]: Creating...
panos_security_policy.rule1: Creation complete after 2s [id=test_lab_dg:pre-rulebase:vsys1]
panos_zone.zones["ethernet1/10"]: Creation complete after 1s [id=test_lab_tp::vsys1:OUTSIDE]
panos_zone.zones["ethernet1/1"]: Creation complete after 1s [id=test_lab_tp::vsys1:USERS]
panos_security_policy.rule2: Creation complete after 2s [id=test_lab_dg:pre-rulebase:vsys1]
Terraform destroy
In the context of the blog post, terraform destroy
would remove all the security policies, address objects, zones, interfaces, device groups, and templates that were created in the previous step. This command can be executed after the terraform apply
command to clean up the resources. This allows you to easily manage your infrastructure lifecycle and keep your environments consistent and clean.
suresh@mac:~/Documents/panos_terraform|blog⚡ ⇒ terraform destroy
panos_device_group.dg: Refreshing state... [id=test_lab_dg]
panos_panorama_template.tp: Refreshing state... [id=test_lab_tp]
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Refreshing state... [id=test_lab_tp::vsys1:ethernet1/10]
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Refreshing state... [id=test_lab_tp::vsys1:ethernet1/1]
panos_address_object.objects["dns_server"]: Refreshing state... [id=test_lab_dg:dns_server]
panos_address_object.objects["user_subnet"]: Refreshing state... [id=test_lab_dg:user_subnet]
panos_security_policy.rule2: Refreshing state... [id=test_lab_dg:pre-rulebase:vsys1]
panos_security_policy.rule1: Refreshing state... [id=test_lab_dg:pre-rulebase:vsys1]
panos_zone.zones["ethernet1/1"]: Refreshing state... [id=test_lab_tp::vsys1:USERS]
panos_zone.zones["ethernet1/10"]: Refreshing state... [id=test_lab_tp::vsys1:OUTSIDE]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# panos_address_object.objects["dns_server"] will be destroyed
- resource "panos_address_object" "objects" {
- device_group = "test_lab_dg" -> null
- id = "test_lab_dg:dns_server" -> null
- name = "dns_server" -> null
- tags = [] -> null
- type = "ip-netmask" -> null
- value = "8.8.8.8/32" -> null
- vsys = "vsys1" -> null
}
# panos_address_object.objects["user_subnet"] will be destroyed
- resource "panos_address_object" "objects" {
- device_group = "test_lab_dg" -> null
- id = "test_lab_dg:user_subnet" -> null
- name = "user_subnet" -> null
- tags = [] -> null
- type = "ip-netmask" -> null
- value = "192.168.10.0/24" -> null
- vsys = "vsys1" -> null
}
# panos_device_group.dg will be destroyed
- resource "panos_device_group" "dg" {
- description = "Test Lab" -> null
- id = "test_lab_dg" -> null
- name = "test_lab_dg" -> null
}
# panos_panorama_ethernet_interface.ports["ethernet1/1"] will be destroyed
- resource "panos_panorama_ethernet_interface" "ports" {
- adjust_tcp_mss = false -> null
- comment = "user traffic" -> null
- create_dhcp_default_route = false -> null
- decrypt_forward = false -> null
- dhcp_default_route_metric = 0 -> null
- dhcp_send_hostname_enable = false -> null
- enable_dhcp = false -> null
- id = "test_lab_tp::vsys1:ethernet1/1" -> null
- ipv4_mss_adjust = 0 -> null
- ipv6_enabled = false -> null
- ipv6_mss_adjust = 0 -> null
- lacp_ha_passive_pre_negotiation = false -> null
- lacp_port_priority = 0 -> null
- lldp_enabled = false -> null
- lldp_ha_passive_pre_negotiation = false -> null
- mode = "layer3" -> null
- mtu = 0 -> null
- name = "ethernet1/1" -> null
- rx_policing_rate = 0 -> null
- static_ips = [
- "10.10.1.1/24",
] -> null
- template = "test_lab_tp" -> null
- tx_policing_rate = 0 -> null
- vsys = "vsys1" -> null
}
# panos_panorama_ethernet_interface.ports["ethernet1/10"] will be destroyed
- resource "panos_panorama_ethernet_interface" "ports" {
- adjust_tcp_mss = false -> null
- comment = "internet traffic" -> null
- create_dhcp_default_route = false -> null
- decrypt_forward = false -> null
- dhcp_default_route_metric = 0 -> null
- dhcp_send_hostname_enable = false -> null
- enable_dhcp = false -> null
- id = "test_lab_tp::vsys1:ethernet1/10" -> null
- ipv4_mss_adjust = 0 -> null
- ipv6_enabled = false -> null
- ipv6_mss_adjust = 0 -> null
- lacp_ha_passive_pre_negotiation = false -> null
- lacp_port_priority = 0 -> null
- lldp_enabled = false -> null
- lldp_ha_passive_pre_negotiation = false -> null
- mode = "layer3" -> null
- mtu = 0 -> null
- name = "ethernet1/10" -> null
- rx_policing_rate = 0 -> null
- static_ips = [
- "185.10.10.1/30",
] -> null
- template = "test_lab_tp" -> null
- tx_policing_rate = 0 -> null
- vsys = "vsys1" -> null
}
# panos_panorama_template.tp will be destroyed
- resource "panos_panorama_template" "tp" {
- default_vsys = "vsys1" -> null
- description = "Test Lab" -> null
- id = "test_lab_tp" -> null
- name = "test_lab_tp" -> null
}
# panos_security_policy.rule1 will be destroyed
- resource "panos_security_policy" "rule1" {
- device_group = "test_lab_dg" -> null
- id = "test_lab_dg:pre-rulebase:vsys1" -> null
- rulebase = "pre-rulebase" -> null
- vsys = "vsys1" -> null
- rule {
- action = "allow" -> null
- applications = [
- "ssl",
] -> null
- categories = [
- "any",
] -> null
- destination_addresses = [
- "any",
] -> null
- destination_devices = [] -> null
- destination_zones = [
- "OUTSIDE",
] -> null
- disable_server_response_inspection = false -> null
- disabled = false -> null
- hip_profiles = [] -> null
- icmp_unreachable = false -> null
- log_end = true -> null
- log_start = false -> null
- name = "users-to-internet" -> null
- negate_destination = false -> null
- negate_source = false -> null
- negate_target = false -> null
- services = [
- "application-default",
] -> null
- source_addresses = [
- "user_subnet",
] -> null
- source_devices = [] -> null
- source_users = [
- "any",
] -> null
- source_zones = [
- "USERS",
] -> null
- tags = [] -> null
- type = "universal" -> null
- uuid = "21b4309f-59b8-4c48-a4d8-de71b4d18193" -> null
}
}
# panos_security_policy.rule2 will be destroyed
- resource "panos_security_policy" "rule2" {
- device_group = "test_lab_dg" -> null
- id = "test_lab_dg:pre-rulebase:vsys1" -> null
- rulebase = "pre-rulebase" -> null
- vsys = "vsys1" -> null
- rule {
- action = "allow" -> null
- applications = [
- "ssl",
] -> null
- categories = [
- "any",
] -> null
- destination_addresses = [
- "any",
] -> null
- destination_devices = [] -> null
- destination_zones = [
- "OUTSIDE",
] -> null
- disable_server_response_inspection = false -> null
- disabled = false -> null
- hip_profiles = [] -> null
- icmp_unreachable = false -> null
- log_end = true -> null
- log_start = false -> null
- name = "users-to-internet" -> null
- negate_destination = false -> null
- negate_source = false -> null
- negate_target = false -> null
- services = [
- "application-default",
] -> null
- source_addresses = [
- "user_subnet",
] -> null
- source_devices = [] -> null
- source_users = [
- "any",
] -> null
- source_zones = [
- "USERS",
] -> null
- tags = [] -> null
- type = "universal" -> null
- uuid = "21b4309f-59b8-4c48-a4d8-de71b4d18193" -> null
}
}
# panos_zone.zones["ethernet1/1"] will be destroyed
- resource "panos_zone" "zones" {
- enable_user_id = false -> null
- exclude_acls = [] -> null
- id = "test_lab_tp::vsys1:USERS" -> null
- include_acls = [] -> null
- interfaces = [
- "ethernet1/1",
] -> null
- mode = "layer3" -> null
- name = "USERS" -> null
- template = "test_lab_tp" -> null
- vsys = "vsys1" -> null
}
# panos_zone.zones["ethernet1/10"] will be destroyed
- resource "panos_zone" "zones" {
- enable_user_id = false -> null
- exclude_acls = [] -> null
- id = "test_lab_tp::vsys1:OUTSIDE" -> null
- include_acls = [] -> null
- interfaces = [
- "ethernet1/10",
] -> null
- mode = "layer3" -> null
- name = "OUTSIDE" -> null
- template = "test_lab_tp" -> null
- vsys = "vsys1" -> null
}
Plan: 0 to add, 0 to change, 10 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
panos_zone.zones["ethernet1/10"]: Destroying... [id=test_lab_tp::vsys1:OUTSIDE]
panos_zone.zones["ethernet1/1"]: Destroying... [id=test_lab_tp::vsys1:USERS]
panos_security_policy.rule2: Destroying... [id=test_lab_dg:pre-rulebase:vsys1]
panos_security_policy.rule1: Destroying... [id=test_lab_dg:pre-rulebase:vsys1]
panos_zone.zones["ethernet1/10"]: Destruction complete after 1s
panos_zone.zones["ethernet1/1"]: Destruction complete after 1s
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Destroying... [id=test_lab_tp::vsys1:ethernet1/10]
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Destroying... [id=test_lab_tp::vsys1:ethernet1/1]
panos_security_policy.rule1: Destruction complete after 1s
panos_security_policy.rule2: Destruction complete after 1s
panos_address_object.objects["user_subnet"]: Destroying... [id=test_lab_dg:user_subnet]
panos_address_object.objects["dns_server"]: Destroying... [id=test_lab_dg:dns_server]
panos_address_object.objects["dns_server"]: Destruction complete after 1s
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Destruction complete after 1s
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Destruction complete after 1s
panos_address_object.objects["user_subnet"]: Destruction complete after 1s
panos_panorama_template.tp: Destroying... [id=test_lab_tp]
panos_device_group.dg: Destroying... [id=test_lab_dg]
panos_panorama_template.tp: Destruction complete after 1s
panos_device_group.dg: Destruction complete after 1s
Destroy complete! Resources: 10 destroyed.
Closing up
In this blog post, we explored how Terraform can be used to configure Palo Alto firewalls, create security policies, address objects, zones, and interfaces. By defining the infrastructure as code, we can easily manage and update the network configuration in a repeatable and scalable way. With the terraform init
, terraform plan
, terraform apply
, and terraform destroy
commands, we can automate the entire infrastructure lifecycle, from initialization to clean-up.