Python Managing Credentials and Secrets

Python Managing Credentials and Secrets
Table of Contents

If you look back at every Netmiko script we have written in this series, you will notice something (very ) uncomfortable, the username and password are sitting right there in the script as plain text. That has been fine for learning, but it is not something you want to do in real scripts.

In this part, we will look at safer ways to manage credentials in your scripts. We will cover a few different approaches, from the simplest (asking the user to type the password when the script runs) to the more common patterns used in real projects (environment variables and .env files). We will also touch on .gitignore

There is no single "correct" way to manage credentials. Different projects and different environments call for different approaches. What matters is that you understand the trade-offs and stop hardcoding passwords into your scripts.

The Problem with Plain Text Credentials

Let's start by being clear about why this matters. Take a look at a script you have written in the last couple of chapters.

device = {
    "device_type": "cisco_ios",
    "host": "192.168.202.101",
    "username": "admin",
    "password": "admin"
}

This looks harmless on your own machine, but think about everything that could happen to this file.

  • You share it with a colleague to help debug something. They now have your credentials.
  • You commit it to a Git repository, even a private one. Anyone with access to the repo can see the credentials, forever, even if you delete the line later. Git history remembers everything.
  • You paste a snippet into a chat, a support ticket, or a Stack Overflow question. Your credentials are now in someone else's inbox or on the public internet.
  • Your machine gets compromised. The attacker does not have to work hard to find your device credentials, they are sitting in your scripts.

Credentials get leaked this way all the time, and it happens to experienced developers too. The safest habit is to just never put them in the script in the first place.

Option 1 - Prompting the User with getpass

The simplest way to keep credentials out of your script is to ask the user for them when the script runs. You already know one way to do this, the input() function from Chapter 3. But there is a problem, input() shows what the user types, which is fine for a hostname or a VLAN ID but not for a password. You do not want the password appearing on the screen, especially if someone is watching over your shoulder or if the terminal is being recorded.

Python has a built-in module for exactly this called getpass. It works just like input(), but the characters the user types are hidden.

from getpass import getpass

username = input("Username: ")
password = getpass("Password: ")

print(f"Got username: {username}")
print(f"Password length: {len(password)}")

Output

Username: admin
Password: 
Got username: admin
Password length: 5

Notice that when you type the password, nothing appears on the screen at all. getpass deliberately hides the input.

We do not print the password itself in the example (that would defeat the purpose), we just print its length to prove that Python did receive the input.

Using getpass with Netmiko

Now let's plug this into a Netmiko script. Instead of hardcoding the credentials into the dictionary, we ask for them at the start.

from getpass import getpass
from netmiko import ConnectHandler

username = input("Username: ")
password = getpass("Password: ")

device = {
    "device_type": "cisco_ios",
    "host": "192.168.202.101",
    "username": username,
    "password": password
}

connection = ConnectHandler(**device)
output = connection.send_command("show ip interface brief")
print(output)
connection.disconnect()

The script prompts you for the username and password when it starts, then uses those values to build the device dictionary.

This is really useful for one-off scripts, quick tools, or situations where you would rather type the password each time than store it anywhere. The downside is that you have to type it every single time, which does not scale well when you want to schedule a script to run automatically for example.

Option 2 - Environment Variables

An environment variable is a value that lives in your shell environment, outside of your Python script. Your operating system already uses environment variables for things like PATH (which tells the shell where to find programs), HOME (your home directory), and USER (your username). You can also set your own, and your script can read them just like any other value.

You set your username and password as environment variables in your shell, and your script reads them from there. The credentials never live inside the script file itself, so there is nothing sensitive to accidentally commit or share.

Setting Environment Variables

On macOS, Linux, or WSL, you set an environment variable in your terminal like this.

export NET_USERNAME="admin"
export NET_PASSWORD="admin"

These are only active in the current terminal session. If you close the terminal or open a new one, they are gone. That is either a feature or a limitation depending on how you look at it. For sensitive things like credentials, it is arguably a good thing that they do not stick around.

If you want them to persist across sessions, you can add these lines to your shell's config file (~/.zshrc, ~/.bashrc, or similar). But keep in mind that anything you add there is stored in a file on your machine, so it is only slightly better than having them in a script. You want to be careful about which config file you put them in and who has access to your machine.

