My vSphere Lab has multiple networks, and even if I have NSX available, I usually prefer the simplicity of a small virtual appliance that acts as the firewall to securely connect all those networks, simulating a secure environment. I always used pfSense for this scope, as it is very powerful and yet very simple to use. I need to focus my lab time on things I need for my job, so the quickness of pfSense has always been an advantage.
From time to time I need to rebuild my lab, and even if I have some configuration backups to restore pfSense, I wanted to see if I could automate the deployment and configuration of my pfSense appliance.
I was able to automate 99% of the process. Here’s how.
VM creation
This operation is crazy fast: once the Terraform plan is created and ready, it takes less than 10 seconds to build the vm. Really? Yes!
I need to explain a bit how the Terraform creation works. Default behaviour of Terraform module for creating vSphere VMs is to wait for the virtual machine to have an IP address: in regular conditions this makes total sense as the goal is to create a “working” VM. But in my case the result will be an empty vm with just the pfsense ISO attached to it, powered on but just waiting for me to start the manual installation. For this reason, the original process would eventually time out:
So, how can we reduce the creation time from 5 minutes to 4 seconds? By simply adding these two lines in our Terraform plan:
wait_for_guest_net_timeout = 0 wait_for_guest_ip_timeout = 0
These parameters tell Terraform vSphere module to do not wait any second for network connectivity nor an IP address. When I re-run the plan with this two options, here is the result:
Another interesting lession I learned by creating this vm is how to create a VM with multiple networks, each connected to a different port group. I’m not posting the entire script, but I want to note the relevant parts. In the vSphere provider I ask Terraform to go and grab the id of these networks, as I need their MorefID:
data "vsphere_network" "mgmt_network" { name = "${var.vsphere_mgmt_network_id}" datacenter_id = "${data.vsphere_datacenter.dc.id}" } data "vsphere_network" "dmz_network" { name = "${var.vsphere_dmz_network_id}" datacenter_id = "${data.vsphere_datacenter.dc.id}" } data "vsphere_network" "public_network" { name = "${var.vsphere_public_network_id}" datacenter_id = "${data.vsphere_datacenter.dc.id}" } data "vsphere_network" "storage_network" { name = "${var.vsphere_storage_network_id}" datacenter_id = "${data.vsphere_datacenter.dc.id}" }
Then, I tell Terraform the name of the networks to look for:
variable "vsphere_mgmt_network_id" { default = "DPG-vcc-mgmt" } variable "vsphere_dmz_network_id" { default = "DPG-vcc-dmz" } variable "vsphere_public_network_id" { default = "DPG-test_net" } variable "vsphere_storage_network_id" { default = "DPG-vcc-storage" }
These are the four different networks that my new pfSense firewall will need to work on. Finally, I configure the VM to have four network cards, each connected to one of the four port groups:
network_interface { network_id = "${data.vsphere_network.mgmt_network.id}" adapter_type = "vmxnet3" } network_interface { network_id = "${data.vsphere_network.dmz_network.id}" adapter_type = "vmxnet3" } network_interface { network_id = "${data.vsphere_network.public_network.id}" adapter_type = "vmxnet3" } network_interface { network_id = "${data.vsphere_network.storage_network.id}" adapter_type = "vmxnet3" }
In just a few seconds, my VM is created and ready to be configured.
pfSense Installation
I couldn’t find any solution to do an unattended installation of pfSense, so after half day of research I decided I could accept 5 minutes of manual work, as this is really the time that it takes to manually configure the needed parameters. From here Ansible can then be used to fully configure the firewall automatically.
UPDATE 18.03.2024: huge thanks to Michael Zenzmaier, who saw my post and suggested me how to automatically install pfSense by leveraging the Set-VMKeystrokes function created by William Lam. Below information as still useful to figure out the interface assignments, but you can follow the new solution in this dedicated post, and then skip to the Ansible part.
The installation wizard in fact asks just a few things, I answer them and once rebooted the firewall asks for just three information.
The first is to map the network link to the interfaces. For this step, I’ve built a map to check the mac addresses in vSphere and see to which portgroups they belong:
I could probably create a powershell script that uses VMware PowerCLI and grabs these information for me, but it was just quicker to do it manually.
So, I use these information to complete the mapping and then I setup IP addresses only for LAN and WAN interfaces; this is needed to go on the Internet and grab an additional package, as you will see in the next section of this post:
Finally I enable SSH to allow Ansible to configure the machine. From here on, I can go back to Ansible for the configuration.
pfSense configuration with Ansible
To help me in the configuration I install pfsensible.core (https://github.com/pfsensible/core ), a series of modules to configure pfSense firewalls with Ansible. These modules must run as root in order to make changes to the system. By default pfSense does not have sudo capability so the become option in pfSense will not work.
For this reason, I need just one more manual activity in the system, this time in the graphical UI, to install the sudo package. I go into System -> Package Manager -> Available Packages, I search for sudo and I install it:
Now it’s time for Ansible to do its magic. I start by creating the usual inventory file for my playbook, and in my case it’s going to be really simple:
all: hosts: 172.27.217.254: ansible_user: root ansible_password: ******** ansible_python_interpreter: /usr/local/bin/python3.11
The stored password is the one I’m going to use to login into pfSense. But the default password for the admin user is “pfsense”, so I need to change it. I didn’t want to spend too much time to build a idempotent task to check if the password is already changed and in case do the change (maybe in a dedicated post), so I just created this quick playbook:
--- - name: 00. Set a new password hosts: all gather_facts: False # we use the default password. # if playbook succeeds, the password has been changed # if playbook fails, the password has already been changed tasks: - name: "default password" set_fact: ansible_password: pfsense - name: Change admin password pfsensible.core.pfsense_user: name: admin password: $2a$12$IYmI.P2rFGao6i8NXqMefO22LpX60Rf5fQT9dc64DOi6oObeX/Aqy # pasword is cypherred using bcrypt, find an online encryption tool and create the hased string you need
As I explain in the comments of the code, the new password has to be cyphered in bcrypt format. I run this playbook and it succeeds the first time. Following executions will fail since the password is not “pfsense” anymore. but it’s ok since I execute it at the beginning, while for the following playbook I will use the new password that I configured in the inventory.
So, we are now at the main part of the automation: I configure the entire firewall by setting all the parameters with a single playbook. There are three three different types of settings, you can check the whole list in the githu page of pfsensible.
Networks and hostname
This code configures the available networks, and the hostname:
- name: 01. Initial configuration hosts: all gather_facts: True tasks: - name: Configure DMZ interface pfsensible.core.pfsense_interface: descr: dmz interface: vmx2 enable: True ipv4_type: static ipv4_address: 172.27.218.254 ipv4_prefixlen: 24 - name: Configure STORAGE interface pfsensible.core.pfsense_interface: descr: storage interface: vmx0 enable: True ipv4_type: static ipv4_address: 172.27.219.254 ipv4_prefixlen: 24 - name: setup the machine pfsensible.core.pfsense_setup: hostname: fw domain: cloudconnect.local dns_hostnames: dc1.cloudconnect.local dns_addresses: 172.27.217.21 language: en_US session_timeout: 30 timeservers: 216.239.35.0 162.159.200.123 timezone: Europe/Rome
Aliases
Then, I create some aliases. Thanks to these, it’s easier to then create firewall rules by referring to them instead of the objects they refer to. Say for example I change the IP address of a server, I don’t need to edit all the rules where the address is used, I only change the alias. I’m not listing all the aliases I created, but it’s interesting to list one for each type I create. There are more types available, you can check them in the github page of PFSensible.
### 02. Create aliases for firewall rules - name: 02. Create aliases hosts: all gather_facts: False tasks: - name: Create alias for VCC server pfsensible.core.pfsense_alias: name: vcc type: host address: 172.27.217.80 - name: Create alias for VSPC server pfsensible.core.pfsense_alias: name: vspc type: host address: 172.27.217.78 - name: Create alias for VCC gateways internal addresses pfsensible.core.pfsense_alias: name: vcc_gtw_int type: host address: 172.27.218.35 172.27.218.36 172.27.218.37 - name: Create alias for MGMT network pfsensible.core.pfsense_alias: name: MGMT_net type: network address: 172.27.217.0/24 - name: Create alias for DMZ network pfsensible.core.pfsense_alias: name: DMZ_net type: network address: 172.27.218.0/24 - name: Create alias for WinRM port pfsensible.core.pfsense_alias: name: WinRM type: port address: 5985 5986
Rules
Finally, with the aliases I can create the firewall rules. Again, I’m leaving here a few examples to give you and idea about the process.
### 03. Create firewall rules - name: 03. Create firewall rules hosts: all gather_facts: False ### LAN network tasks: - name: Allow ICMP from LAN pfsensible.core.pfsense_rule: name: Allow ICMP from LAN interface: LAN action: pass source: MGMT_net destination: any icmptype: any protocol: icmp - name: Allow RDP from LAN to gateways pfsensible.core.pfsense_rule: name: Allow RDP from LAN to gateways interface: LAN action: pass source: MGMT_net destination: vcc_gtw_int destination_port: 3389 protocol: tcp
In the example above, I create a rule to allow the connection from the management network to the RDP port of the Veeam Cloud Gateways.