Now Available: Ansible Collection for Sophos Firewall

Now Available: Ansible Collection for Sophos Firewall

Today we are announcing the availability of the Ansible Collection for Sophos Firewall. The sophosfirewall-ansible collection provides a set of Ansible modules that can automate tasks using the embedded XML API of Sophos Firewall. Security administrators can now write Ansible playbooks to define various configuration settings on the firewall, eliminating the need to write code to automate against the firewall. Instead, the more human-readable YAML language is used to describe the configuration, making it easier for administrators to begin building automation.

This initial release of collection includes the below modules:

  • sfos_admin_settings module – Manage Admin and user settings (System > Administration)
  • sfos_atp module – Manage Active Threat Protection (Protect > Active threat response > Sophos X-Ops threat feeds)
  • sfos_backup module – Manage Backup settings (System > Backup & firmware)
  • sfos_device_access_profile module – Manage Device Access Profiles (System > Profiles > Device Access)
  • sfos_dns module – Manage DNS settings (Configure > Network > DNS)
  • sfos_firewall_rule module – Manage Firewall Rules (Protect > Rules & policies)
  • sfos_fqdn_host module – Manage FQDN hosts (System > Hosts & services > FQDN host)
  • sfos_fqdn_hostgroup module – Manage FQDN Host Groups (System > Hosts & services > FQDN host group)
  • sfos_ip_host module – Manage IP Host (System > Hosts & services > IP host)
  • sfos_ip_hostgroup module – Manage IP Hostgroup (System > Hosts & services > IP host group)
  • sfos_ips module – Manage IPS protection (Protect > Intrusion Protection > IPS policies)
  • sfos_malware_protection module – Manage Malware Protection (Configure > System services > Malware protection)
  • sfos_service module – Manage Service (System > Hosts and services > Services)
  • sfos_service_acl_exception module – Manage Local Service Exception ACL Rules (System > Administration > Device Access)
  • sfos_servicegroup module – Manage Service Group (System > Hosts and services > Service Group)
  • sfos_snmp_agent module – Manage SNMP Agent (System > Administration > SNMP)
  • sfos_snmp_user module – Manage SNMPv3 User (System > Administration > SNMP)
  • sfos_syslog module – Manage Syslog servers (Configure > System services > Log settings)
  • sfos_time module – Manage Date and Time settings (System > Administration > Time)
  • sfos_user module – Manage Users (Configure > Authentication > Users)
  • sfos_xmlapi module – Use the XML API to get, create, update, or delete settings on Sophos Firewall.
  • sfos_zone module – Manage Zones (Configure > Network > Zones)

For detailed usage instructions and examples please see the module documentation.

To get started working with the Ansible collection, a system must first be installed with Python and the open source Ansible CLI tool. The Ansible CLI can be installed with the command pip install ansible. Once Ansible CLI is installed, the Ansible collection can be installed with the ansible-galaxy CLI command, which is included with Ansible. It is also necessary to install the Sophos Firewall Python SDK. Use the below commands to install both the SDK and the Ansible Collection:

$ pip install sophosfirewall-python
$ ansible-galaxy collection install sophos.sophos_firewall

In order for the host running Ansible to be able to access the API of the firewall, the Ansible host IP address needs to be added to the list of IP addresses that are permitted to use the API. This is accomplished by accessing the firewall dashboard and navigating to System > Backup & firmware, and on the API tab add the Ansible host IP address to the Allowed IP address field. The API configuration field must also be set to Enabled.

A basic Ansible setup requires two files; an inventory and a playbook. The inventory file includes the IP address or hostname of the firewall(s) to be configured. Below is an example inventory file with a single firewall:

testfirewalls:
  hosts:
    testfirewall1.sophos.net:
      ansible_connection: local

In this example, we have an inventory group called testfirewalls. Within the group, there is a single host testfirewall1. The ansible_connection: local command is necessary to prevent the modules from trying to connect to the firewalls with SSH, which is the default behavior for Ansible. The modules will use HTTPS to connect to the firewalls on the specified TCP port. The inventory file can have any name, and standard practice is to use a .yml extension. In this case, we'll use inventory.yml.

More details can be found on building Ansible inventory files in the Ansible documentation.

An Ansible playbook defines a set of tasks to be executed against the devices in the inventory file. Below is an example playbook that uses the sfos_ip_host module to create an IP Host.

