Environment Variables vs Secrets in Python

One of the most frustrating challenges in application development today is environment parity. While recent years have shown that virtualization and containerization tools like Vagrant and Docker (and ActiveState’s own State Tool) can ensure that the operating systems and dependencies that power an application’s infrastructure are consistent between environments, external dependencies are all-but-impossible to replicate in a non-production environment.

In a world where third-party dependencies have become irreversibly intertwined with the core business logic of nearly every software project, it is becoming increasingly difficult to keep development, staging, and production environments consistent with one another. While some products with self-hosted lookalikes – such as Minio to Amazon S3 – ease the pain of managing multiple deployments of a single service, it does little to ease the challenge of configuration management across environments.

When Production, Staging, Development, Bob’s Local Machine, and the QE Environment all require a different deployment of a given service, passing configuration files back and forth, or relying on complex conditionals to determine which deployment a given environment should speak to, becomes increasingly impractical. The need for flexibility also highlights the difficulty of these solutions in a large-scale environment. What if, for example, Bob’s Local Machine needs to temporarily speak to a service that is reserved for the Staging environment?

Environment Variables in Python

Cross-environment configuration is a pain, but thankfully the problems outlined above have become common enough in recent years that they are all-but-solved. Thanks to the support of the open source community, and the evangelism of best-practices like the 12 Factor Application, there has been a shift in recent years from file-based application configuration management to environment variable-based configuration management.

Available in every major OS, environment variables are – just as their name implies – variables that are implemented at the environment level to define the way applications running underneath it should behave. In simpler terms, they are application-agnostic variables that are managed outside the context of a given process. The primary advantage to using environment variables is that this allows developers to define how an application should run without changing a line of code.

For example, if Bob’s Local Machine needs to speak to the CDN service that is reserved for the Staging environment, he may change the CDN_URL environment variable to reflect what is defined in Staging without having to touch any managed code. More importantly, this allows Bob to define mock or internal services for use with unit and integration tests, all without having to write a single line of extra code.

Defining Environment Variables

While the act of defining environment variables is generally OS-dependent, the vast majority of programming languages have abstracted away these differences through the use of development packages like Python’s dotenv project. For instance, rather than having to define an API_USER environment variable at the OS level, a locally gitignored .env file can maintain environment variables across dev environments. This allows developers to utilize a locally-managed config file for setting environment variables, while at the same time enabling configuration via “true” environment variables in non-development environments.

As an example, here’s a simple .env file that a developer may use in their local environment:

APP_ENVIRONMENT=local
APP_NAME=localhost

QUEUE_DRIVER=sync

API_USER=bob

On the flip side, the Production environment may define its environment variables within a Dockerfile, both of which are valid methods for maintaining environment variables.

Retrieving Environment Variables

Regardless of how environment variables are defined, they can always be retrieved in Python using the os.getenv() method:

import os

# Get environment variables
USER = os.getenv('API_USER')

Take note that, in the event that the environment variable is undefined, the value will default to None.

Getting Started with Secrets

Now, while environment variables are an excellent solution for managing disparate configurations across environments, they aren’t a silver bullet. In today’s development climate, security must be a top priority, and sensitive data must be kept in a secure manner.

Unfortunately, environment variables on their own are not secure. While they do a great job of storing configuration data, the way in which we define more sensitive data like passwords, API keys, and encryption keys should require more care. This is where secrets come into play. Encrypted at rest, secrets should only be retrieved in a single runtime as needed in order to reduce the odds of a data breach. In this way, even if your hosting provider gets compromised, you can rest assured that your sensitive secrets are locked up tight.

Creating & Managing Secrets with the State Tool

So, how do we create and manage secrets? While there are a number of different ways to tackle this problem, ActiveState’s State Tool is an excellent solution for the Python language. Similar to virtualenv or pipenv, the State Tool is a virtual environment management interface that will prevent cross-contamination of Python installations and configurations between projects. What sets it apart from other virtual environment management tools is its integration with the ActiveState platform, allowing for a central interface for managing environment configurations and, yes, secrets.

Before we can take advantage of the State Tool’s secrets management capabilities, we first need to use the State Tool to set up a virtual environment within our project directory. To do this, first identify the ActiveState project you’ll be working with (for this tutorial, you can use my project zachflower/envs-vs-secrets-demo as long as you have a free ActiveState Platform account, or you can create your own). Then, execute the state activate command for your given project:

$ state activate zachflower/envs-vs-secrets-demo
 Where would you like to checkout zachflower/envs-vs-secrets-demo? 
/home/zach/Projects/miscellaneous/activestate-variables/zachflower/envs-vs-secrets-demo
Activating state: zachflower/envs-vs-secrets-demo
The State Tool is currently in beta, we are actively changing 
and adding features based on developer feedback.
Downloading required artifacts
Downloading  1 / 1    
Installing   0 / 1 [-----------------------------------------]   0 %
You are now in an 'activated state', this will give you a virtual 
environment to work in that doesn't affect the rest of your system.