On Windows, the command is slightly different.

set NET_USERNAME=admin
set NET_PASSWORD=admin

Or in PowerShell.

$env:NET_USERNAME = "admin"
$env:NET_PASSWORD = "admin"

Reading Environment Variables in Python

Python's os module (which we mentioned briefly back in Chapter 7) has a way to read environment variables. The most common approach is os.environ.get().

export NET_USERNAME="admin"
export NET_PASSWORD="admin"
import os

username = os.environ.get("NET_USERNAME")
password = os.environ.get("NET_PASSWORD")

print(f"Got username: {username}")
print(f"Password length: {len(password)}")

Output

Got username: admin
Password length: 5

os.environ.get() works just like the .get() method on a dictionary, which we covered in Chapter 4. If the environment variable exists, you get its value. If it does not exist, you get None.

You can also pass a default value as a second argument.

import os

username = os.environ.get("NET_USERNAME", "guest")
print(username)

If NET_USERNAME is not set, this returns "guest" instead of None. That is useful when you want a sensible fallback, but for credentials, you usually want to fail loudly if the variable is missing rather than silently use a wrong value.

Using Environment Variables with Netmiko

Plugging this into a Netmiko script is straightforward.

import os
from netmiko import ConnectHandler

username = os.environ.get("NET_USERNAME")
password = os.environ.get("NET_PASSWORD")

if not username or not password:
    print("Error: NET_USERNAME and NET_PASSWORD must be set")
    exit(1)

device = {
    "device_type": "cisco_ios",
    "host": "192.168.202.101",
    "username": username,
    "password": password
}

connection = ConnectHandler(**device)
output = connection.send_command("show ip interface brief")
print(output)
connection.disconnect()

At the top, we read both environment variables. If either one is missing, the script prints an error and exits before trying to connect.

exit(1) stops the script immediately with a non-zero exit code, which by convention means "something went wrong." This is useful when scripts are being run automatically, because the calling process can detect that something failed.

The Trade-offs

Environment variables are a huge improvement over hardcoding credentials. Your script file no longer contains anything sensitive, which means you can safely share it, commit it to Git, or paste snippets of it into a chat. The credentials only exist in the shell environment where you actually run the script.

The downside is that you have to set them every time you open a new terminal, or manage them in your shell config file. This gets annoying, especially if you have several projects with different credentials. That is where .env files come in, which is what we will look at next.

Option 3 - A .env File with python-dotenv

Environment variables are great, but manually setting them every time you open a new terminal can be annoying. And if you work on multiple projects, each with its own set of credentials, keeping track of which variables belong to which project becomes a headache.

The common solution is a .env file. It is just a plain text file that lives in your project folder and holds your environment variables. A Python library called python-dotenv reads that file when your script starts and loads its contents into the environment automatically. Your script then reads the variables the same way as before, with os.environ.get().

Installing python-dotenv

python-dotenv is an external library, so you need to install it into your virtual environment.

pip install python-dotenv

Creating a .env File

Create a file called .env in the same folder as your script. The filename starts with a dot, which is important, that is what makes it a hidden file on Linux and macOS by convention.

NET_USERNAME=admin
NET_PASSWORD=admin

You do not need quotes around the values, but you can add them if the value contains spaces or special characters.

Loading the .env File in Python

At the top of your script, you import and call load_dotenv(). This reads the .env file in the current directory and loads its contents into the environment, just as if you had exported them yourself.

from dotenv import load_dotenv
import os

load_dotenv()

username = os.environ.get("NET_USERNAME")
password = os.environ.get("NET_PASSWORD")

print(f"Got username: {username}")
print(f"Password length: {len(password)}")

Output

Got username: admin
Password length: 5

That is it. load_dotenv() finds the .env file, reads it, and adds each variable to os.environ. From that point on, os.environ.get() works exactly the same as it did in the previous section.

If the .env file does not exist, load_dotenv() does nothing and does not raise an error. It just silently moves on. It lets your script fall back to real environment variables in production while still using the .env file during development.

Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.