---
- name: MANAGE IP HOST ON SOPHOS FIREWALL
  hosts: testfirewalls
  gather_facts: false

  tasks:
    - name: MANAGE IP HOST
      sophos.sophos_firewall.sfos_ip_host:
        username: "{{ username }}"
        password: "{{ password }}"
        hostname: "{{ inventory_hostname }}"
        port: 4444
        verify: false
        name: testhost1
        ip_address: 1.1.1.1
        state: present
      delegate_to: localhost

A module is referenced within the task using the format namespace.collection.module_name. In this case, sophos.sophos_firewall.sfos_ip_host is the module being used. Each module contains a set of common configuration arguments:

  • username: Firewall username
  • password: Firewall password
  • hostname: Firewall hostname or IP address
  • port: TCP port for the admin interface of the firewall (usually 4444)
  • verify: Indicates whether certificate checking should be performed

Each module also has a set of arguments that are unique to the feature being configured. In this case, the sfos_ip_host module takes the name and ip_address arguments.

Finally, the state argument indicates to Ansible what action should be taken:

  • present: Create the configuration if not already present
  • updated: Update the configuration with the specified parameters
  • absent: Delete the configuration
  • query: Retrieve the existing configuration

You can find all the arguments supported by each module in the documentation

The values surrounded by double brackets {{ val }} in the task are references to variables. The inventory_hostname variable is a special variable in Ansible that refers to the current hostname that is being operated on by the playbook. For this example, we'll pull in the username and password variables from the command line.

In the Getting Started tutorial, we discuss a more secure method of providing the credentials.

As with the inventory file, you may choose any name for the playbook file, and it is common practice to use a .yml extension. For the example, we'll name the file playbook.yml.

Each playbook can contain multiple tasks that will be executed against the firewall(s) in the inventory. To execute the playbook, the ansible-playbook CLI command is used:

ansible-playbook -i inventory.yml playbook.yml -e '{"username: "apiuser", "password": "supers3cr3tp@ssw0rd"}' -v

In this command, the -i flag specifies the inventory file. The -e is for extra variables. For this example playbook, we are passing the username and password as extra variables to be used in the task. The -v increases the verbosity of the task, which allows us to see the API responses returned from the firewall displayed in the terminal session.

This is an insecure method of passing in the credentials, a production deployment should pull in the credentials from a secrets manager

$ ansible-playbook playbook.yml -i inventory.yml -e '{"username": "apiuser", "password": "supers3cr3tp@ssw0rd"}' -v
No config file found; using defaults

PLAY [MANAGE IP HOST ON SOPHOS FIREWALL] *********************************************************************************************

TASK [MANAGE IP HOST] ****************************************************************************************************************
changed: [testfirewall1.sophos.net -> localhost] => {"api_response": {"Response": {"@APIVersion": "2000.2", "@IPS_CAT_VER": "1", "@IS_WIFI6": "0", "IPHost": {"@transactionid": "", "Status": {"#text": "Configuration applied successfully.", "@code": "200"}}, "Login": {"status": "Authentication Successful"}}}, "changed": true, "check_mode": false}

PLAY RECAP ***************************************************************************************************************************
testfirewall1.sophos.net              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The status of changed and the status message of "Configuration applied successfully" is a good indication that the IP host was created on the firewall. The existence of the IP host can be checked by changing the task state argument to query, and then running the playbook again.

    - name: MANAGE IP HOST
      sophos.sophos_firewall.sfos_ip_host:
        username: "{{ username }}"
        password: "{{ password }}"
        hostname: "{{ inventory_hostname }}"
        port: 4444
        verify: false
        name: testhost1
        state: query
      delegate_to: localhost
$ ansible-playbook playbook.yml -i inventory.yml -e '{"username": "apiuser", "password": "supers3cr3tp@ssw0rd"}' -v
No config file found; using defaults

PLAY [MANAGE IP HOST ON SOPHOS FIREWALL] *********************************************************************************************

TASK [MANAGE IP HOST] ****************************************************************************************************************
ok: [testfirewall1.sophos.net -> localhost] => {"api_response": {"Response": {"@APIVersion": "2000.2", "@IPS_CAT_VER": "1", "@IS_WIFI6": "0", "IPHost": {"@transactionid": "", "Description": null, "HostType": "IP", "IPAddress": "1.1.1.1", "IPFamily": "IPv4", "Name": "testhost1"}, "Login": {"status": "Authentication Successful"}}}, "changed": false, "check_mode": false}