Your 'activated state' allows you to define scripts, events and 
constants via the activestate.yaml file at the root of your project 
directory.

To expand your language and/or package selection, or to define 
client-side encrypted secrets, please visit 
https://platform.activestate.com/zachflower/envs-vs-secrets-demo.

To try out scripts with client-side encrypted secrets we've created 
a simple script for you in your activestate.yaml, try it out by 
running 'helloWorld'

As you can see, the activate command sets up the virtual environment as defined in your ActiveState project. In the event that the project has not yet been configured, a simple project will be seeded with default parameters to provide you with an example of how everything should go together. This configuration is stored in a file at the root of your project directory called activestate.yaml

Configuration File for Secrets & More

The activestate.yaml file defines a development runtime under which your application will run. For example, the default one defines a simple script that utilizes a secret, as well as a few event listeners that perform actions whenever a defined event occurs:

project: https://platform.activestate.com/zachflower/envs-vs-secrets-demo
scripts:
# This script uses a secret. Note that you can define your own secrets at
# https://platform.activestate.com/zachflower/envs-vs-secrets-demo/scripts
  - name: helloWorld
    value: echo ${secrets.user.world}
events:
  # This is the ACTIVATE event, it will run whenever a new virtual 
environment is created (eg. by running `state activate`)
  # On Linux and macOS this will be ran as part of your shell's rc 
file, so you can use it to set up aliases, functions, environment 
variables, etc.
  - name: ACTIVATE
     constraints:
     os: macos,linux
     value: |
   echo "You are now in an 'activated state', this will give you a 
virtual environment to work in that doesn't affect the rest of your 
system."
   echo ""
   echo "Your 'activated state' allows you to define scripts, events 
and constants via the activestate.yaml file at the root of your 
project directory."
   echo ""
   echo "To expand your language and/or package selection, or to 
define client-side encrypted secrets, please visit 
https://platform.activestate.com/zachflower/envs-vs-secrets-demo."
   echo ""
   echo "To try out scripts with client-side encrypted secrets we've 
created a simple script for you in your activestate.yaml, try it out 
by running 'helloWorld'"

While there’s nothing particularly groundbreaking here, the potential power of this file should be immediately obvious. For example, the following script provides a basic example of how to utilize secrets in the configuration file:

scripts:
# This script uses a secret. Note that you can define your own secrets at
# https://platform.activestate.com/zachflower/envs-vs-secrets-demo/scripts
  - name: helloWorld
     value: echo ${secrets.user.world}

When the helloWorld command is executed (from within an activated state), it will echo the value of the user.world secret and, in the event that secret is not yet defined, will prompt for a value:

$ helloWorld

The action you are taking uses a secret that has not been given a 
value yet.
Name: world
Description: - (This secret has no description, you can set one via 
the web dashboard)
Scope: user (Only you can access the value)

 Please enter a value for secret "world": ******

Using Secrets

Although the example is relatively simplistic, the true value of secrets comes from the activation events. Instead of retrieving a secret value within the context of a script, environment variables can be defined using retrieved secrets without ever exposing those secrets outside of this secure environment:

project: https://platform.activestate.com/zachflower/envs-vs-secrets-demo?commitID=5fd1c161-c5a4-480c-8aba-29d8ab361b42
events:
  - name: ACTIVATE
     constraints:
     os: macos,linux
     value: |
     export WORLD=${secrets.user.world}

Now, if the user.world secret is defined, then the WORLD environment variable will be defined and can be retrieved like any other environment variable:

$ echo $WORLD
world!

However, if it is not defined, then the user will be prompted to define it upon activation of the ActiveState virtual environment:

The action you are taking uses a secret that has not been given a 
value yet.
Name: hello
Description: - (This secret has no description, you can set one via 
the web dashboard)
Scope: user (Only you can access the value)

 Please enter a value for secret "world": ******

Pretty cool, right?

Taking it Further

Configuration management in application development is, for the most part, a solved issue, but secrets management just isn’t yet. The number of projects that have checked sensitive data into a version control repository is staggering, and is something even highly respected companies have done, but through the use of proper security hygiene and products like ActiveState’s State Tool, keeping sensitive configuration data safe and secure is becoming easier by the day.

Related Blogs:

The Secret to Managing Shared Secrets

Developers can Share Secrets Quickly and Easily without Sacrificing Security

Zachary Flower

Zachary Flower

Zachary Flower (@zachflower) is a Fixate IO Contributor, principal engineer at Automox—a Boulder-based patch management company—and freelance writer. With a passion for simplicity and usability within the development pipeline, Zach puts a strong emphasis on the importance of documentation, developer productivity, and shift-left testing strategies.