Deep dive on Cisco ASA resource modules
Recently, we published our thoughts on resource modules applied to
the use cases targeted by the Ansible security automation initiative.
The principle is well known from the network automation
space and we follow the established path. While the last blog post
covered a few basic examples, we'd like to show more detailed use cases
and how those can be solved with resource modules.
This blog post goes in depth into the new Cisco ASA Content Collection,
which was already introduced in the previous article. We will walk
through several examples and describe the use cases and how we envision
the Collection being used in real world scenarios.
The Cisco ASA Certified Content Collection: what is it about?
The Cisco ASA Content Collection provides means to automate the
Cisco Adaptive Security Appliance family of security devices -
short Cisco ASA, hence the name. With a focus on firewall and network
security they are well known in the market.
The aim of the Collection is to integrate the Cisco ASA devices into
automated security workflows. For this, the Collection provides modules
to automate generic commands and config interaction with the devices as
well as resource oriented automation of access control lists (ACLs) and
object groups (OGs).
How to install the Cisco ASA Certified Ansible Content Collection
The Cisco ASA Collection is available to Red Hat Ansible Automation
Platform customers at Automation
Hub, our software as a
service offering on cloud.redhat.com and a
place for Red Hat subscribers to quickly find and use content that is
supported by Red Hat and our technology partners.
Once that is done, the Collection is easily installed:
ansible-galaxy collection install cisco.asa
Alternatively you can also find the collection in Ansible
Galaxy, our open source hub for
sharing content in the community.
What's in the Cisco ASA Content Collection?
The focus of the Collection is on the mentioned modules (and the plugins
supporting them): there are three modules for basic interaction,
asa_facts, asa_cli and asa_config. If you are familiar with other
networking and firewall Collections and modules of Ansible you will
recognize this pattern: these three modules provide the most simple way
of interacting with networking and firewall solutions. Using those,
general data can be received, arbitrary commands can be sent and
configuration sections can be managed.
While these modules already provide a great value for environments where
the devices are not automated at all, the focus of this blog article is
on the other modules in the Collection: the resource modules asa_ogs and
asa_acls. Being resource modules they have a limited scope, but enable
users of the Collection to focus on that particular resource without
being disturbed by other content or configuration items. They also
enable a simpler cross-product automation since other Collections follow
the same pattern.
If you take a closer look, you will find two more modules: asa_ogs and
asa_acls. As mentioned in our first blog post about security automation
resource modules, those are deprecated modules, which previously were
used to configure ACLs and OGs. They are superseded by the resource
modules.
Connect to Cisco ASA, the Collection way
The Collection supports network_cli as a connection type. Together with
the network OS cisco.asa.asa, a username and a password, you are good to
go. To get started quickly, you can simply provide these details as part
of the variables in the inventory:
[asa01]
host_asa.example.com
[asa01:vars]
ansible_user=admin
ansible_ssh_pass=password
ansible_become=true
ansible_become_method=ansible.netcommon.enable
ansible_become_pass=become_password
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.asa.asa
ansible_python_interpreter=python
Note that in a productive environment those variables should be
supported in a secure way, for example, with the help of Ansible Tower
credentials.
Use Case: ACLs
After all this is setup, we are now ready to dive into the actual
Collections and how they can be used. For the first use case, we want to
look at managing ACLs within ASA. Before we dive into Ansible Playbook
examples, let's quickly discuss what ASA ACLs are and what an automation
practitioner should be aware of.
ASA Access-lists are created globally and are then applied with the
access-group "command". They can either be applied inbound or outbound.
There are few things users should be aware with respect to access-lists
on the Cisco ASA firewall:
-
When a user creates an ACL for higher to lower security level i.e.
outbound traffic then the source IP address is the address of the
host or the network (not the NAT translated one).
-
When a user creates an ACL for lower to higher security level i.e.
inbound traffic then the destination IP address has to be either of
the below two:
-
- The translated address for any ASA version before 8.3.
- The address for ASA 8.3 and newer.
-
The access-list is always checked before NAT translation.
Additionally, changing ACLs can become very complex quickly. It is not
only about the configuration itself, but also the intent of the
automation practitioner: should a new ACL just be added to the existing
configuration? Or should it replace it? And what about merging them?
The answer to these questions usually depends on the environment and
situation the change is deployed in. The different ways of changing ACLs
are noted here and in the Cisco ASA Content Collection as "states":
different ways to deploy changes to ACLs.
The ACLs module knows the following states:
- Gathered
- Merged
- Overridden
- Replaced
- Deleted
- Rendered
- Parsed
In this use case discussion, we will have a look at all of them, though
not always in full detail. However, we will provide links to full code
listings for the interested readers.
Please note that while we usually use network addresses for the source
and destination examples, other values like network object-groups are
also possible.
State Gathered: Populating an inventory with configuration data
Given that resource modules allow to read-in existing network
configuration and convert that into structured data models, the state
"gathered" is the equivalent for gathering Ansible Facts for this
specific resource. That is helpful if specific configuration pieces
should be reused as variables later on. Another use case is to read-in
the existing network configuration and store it as a flat-file. This
flat file can be committed to a git repository on a scheduled base,
effectively tracking the current configuration and changes of your
security tooling.
To showcase how to store existing configuration as a flat file, let's
take the following device configuration:
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
access-list test_access; 2 elements; name hash: 0x96b5d78b
access-list test_access line 1 extended deny tcp 192.0.2.0 255.255.255.0 192.0.3.0 255.255.255.0 eq www log default (hitcnt=0) 0xdc46eb6e
access-list test_access line 2 extended deny icmp 198.51.100.0 255.255.255.0 198.51.110.0 255.255.255.0 alternate-address log errors interval 300 (hitcnt=0) 0x68f0b1cd
access-list test_R1_traffic; 1 elements; name hash: 0x2c20a0c
access-list test_R1_traffic line 1 extended deny tcp 2001:db8:0:3::/64 eq www 2001:fc8:0:4::/64 eq telnet (hitcnt=0) 0x11821a52
To gather and store the content as mentioned above, we need to first
gather the data from each device, then create a directory structure
mapping our devices and then store the configuration there, in our case
as YAML files. The following playbook does exactly that. Note the
parameter state: gathered in the first task.
---
- name: convert interface to structured data
hosts: asa
gather_facts: false
tasks:
- name: Gather facts
cisco.asa.asa_acls:
state: gathered
register: gather
- name: Create inventory directory
become: true
delegate_to: localhost
file:
path: "{{ inventory_dir }}/host_vars/{{ inventory_hostname }}"
state: directory
- name: Write each resource to a file
become: true
delegate_to: localhost
copy:
content: "{{ gather[‘gathered’][0] | to_nice_yaml }}"
dest: "{{ inventory_dir }}/host_vars/{{ inventory_hostname }}/acls.yaml"
The state "gathered" only collects existing data. In contrast to most
other states, it does not change any configuration. The resulting data
structure from reading in a brownfield configuration can be seen below:
$ cat lab_inventory/host_vars/ciscoasa/acls.yaml
- acls:
- aces:
- destination:
address: 192.0.3.0
netmask: 255.255.255.0
port_protocol:
eq: www
grant: deny
line: 1
log: default
protocol: tcp
protocol_options:
tcp: true
source:
address: 192.0.2.0
netmask: 255.255.255.0
...
You can the full detailed listing of all the commands and outputs of the
example in the state: gathered reference
gist.
State Merged: Add/Update configuration
After the first, non-changing state we now have a look at a state which
changes the target configuration: "merged". This state is also the
default state for any of the available resource modules - because it
just adds or updates the configuration provided by the user. Plain and
simple.
For example, let's take the following existing device configuration:
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
access-list test_access; 1 elements; name hash: 0x96b5d78b
access-list test_access line 1 extended deny tcp 192.0.2.0 255.255.255.0 192.0.3.0 255.255.255.0 eq www log debugging interval 300 (hitcnt=0) 0xdc46eb6e
Let us assume we want to deploy the configuration which we stored as a
flat-file in the gathered example. Note that the content of the flat
file is basically one variable called "acls". Given this flat file and
the variable name, we can use the following playbook to deploy the
configuration on a device:
---
- name: Merged state play
hosts: cisco
gather_facts: false
collections:
- cisco.asa
tasks:
- name: Merge ACLs config with device existing ACLs config
asa_acls:
state: merged
config: "{{ acls }}"
Once we run this merge play all of the provided parameters will be
pushed and configured on the Cisco ASA appliance.
Afterwards, the network device configuration is changed:
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
ccess-list test_access; 2 elements; name hash: 0x96b5d78b
access-list test_access line 1 extended deny tcp 192.0.2.0 255.255.255.0 192.0.3.0 255.255.255.0 eq www log default (hitcnt=0) 0xdc46eb6e
access-list test_access line 2 extended deny icmp 198.51.100.0 255.255.255.0 198.51.110.0 255.255.255.0 alternate-address log errors interval 300 (hitcnt=0) 0x68f0b1cd
access-list test_R1_traffic; 1 elements; name hash: 0x2c20a0c
access-list test_R1_traffic line 1 extended deny tcp 2001:db8:0:3::/64 eq www 2001:fc8:0:4::/64 eq telnet (hitcnt=0) 0x11821a52
All the changes we described in the playbook with the resource modules
are now in place in the device configuration.
If we dig slightly into the device output there are following
observations:
-
The merge play configured 2 ACLs:
-
- test_access, configured with 2 Access Control Entries (ACEs)
- test_R1_traffic with only 1 ACEs
-
test_access is an IPV4 ACL where for the first ACE we have specified
the line number as 1 while for the second ACE we only specified the
name which is the only required parameter. All the other parameters
are optional and can be chosen depending on the particular ACE
policies . Note however that it is considered best practice to
configure the line number if we want to avoid an ACE to be
configured as the last in an ACL.
-
test_R1_traffic is an IPV6 ACL
-
As there weren't any pre-existing ACLs on this device, all the play
configurations have been added. If we had any pre-existing ACLs and
the play also had the same ACL with either different ACEs or same
ACEs with different configurations, the merge operation would have
updated the existing ACL configuration with the new provided ACL
configuration.
Another benefit of automation shows when we run the respective merge
play a second time: Ansible's charm of idempotency comes into the
picture! The play run results in "changed=False" which confirms to the
user that all of the provided configurations in the play are already
configured on the Cisco ASA device.
You can the full detailed listing of all the commands and outputs of the
example in the state: merged reference gist.
State Replaced: Old out, new in
Another typical situation is when a device is already configured with an
ACL with existing ACEs, and the automation practitioner wants to update
the ACL with a new set of ACEs while entirely discarding all the already
configured ones.
In this scenario the state "replaced" is an ideal choice: as the name
suggests, the replaced state will replace ACL existing ACEs with a new
set of ACEs given as input by the user. If a user tries to configure any
new ACLs that are not already pre-configured on the device it'll act as
a merge state and the asa_acls module will try to configure the ACL ACEs
given as input by the user inside the replace play.
Let's take the following brown field configuration:
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
access-list test_access; 2 elements; name hash: 0x96b5d78b
access-list test_access line 1 extended deny tcp 192.0.2.0 255.255.255.0 192.0.3.0 255.255.255.0 eq www log default (hitcnt=0) 0xdc46eb6e
access-list test_access line 2 extended deny icmp 198.51.100.0 255.255.255.0 198.51.110.0 255.255.255.0 alternate-address log errors interval 300 (hitcnt=0) 0x68f0b1cd
access-list test_R1_traffic; 1 elements; name hash: 0x2c20a0c
access-list test_R1_traffic line 1 extended deny tcp 2001:db8:0:3::/64 eq www 2001:fc8:0:4::/64 eq telnet (hitcnt=0) 0x11821a52
Now we assume we want to configure a new ACL named "test_global_access",
and we want to replace the already existing "test_access" ACL
configuration with a new source and destination IP. The corresponding
ACL configuration for our new desired state is:
- acls:
- name: test_access
acl_type: extended
aces:
- grant: deny
line: 1
protocol: tcp
protocol_options:
tcp: true
source:
address: 192.0.3.0
netmask: 255.255.255.0
destination:
address: 192.0.4.0
netmask: 255.255.255.0
port_protocol:
eq: www
log: default
- name: test_global_access
acl_type: extended
aces:
- grant: deny
line: 1
protocol_options:
tcp: true
source:
address: 192.0.4.0
netmask: 255.255.255.0
port_protocol:
eq: telnet
destination:
address: 192.0.5.0
netmask: 255.255.255.0
port_protocol:
eq: www
Note that the definition is again effectively contained in the variable
"acls" - which we can reference as a value for the "config" parameter of
the asa_acls module just as we did in the last example. Only the value
for the state parameter is different this time:
---
- name: Replaced state play
hosts: cisco
gather_facts: false
collections:
- cisco.asa
tasks:
- name: Replace ACLs config with device existing ACLs config
asa_acls:
state: replaced
config: "{{ acls }}"
After running the playbook, the network device configuration has changed
as intended: the old configuration was replaced with the new one. In
cases where there was no corresponding configuration in place to be
replaced, the new one was added:
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
access-list test_access; 1 elements; name hash: 0x96b5d78b
access-list test_access line 1 extended deny tcp 192.0.3.0 255.255.255.0 192.0.4.0 255.255.255.0 eq www log default (hitcnt=0) 0x7ab83be2
access-list test_R1_traffic; 1 elements; name hash: 0x2c20a0c
access-list test_R1_traffic line 1 extended deny tcp 2001:db8:0:3::/64 eq www 2001:fc8:0:4::/64 eq telnet (hitcnt=0) 0x11821a52
access-list test_global_access; 1 elements; name hash: 0xaa83124c
access-list test_global_access line 1 extended deny tcp 192.0.4.0 255.255.255.0 eq telnet 192.0.5.0 255.255.255.0 eq www (hitcnt=0) 0x243cead5
Note that the ACL test_R1_traffic was not modified or removed in this example!
You can the full detailed listing of all the commands and outputs of the
example in the state: replaced reference gist.
State Overridden: Drop what is not needed
As noted in the last example, ACLs which are not explicitly mentioned in
the definition remain untouched. But what if there is the need to
reconfigure all existing and pre-configured ACLs with the input ACL ACEs
configuration - and also affect those that are not mentioned? This is
where the state "overridden" comes into play.
If you take the same brown field environment from the last example and
deploy the same ACL definition against it, but this time switch the
state to "overridden", the resulting configuration of the device looks
quite different:
Brownfield device configuration before deploying the ACLs:
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
access-list test_access; 2 elements; name hash: 0x96b5d78b
access-list test_access line 1 extended deny tcp 192.0.2.0 255.255.255.0 192.0.3.0 255.255.255.0 eq www log default (hitcnt=0) 0xdc46eb6e
access-list test_access line 2 extended deny icmp 198.51.100.0 255.255.255.0 198.51.110.0 255.255.255.0 alternate-address log errors interval 300 (hitcnt=0) 0x68f0b1cd
access-list test_R1_traffic; 1 elements; name hash: 0x2c20a0c
access-list test_R1_traffic line 1 extended deny tcp 2001:db8:0:3::/64 eq www 2001:fc8:0:4::/64 eq telnet (hitcnt=0) 0x11821a52
Device configuration after deploying the ACLs via the resource module
just list last time, but this time with state "overridden":
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
access-list test_access; 1 elements; name hash: 0x96b5d78b
access-list test_access line 1 extended deny tcp 192.0.3.0 255.255.255.0 192.0.4.0 255.255.255.0 eq www log default (hitcnt=0) 0x7ab83be2
access-list test_global_access; 1 elements; name hash: 0xaa83124c
access-list test_global_access line 1 extended deny tcp 192.0.4.0 255.255.255.0 eq telnet 192.0.5.0 255.255.255.0 eq www (hitcnt=0) 0x243cead5
Note that this time the listing is considerably shorter - the ACL
test_R1_traffic was dropped since it was not explicitly mentioned in the
ACL definition which was deployed. This showcases the difference between
"replaced" and "overridden" state.
You can the full detailed listing of all the commands and outputs of the
example in the state: overridden reference gist.
State Deleted: Remove what is not wanted
Another more obvious use case is the deletion of existing ACLs on the
device, which is implemented in the "deleted" state. In that case the
input is the ACL name to be deleted and the corresponding delete
operation will delete the entry of the particular ACL by deleting all of
the ACEs configured under the respective ACL.
As an example, let's take our brown field configuration already used in
the other examples. To delete the ACL test_access we name it in the
input variable:
- acls:
- name: test_access
The playbook looks just like the one in the other examples, just with
the parameter and value state: deleted. After executing it, the
configuration of the device is:
ciscoasa# sh access-list
access-list cached ACL log flows: total 0, denied 0 (deny-flow-max 4096)
alert-interval 300
access-list test_R1_traffic; 1 elements; name hash: 0x2c20a0c
access-list test_R1_traffic line 1 extended deny tcp 2001:db8:0:3::/64 eq www 2001:fc8:0:4::/64 eq telnet (hitcnt=0) 0x11821a52
The output is clearly shorter than the previous configuration since an
entire ACL is missing.
You can the full detailed listing of all the commands and outputs of the
example in the state: deleted reference gist.
State Rendered and State Parsed: For development and offline work
There are two more states currently available : "rendered" and "parsed".
Both are special in that they are not meant to be used in production
environments, but during development of your playbooks and device
configuration. They do not change the device configuration - instead
they output what would be changed in different formats.
The state "rendered" returns a listing of the commands that would be
executed to apply the provided configuration. The content of the
returned values given the above used configuration against our brown
field device configuration:
"rendered": [
"access-list test_access line 1 extended deny tcp 192.0.2.0 255.255.255.0 192.0.3.0 255.255.255.0 eq www log default",
"access-list test_access line 2 extended deny icmp 198.51.100.0 255.255.255.0 198.51.110.0 255.255.255.0 alternate-address log errors",
"access-list test_R1_traffic line 1 extended deny tcp 2001:db8:0:3::/64 eq www 2001:fc8:0:4::/64 eq telnet"
]
You can the full detailed listing of all the commands and outputs of the
example in the state: rendered reference
gist.
State "parsed" acts similar, but instead of returning the commands that
would be executed, it returns the configuration as a JSON structure,
which can be reused in subsequent automation tasks or by other programs.
See our full detailed listing of all the commands and outputs of the
parsed example in the state: parsed reference
gist.
Use Case: OGs
As mentioned before, the Ansible Content Collection does support a
second resource: object groups. Think of networks, users, security
groups, protocols, services and the like. The resource module can be
used to define them or alter their definition. Much like the ACLs
resource module, the basic workflow defines them via a variable
structure and then deploys them in a way identified by a state
parameter. The states are basically the same as the ACLs resource module
understands.
Due to this similarity, we will not go into further details here but
instead refer to the different state examples mentioned above.
From a security perspective however, the object group resource module is
crucial: in a modern IT environment, communication relations are not
only defined by IP addresses, but can also be defined by the types of
objects that are in focus: it is crucial for security practitioners to
be able to abstract those types in object groups and address their
communication relations in ACLs later on.
This also explains why we picked these two resource modules to start
with: they work closely hand in hand and together pave the way for an
automated security approach using the family of Cisco ASA devices.