Cisco Port Security and Ansible
Putting Port Security and Ansible together gave me some headaches but this configuration is running stable for some weeks already.
- Data
- Make an exception for Port Security
- Configure max allowed MAC addresses
- Check for changes in the data
- Configure Port Security
Data
Lets start with the data:
1
2
3
4
5
6
7
8
9
devices:
workstation:
linux-1: { name: 'linux-1', user: 'test1', ip_addr: '10.0.20.3', mac_addr_eth: ['44:8a:5b:d0:9e:1a'] }
linux-2: { name: 'linux-2', user: 'test2', ip_addr: '10.0.20.4', mac_addr_eth: ['44:8a:5b:d0:9e:2a'] }
linux-3: { name: 'linux-3', user: 'test2', ip_addr: '10.0.20.4', mac_addr_eth: ['44:8a:5b:d0:9e:2a'] }
bridge:
# unmanaged switches
swi-brg-01: { name: 'swi-brg-02', mac_addr_eth: ['00:15:65:b4:b9:f0', '00:15:65:b4:ba:7d', '00:15:65:b4:ba:e1'] }
swi-brg-02: { name: 'swi-brg-03', mac_addr_eth: ['00:15:65:b4:b9:f1', '00:15:65:b4:ba:71', '00:15:65:b4:ba:e2'] }
For every interface port security should be enabled, except port_security_enabled
is defined.
I’ve some unmanaged switches where I also want to enforce port security so that nobody can just plug in anything he wants.
The data for the interface configuration looks like this:
1
2
3
4
5
6
7
8
# host_vars/swi-acs-01.yml
# the numbers are the port numbers - 1 = Gi1/0/1, 2 = Gi1/0/2
switch_interface_hash:
1: { device: "{{ devices['workstation']['linux-1'] }}" }
2: { device: "{{ devices['workstation']['linux-2'] }}" }
3: { device: "{{ devices['bridge']['swi-brg-01'] }}" }
4: { device: "{{ devices['bridge']['swi-brg-02'] }}" }
5: { device: "{{ devices['workstation']['linux-3'] }}", port_security_enabled: false }
Make an exception for Port Security
For some devices (linux-3) there needs to be the possibility to opt out from port security.
The command show port-security
gives us a list of all interfaces where port security is enabled.
1
2
3
4
5
6
7
8
9
swi-acs-01#show port-security
Secure Port MaxSecureAddr CurrentAddr SecurityViolation Security Action
(Count) (Count) (Count)
---------------------------------------------------------------------------
Gi1/0/1 1 1 0 Shutdown
Gi1/0/2 1 1 0 Shutdown
Gi1/0/3 1 1 0 Shutdown
Gi1/0/4 1 1 0 Shutdown
Gi1/0/5 1 1 0 Shutdown # will be disabled
Get list of enabled port security interfaces
1
2
3
4
5
6
7
8
9
10
11
12
# first fetch the config
- name: get running-config
ios_config:
backup: true
defaults: true
register: running_config_backup_result
- name: get port-security list
ios_command:
commands: "show port-security"
register: check_port_security_to_disable
changed_when: false
Disable Port Security
If the interface is in the show port-security
output, disable it.
1
2
3
4
5
6
7
8
9
10
11
12
- name: disable port-security
ios_config:
lines:
- 'no switchport port-security'
- 'no switchport port-security mac-address'
- 'no switchport port-security maximum'
parents: "interface GigabitEthernet1/0/{{ item.key }}"
running_config: "{{ lookup('file', running_config_backup_result['backup_path']) }}"
loop: "{{ switch_interface_hash | dict2items }}"
when:
- '"Gi1/0/" ~ item.key in check_port_security_to_disable.stdout[0]'
- item.value.port_security_enabled is defined
Configure max allowed MAC addresses
For the unmanaged switches we need to increase the max allowed mac addresses ‘cause the default is 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
- name: configure port-security max allowed
ios_config:
before:
- "interface GigabitEthernet1/0/{{ item.key }}"
- 'no switchport port-security maximum'
lines:
- "switchport port-security maximum {{ item.value.device.mac_addr_eth | length }}"
parents: "interface GigabitEthernet1/0/{{ item.key }}"
running_config: "{{ lookup('file', running_config_backup_result['backup_path']) }}"
loop: "{{ switch_interface_hash | dict2items }}"
when:
- (item.value.port_security_enabled is undefined and item.value.device.mac_addr_eth is defined)
- item.value.device.mac_addr_eth | length > 1
Check for changes in the data
We need to detect if there is a change in the data and later clear the port security before configuring it otherwise removed mac addresses would be still there and the task would also fail ‘cause of too much configured mac addresses versus actual array length.
For every mac address we would need to execute switchport port-security mac-address <MAC>
on the switch.
The with_subelements
loop expects an array for the nested subkey, but I’ve a hash. The query filter is helping with this problem.
1
2
3
4
5
6
7
8
9
10
11
12
13
- name: check port-security
ios_config:
lines: "switchport port-security mac-address {{ item.1 | hwaddr('cisco') }}"
parents: "interface GigabitEthernet1/0/{{ item.0.key }}"
running_config: "{{ lookup('file', running_config_backup_result['backup_path']) }}"
with_subelements:
- "{{ query('dict', switch_interface_hash) }}"
- value.device.mac_addr_eth
- flags:
skip_missing: true
when: (item.0.value.port_security_enabled is undefined and item.1 is defined)
register: check_port_security
check_mode: true
Configure Port Security
First we clear all already configured mac addresses and disable port security if there was a change detected by the module.
1
2
3
4
5
6
7
8
9
10
11
- name: clear port-security
ios_config:
lines:
- 'no switchport port-security'
- 'no switchport port-security mac-address'
parents: "interface GigabitEthernet1/0/{{ item.item.0.key }}"
running_config: "{{ lookup('file', running_config_backup_result['backup_path']) }}"
loop: "{{ check_port_security['results'] }}"
when:
- item.changed
- item.item.0.value.port_security_enabled is undefined
And then configure mac addresses for the interface.
1
2
3
4
5
6
7
8
9
10
11
- name: configure port-security mac addresses
ios_config:
lines: "switchport port-security mac-address {{ item.1 | hwaddr('cisco') }}"
parents: "interface GigabitEthernet1/0/{{ item.0.key }}"
running_config: "{{ lookup('file', running_config_backup_result['backup_path']) }}"
with_subelements:
- "{{ query('dict', switch_interface_hash) }}"
- value.device.mac_addr_eth
- flags:
skip_missing: true
when: item.0.value.port_security_enabled is undefined
At the end we enable port security
1
2
3
4
5
6
7
- name: enable port-security
ios_config:
lines: 'switchport port-security'
parents: "interface GigabitEthernet1/0/{{ item.key }}"
running_config: "{{ lookup('file', running_config_backup_result['backup_path']) }}"
loop: "{{ switch_interface_hash | dict2items }}"
when: item.value.port_security_enabled is undefined
Tested with:
- Ansible 2.6.16
- Cisco 2960X