AUTOMATING THE DEPLOYMENT OF THE CISCO ASA REST API

Here at CDA we love automation because it is an efficient way to perform mundane or repeatable tasks. In a previous blog article, I talked about automating the process of changing passwords on Cisco ASA devices using the REST API. In this article, I will discuss deploying the Cisco ASA REST API using automation via Ansible. This is a simple process and demonstration. The approach could be used for pushing out updated code, patches, or even common configuration changes in bulk.


Development Process Overview

The steps required to build this in your test environment, before you try it in production, are listed below:

NOTE: I am assuming your firewalls are already setup, accessible on the network, and you have an account with the proper permissions to write configuration data to the firewall configuration.

  • Build Ansible Host

  • Install and Setup Ansible

  • Configure Inventory

  • Build YAML Ansible-Playbook

  • Run the Ansible-Playbook


Lab Overview My lab environment consists of the following:

  • Ansible Host: (192.168.0.49), This is a CentOS Linux release 8.1.1911 (Core) virtual machine with the following components installed.

- Ansible and Python

[analyst@localhost]$ ansible --version
ansible 2.9.11
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/analyst/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.6.8 (default, Nov 21 2019, 19:31:34) [GCC 8.3.1 20190507 (Red Hat 8.3.1-4)]

- TFTP Server (for convenience)

  • ASAv-01: (192.168.0.102), the table below represents the basic configuration required to use this script in your test environment.

ASA Version 9.9(2)
!
terminal width 511
hostname ASA-01
enable password cisco123== pbkdf2
passwd cisco123== encrypted

!
interface GigabitEthernet0/0
 nameif inside
 security-level 100
 ip address 192.168.0.102 255.255.255.0
!
user-identity default-domain LOCAL
aaa authentication ssh console LOCAL
aaa authentication http console LOCAL
aaa authentication login-history
ssh scopy enable
ssh 192.168.0.0 255.255.255.0 inside
ssh timeout 60
ssh version 2
ssh key-exchange group dh-group1-sha1
console timeout 0
management-access inside
username ansible password Cisco123 privilege 15
username ansible password Cisco123 privilege 15
  • ASAv-02: 192.168.0.105

Same as above with a different IP address


Install and Setup Ansible

The process of installing Ansible on CentOs 8 is very well represented in your typical google search. This article should suffice if you want to follow the steps in your lab: https://www.tecmint.com/install-ansible-on-centos-rhel-8/


Build Your Staging Script for TFTP Automation

There are a couple of ways to upload an image to a Cisco ASA device. While it can be done manually at the command prompt, I chose to push the files from my TFTP server (in this case, my ansible host) instead of pulling the files from TFTP. I did this for two main reasons. The first reason is that running it as a serial process, one after the other for each firewall in the list, controls the bandwidth used in a larger environment. The second reason I used the push versus pull method was to avoid the “[CONFIRM]” prompts typically presented at the command line during a pull operation. The table (Figure 2) below shows the contents of the shell script I used to push the files to every firewall in my environment. The input to the script is driven by the entries in DEVICE.TXT.


Figure 1 DEVICE.TXT

  • 192.168.0.102

  • 192.168.0.105

The shell script below does some relatively simple things. It does require you to install “sshpass” if you do not have it already on your system. The logic is as such: Set the variables for $file and $user. This will be the Cisco ASA SCP enabled user account and the location to the REST API “.SPA” file from Cisco. I have a copy in the Github repo for testing. Next, challenge the operator for their SCP password before executing the for loop and write it to the “$password” variable for later use in the “SCP” commands. Followed lastly by a one line command, executed as a for loop for each device in the “DEVICE.TXT” file. NOTE: You can change the “KeyAlgorithms” parameter as you like, however it must match the configuration of your actual firewalls. Since this is my lab, I am using some pretty weak ciphers for the ASAv devices on my EVE-NG virtual machine. If the key algorithms are different on the devices in your environment, you will need to address that first. The command executed in the for loop performs a simple “SCP” command to copy the .SPA file to the disk0: of the Cisco ASA.


Figure 2 COPY-SPA.SH

#!/bin/bash
# Read Password
echo -n Password: 
read -s password
echo
# Run Command
echo $password

file=asa-restapi-7141-lfbff-k8.SPA
user=ansible

for device in $(cat device.txt)
do
 sshpass -p $password scp -o KexAlgorithms=+diffie-hellman-group1-sha1 -o StrictHostKeyChecking=no $file $user@$device:disk0:$file
done
wait


Configure Inventory

When you are using Ansible to automate infrastructure changes your need to provide that list of devices and parameters for each device to the ansible-playbook command. For the purposes of building this solution I created a file called “INV.INI”, you can name it any way you like. The table below represents the output of my file:


[firewalls]
192.168.0.102
192.168.0.105

[firewalls:vars]
ansible_user = ansible
ansible_password = <your Cisco ASA password here without brackets>
ansible_become_method=enable
ansible_become_pass= <your Cisco ASA enable password here without brackets>
ansible_connection = network_cli
ansible_network_os = asa


Build YAML Ansible-Playbook

