Environment Variables for Secrets

Learn to manage secrets with environment variables in Python for DevOps automation — practical steps, troubleshooting, and next steps.

Focus: manage secrets with environment variables

Sponsored

Your deployment script has the API key hard-coded, your database password is in the repo, and your CI logs are a secret goldmine. If that sounds familiar, you are one git push away from a breach that brings your infrastructure down. This lesson teaches you how to manage secrets with environment variables — the standard, practical way to keep credentials out of your code while still using them in your Python automation. By the end, you'll read secrets at runtime, keep them out of version control, and know what to do when things go wrong.

The problem this lesson solves

Hard-coding secrets like API tokens, database passwords, and SSH keys directly into Python scripts creates three serious problems:

  1. Secrets leak into version control. Once a secret is committed, it lives in your Git history forever, even if you remove it later. Attackers scan public repos for these patterns 24/7.
  2. Secrets are shared too widely. Everyone with access to the repo — including contractors, CI systems, and sometimes the public — gets the credentials.
  3. Rotating secrets is a nightmare. If you change a password, you must edit code, rebuild containers, and redeploy everything. That's slow and error-prone.

Environment variables solve all three by separating configuration from code. The code reads the value at runtime from the environment, not from a literal in the source file. This is a foundational practice in DevOps — think of it as the difference between writing your password on a sticky note on your monitor versus having a key-card reader at the door. The sticky note is visible to anyone who walks by; the key-card lets only authorized people in, and you can revoke it instantly.

Pro tip: Even if you're writing a quick one-off script, never put a real secret in the code. Bad habits form fast, and the one time you forget is the time it leaks.

Core concept / mental model

The mental model for managing secrets with environment variables is simple: your application is a function of both code (what it does) and environment (where it runs). Secrets belong to the environment — the machine, container, or CI runner — not to the codebase.

In Python, the os.environ dictionary holds all environment variables of the current process. It behaves like a regular dict, but reading a missing key raises a KeyError — which is actually a good thing, because it forces you to handle missing secrets deliberately.

Think of it this way: - Config (URLs, timeouts) is often non-sensitive and can have defaults. - Secrets (API keys, passwords, tokens) must never have defaults and must come from the environment.

The flow is straightforward:

Development → you set a secret in your shell or a .env file (which is not committed). CI/CD → the pipeline injects secrets from its secure store (e.g., GitHub Actions secrets, GitLab variables). Production → the container orchestrator or cloud provider provides the secret via environment variables.

Your Python code stays identical across all three environments — it just reads from os.environ.

How it works step by step

Let's walk through the lifecycle of a secret managed via environment variables:

  1. Store the secret — set it in your local shell (export API_KEY=...), in a .env file for development, or in your CI/CD platform's secure settings.
  2. Load the secret in Python — use the os module to access the value at runtime.
  3. Use the secret — pass it to your HTTP client, database connection, or authentication function.
  4. Remove the secret when done — unset it or let the process exit; environment variables are per-process and vanish when the process ends.
  5. Never log the secret — avoid printing it or including it in error messages.

The os.getenv function is the most common way to read a variable, because you can provide a fallback. For secrets, though, you usually want to crash if the variable is missing, not silently use a default. So os.environ["SECRET"] is safer for required secrets.

You also need to manage environment variables in files like .env for local development. The python-dotenv library loads a .env file into os.environ at the start of your script, so you can keep the file out of Git.

Hands-on walkthrough

Let's see this in action with a real DevOps scenario: a script that queries an external API using a token.

Example 1: Reading a secret from the environment

import os

# Dangerous hard-coded way / NEVER do this
# API_KEY = "abc123secret"

# Safe way: read from environment
API_KEY = os.environ["API_KEY"]  # raises KeyError if not set

print(f"API key loaded (length={len(API_KEY)})")

Run it with the variable set:

export API_KEY="sk-1234-abcd"
python script.py

Output:

API key loaded (length=13)

The script didn't print the key itself — a good habit.

Example 2: Using getenv with a default (for non-secret config)

import os

# For non-secret config, a default is fine
TIMEOUT = int(os.getenv("TIMEOUT", "30"))
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")

# For secrets, require them explicitly
DATABASE_URL = os.environ["DATABASE_URL"]

print(f"Timeout: {TIMEOUT}s, Log level: {LOG_LEVEL}")
print(f"DB URL loaded (length={len(DATABASE_URL)})")

Set only the secret: export DATABASE_URL="postgres://user:pass@localhost:5432/mydb" and run — you'll see the defaults used.

Example 3: Loading from a .env file with python-dotenv

# pip install python-dotenv
from dotenv import load_dotenv
import os

# Loads .env into environment variables
load_dotenv()

# Now os.getenv / os.environ see the .env contents
API_KEY = os.getenv("API_KEY")
if not API_KEY:
    raise SystemExit("API_KEY is not set. Create a .env file from .env.example.")

