Best Practices for Storing API Keys in Environment Variables

Published March 9, 2026 · 11 min read · By SPUNK LLC

Environment variables are the most common method for storing API keys in application code. The twelve-factor app methodology recommends them, every hosting platform supports them, and they keep secrets out of your source code. But environment variables have real security limitations that many developers overlook. This guide covers how to implement them correctly across languages and environments, and when to upgrade to a dedicated secrets manager.

The Basics: Why Environment Variables

Environment variables solve the fundamental problem of separating configuration from code. Instead of hardcoding sk_live_abc123 into your source file where it will inevitably end up in a Git repository, you reference process.env.STRIPE_KEY and set the value outside your codebase.

This approach provides three key benefits: secrets never enter version control, the same code runs across environments (development, staging, production) with different keys, and access to secrets can be controlled at the infrastructure level rather than the code level.

Implementation by Language

Node.js with dotenv

The dotenv package loads variables from a .env file into process.env at application startup. This is the standard pattern for Node.js applications.

# .env file (NEVER commit this file)
STRIPE_SECRET_KEY=sk_live_abc123def456
DATABASE_URL=postgresql://user:pass@host:5432/db
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-256-bit-secret
// server.js
import 'dotenv/config';

// Access secrets through process.env
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

// Always validate that required variables exist at startup
const required = ['STRIPE_SECRET_KEY', 'DATABASE_URL', 'JWT_SECRET'];
for (const key of required) {
  if (!process.env[key]) {
    console.error(`Missing required environment variable: ${key}`);
    process.exit(1);
  }
}

Python with python-dotenv

# .env file
OPENAI_API_KEY=sk-proj-abc123
AWS_ACCESS_KEY_ID=AKIA1234567890
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG
# app.py
import os
from dotenv import load_dotenv

load_dotenv()  # Loads .env file into os.environ

# Access with a default fallback for optional variables
openai_key = os.environ["OPENAI_API_KEY"]  # Raises KeyError if missing
debug_mode = os.getenv("DEBUG", "false")    # Returns "false" if missing

# Type-safe configuration class pattern
class Config:
    OPENAI_API_KEY: str = os.environ["OPENAI_API_KEY"]
    AWS_ACCESS_KEY_ID: str = os.environ["AWS_ACCESS_KEY_ID"]
    AWS_SECRET_ACCESS_KEY: str = os.environ["AWS_SECRET_ACCESS_KEY"]
    DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"

Go with godotenv

// main.go
package main

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)

func main() {
    // Load .env file in development; skip in production
    if os.Getenv("GO_ENV") != "production" {
        if err := godotenv.Load(); err != nil {
            log.Println("No .env file found, using system environment")
        }
    }

    apiKey := os.Getenv("API_KEY")
    if apiKey == "" {
        log.Fatal("API_KEY environment variable is required")
    }

    // Use the key
    client := NewAPIClient(apiKey)
}

Securing Your .env Files

A .env file is just a plain text file sitting on disk. Without proper precautions, it is the weakest link in your security chain.

Critical Rule

Add .env to your .gitignore before creating the file. If you create the .env first and commit it even once, the secrets are permanently in your Git history. Use git-filter-repo or BFG Repo Cleaner to purge them and rotate every exposed key immediately.

# .gitignore — add these lines
.env
.env.local
.env.*.local
.env.production

Additional protections for .env files:

# .env.example (safe to commit)
STRIPE_SECRET_KEY=sk_live_YOUR_KEY_HERE
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=generate-a-random-256-bit-string
# Install gitleaks as a pre-commit hook
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

CI/CD Secrets Injection

In CI/CD pipelines, you should never use .env files. Instead, inject secrets through your platform's built-in secrets management.

GitHub Actions

# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        env:
          STRIPE_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: |
          # Secrets are available as environment variables
          # They are automatically masked in log output
          npm run deploy

Docker

# NEVER bake secrets into Docker images
# Bad: ENV STRIPE_KEY=sk_live_abc123

# Good: Pass at runtime
docker run -e STRIPE_KEY="$STRIPE_KEY" -e DATABASE_URL="$DATABASE_URL" myapp

# Better: Use Docker secrets in Swarm mode
echo "sk_live_abc123" | docker secret create stripe_key -
docker service create --secret stripe_key myapp

Kubernetes

# Create a secret from literal values
kubectl create secret generic api-keys \
  --from-literal=STRIPE_KEY=sk_live_abc123 \
  --from-literal=JWT_SECRET=my-secret

# Reference in a pod spec
# spec.containers[0].envFrom:
#   - secretRef:
#       name: api-keys

Documentation Without Exposure

Teams need to know which environment variables are required, what format they should be in, and where to obtain them, all without exposing actual values. Establish these documentation patterns:

  1. Maintain a .env.example file with every variable, a placeholder value, and an inline comment explaining its purpose.
  2. Include a "Secrets" section in your README that lists each variable, its source (which provider dashboard), its format, and whether it differs between environments.
  3. Tag secrets with sensitivity levels in your documentation: "critical" (payment keys, database passwords), "standard" (third-party API keys), and "low" (analytics IDs, public keys).
  4. Document the rotation procedure for each secret, including who has permission to rotate it and the expected impact during rotation.

Incident Response When Keys Leak

Despite every precaution, key leaks happen. Having a response plan turns a potential breach into a contained incident. Follow this runbook:

  1. Immediately rotate the exposed key. Do not investigate first. Rotate first, then determine scope. Every minute the old key is active after exposure is a minute an attacker can use it.
  2. Check the provider's access logs. Most API providers (Stripe, AWS, GitHub) provide detailed logs of every request made with a key. Review these logs for unauthorized access during the exposure window.
  3. Identify the exposure source. Was it a committed .env file, a log entry, an error message, a screenshot, or a compromised developer machine?
  4. Purge the exposed value. If the key appeared in a Git commit, use git-filter-repo to remove it from history. If it appeared in logs, purge those log entries. If it was in a Docker image, rebuild and republish.
  5. Implement the fix. Add the pre-commit hook, .gitignore rule, or log redaction pattern that would have prevented the exposure. Update your documentation with the incident and the fix.
When to Upgrade Beyond Environment Variables

Environment variables are a starting point, not a destination. Upgrade to a dedicated secrets manager (Vault, Infisical, or Doppler) when you have more than 10 secrets, multiple environments, a team larger than 5 developers, or compliance requirements that mandate access auditing and automatic rotation.

Quick Reference Checklist

Environment variables remain the most practical way to manage API keys for most applications. By following these practices across your language of choice, CI/CD pipeline, and team documentation, you minimize the risk of exposure while keeping your development workflow simple and fast.

Recommended Resources