YAML can be a bit tricky, I use NotePad++ to edit YML files and copy paste them into my SSH session with “nano” on the Ansible host. Any editor will work (vim, nano, vi, etc.). The table below shows you the output of my ansible-playbook in YAML format:


---
- name: ASA Deploy REST API.
  hosts: all
  connection: network_cli
  #connection: local
  gather_facts: no
  become_method: enable
  become: yes

  tasks:
  - name: ASA Deploy REST API
    cisco.asa.asa_config:
      commands:
       #- "show run"
       - "rest-api image disk0:/asa-restapi-7141-lfbff-k8.SPA"
       - "rest-api agent"
       - "http server enable"
       - "http 192.168.0.0 255.255.255.0 inside"
       #- "show run rest-api"

The image below shows the format in the SSH session:


Run the Ansible-Playbook

Running the ansible-playbook is simple. Run the following command to execute and run the playbook:

ansible-playbook -vvvv -i inv.ini ansible-cisco-rest-api-deploy.yml -Kkb

The parameters are also simple, one is optional, worst case if you need more details use “ansible-playbook –help”:

  • Required:

  • Ansible-playbook, this is the command to run your playbook and requires the parameters described below.

  • -i INV.INI, this option and file contains the inventory for your playbook.

  • , this is your actual playbook

  • -Kkb or (-K -k -b)

- -K= –ask-become-pass, ask for privilege escalation password

- -k= –ask-pass ask for connection password

- -b= –become run operations with become (does not imply password prompting)


  • Optional:

- -vvvv, this is used for verbose debug information.


The table below provides a very simple output from the playbook output in my lab, this output occurred after the first run and only shows the updates of me uncommenting out the first and last command:

