One thing I struggled a long time with is the following:
How do we code our network while securely handling our device credentials? How do we do this in a way that is highly collaborative?
Here’s one issue that I ran into. It is easy to get roped into baking your credentials into a script (completely guilty here). But what happens when it’s time to deliver your code to a colleague, or even an external customer? You will need to refactor your code to deal with the AAA credentials that are displayed (plaintext) in your code.
With python and GnuPG, we can securely deal with device credentials in sharable code.
One of my favorite parts about this strategy is thinking about the extensibility of GnuPG….particularly with its ability send and receive secure messages. This post won’t dive into that much. Instead we’ll stick to the following objectives:
- Install GnuPG, the associated python libraries, and generate keys.
- Build an encrypted credentials file in yaml or json.
- Use python to interface with your keys and securely load your credentials.
Ok… that was highly summarized..let’s get into the details:
Installing gpg via brew…there is more chatter in real life, but this is a blog:
1 2 3 4 5 6 7 8 |
$ xcode-select --install $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" $ brew doctor Your system is ready to brew. $ brew install gpg |
Installing the required python libraries.
1 2 3 |
$ sudo easy_install pip $ sudo pip install python-gpg |
Generating keys…Please read the entire section before starting.
This step generates a public and private key, in the .gnupg folder. When you proceed to using this in code, you encrypt with the specified users public key, and decrypt with your own private key.
Run this command and follow the self explanatory prompts. Be advised that not generating a passphrase is less secure. In this scenario I’m treating my keys like ssh rsa keys and giving them file permissions of 600.
1 2 3 |
$ $gpg --gen-key $ |
Cool…lets play with gnupg in the interpreter:
We specify our .gnupg location and begin to interact with our keys:
1 2 3 4 5 6 7 8 9 10 11 |
$ python Python 2.7.10 (default, Oct 6 2017, 22:29:07) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import gnupg >>> >>> gpg = gnupg.GPG(gnupghome='/Users/simsondm/.gnupg') >>> >>> gpg.list_keys() [{'origin': u'0', 'cap': u'scESC', 'subkeys': [[u'7D48010AC98F2356', u'e', u'734616DF402948BACFB178E67D48010AC98F2356']], 'sigs': [], 'subkey_info': {u'7D48010AC98F2356': {'origin': 'unavailable', 'dummy': u'', 'updated': u'', 'keyid': u'7D48010AC98F2356', 'hash': u'', 'uid': u'', 'expires': u'1610070480', 'curve': u'', 'flag': u'', 'length': u'2048', 'ownertrust': u'', 'sig': u'', 'algo': u'1', 'compliance': u'23', 'date': u'1546998480', 'trust': u'u', 'type': u'sub', 'cap': u'e', 'token': u'', 'issuer': u''}}, 'trust': u'u', 'issuer': u'', 'ownertrust': u'u', 'token': u'', 'sig': u'', 'type': u'pub', 'updated': u'', 'hash': u'', 'expires': u'1610070480', 'flag': u'', 'fingerprint': u'20041D5DF00676FA83278BFCAE3DB80A68069FDB', 'date': u'1546998480', 'dummy': u'', 'keyid': u'AE3DB80A68069FDB', 'uids': [u'Timothy Simson <simsontj@yahoo.com>'], 'compliance': u'23', 'curve': u'', 'length': u'2048', 'algo': u'1'}] >>> |
Lets encrypt some stuff. We setup a string to encrypt and perform the encryption with the gpg.encrypt() function. We also have ways to make sure the encryption worked, and see the encrypted object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
>>> >>> unc = 'This is a cool test of encryption' >>> >>> enc = gpg.encrypt(unc, 'simsontj@yahoo.com') >>> >>> enc.status 'encryption ok' >>> >>> print enc -----BEGIN PGP MESSAGE----- hQEMA31IAQrJjyNWAQf/WqyJHdwJ5gBePahCwqd/dKaSXFnHYgppdHkf9J7Iygwp yz9gY0I1tKhmADJp6zsMBGzh0vFdotswk21BzEAkzpXzmFTaK64TF5gFfhEHeoim i7ZRuPRVzHUm7+ayrpexKyZjEbThmWJmTIQVt1+jAQKAb+I7i9qYqdkmHaUL5FUf DvhNKwMFK+VltMhLxQs+IEa+IZTp4pbA+pWfPwhc9lwUFQrTtEeWc6Jzx0BaC2xR kkas5D5oVT2vJidpu3Dsv2Ydt1RWOz9mbP269W6XDfQTHC5xqfeuyVOF7NIx2b76 i0F7imBmQtbIeklDjljwXePP/Lhde1PCX6VpIyydRtJaAWYxG/suh92TzodPqLr5 IcS2MO5P+EzwgTRPg0YAfefe9JR4dh2b7WaiHZSsYXyCkYY5kmV0bwZs8XTVTNBt 3jOKP81Ymc3yjcci3DjxdtO9gmUY/XmLDF6C =7chC -----END PGP MESSAGE----- >>> |
Yes this is an object!
1 2 3 4 |
>>> >>> enc <gnupg.Crypt object at 0x105a75590> >>> |
That means you have to convert to a string with the str() function to decrypt it…you guessed it, that’s next:
1 2 3 4 5 6 |
>>> >>> test_unc = gpg.decrypt(str(enc)) >>> >>> print test_unc This is a cool test of encryption >>> |
Ok, we have gnupg working in python and bash. How do we automate our network credentials?
First we need to encrypt credentials_file.txt from bash:
Here is the credentials file:
1 2 3 |
--- cisco_user: ciscoUsername cisco_pass: ciscoP4ssword |
Here’s how we encrypt it.
1 2 3 |
$ $ gpg --output credentials_file.gpg --encrypt --recipient blake@cyb.org credentials_file.txt $ |
And here is our python…we made it!
There might be a lot to look at below, but look at the “Decrypt/Load credentials” section. We’re automating our network credentials securely! The creds are loaded and used by the connection handler…in code that’s shareable.
This script deploys a new vlan to a datacenter ethernet fabric, and ensures the new vlan is available via 802.1q tag to a pre-specified Vmware cluster.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
import os import gnupg import yaml from jinja2 import Environment import argparse import sys import json import re from netmiko import ConnectHandler ### Take command line arguments, VLAN_ID, and VLAN_NAME. parser = argparse.ArgumentParser() parser.add_argument("VLAN_ID", help="Required. Vlan ID.") parser.add_argument("VLAN_NAME", help="Required. Name of the vlan.") args = parser.parse_args() ### Decrypt/Load credentials .yml file using gnupg creds_list = [] gpg = gnupg.GPG(gnupghome='/Users/tsimson/.gnupg') creds_stream = open('credentials_file.gpg', 'rb') creds_decrypt = gpg.decrypt_file(creds_stream) creds_gen = yaml.load_all(str(creds_decrypt)) for i in creds_gen: creds_list = i ### Establish switch inventory datacenter_nexus_1_2 = [ {"name": "DataCenter_Nexus-1", "ip": "172.38.99.37"}, {"name": "DataCenter_Nexus-2", "ip": "172.38.99.38"} ] # Establish interface inventory. cluster_interface_list = [ {"int_id": "Port-channel15", "int_desc": "FABRIC_UPLINK"}, {"int_id": "Eth132/1/1", "int_desc": "DataCenter_ESX602"}, {"int_id": "Eth132/1/8", "int_desc": "DataCenter_ESX611"}, {"int_id": "Eth132/1/9", "int_desc": "DataCenter_ESX607"}, {"int_id": "Eth132/1/11", "int_desc": "DataCenter_ESX604"}, {"int_id": "Eth132/1/15", "int_desc": "DataCenter_ESX605"}, {"int_id": "Eth132/1/16", "int_desc": "DataCenter_ESX610"}, {"int_id": "Eth133/1/1", "int_desc": "DataCenter_ESX601"}, {"int_id": "Eth133/1/8", "int_desc": "DataCenter_ESX609"}, {"int_id": "Eth133/1/9", "int_desc": "DataCenter_ESX608"}, {"int_id": "Eth133/1/10", "int_desc": "DataCenter_ESX612"}, {"int_id": "Eth133/1/2", "int_desc": "DataCenter_ESX614"}, {"int_id": "Eth135/1/10", "int_desc": "DataCenter_ESX602"}, {"int_id": "Eth133/1/3", "int_desc": "DataCenter_ESX603"}, {"int_id": "Eth133/1/16", "int_desc": "DataCenter_ESX606"}, {"int_id": "Eth135/1/3", "int_desc": "DataCenter_ESX607"} ] ### Run tests to ensure environment is ready for automated configurations. for i in datacenter_nexus_1_2: net_connect = ConnectHandler(device_type="cisco_nxos", ip=i['ip'], username=creds_list['cisco_user'], password=creds_list['cisco_pass']) existing_vlan = net_connect.send_command('show vlan | in "^' + args.VLAN_ID + ' "') print 'show vlan | in "^' + args.VLAN_ID + ' "' if existing_vlan: print "Vlan already exists...aborting" quit() else: print "No vlan conflict detected...proceeding" ### Establish vlan configuration template vlan_config_template = """ vlan {{ VLAN_ID }} name {{ VLAN_NAME }} """ ### Establish interface configuration template. interface_config_template = """ interface {{ INTERFACE_ID }} switchport trunk allowed vlan add {{ VLAN_ID }} """ ### Function to configure vlan on nexus switch def configure_doc_nexus_vlan(device_name, ip, username, password, vlan_id, vlan_name): print "Connecting to " + device_name + " !!!" net_connect = ConnectHandler(device_type="cisco_nxos", ip=ip, username=username, password=password) print "Configuring " + device_name + " !!!" vlan_config_return = net_connect.send_config_set(Environment().from_string(vlan_config_template).render(VLAN_NAME=vlan_name, VLAN_ID=vlan_id)) print vlan_config_return for i in datacenter_nexus_1_2: configure_doc_nexus_vlan(i["name"], i["ip"], creds_list['cisco_user'], creds_list['cisco_pass'], args.VLAN_ID, args.VLAN_NAME) ### Function to configure cluster interfaces to carry new vlan. def config_doc_nexus_interfaces(device_name, ip, username, password, vlan_id): print "Connecting to " + device_name + " !!!" net_connect = ConnectHandler(device_type="cisco_nxos", ip=ip, username=username, password=password) print "Configuring " + device_name + " !!!" config = '' for i in cluster_interface_list: config = config + Environment().from_string(interface_config_template).render(INTERFACE_ID=i['int_id'], VLAN_ID=vlan_id) interface_config_return = net_connect.send_config_set(config) print interface_config_return for i in datacenter_nexus_1_2: config_doc_nexus_interfaces(i["name"], i["ip"], creds_list['cisco_user'], creds_list['cisco_pass'], args.VLAN_ID) ### Run tests to ensure configuration are sane before saving. ### Envisioning a spanning tree check here. ### Save the configurations. for i in datacenter_nexus_1_2: net_connect = ConnectHandler(device_type="cisco_nxos", ip=i['ip'], username=creds_list['cisco_user'], password=creds_list['cisco_pass']) save_configs = net_connect.send_command('copy run start') |