Configuring BIG-IP AFM firewall policies and rules with Ansible
Firewall policies and rules are often repetitive lines of configurations with minor variations in the parameter values, making them a low hanging fruit for introducing automation into your AFM operations. In this article, I will go through an sample workflow of building up the automation of AFM firewall policy configuration using Ansible.
F5 provides two Ansible collections to interact with the configuration objects on BIG-IP:
- f5_modules which uses iControl REST APIs
- f5_bigip which uses Application Services 3 (AS3) extension
If you are introducing this automation on an BIG-IP that already has a number of configurations on it, f5_modules is a better choice as it allows you to focus on the AFM firewall policies/rules without affecting other configurations on the BIG-IP.
The declarative nature of f5_bigip means that it's best used on BIG-IPs which have their configurations fully managed by Declarative Onboarding (DO) and AS3, meaning configuration changes do not occur in the CLI or TMUI, else you run the risk of having configuration conflicts. That said, I will cover how this can be circumvented for BIG-IPs with existing configurations further down, for the purpose of demonstration.
f5_modules
The f5_modules collection has the bigip_firewall_policy and bigip_firewall_rule modules which configure the AFM firewall policies and rules. The policies and rules can be represented as an Ansible variable (arbitrarily named f5_modules_policies here) in an intuitive manner, where:
- each policy contains a list of rules evaluated in order, and
- each rule contains the source/destination IP/port and the action for the rule
# Ansible host variable
f5_modules_policies:
- name: "f5_modules_http_policy"
rules:
- name: rule-1
description: Allow anyone to access HTTP VS
protocol: tcp
source:
- address: "0.0.0.0/0"
destination:
- address: "10.1.20.100"
- port: 443
action: accept
- name: rule-2
description: Deny all other traffic
action: drop
- name: "f5_modules_ftp_policy"
rules:
- name: rule-1
description: Allow internal clients access to FTP VS
protocol: tcp
source:
- address: "10.1.1.50"
- address_range: "192.168.0.100-192.168.0.200"
destination:
- address: "10.1.20.100"
- port: 20
- port: 21
action: accept
- name: rule-2
description: Deny all other traffic
action: drop
As f5_modules uses iControl REST APIs which are imperative, it requires the firewall policies to be defined first before the firewall rules can be associated with the policies, as seen in the following Ansible playbook:
# playbook
- name: Use Collections
hosts: all
connection: local
gather_facts: no
collections:
- f5networks.f5_modules
tasks:
- name: Create policies
include_tasks: tasks/deploy-policy.yaml
loop: "{{ f5_modules_policies }}"
loop_control:
loop_var: policy
and the referenced task file:
# tasks/deploy-policy.yaml
- name: "Create policy {{ policy.name }}"
bigip_firewall_policy:
name: "{{ policy.name }}"
rules: "{{ policy.rules | json_query(query) }}"
provider: "{{ provider }}"
vars:
query: "[*].name"
delegate_to: localhost
- name: "Create rules for policy {{ policy.name }}"
bigip_firewall_rule:
name: "{{ rule.name }}"
description: "{{ rule.description }}"
parent_policy: "{{ policy.name }}"
protocol: "{{ rule.protocol | default('any') }}"
source: "{{ rule.source | default([]) }}"
destination: "{{ rule.destination | default([]) }}"
action: "{{ rule.action }}"
logging: yes
provider: "{{ provider }}"
delegate_to: localhost
loop: "{{ policy.rules }}"
loop_control:
loop_var: rule
label: "{{ rule.name }}"
These tasks result in the creation of policies and rules on the BIG-IP
which then can be referenced by other objects on the BIG-IP, such as attaching a policy to a virtual server:
f5_bigip
As mentioned above, f5_bigip uses AS3 to enable a declarative approach to configuring AFM firewall policies, meaning the firewall policies and rules can be defined in any order within the JSON declaration. The AS3 extension on the BIG-IP then translates the declaration to the correct order of operation on the BIG-IP. An important thing to note is that AS3 manages configurations at a per tenant/partition basis. If AS3 is used to create the firewall policies/rules in a tenant, the created objects can only be referenced by other objects in the same tenant. This present a challenge on BIG-IPs which already have existing objects (e.g. a virtual server in the /Common partition) that needs to have a firewall policy attached. There are two ways to proceed:
- migrate the existing objects to be managed by AS3 in a new tenant/partition, or
- create the firewall policies/rules in the /Common/shared partition using AS3, which can then be referenced by other objects. This is what I will be demonstrating in this article.
f5_bigip provides the bigip_as3_deploy module which takes in an AS3 JSON declaration such as the following:
{
"action": "deploy",
"class": "AS3",
"declaration": {
"Common": {
"Shared": {
"class": "Application",
"f5_bigip_http_policy": {
"class": "Firewall_Policy",
"rules": [
{
"action": "accept",
"destination": {
"addressLists": [
{
"use": "server"
}
],
"portLists": [
{
"use": "httpsPort"
}
]
},
"loggingEnabled": true,
"name": "rule-1",
"protocol": "tcp",
"remark": "Allow anyone to access HTTP VS",
"source": {
"addressLists": [
{
"use": "anyone"
}
]
}
}
]
},
"httpsPort": {
"class": "Firewall_Port_List",
"ports": [
"443"
]
},
"anyone": {
"addresses": [
"0.0.0.0/0"
],
"class": "Firewall_Address_List"
},
"server": {
"addresses": [
"10.1.20.100"
],
"class": "Firewall_Address_List"
},
"template": "shared"
},
"class": "Tenant"
},
"class": "ADC",
"id": "Firewall_Rule_List",
"schemaVersion": "3.13.0"
},
"persist": true
}
Building this JSON from a dynamic list of policies and rules can be achieved with Ansible's templating language of choice - Jinja2.
Whilst it's tempting to reuse the Ansible variable f5_modules_policies referenced in the previous section, it's important to note that AS3 does not support specifying the source/destination address/port combination inline in the policy, but instead refers to additional objects - Firewall_Address_List and Firewall_Port_List. Using f5_modules_policies as an input to the template would require additional logic built in for generating the Firewall_Address_List and Firewall_Port_List objects.
Instead, we can redefine the structure of the Ansible variable to match the AS3 schema more closely, where the address/port lists are defined separately and referenced in the f5_bigip_policies variable:
# Ansible host variables
address_lists:
anyone:
- "0.0.0.0/0"
ftpClients:
- "10.1.1.50"
- "192.168.0.100-192.168.0.200"
server:
- "10.1.20.100"
port_lists:
httpsPort:
- 443
ftpPorts:
- 20
- 21
f5_bigip_policies:
- name: "f5_bigip_http_policy"
rules:
- name: rule-1
description: Allow anyone to access HTTP VS
protocol: tcp
source:
address_lists:
- anyone
destination:
address_lists:
- server
port_lists:
- httpsPort
action: accept
- name: rule-2
description: Deny all other traffic
action: drop
- name: "f5_bigip_ftp_policy"
rules:
- name: rule-1
description: Allow internal clients access to FTP VS
protocol: tcp
source:
address_lists:
- ftpClients
destination:
address_lists:
- server
port_lists:
- ftpPorts
action: accept
- name: rule-2
description: Deny all other traffic
action: drop
Next, we define the Jinja2 template using the above host variables as inputs:
# templates/firewall_policy.json.j2
{
"class": "AS3",
"action": "deploy",
"persist": true,
"declaration": {
"class": "ADC",
"schemaVersion": "3.13.0",
"id": "Firewall_Rule_List",
"Common": {
"class": "Tenant",
"Shared": {
"class": "Application",
"template": "shared",
{% for name, list in address_lists.items() %}
"{{ name }}": {
"class": "Firewall_Address_List",
"addresses": [
{% for addr in list %}
"{{ addr }}"{% if not loop.last %},{% endif %}
{% endfor %}
]
},
{% endfor %}
{% for name, list in port_lists.items() %}
"{{ name }}": {
"class": "Firewall_Port_List",
"ports": [
{% for port in list %}
"{{ port }}"{% if not loop.last %},{% endif %}
{% endfor %}
]
},
{% endfor %}
{% for policy in f5_bigip_policies %}
"{{ policy.name }}": {
"rules": [
{% for rule in policy.rules %}
{
"remark": "{{ rule.description }}",
"name": "{{ rule.name }}",
"action": "{{ rule.action }}",
"protocol": "{{ rule.protocol | default('any') }}",
{% if "source" in rule %}
"source": {
{% if "address_lists" in rule.source %}
"addressLists": [
{% for list in rule.source.address_lists %}
{ "use": "{{ list }}" }{% if not loop.last %},{% endif %}
{% endfor %}
]{% if "port_lists" in rule.source %},{% endif %}
{% endif %}
{% if "port_lists" in rule.source %}
"portLists": [
{% for list in rule.source.port_lists %}
{ "use": "{{ list }}" }{% if not loop.last %},{% endif %}
{% endfor %}
]
{% endif %}
},
{% endif %}
{% if "destination" in rule %}
"destination": {
{% if "address_lists" in rule.destination %}
"addressLists": [
{% for list in rule.destination.address_lists %}
{ "use": "{{ list }}" }{% if not loop.last %},{% endif %}
{% endfor %}
]{% if "port_lists" in rule.destination %},{% endif %}
{% endif %}
{% if "port_lists" in rule.destination %}
"portLists": [
{% for list in rule.destination.port_lists %}
{ "use": "{{ list }}" }{% if not loop.last %},{% endif %}
{% endfor %}
]
{% endif %}
},
{% endif %}
"loggingEnabled": true
}{% if not loop.last %},{% endif %}
{% endfor %}
],
"class": "Firewall_Policy"
}{% if not loop.last %},{% endif %}
{% endfor %}
}
}
}
}
And finally, call the bigip_as3_deploy module, rendering the template above as input to the module
- name: Use Collections
hosts: all
connection: httpapi
gather_facts: no
collections:
- f5networks.f5_bigip
tasks:
- name: Create policies
bigip_as3_deploy:
content: "{{ lookup('template', 'templates/firewall_policy.json.j2') }}"
This deploys the address/port lists and firewall policies/rules on the AFM, in the /Common/Shared partition, meaining they can be attached to existing virtual servers in the any partition
Summary
Here I've shown how the AFM firewall policies and rules can be managed in an inventory and configured via Ansible. The Ansible playbooks can be found in this GitHub repository.
Once you are comfortable with the idea of NOT managing the firewall configuration through the CLI or TMUI, consider automating the execution of Ansible playbooks with something like GitHub actions or GitLab CI/CD that triggers on changes to your inventory or firewall configurations, or build a CI/CD pipeline that performs pre-checks, deployment, post-checks etc.