Scripted ACL Management

Managing ACLs can be a bit time consuming depending on the environment. When you have one or two routers with a few ACLs, its not big deal though. But what happens when you have dozens of routers each with their own set of ACLs? I know there are better ways to design a network than having a bunch of ACLs controlling your network security, but sometimes that is what you have to work with. Especially if you just started working for a company and that is what you inherited.

A big issue with trying to maintain so many ACLs is making sure they are all accurate and tidy. For instance, a server gets decommissioned and the IP it was using can be reclaimed. If the IP stays in the ACL(s), then if that IP is reassigned then the new server will inherit the old servers access. Depending on the server this might not be a bad thing, but it could also lead to way too much access. Logging into 30 routers manually just to see that the ACLs may or may not contain that one IP can take a lot of time. So why not script it?

I typically work on at least three network operating systems when managing my works network. So we need to make sure the script is vendor agnostic, and it would be nice to quickly add more vendors if necessary. One of my previous scripts used SNMP to detect the network OS, however that quickly became cumbersome from a config and a management standpoint. Make sure the right OID is being used, make sure the SNMP details are correct, make sure ports are open, etc… So instead, I started using CSV like files to make this faster and easier. I’m sure there might be a better way yet, but for now this way works well for me.

The basics of a network script, at least for my use, is to setup a Netmiko connection. Quick and easy: import the library, define a dictionary variable, and establish the connection (don’t worry about the values in the dictionary just yet).

from netmiko import ConnectHandler

device = {
    'device_type': router_list[counter].split(' ')[1],
    'ip': router_list[counter].split(' ')[0],
    'username': args.u,
    'password': password,
}
nc = ConnectHandler(**device)

Now let’s make it useful and add an argparser so we can pass arguments via CLI when the script is ran.

import argparse
import getpass

parser = argparse.ArgumentParser(description = 'Check ACLs for a matching IP')
parser.add_argument('-i', help = 'IP to check', required=True)
parser.add_argument('-u', help = 'Username for SSH auth', default = 'admin')
parser.add_argument('-k', help = 'Prompt for SSH password', action = 'store_true')
parser.add_argument('-r', help = 'file that contains router list', default = 'routers.txt')
parser.add_argument('-w', help = 'filename to write report to', default = 'report.txt')
args = parser.parse_args()

if args.w == 'report.txt':
    args.w = '{}-report.txt'.format(args.i)

if args.k:
    password = getpass.getpass()

Okay, so now the script will take the IP you want to check, the username to pass to the device dictionary variable, and a list of routers. However, we still need to create the router file and write the logic that will process everything. An important part of this logic will be using regular expressions to search for the IP in the config files.

The next step is to open the router file, read its contents into a list line by line, then split each line by the comma delimiter to use each available value from each line. We will also try to get an estimated runtime, individual runtime, and overall runtime. The estimated runtime won’t be anywhere near perfect. I just wrote it based on the average time spent on the routers in my router file. The regex is also just a simple “\b{}\b” to find the IP. This is all shown in the code below.

import re
import time

match = False
foundips = 0
counter = 0
progress_counter = 1

try:
    with open(args.r, 'r') as f:
        ff = f.read() # The read() function is called so the first line is grabbed from the text file
        for router in ff:
            router_list = ff.splitlines() # Load a list of the file names
    print('Searching routers now...')
    print('Estimated runtime: %.2f minutes' % (len(router_list) * 23 / 60))
    startTime = time.time()
    for router in router_list:
        device = {
            'device_type': router_list[counter].split(',')[1],
            'ip': router_list[counter].split(',')[0],
            'username': args.u,
            'password': password,
        }
        searchTime = time.time()
        nc = ConnectHandler(**device)
        if router_list[counter].split(',')[1] == 'enterasys':
            output = nc.send_command('show config')
        elif router_list[counter].split(',')[1] == 'extreme_exos':
            policy = re.findall(r"[a-zA-Z0-9_\\\-\.\(\):]+\.pol", nc.send_command('ls'))
            if policy != []:
                for i in range(len(policy)):
                    policyfile = policy[i].strip('.pol')
                    ipcheck = nc.send_command('show policy {} | i {}'.format(policyfile, args.i))
                    if re.search(r'\b{}\b'.format(args.i), ipcheck) and not "Unrecognized" in ipcheck:
                        output = policyfile + " has line " + ipcheck
        elif router_list[counter].split(',')[1] == 'cisco_ios':
            output = nc.send_command('show run')
        else:
            raise
        nc.disconnect()
        elapsedTime = time.time() - searchTime
        config_list = output.splitlines() # Read each config line into a new list
        for item in config_list:
            if re.search(r'\b{}\b'.format(args.i), item): # Use regex word boundaries to search for an exact IP match anywhere in the configuration
                print('({}/{})...%.2fs...{}({}) --> {}'.format(progress_counter, len(router_list), router_list[counter].split(',')[0], router_list[counter].split(',')[2], item) % (elapsedTime))
                with open(args.w, 'a') as f: # Write out to a report file
                    print('({}/{})...%.2fs...{}({}) --> {}'.format(progress_counter, len(router_list), router_list[counter].split(',')[0], router_list[counter].split(',')[2], item) % (elapsedTime), file=f)
                foundips += 1
        if foundips == 0:
            print('({}/{})...%.2fs...{}({}) --> No match found'.format(progress_counter, len(router_list), router_list[counter].split(',')[0], router_list[counter].split(',')[2]) % (elapsedTime))
        foundips = 0
        progress_counter+=1
        counter+=1
    total_time = time.time() - startTime
    if total_time < 60:
        print('Total search time: %.2f seconds' % (total_time))
    else:
        print('Total search time: %.1f minutes' % (total_time / 60))
except BaseException as e:
    print(e)
    exit(255)

To explain a few things…

The text file can have any information you want to grab as long as it is separated by a comma and you keep the index location for the type of information consistent. Here is an example:

10.1.1.1,extreme_exos,buildingA-switch1
10.1.1.2,cisco_ios,buildingB-switch1
10.1.1.3,enterasys,buildingC-switch1

The second value is the device_type required by Netmiko when establishing a connection. The script checks for what the device_type is and grabs the required commands for the network OS. So to add a vendor, just figure out the Netmiko device_type for that vendor, add to the elif statements and the corresponding router commands to view the ACLs. Sounds easy right? Well that was until I added Extreme EXOS support. This is because EXOS doesn’t output the ACLs (well, Policy files) in the ‘show config’ command. I didn’t let this stop me though and I wrote another regex to search the output of ‘ls’ for .pol files (the ACL/Policy files that Extreme uses). Then simply do a ‘show policy’ on those files and search for the IP that way.

Summary

Now we have a script that we can run against a custom list of routers with any bit of custom information we want to be present in the output. This script runs live on the network so it is as accurate as the current configs are on the routers. While it can still take a while to log into each router one after the other, depending on the amount of routers in the loaded file, it is still convenient to be able to hit enter and let it do its thing in the background. One day I might look into implementing threading to run parallel SSH connections to speed up the process, which will be a fun challenge! I hope this script is as helpful to someone as it is to me!

The full script can be downloaded from the Downloads page or here.

Leave a Reply

Your email address will not be published. Required fields are marked *

1 thought on “Scripted ACL Management”

  1. Yeah, that will save a significant amount of time by utilizing your script! However, I do have a question for you. Looking at so much code in a command line interface, would this be easier to update, modify, or search within a GUI, such as Ubiquiti’s network controller? I agree that command line will always have its place, but what are your thoughts around the pros and cons of leveraging a GUI versus a CLI for regular interaction with the network components?