This repo contains a series of demos related to security best practices and suggestions. These demos are intended to accompany the content in the NetDevOps Live! Episode Automate Safely, NetDevOps Security Strategies. Checkout the episode for a video delivery of the demos and background content.
These demo's leverage the IOS XE on CSR Latest Code Always On Sandbox from DevNet. This sandbox offers direct access to SSH, NETCONF, & RESTCONF interfaces on a sample IOS XE device right on the internet.
If you'd like to run the demos, here's everything you should need to know and do.
Note: You'll need Python3.6+ installed, along with Docker. And for the Ansible and Genie demo examples you'll need a Linux or macOS environment to run them within. Windows Subsystem for Linux (WSL) should work.
-
Clone down this demo repo and jump into the folder for this demo.
git clone https://github.com/hpreston/netdevops_demos cd netdevops-security -
Create a Python Virtual Environment (if you aren't already in one).
python3 -v venv venv source venv/bin/activate -
Install the Python libraries used in the demo.
pip install -r requirements.txt
To run the secret_vault/vault_netbox_demo.py you'll need to have a local development instance of NetBox up and running. Luckily it is pretty easy to start an instance of NetBox right on your laptop as long as you've docker installed.
Instructions from netbox-community/netbox-docker on GitHub
-
Open a new terminal window or tab, and clone down the repo and change into it.
git clone https://github.com/netbox-community/netbox-docker cd netbox-docker -
You can hard set the port for NetBox before running
docker-compose upby opening up thedocker-compose.ymlfile and updating the ports line for nginx to set a specific port. For example here it is set to use port8081locally. You'll want to do this so the NetBox URL from the examples match.nginx: command: nginx -c /etc/netbox-nginx/nginx.conf image: nginx:1.15-alpine depends_on: - netbox ports: - 8081:8080
-
Start up netbox with docker-compose
docker-compose up -d
- This can take a couple mintues to complete fully.
-
Open up a web browser and navigate to
http://localhost:8081 -
You can login with
admin / adminand look around. You shouldn't see any data just yet. -
Now return to the terminal window for these demos and run the
netbox_sandbox_add.pyscript. This adds the IOS XE Latest Always On device to Netbox with minimal settings.$ python netbox_sandbox_add.py
There won't be any output, but if you check NetBox you should now have a
csr1000v-1device listed.
NACM is part of the NETCONF standard and allows for very granular control over the actions and models users have access to.
-
First, we'll create a new PRIV01 user on the sandbox for our demonstration. We'll go old-school and do this over ssh
☺️ .ssh -p 8181 developer@ios-xe-mgmt-latest.cisco.com # password is C1sco12345 config t username priv1user password C1sco12345 -
Now log in with the user to test and show the privilege.
ssh -p 8181 priv1user@ios-xe-mgmt-latest.cisco.com # password is C1sco12345 show priv ! Output Current privilege level is 1 -
By default, IOS XE NACM policy only allows PRIV15 users to leverage NETCONF. The script
api-access/api_priv_readonly_netconf.pywill apply a new rule that will allowPRIV01users ability to execute<get>RPCs against all models.<nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm"> <rule-list> <name>only-get</name> <group>PRIV01</group> <rule> <name>deny-edit-config</name> <module-name>ietf-netconf</module-name> <rpc-name>edit-config</rpc-name> <access-operations>exec</access-operations> <action>deny</action> </rule> <rule> <name>allow-get</name> <module-name>ietf-netconf</module-name> <rpc-name>get</rpc-name> <access-operations>exec</access-operations> <action>permit</action> </rule> <rule> <name>allow-models</name> <module-name>*</module-name> <access-operations>read</access-operations> <action>permit</action> </rule> </rule-list> </nacm>
The script will first print the starting NACM rules. You can re-run the script to see the changes, re-applying over and over is fine.
-
Run the script to apply the changes.
python api-access/api_priv_readonly_netconf.py
-
Let's see if it works. Run
api-access/api_priv_demo1.pywhich is a script that will use NETCONF to retrieve (<get>) the list of interfaces from the sandbox.The script will prompt for the user to test with. Use the
priv1user / C1sco12345user we setup.python api-access/api_priv_demo1.py
- This works, as expected from the NACM rules.
-
Now let's try an edit operation. Run
api-access/api_priv_demo2.pywhich is a script to use NETCONF to add (<edit-config>) a loopback interface to the router.The script will prompt for the user to test with. Use the
priv1user / C1sco12345user we setup.python api-access/api_priv_demo2.py
#Output Network Username? priv1user Network Password? Let's create a new loopback interface. What loopback number? 89 What interface description? New Loopback by Priv01 User What IP address? 172.16.100.1 What subnet mask? 255.255.255.255 There was an error (access-denied) with your transaction.
- Okay, that didn't work (we didn't expect it to)
-
Now re-run that script, but use the admin account of
developer / C1sco12345.$ python api-access/api_priv_demo2.py
# Output Network Username? developer Network Password? Let's create a new loopback interface. What loopback number? 199 What interface description? New Loopback by Priv15 user What IP address? 172.32.255.1 What subnet mask? 255.255.255.255 NETCONF RPC OK: True- That worked!
-
Re-run
python api-access/api_priv_demo1.pyaspriv1userand see that you can see the new interface as a low priv user!
Secrets are anything you don't want everyone to know (pretty obvious huh). Let's see some ways you can keep them more secure.
First let's checkout environment variables in an interactive python setup.
-
First, let's setup some environment variables. From bash, run this to setup 4 environment variables.
export SBX_ADDRESS=ios-xe-mgmt-latest.cisco.com export SBX_NETCONF_PORT=10000 export SBX_USER=developer export SBX_PASS=C1sco12345
-
Now start up
ipython. -
Now in python, run these commands to see how the
oslibrary provides easy access to environment variables.# Import the OS library import os # Print out all environment variables os.environ # Get value of relevant device information os.environ["SBX_ADDRESS"] os.environ["SBX_USER"] os.environ["SBX_PASS"]
-
But what about when you ask for an ENV that doesn't exist?
os.getenvis a method that will returnNoneif not found.# See what happens when you try to access an ENVAR that doesn't exist os.environ["SBX_MISSING"] # Use os.getenv to lookup value or return None address = os.getenv("SBX_ADDRESS") missing = os.getenv("SBX_MISSING") # See what values are address missing # You can test if an env_var existed missing is None
-
Now create a couple device dictionaries for potential network devices.
bad_devicetries to use aMISSINpassword.# Creating our Device information from EnvVars device = { "address": os.getenv("SBX_ADDRESS"), "username": os.getenv("SBX_USER"), "password": os.getenv("SBX_PASS"), "netconf_port": os.getenv("SBX_NETCONF_PORT"), } # Creating a device where some infromation not available bad_device = { "address": os.getenv("SBX_ADDRESS"), "username": os.getenv("SBX_USER"), "password": os.getenv("SBX_MISSING"), "netconf_port": os.getenv("SBX_NETCONF_PORT"), }
-
But you can test to make sure you have everything you need to work.
# How we can verify all details exist None in device.values() None in bad_device.values() # Great if check before continuing if None in device.values(): raise Exception("Missing key piece of data to connect to device")
-
With our details, we can now use them to retrieve network info.
# Import our Network Autoamtion libraries from ncclient import manager import xmltodict # Use NETCONF to connect to device and retrieve interface list with manager.connect( host=device["address"], port=device["netconf_port"], username=device["username"], password=device["password"], hostkey_verify=False, ) as m: filter = """ <filter> <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces" /> </filter> """ r = m.get_config("running", filter) # Convert XML data into interface list interfaces = xmltodict.parse(r.xml)["rpc-reply"]["data"]["interfaces"]["interface"] # How many we get? len(interfaces) # Print out and process for interface in interfaces: print(interface["name"])
Manually exporting in a terminal is error prone and time consuming... there is a better way.
-
Make a copy of
src_env.templateassrc_env.cp src_env.template src_env
-
Edit this new file and provide the details for our sandbox. Full output shown.
# Copy file to src_env # Complete with "secrets" # 'source src_env' before running code export SBX_ADDRESS=ios-xe-mgmt-latest.cisco.com export SBX_NETCONF_PORT=10000 export SBX_SSH_PORT=8181 export SBX_RESTCONF_PORT=9443 export SBX_USER=developer export SBX_PASS=C1sco12345
-
Now use
source src_envto setup these environment variables in your active shell. -
Now your scripts can use these variables.
python env_vars/env_var_demo1.py
Ansible can leverage environment variables for secrets as well.
-
Take a look at
env_vars/host_vars/device.yml. Thelookup()filter supports retrieving ENV.--- ansible_connection: network_cli ansible_network_os: ios ansible_host: "{{ lookup('env', 'SBX_ADDRESS') }}" # Statically setting port due to Jinja only returning strings # See issue: https://github.com/ansible/ansible/issues/30366 # Prefer to use: "{{ lookup('env', 'SBX_SSH_PORT') | int }}" ansible_port: 8181 ansible_user: "{{ lookup('env', 'SBX_USER') }}" ansible_password: "{{ lookup('env', 'SBX_PASS') }}" ansible_become: yes ansible_become_method: enable ansible_become_password: "{{ lookup('env', 'SBX_PASS') }}"
Note: due to the way Jinja works, convering the SSH port to an integer wasn't straightforward so it's statically set in this demo.
-
Run the playbook and see that all works as expected.
cd env_vars ansible-playbook -i ansible-hosts env_var_ansible_demo.yml
Genie/pyATS can pull details from environment variables within testbed files.
-
Take a look at
env_vars/genie-testbed.yaml."%ENV{SBX_PASS}"pulls the details into the testbed dynamically.--- testbed: name: netdevops-security tacacs: username: "%ENV{SBX_USER}" passwords: tacacs: "%ENV{SBX_PASS}" enable: "%ENV{SBX_PASS}" devices: csr1000v-1: os: iosxe type: iosxe connections: defaults: class: unicon.Unicon ssh: protocol: ssh ip: "%ENV{SBX_ADDRESS}" port: "%ENV{SBX_SSH_PORT}"
-
Run the
env_vars/env_var_genie_demo.pyto put this testbed to work.You'll need to be in the
env_varsdirectory .python env_var_genie_demo.py
Another option for secrets is to encrypt them into a secure file that can be safely included in source control. Ansible includes ansible-vault which can do this for you.
-
Move into the
secret_vaultdirectory within this demo folder. -
Take a look at
host_vars/device.insecure.yaml. This file contains clear text versions of all the secrets.Note: normally you would NEVER include the insecure version of a file in a repository, but would include the secured version. This demonstration is about how to do the encryption so it's a bit backward here... 🤔
--- ansible_connection: network_cli ansible_network_os: ios ansible_host: ios-xe-mgmt-latest.cisco.com ansible_port: 8181 ansible_user: developer ansible_password: C1sco12345 ansible_become: yes ansible_become_method: enable ansible_become_password: C1sco12345
-
Now use
ansible-vaultto encrypt this file asdevice.yml. You'll be prompted for a password to use for the encryption. Don't forget what you provide, you'll need it to use the encrypted data.ansible-vault encrypt --output=host_vars/device.yml \ host_vars/device.insecure.yml New Vault password: Confirm New Vault password: Encryption successful
-
Take a look at the new
host_vars/device.ymlfile. This is encrypted and now safe to store in source control.$ANSIBLE_VAULT;1.1;AES256 36653965633365323237656639373032356462383232386662396231353430613131616265376366 3735623763623835343666613637663537656162643236350a343865306339333865633161636231 33763623835343666613637663....- Now that you've encrypted your secrets, you can use them when running a playbook.
ansible-playbook -i ansible-hosts \ --ask-vault-pass vault_ansible_demo.yml Vault password:
NetBox is an open source DCIM and IPAM tool that is becoming very popular with network automation engineers and enterprises looking for a powerful and programmable Source of Truth for their automation. NetBox can store secrets along with other device information.
Be sure you've setup NetBox as described above before continuing.
We'll start with taking a look at storing secrets in NetBox using the GUI.
-
Log into http://localhost:8081 as
admin / admin. -
Navigate to
Devices > Devicesand select thecsr1000v-1. -
Click the button to + Add secret.
-
NetBox uses public/private keys to secure keys. You'll be presented with a screen to add a new key.
-
Click +Create a User Key. Click the button to Create new keypair.
-
Create a new file in the
secret_vaultdirectory calledid_rsaand copy the contents of the Private Key to this file. Repeate with new fileid_rsa.puband the Public Key.IMPORTANT TO COPY THESE KEYS BEFORE CLICKING
I have saved my new private key. IF YOU DON'T, YOU WON'T BE ABLE TO GET THE PRIVATE KEY AGAIN. -
After saving the key files locally, click the I have saved my new private key button. Then click Save on the User Key page.
-
Great, you've now setup the keys so you can begin adding Secrets to NetBox.
-
Navigate back to
Devices > Devicesand select thecsr1000v-1. -
Click +Add secret again.
-
Choose "General" as the role and name the secret
username. Provide a Plaintext value ofdeveloperand confirm (it will be hidden).The secret role of "General" was created by the initial baseline script that you ran before starting.
-
Click Create
-
You'll be prompted to Enter your private RSA Key. While you provide the public key already, in order to add (or view) keys you need to authenticate properly with the private key.
-
Open up the
id_rsafile that is your private key, and copy/paste the data into the window. Click Request session key. -
You should get a success message like this.
-
Now click Create and Add Another. Add two more secrets of:
- Name:
passwordPlaintext:C1sco12345 - Name:
netconf_portPlaintext:10000
- Name:
-
After added, return to
Devices > Devicesand select thecsr1000v-1. You should now see your secrets listed. Click Unlock to view them in clear text.If you log out and back in, you'll need to provide your private key again to unlock or add new secrets.
Now that we have our secrets stored, let's use them in our script.
-
Take a look at the file
secret_vault/vault_netbox_demo.py. This is a Python script that will retrieve the plaintext values of the secrets from NetBox for the device and use them with a NETCONF request. -
This part of the script show's how the
pynetboxlibrary can be used to interact with NetBox.# Create a NetBox API connection nb = pynetbox.api(nb_url, token=nb_token, private_key_file=private_key_file) # Retrieve device details nb_device = nb.dcim.devices.get(name=device_name) # Gather needed connection details for the device # username, password and netconf_port being retrieved from NetBox Secret Store device = { "address": nb_device.primary_ip.address.split("/")[0], "username": nb.secrets.secrets.get( device_id=nb_device.id, name="username" ).plaintext, "password": nb.secrets.secrets.get( device_id=nb_device.id, name="password" ).plaintext, "netconf_port": nb.secrets.secrets.get( device_id=nb_device.id, name="netconf_port" ).plaintext, }
-
Some key details about the code to notice:
- In order to use the API for NetBox, you need to provide a
token. You'll see where to find this in the GUI in the next tep. - If you will be working with secrets, you need to provide your
private_keyorprivate_key_filewhen you create the API object. If you don't plan to access secrets, you can leave this out. - The method
nb.secrets.secrets.get()is used to retrieve a secret. You need to provide adevice_idordevicename, and thenameof the secret you want. Once you have a secret, the.plaintextproperty will be the clear text version of the secret.
- In order to use the API for NetBox, you need to provide a
-
Now let's run the script
secret_vault/vault_netbox_demo.py. When you run it you'll be asked for your device name (csr1000v-1), your API Token, and the Private Key File (id_rsa).-
Find the API key in your "Profile" and "API Tokens".
Note: as a development instance of NetBox, the default API token is always
0123456789abcdef0123456789abcdef01234567. If you are going to use NetBox in production see documentation on how to deploy securely.
$ python vault_netbox_demo.py What is your device name? csr1000v-1 What is your NetBox API Token? Where is your Private Key for Secret Access? id_rsa
-