TASK [ASA Deploy REST API] **********************************************************************************************************************************
task path: /home/ansible/tmp/iosxeSTIG-ansible/ansiblestuff/ansible-cisco-rest-api-deploy.yml:11
<192.168.0.102> attempting to start connection
<192.168.0.102> using connection plugin network_cli
<192.168.0.105> attempting to start connection
<192.168.0.105> using connection plugin network_cli
<192.168.0.105> local domain socket does not exist, starting it
<192.168.0.105> control socket path is /home/ansible/.ansible/pc/deedb496be
<192.168.0.105> local domain socket listeners started successfully
<192.168.0.105> loaded cliconf plugin asa from path /usr/lib/python3.6/site-packages/ansible/plugins/cliconf/asa.py for network_os asa
<192.168.0.105>
<192.168.0.105> local domain socket path is /home/ansible/.ansible/pc/deedb496be
<192.168.0.105> ESTABLISH LOCAL CONNECTION FOR USER: ansible
<192.168.0.105> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b `"&& mkdir /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8122892-102267-259649183478801 && echo ansible-tmp-1607940375.8122892-102267-259649183478801="` echo /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8122892-102267-259649183478801 `" ) && sleep 0'
<192.168.0.102> local domain socket does not exist, starting it
<192.168.0.102> control socket path is /home/ansible/.ansible/pc/3def5139f3
<192.168.0.102> local domain socket listeners started successfully
<192.168.0.102> loaded cliconf plugin asa from path /usr/lib/python3.6/site-packages/ansible/plugins/cliconf/asa.py for network_os asa
<192.168.0.102>
<192.168.0.102> local domain socket path is /home/ansible/.ansible/pc/3def5139f3
<192.168.0.102> ESTABLISH LOCAL CONNECTION FOR USER: ansible
<192.168.0.102> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b `"&& mkdir /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8278008-102266-163338235274902 && echo ansible-tmp-1607940375.8278008-102266-163338235274902="` echo /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8278008-102266-163338235274902 `" ) && sleep 0'
<192.168.0.105> Attempting python interpreter discovery
<192.168.0.102> Attempting python interpreter discovery
<192.168.0.102> EXEC /bin/sh -c 'echo PLATFORM; uname; echo FOUND; command -v '"'"'/usr/bin/python'"'"'; command -v '"'"'python3.7'"'"'; command -v '"'"'python3.6'"'"'; command -v '"'"'python3.5'"'"'; command -v '"'"'python2.7'"'"'; command -v '"'"'python2.6'"'"'; command -v '"'"'/usr/libexec/platform-python'"'"'; command -v '"'"'/usr/bin/python3'"'"'; command -v '"'"'python'"'"'; echo ENDFOUND && sleep 0'
<192.168.0.105> EXEC /bin/sh -c 'echo PLATFORM; uname; echo FOUND; command -v '"'"'/usr/bin/python'"'"'; command -v '"'"'python3.7'"'"'; command -v '"'"'python3.6'"'"'; command -v '"'"'python3.5'"'"'; command -v '"'"'python2.7'"'"'; command -v '"'"'python2.6'"'"'; command -v '"'"'/usr/libexec/platform-python'"'"'; command -v '"'"'/usr/bin/python3'"'"'; command -v '"'"'python'"'"'; echo ENDFOUND && sleep 0'
<192.168.0.102> EXEC /bin/sh -c '/usr/bin/python3.6 && sleep 0'
<192.168.0.105> EXEC /bin/sh -c '/usr/bin/python3.6 && sleep 0'
Using module file /home/ansible/.ansible/collections/ansible_collections/cisco/asa/plugins/modules/asa_config.py
Using module file /home/ansible/.ansible/collections/ansible_collections/cisco/asa/plugins/modules/asa_config.py
<192.168.0.105> PUT /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/tmp1rbsm758 TO /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8122892-102267-259649183478801/AnsiballZ_asa_config.py
<192.168.0.102> PUT /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/tmp452us9ab TO /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8278008-102266-163338235274902/AnsiballZ_asa_config.py
<192.168.0.102> EXEC /bin/sh -c 'chmod u+x /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8278008-102266-163338235274902/ /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8278008-102266-163338235274902/AnsiballZ_asa_config.py && sleep 0'
<192.168.0.105> EXEC /bin/sh -c 'chmod u+x /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8122892-102267-259649183478801/ /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8122892-102267-259649183478801/AnsiballZ_asa_config.py && sleep 0'
<192.168.0.102> EXEC /bin/sh -c '/usr/libexec/platform-python /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8278008-102266-163338235274902/AnsiballZ_asa_config.py && sleep 0'
<192.168.0.105> EXEC /bin/sh -c '/usr/libexec/platform-python /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8122892-102267-259649183478801/AnsiballZ_asa_config.py && sleep 0'
<192.168.0.105> EXEC /bin/sh -c 'rm -f -r /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8122892-102267-259649183478801/ > /dev/null 2>&1 && sleep 0'
changed: [192.168.0.105] => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": true,
    "invocation": {
        "module_args": {
            "after": null,
            "authorize": null,
            "backup": false,
            "backup_options": null,
            "before": null,
            "commands": [
                "show run",
                "rest-api image disk0:/asa-restapi-7141-lfbff-k8.SPA",
                "rest-api agent",
                "http server enable",
                "http 192.168.0.0 255.255.255.0 inside",
                "show run rest-api"
            ],
            "config": null,
            "context": null,
            "defaults": false,
            "lines": [
                "show run",
                "rest-api image disk0:/asa-restapi-7141-lfbff-k8.SPA",
                "rest-api agent",
                "http server enable",
                "http 192.168.0.0 255.255.255.0 inside",
                "show run rest-api"
            ],
            "match": "line",
            "parents": null,
            "passwords": null,
            "provider": null,
            "replace": "line",
            "save": false,
            "src": null
        }
    },
    "updates": [
        "show run",
        "show run rest-api"
    ]
}
<192.168.0.102> EXEC /bin/sh -c 'rm -f -r /home/ansible/.ansible/tmp/ansible-local-102259__n3us4b/ansible-tmp-1607940375.8278008-102266-163338235274902/ > /dev/null 2>&1 && sleep 0'
changed: [192.168.0.102] => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": true,
    "invocation": {
        "module_args": {
            "after": null,
            "authorize": null,
            "backup": false,
            "backup_options": null,
            "before": null,
            "commands": [
                "show run",
                "rest-api image disk0:/asa-restapi-7141-lfbff-k8.SPA",
                "rest-api agent",
                "http server enable",
                "http 192.168.0.0 255.255.255.0 inside",
                "show run rest-api"
            ],
            "config": null,
            "context": null,
            "defaults": false,
            "lines": [
                "show run",
                "rest-api image disk0:/asa-restapi-7141-lfbff-k8.SPA",
                "rest-api agent",
                "http server enable",
                "http 192.168.0.0 255.255.255.0 inside",
                "show run rest-api"
            ],
            "match": "line",
            "parents": null,
            "passwords": null,
            "provider": null,
            "replace": "line",
            "save": false,
            "src": null
        }
    },
    "updates": [
        "show run",
        "show run rest-api"
    ]
}
META: ran handlers
META: ran handlers

PLAY RECAP **************************************************************************************************************************************************
192.168.0.102              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

192.168.0.105              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Once the script and playbook successfully completes, you should be able to connect to each of your firewalls and get prompted for credentials using a URL like this “https:///doc/”


Project Notes and Source

This prototype solution can be added to any job scheduler or automation engine (Jenkins), with some production ready refinements, to run on a schedule. I would recommend designing your logging and security since the credentials and output of this script could very easily create another audit issue because the passwords are exposed. Maybe later I will introduce some functionality to send the credentials to a vault for encrypted storage and recovery. For now, this is merely a prototype you can use to get up and running with your security automation. If you need help building a production ready automation tool please contact me (adidonato@criticaldesign.net).

If you plan on using this script you will want to make sure you have the following:

  • Install Ansible

  • Create your inventory file

  • Create your playbook

  • Run and test your playbook

Test Functions (in a testing environment – don’t be that guy/girl)


Script Download/Source

Disclaimer: While this may go without saying, “Do NOT test this in your production environment.”

You can access this solution and supporting files at the following location. Simply “git clone” the repository and run it against your test environment.


Get Source Code Here


  • Facebook
  • Twitter
  • LinkedIn
  • YouTube