Architecting Network Automation: A Comprehensive Guide to pfSense Configuration via Ansible

The intersection of professional network administration and Infrastructure as Code (IaC) has evolved to a point where the manual configuration of firewalls is increasingly viewed as a liability. pfSense, as a powerhouse open-source firewall based on FreeBSD, provides an immense array of features, from BGP routing via FRR to complex certificate management and VPN tunneling. However, the traditional reliance on the WebUI for these configurations introduces the risk of human error and "configuration drift," where the actual state of the device deviates from the intended design. Integrating Ansible into this ecosystem transforms the pfSense firewall from a manually managed appliance into a programmable asset. By leveraging specialized collections such as pfsensible.core, administrators can define their network topology, firewall rules, and system settings in YAML, ensuring that deployments are idempotent, repeatable, and version-controlled. This transition requires a deep understanding of the communication layers between the Ansible control node and the pfSense target, specifically regarding the use of the PHP shell, SSH authentication, and the necessity of privilege escalation tools like sudo on a BSD-based system.

Fundamental Prerequisites and System Preparation

Before the first playbook can be executed, the pfSense environment must be conditioned to accept remote configuration commands. Because pfSense is designed for security, it does not ship with the necessary tools for seamless Ansible integration enabled by default.

The installation of sudo is the most critical preliminary step. In standard pfSense installations, the become functionality of Ansible—which allows a non-privileged user to execute commands as root—will fail because the sudo utility is not present. This must be handled manually through the WebUI by navigating to System -> Package Manager -> Available Packages, searching for the sudo package, and completing the installation. Without this, any task requiring root-level access will trigger a catastrophic failure in the playbook execution.

Secure Shell (SSH) must be explicitly enabled to allow the Ansible control node to establish a connection. This is configured under System -> Advanced -> Secure Shell. To align with security best practices, it is highly recommended to configure the system to only allow public key authentication, thereby disabling password-based logins which are susceptible to brute-force attacks.

User management is another pillar of the initial setup. It is advisable to create a dedicated user for automation purposes, such as a user named ansible. This user must be added to the admins group to ensure it has the necessary permissions to modify system settings. To facilitate passwordless authentication, a public SSH key pair must be generated on the local Ansible control machine and the public key must be uploaded to the pfSense user account.

Technical Implementation of the Ansible Inventory

The inventory file serves as the source of truth for the target environment. For a pfSense deployment, the inventory must define not only the host address but also the specific Python interpreter and connection optimizations required for FreeBSD.

A typical inventory configuration for a pfSense host would look like this:

yaml all: hosts: pfsense: ansible_host: 192.168.X.X ansible_ssh_user: ansible ansible_python_interpreter: /usr/local/bin/python3.11 ansible_pipelining: true

The inclusion of the ansible_python_interpreter path /usr/local/bin/python3.11 is a technical requirement to suppress warning messages regarding auto-discovered Python versions. By explicitly defining the path, Ansible avoids the overhead of searching the filesystem for a compatible interpreter.

The ansible_pipelining directive is a critical performance optimization. In standard operations, Ansible transfers modules to the remote host and executes them individually. Pipelining reduces the number of SSH operations by executing modules in a stream, which significantly increases the speed of playbook execution on pfSense hardware, where the overhead of multiple SSH connections can be noticeable.

For those operating in environments where SSH keys are not yet deployed, the ansible_password variable can be used in the inventory or a host_vars file. However, for security reasons, these files must never be stored in source control. Alternatively, the -K flag can be used during command execution to prompt for the password manually:

ansible-playbook your-playbook.yml -K

Deep Dive into the pfsensible.core Collection

The primary mechanism for managing pfSense is the pfsensible.core collection, which is the successor to the original modules found in the opoplawski/ansible-pfsense repository. This collection provides a programmatic interface to the pfSense configuration.

The architectural design of these modules is centered around the PHP shell (pfSsh.php). Unlike some automation tools that might attempt to modify the config.xml file directly on the disk—which can be dangerous as it may not trigger the necessary system reloads—the pfsensible.core modules call the internal PHP shell of pfSense. This ensures that changes are processed through the official pfSense API, maintaining the integrity of the configuration and ensuring that settings are applied correctly.

The collection covers a wide array of base features, including:

  • Interface and VLAN management
  • DNS and NTP configuration
  • Timezone and SNMP settings
  • Firewall filter rules and Aliases
  • Authentication servers and Groups
  • Certificate management
  • Virtual IPs
  • High Availability (HA) synchronization

One notable limitation is that while base features are well-supported, some third-party packages do not have native modules within the collection. For example, while haproxy is supported, other packages like FRR (Free Range Routing) or Tailscale may require alternative approaches. Attempting to use the generic Ansible frr collection on pfSense can lead to a state of desynchronization, where the actual FRR configuration differs from the settings displayed in the pfSense WebUI.

Practical Playbook Development and Execution

To validate the connectivity and the environment setup, a minimal "ping" playbook should be executed. This confirms that the SSH agent is functioning and that the ansible user has the correct permissions.

```yaml

  • name: Test playbook
    hosts: pfsense
    gather_facts: False
    tasks:
    • name: ping

      ping:

      ```

This playbook can be executed using the command:

ansible-playbook test.yml

When moving beyond simple connectivity tests to actual system modifications, the become: yes directive becomes mandatory. Because pfSense requires root privileges for almost all configuration changes, the become keyword tells Ansible to escalate privileges via sudo. This can be applied at the play level or to specific tasks.