PLAY RECAP ***************************************************************************************************************************
testfirewall1.sophos.net              : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

In the MANAGE IP HOST task above, we can see the API representation of the IP Host object in the output which indicates it is present in the firewall configuration.

This is a simple example of displaying the information in the terminal, but we could also register the output of the query as a variable and use it in a later task.

To update the IP Host on the firewall, we would change an argument value and then specify state: updated. Below the ip_address argument is used to change the IP from 1.1.1.1 to 2.2.2.2.

    - name: MANAGE IP HOST
      sophos.sophos_firewall.sfos_ip_host:
        username: "{{ username }}"
        password: "{{ password }}"
        hostname: "{{ inventory_hostname }}"
        port: 4444
        verify: false
        name: testhost1
        ip_address: 2.2.2.2
        state: updated
      delegate_to: localhost
ansible-playbook playbook.yml -i inventory.yml -e '{"username": "apiuser", "password": "supers3cr3tp@ssw0rd"}' -v
No config file found; using defaults

PLAY [MANAGE IP HOST ON SOPHOS FIREWALL] *********************************************************************************************

TASK [MANAGE IP HOST] ****************************************************************************************************************
changed: [testfirewall1.sophos.net -> localhost] => {"api_response": {"Response": {"@APIVersion": "2000.2", "@IPS_CAT_VER": "1", "@IS_WIFI6": "0", "IPHost": {"@transactionid": "", "Status": {"#text": "Configuration applied successfully.", "@code": "200"}}, "Login": {"status": "Authentication Successful"}}}, "changed": true, "check_mode": false}

PLAY RECAP ***************************************************************************************************************************
testfirewall1.sophos.net              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Finally, to delete the IP Host we would change the state to absent and run the playbook.

    - name: MANAGE IP HOST
      sophos.sophos_firewall.sfos_ip_host:
        username: "{{ username }}"
        password: "{{ password }}"
        hostname: "{{ inventory_hostname }}"
        port: 4444
        verify: false
        name: testhost1
        ip_address: 2.2.2.2
        state: absent
      delegate_to: localhost
ansible-playbook playbook.yml -i inventory.yml -e '{"username": "apiuser", "password": "supers3cr3tp@ssw0rd"}' -v
No config file found; using defaults

PLAY [MANAGE IP HOST ON SOPHOS FIREWALL] *********************************************************************************************

TASK [MANAGE IP HOST] ****************************************************************************************************************
changed: [testfirewall1.sophos.net -> localhost] => {"api_response": {"Response": {"@APIVersion": "2000.2", "@IPS_CAT_VER": "1", "@IS_WIFI6": "0", "IPHost": {"@transactionid": "", "Status": {"#text": "Configuration applied successfully.", "@code": "200"}}, "Login": {"status": "Authentication Successful"}}}, "changed": true, "check_mode": false}

PLAY RECAP ***************************************************************************************************************************
testfirewall1.sophos.net              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

While we don't currently have modules for every possible configuration of the firewall, the sfos_xmlapi module can be used to cover those cases where no module yet exists. With the sfos_xmlapi module, it is necessary to obtain the XML payload from the API documentation. The payload can then be used within the task. Below is an example task that creates a MAC Host on the firewall.

- name: CREATE MAC HOST
  sophos.sophos_firewall.sfos_xmlapi:
    username: "{{ username }}"
    password: "{{ password }}"
    hostname: "{{ inventory_hostname }}"
    port: 4444
    verify: false
    xml_tag: MACHost
    data: |
            <MACHost>
                <Name>TESTMACHOST1</Name>
                <Description>Created by Ansible xmlapi module</Description>
                <Type>MACAddress</Type>
                <MACAddress>00:16:76:49:33:FF</MACAddress>
            </MACHost>
    state: present
  delegate_to: localhost

Variables can also be used inside the payload if necessary. For example if we wanted to define the MAC address as an Ansible variable we could reference it inside the payload using {{ mac_address }}.

<MACHost>
    <Name>TESTMACHOST1</Name>
    <Description>Created by Ansible xmlapi module</Description>
    <Type>MACAddress</Type>
    <MACAddress>{{ mac_address }}</MACAddress>
</MACHost>

As this is a community-supported open source project, support cannot be provided by Sophos Technical Support. Please feel free to open an issue to report bugs and/or make feature requests to be addressed by the project maintainers.