# Use the API key to make a request
import requests
resp = requests.get("https://api.example.com/data", headers={"Authorization": f"Bearer {API_KEY}"})
print(resp.status_code)

Create a .env file:

API_KEY=sk-1234

And a .gitignore entry:

.env

The output is just the HTTP status code — no secret revealed.

Example 4: Using a secrets helper module

# secrets.py
import os
from functools import lru_cache

@lru_cache()
def get_secret(name: str) -> str:
    """Return the value of a required environment variable."""
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"Missing required environment variable: {name}")
    return value

Now in your main script:

from secrets import get_secret

db_password = get_secret("DB_PASSWORD")
api_token = get_secret("API_TOKEN")

# Use them in your automation...

This caches the values so they're read only once — good for performance and consistency.

Pro tip: Keep your .env.example committed with placeholder variable names but no real values. It documents what secrets your app needs.

Compare options / when to choose what

You have several ways to manage secrets in Python. Let's compare them:

Approach Pros Cons Best for
Environment variables Simple, universal, no extra dependencies You must manage them outside code Most DevOps scripts and containers
.env files + python-dotenv Easy for local dev, groups secrets Risk of committing if not ignored Local development and small services
Secret managers (e.g., AWS Secrets Manager, Vault) Centralized, rotation, access control Requires setup and SDK, more complex Production clusters, team collaboration
Hard-coded (never) None Leaks, rotation nightmare Never

When to choose what: - For a quick script on your laptop: environment variables or a .env file. - For a deployed service: environment variables injected by your orchestrator (Kubernetes secrets, Docker --env-file, etc.). - For high-security environments: a dedicated secret manager with API access from Python.

Environment variables remain the baseline because they are language-agnostic, simple, and supported everywhere. Secret managers often export their values as environment variables to your processes anyway.

Troubleshooting & edge cases

Common issues you'll hit and how to fix them:

  • KeyError when reading a secret — The variable isn't set in the current shell or the process doesn't inherit it. Check your export statement or container environment. Add a clear error message: raise SystemExit("Missing API_KEY").
  • Secret appears as None — You used os.getenv without a default, and the variable isn't set. Use os.environ.get("KEY") and check for None or empty string.
  • .env file not loading — The file isn't in the current working directory, or you forgot load_dotenv() at the top. Use an absolute path or ensure you're in the right directory.
  • .env accidentally committed — Even if you delete it, it's in history. Use tools like git-secrets or trufflehog to scan past commits, and rotate the leaked secret immediately.
  • Secrets showing in logs — Never print them. Use len() to confirm presence, and configure your logging to filter out sensitive fields.
  • Special characters in values — Quotes and spaces in .env values can break parsing. Use quotes in the file: SECRET="my secret", and use a library that handles it (python-dotenv does).
  • Environment variables not available in Docker — Use --env or --env-file when running containers, or define them in your deployment manifest.

What you learned & what's next

You now know how to manage secrets with environment variables — the cornerstone of secure DevOps automation. You can read secrets from the environment, load them from .env files locally, and choose between environment variables and dedicated secret managers when needed. You also understand the pitfalls, like missing variables, committed .env files, and logging secrets.

This skill applies to every piece of automation you'll write: boto3 scripts that need AWS credentials, kubectl helpers that require cluster tokens, and CI pipelines that deploy your infrastructure. The next lesson in this track builds on this foundation — likely covering how to securely handle configuration files or integrate with a secret manager like AWS Secrets Manager for more advanced scenarios. With this knowledge, your automation is now both powerful and secure.

Practice recap

Create a small script that connects to a mock API using an API key. Store the key in a .env file, load it with python-dotenv, add .env to .gitignore, and verify it works when the file exists and crashes when it's removed. Then refactor to use a get_secret helper function.

Common mistakes

  • Hard-coding secrets directly in source code or scripts
  • Committing a .env file to version control without adding it to .gitignore
  • Using os.getenv with a default value for secrets (falls back to insecure value)
  • Printing or logging secrets, either intentionally or via exception messages
  • Forgetting to load .env in the correct working directory, leading to KeyError

Variations

  1. Use dotenv_values to load .env into a dict without modifying os.environ
  2. Use a secret manager like AWS Secrets Manager or HashiCorp Vault and fetch at runtime
  3. In CI/CD, inject secrets directly as environment variables from the platform (GitHub Actions, GitLab CI)

Real-world use cases

  • CI pipeline deploying to AWS uses an access key stored as a CI secret, exposed to the Python deployment script via env var
  • A Dockerized cron job reads a database password from the container's environment, set by docker run --env
  • A Kubernetes pod authenticates to an external API using a token injected as an environment variable from a secret object

Key takeaways

  • Always separate secrets from code by using environment variables
  • Read secrets with os.environ["NAME"] to fail fast when missing
  • Use .env files and python-dotenv only for local development
  • Never commit .env files or hard-code credentials
  • Choose a secret manager for production-grade security and rotation
  • Never log secret values

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.