A concrete example of using the pfsensible.core.pfsense_vlan module to create a network segment is as follows:

```yaml

  • name: Configure vlan
    hosts: pfsense
    gather_facts: true
    become: yes
    tasks:
    • name: Add vlan 55

      pfsensible.core.pfsensevlan:

      interface: mvneta1

      vlan
      id: 55

      descr: for vlan 55

      state: present

      ```

In this scenario, the module ensures that VLAN 55 is present on the mvneta1 interface. If the VLAN already exists with these parameters, Ansible will report a "success" without making changes, maintaining idempotency.

Handling Initial Password Rotation and Security

In new deployments, specifically those using virtualization like vSphere, the initial setup often involves a default password (e.g., "pfsense"). Automating the change of this password is a critical security step.

The pfsensible.core.pfsense_user module can be used to rotate the admin password. However, it is important to note that pfSense requires passwords to be provided in a hashed format, specifically using bcrypt.

The following playbook demonstrates the rotation of the default admin password:

```yaml

  • name: 00. Set a new password
    hosts: all
    gatherfacts: False
    tasks:
    • name: "default password"

      setfact:

      ansible
      password: pfsense
    • name: Change admin password

      pfsensible.core.pfsense
    user:

    name: admin

    password: $2a$12$IYmI.P2rFGao6i8NXqMefO22LpX60Rf5fQT9dc64DOi6oObeX/Aqy

    ```

In this logic, the playbook first sets the ansible_password to the default "pfsense" to gain access. Once the pfsense_user module updates the password to the bcrypt-hashed string, any subsequent run of the playbook will fail at the authentication stage unless the ansible_password variable is updated to the new value. This creates a one-time transition from default to secure credentials.

Comparative Analysis of Automation Methodologies

There are two primary schools of thought regarding pfSense automation: the XML-based approach and the PHP shell-based approach.

Feature XML-to-YAML Method PHP Shell Method (pfsensible.core)
Mechanism Exports config.xml, modifies it, and re-uploads Calls pfSsh.php via SSH
Integrity Risk of corruption if XML is malformed High; uses official internal API
Speed Faster for bulk initial config Slower per-task, but more reliable
State Management Difficult to track individual changes Native Ansible idempotency
Requirement Often requires manual reboot/reload Changes applied via system calls

The PHP shell method, utilized by pfsensible.core, is generally superior for ongoing management because it interacts with the running system. The XML method is occasionally useful for "seeding" a brand new firewall from a template, but it lacks the granularity required for day-to-day operations.

Integration with Virtualization and Cloud-Init

For those deploying pfSense in virtualized environments, the process begins with mapping hardware addresses to portgroups. In vSphere, this involves identifying the MAC addresses of the virtual NICs to ensure the WAN and LAN interfaces are correctly assigned.

The workflow for a fully automated deployment typically follows this sequence:

  1. Deployment of the pfSense VM via Terraform or manual vSphere templates.
  2. Initial assignment of IP addresses to the LAN and WAN interfaces to provide internet connectivity.
  3. Manual or semi-automated installation of the sudo package via the WebUI.
  4. Enabling SSH and configuring the ansible user.
  5. Handoff to Ansible for all subsequent configuration of VLANs, firewall rules, and services.

While some users have expressed a desire for cloud-init support to eliminate the manual "wizard" steps after installation, current workflows still require a brief period of manual intervention in the WebUI to establish the initial connectivity and install the necessary prerequisites for Ansible to take over.

Technical Specifications and Module Capabilities

The current state of the pfsensible.core collection allows for the management of a vast array of network services. The following table outlines the capabilities and the specific modules associated with them.

Network Component Ansible Module Functional Capability
VLANs pfsensible.core.pfsense_vlan Create, modify, and delete 802.1Q tagged interfaces
Users pfsensible.core.pfsense_user Manage administrative and user accounts, including password hashing
Firewall Rules pfsensible.core.pfsense_filter_rule Define inbound and outbound traffic filtering rules
Aliases pfsensible.core.pfsense_alias Create groups of IPs or hostnames for use in rules
System Settings pfsensible.core.pfsense_system Configure DNS, NTP, and timezone
Virtual IPs pfsensible.core.pfsense_vip Manage CARP, Virtual IP, and Proxy ARP

Conclusion

The automation of pfSense via Ansible represents a significant leap in network operational maturity. By moving away from the manual manipulation of the WebUI and embracing a declarative approach, administrators can ensure that their firewall configurations are documented, testable, and easily recoverable. The requirement for sudo and the specific use of the PHP shell via pfsensible.core highlights the unique nature of FreeBSD-based networking devices, where the operating system's security model must be carefully navigated to allow for remote orchestration. While the initial setup requires a manual touch—specifically for the installation of sudo and the enabling of SSH—the long-term benefits of using pipelining and idempotent modules far outweigh the startup cost. The ability to define complex firewall zones, manage VLANs, and rotate administrative credentials through a single source of truth transforms the firewall from a static appliance into a dynamic component of a modern DevOps pipeline.

Sources

  1. Xoid.net - Configuring a pfSense router/firewall with Ansible
  2. GitHub - opoplawski/ansible-pfsense
  3. Netgate Forum - New Ansible module for pfSense that uses developer shell pfssh.php
  4. Virtual to the Core - Automatically deploy pfSense with Terraform and Ansible

Related Posts