Hash Verification Microservice¶
A microservice to validate hashes from device configurations against securely stored passwords.
Motivation and Scope¶
Secrets are sensitive. Their need for secure handling presents challenges to automation. Many network automation platforms exclude managing secrets from their scope, leaving customers to find their own way. In this article, we will mock up a system to securely manage secrets, demonstrating the value of the chosen approach.
Secrets requires special handling in order to maintain their confidentiality:
- Secrets need to be stored securely. This requires a password vault.
- Secrets should only be transmitted over secure communication channels.
- Any system for managing secrets should restrict access to those secrets as much as possible.
The first two requiremens are straightforward. Firstly, the solution must include a password vault. Secondly, any remote API calls need to support SSL. Setting up SSL itself is beyond the scope of this exercise, but can be addressed with a terminating proxy.
To simplify the code base, we will limit our demo's scope to managing local login credentials for Cisco devices. Organizations can extend this approach to apply to other sensitive configuration data, i.e. PSKs for ipsec, RADIUS, or TACACS. The local user credential also presents an interesting use case in that it is not trivial to verify login credentials when TACACS is in use.
Source Code¶
The full source code for this workshop is available on github. Cloning the repository will save a lot of typing (or copy-pasting).
Design¶
Components¶
Vault: a secure password manager. We will use the keyrings.cryptfile module, which stores encrypted passwords on the local filesystem.
Validator: a microservice that recieves a key ID and hash from a client, fetches the referenced password from the vault, and determines whether the hash is valid for the given password. This is the only code that can access the vault.
Client: a client to demonstrate how to use the microservice to validate password hashes from device configurations.
Configuration: the device configuration to validate. Here, we will use a configuration file on the local filesystem.
NSoT: identifies the password that is used for local users. We will maintain those identifiers in a nornir inventory file.
Configuration Validation Sequence¶
sequenceDiagram
autonumber
Client->>Configuration: derive username and hash
Client->>NSoT: request password ID
NSoT->>Client: return password ID
Client->>Validator: send password ID and hash
Validator->>Vault: request password
Vault-->>Validator: return password
Validator-->>Client: return validation status and hash
Implementation¶
We will begin by implementing the validation microservice, which we can validate using curl commands. After that, we will devlop a client script to demonstrate how to consume the service.
The file structure will look like this:
treeView-beta
├── cisco-hash-microservice
│ ├── env.sh
│ ├── initdb.py
│ ├── main.py
│ └── tests.sh
├── secrets-validator
│ ├── configs
│ | ├── r1
│ | └── r2
│ ├── inventory
│ | └── hosts.yaml
│ └── validate-secrets.py
Initialize Vault¶
For the next two sections, we will be working in the cisco-hash-microservice directory.
The microservice fetches passwords from the vault. The vault is implemented with the keyrings.cryptfile module. We need to initialize the vault prior to implementing the microservice.
In a bash shell, run source env.sh to load the keyring configuration into your environment.
Then run the initdb.py script below to initialize the keyring. The keyrings.cryptfile module identifies passwords with a service name and user name. Here, we create a password for the localuser user on the router service. The password itself is weakpassword.
| initdb.py | |
|---|---|
That's it! We now have a password in the vault to verify against.
Implement Microservice¶
Our microservice is implemented using the fastapi web framework. Secrets are stored using keyrings.cryptfile. The cisco_hashgen.cli module provides functions to verify and generate password hashes. We only need to import these dependnciesm and add a few lines of code:
Lines 1-9 import dependencies and initialize the needed objects.
Line 11 instructs fastapi to route GET requests for the server root to the root function.
Lines 12-21 implement the main functionality. It queries the vault for a password identified by the username and service arguments, checks whether the hash argument is valid for the password, and returns the validation result to the client. If the hash is valid, the service returns a status of pass and includes the input hash, unchanged. Otherwise, it returns a fail status and includes a newly-generated, valid hash. We'll see how the client uses these later.
You can now launch the microservice with the fastapi dev command. The service will bind to the localhost IP address, 127.0.0.1, and will automatically reload if main.py changes. For production, use fastapi run.
Tip
Launch fastapi from the directory that contains main.py.
When the service launches, fastapi should produce output similar to:
(venv) jgilby@jsurf11:~/projects/mockups/cisco-hash-microservice$ fastapi dev
FastAPI Starting development server 🚀
Searching for package file structure from directories with __init__.py files
Importing from /home/jgilby/projects/mockups/cisco-hash-microservice
code Importing the FastAPI app object from the module with the following code:
from main import app
app Using import string: main:app
info Configuration sources:
• Import string: entrypoint in pyproject.toml
server Server started at http://127.0.0.1:8000
server Documentation at http://127.0.0.1:8000/docs
tip Running in development mode, for production use: fastapi run
Logs:
INFO Will watch for changes in these directories: ['/home/jgilby/projects/mockups/cisco-hash-microservice']
INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO Started reloader process [232371] using WatchFiles
INFO Started server process [232499]
INFO Waiting for application startup.
INFO Application startup complete.
The microservice is now running and ready to process requests. Use the curl commands in tests.sh to validate that it works as expected:
curl 'http://localhost:8000/?username=localuser&service=router&hash=$9$UK9FYKZUD.n94E$qcLQeaiNaUjVj181Q8Hh2cUya7qdMV4q.qszxl3H0Ha' # should fail, returning new hash
curl 'http://localhost:8000/?username=localuser&service=router&hash=$8$LkGlosq.R44sx.$VLpv7K56GEx6jhU4aMKgsGXvMo1n1EE/fElkbpJXQfY' # should pass, returning the original hash
If the first test fails, and the second test passes, the microservice is functional and we can move to the client.
Initialize Device Configuration and Source of Truth¶
To implement the client side, change to the secrets-validator directory.
The validation script fetches device configurations from the configs directory, and uses inventory/hosts.yaml as its source of truth:
username localuser privilege 15 secret 8 $8$be6NbC1TntHC6E$S8XxiiHUGFtMgA3zyP5Maiq/7FSI3JrbMRTiLVME73A
username localuser privilege 15 secret 8 $8$JOcpQMJFctsq7.$I1UhdjAJbm9zk4T3h2CdQmbyFzFQAxlwncQQD15S.Ck
Verify that these files are in place and proceed with the client implementation.
Implement Client¶
A service isn't much use without a client to consume it. Our client will use the nornir automation framework. Nornir provides an inventory, which we will use to manage the user names configured on each router. It also dispatches tasks per device and aggregates the results from each task and device.
After importing dependencies and initializing nornir, we define four tasks:
get_facts()gathers user names and hashes from the router's configuration.get_truth()computes the intended user names and hashes. This is the point of the client. It fetches intended user names from the inventory. If an intended user exists in the result of get_facts(), we validate the hash.generate_artifact()compares the output of get_facts() with the output of get_truth(). For any discrepancies it builds the commands to deploy the intended configuration to the router.some_task()runs the previous three tasks against a device.
Tip
Notice that some_task() is invoked with nr.run(). This runs the selected task against all hosts in the inventory. On the other hand, we call task.run() to invoke the other tasks from within some_task() and only affects the host that is being processed by the parent task.
Finally, we instruct nornir to run some_task() against every device in the inventory and print the result. Here is the expected output from running the client:
some_task***********************************************************************
* r1 ** changed : False ********************************************************
vvvv some_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- get_facts ** changed : False ---------------------------------------------- INFO
{'localuser': '$8$be6NbC1TntHC6E$S8XxiiHUGFtMgA3zyP5Maiq/7FSI3JrbMRTiLVME73A'}
---- get_truth ** changed : False ---------------------------------------------- INFO
{'localuser': '$8$pu68dNxCpIJgLU$.Gb4fYbUh1n6RkfYQHsGckEjsYwdX4jEodvysw3HzzM'}
---- generate_artifact ** changed : False -------------------------------------- INFO
[ 'username localuser privilege 15 secret 8 '
'$8$pu68dNxCpIJgLU$.Gb4fYbUh1n6RkfYQHsGckEjsYwdX4jEodvysw3HzzM']
^^^^ END some_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* r2 ** changed : False ********************************************************
vvvv some_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- get_facts ** changed : False ---------------------------------------------- INFO
{'localuser': '$8$JOcpQMJFctsq7.$I1UhdjAJbm9zk4T3h2CdQmbyFzFQAxlwncQQD15S.Ck'}
---- get_truth ** changed : False ---------------------------------------------- INFO
{'localuser': '$8$JOcpQMJFctsq7.$I1UhdjAJbm9zk4T3h2CdQmbyFzFQAxlwncQQD15S.Ck'}
---- generate_artifact ** changed : False -------------------------------------- INFO
^^^^ END some_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The script produces two aggregated results, one for each router. The aggregated result for r1 shows us that the results of get_facts and get_truth do not match, so the script build a configuration artifact. For r2, however, get_facts and get_truth do match, so no configuration artifact is needed.
Conclusion¶
The hash verification microservice effectively limits the exposure of managed secrets while facilitating the validateion of password hashes stored in device configurations. While not suitable for production, this exercise proves the premise's feasability and value. We will add functionality to this system in upcoming articles.
Happy automating!