Post

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

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
This post is licensed under CC BY 4.0 by the author.