As I have been going through my list of configuration items for the security audit, I have only used Ansible to send commands. I haven’t used the ios_config module for any of its other abilities like interface configuration, gathering facts or ACL configuration.
This post will cover 2/3 of those. Gathering facts, specifically ACL facts and ACL configuration.
A great resource I used for this post can be found on the Ansible website blog. It covers more of the ACL configuration differences that can be performed. I did run into a couple of issues, so I’ll note them down.
The playbooks I will use here can be found on my GitHub.
Gathering ACL Configuration and Creating ACL Host Vars
The gathering of the current ACLs and saving them as host_vars is important. The ios_acls module creates the ACLs in a specific format that would be time-consuming and frustrating to manually create.
Current ACL. Very basic standard ACL.
0 1 2 3 4 5 6 7 8 |
ip access-list standard 99 10 permit host 10.1.1.1 20 permit 172.18.0.12 0.0.0.0 30 permit 10.0.0.0 0.255.255.255 40 permit 172.16.1.0 0.0.0.255 200 deny 192.168.0.0 0.0.255.255 210 deny any |
Ansible will need a variable that looks like the below. This is readable, if you compare to the Cisco config above. It’s very long and not easily readable. Nobody wants to manually create this.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
acls: - acls: - aces: - grant: permit sequence: 10 source: host: 10.1.1.1 - grant: permit sequence: 20 source: host: 172.18.0.12 - grant: permit sequence: 30 source: address: 10.0.0.0 wildcard_bits: 0.255.255.255 - grant: permit sequence: 40 source: address: 172.16.1.0 wildcard_bits: 0.0.0.255 - grant: deny sequence: 200 source: address: 192.168.0.0 wildcard_bits: 0.0.255.255 - grant: deny sequence: 210 source: address: any # modified from host as it casues an error acl_type: standard name: '99' afi: ipv4 |
To avoid manually creating the ios_acl module can do it and save it in the correct location for host_vars. Although I am going to use this as a group_var, so I will just manually copy the yaml over.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
--- - name: convert configured ACLs to structured data hosts: lab_core gather_facts: false connection: network_cli tasks: - name: Use the ACLs resource module to gather the current config cisco.ios.ios_acls: state: gathered register: acls - name: Create inventory directory file: path: "{{ inventory_dir }}/host_vars/{{ inventory_hostname }}" state: directory - name: Write the ACL configuration to a file copy: content: "{{ {'acls': acls['gathered']} | to_nice_yaml }}" dest: "{{ inventory_dir }}/host_vars/{{ inventory_hostname }}/acls.yaml" |
I did have an issue with the resulting host_var files. The last line of my ACL is a deny any.
0 1 2 3 4 5 |
ip access-list standard 99 10 permit host 10.1.1.1 ... 210 deny any |
The deny any line on 210 is similar to the host line on 10. The ios_acl has interpreted the “any” as a hostname. This causes an error when the ACL is run as the command “210 deny host any” is sent to the device, resulting in an error.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
stef@stef-VirtualBox:~/Ansible_projects$ ansible-playbook -c paramiko playbooks/pb4_securityaudit9.yml --ask-vault-pass Vault password: PLAY [VTY ACL - Replaced state play] ********************************************************************************************************************************************* TASK [Replace ACLs config with device existing ACLs config] ********************************************************************************************************************** [WARNING]: ansible-pylibssh not installed, falling back to paramiko [WARNING]: ansible-pylibssh not installed, falling back to paramiko fatal: [172.16.1.104]: FAILED! => { "changed": false } MSG: MODULE FAILURE See stdout/stderr for the exact error MODULE_STDERR: 210 deny host any Translating "any" Translating "host" 210 deny host any ^ % Invalid input detected at '^' marker. R1(config-std-nacl)# ok: [172.16.1.125] PLAY RECAP *********************************************************************************************************************************************************************** 172.16.1.104 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0 172.16.1.125 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 |
To fix this I simply opened the yaml file where the ACL variable is and changed the key of “host” for “address”. This worked, and the ACL can be applied successfully.
This does cause problems with the ability for Ansible to compare the ACL that is gathered from the device and to what the group_var has. They will always be different due to ios_acl incorrectly assuming that “any” is a host.
The only solution to this is to remove the explicit deny statement from the ACL.
Configuring Devices With ACLs
Now that the ACL in yaml format has been added to the group_vars file (below). The next step is to create a playbook that can use this to make changes.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
--- ansible_network_os: ios ansible_user: "{{ vault_ansible_username }}" ansible_password: "{{ vault_ansible_password }}" ansible_become_password: "{{ vault_ansible_become_password }}" acls: - acls: - aces: - grant: permit sequence: 10 source: host: 10.1.1.1 - grant: permit sequence: 20 source: host: 172.18.0.12 - grant: permit sequence: 30 source: address: 10.0.0.0 wildcard_bits: 0.255.255.255 - grant: permit sequence: 40 source: address: 172.16.1.0 wildcard_bits: 0.0.0.255 - grant: deny sequence: 200 source: address: 192.168.0.0 wildcard_bits: 0.0.255.255 - grant: deny sequence: 210 source: address: any # modified from host as it casues an error acl_type: standard name: '99' afi: ipv4 |
The ACL configuration playbook will utilise the ios_acl module, and therefore it’s very easy to configure as the complex checking is handled by the module.
For this configuration, I have chosen to use the “replaced” state for the ACL configuration.
This means that the ios_acl will remove the ACLs referenced in the group_vars file before applying the correct ACLs.
In my example case, there is only a single ACL in the group_vars, ACL 99. So if there were another ACL, say ACL 10 on the device, this one would not be touched, it will remain as last configured.
The below playbook will simply remove entries in ACL 99 and then apply the entries from the group_vars file. This task will always show as changing the configuration. There is no comparison taking place to check if it is required.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
--- - name: VTY ACL - Replaced state play hosts: lab_core gather_facts: false connection: network_cli tasks: - name: Replace ACLs config with device existing ACLs config ios_acls: state: replaced config: "{{ acls }}" |
The output of this playbook.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
stef@stef-VirtualBox:~/Ansible_projects$ ansible-playbook -c paramiko playbooks/pb4_securityaudit9.yml --ask-vault-pass Vault password: PLAY [VTY ACL - Replaced state play] ********************************************************************************************************************************************* TASK [Use the ACLs resource module to gather the current config] ***************************************************************************************************************** [WARNING]: ansible-pylibssh not installed, falling back to paramiko [WARNING]: ansible-pylibssh not installed, falling back to paramiko ok: [172.16.1.104] ok: [172.16.1.125] TASK [Replace ACLs config with device existing ACLs config] ********************************************************************************************************************** changed: [172.16.1.104] changed: [172.16.1.125] PLAY RECAP *********************************************************************************************************************************************************************** 172.16.1.104 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 172.16.1.125 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 |
The ACL 10 is still present after the change. As this is not in the group_vars file, it is not modified by Ansible.
0 1 2 3 4 5 6 7 8 9 10 11 12 |
R1(config-std-nacl)#do sh ip access-lists Standard IP access list 10 10 permit 10.1.1.1 20 permit 172.18.0.12 Standard IP access list 99 10 permit 10.1.1.1 20 permit 172.18.0.12 30 permit 10.0.0.0, wildcard bits 0.255.255.255 40 permit 172.16.1.0, wildcard bits 0.0.0.255 200 deny 192.168.0.0, wildcard bits 0.0.255.255 210 deny any |
Configuring ACL 99 on VTY Lines
I have left the configuration of the VTY lines to the very last step. I had hoped to perform a comparison of the ACL 99 applied on the device with what is in the group_var. However, they do not match, due to my workaround and also because they are named differently. A comparison using “show run” commands is probably easier.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
--- - name: VTY ACL PLAY hosts: lab_core gather_facts: false connection: network_cli tasks: - name: Configure VTY ios_config: lines: - access-class 99 in parents: line vty 0 4 |