Best Practices for Storing API Keys in Environment Variables
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.
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:
- Set restrictive file permissions:
chmod 600 .envensures only the file owner can read it. - Create a .env.example template: Commit a
.env.examplefile with placeholder values so new developers know which variables are needed without seeing actual secrets. - Use pre-commit hooks: Tools like
detect-secretsorgitleaksscan staged files and block commits that contain secret patterns.
# .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:
- Maintain a .env.example file with every variable, a placeholder value, and an inline comment explaining its purpose.
- Include a "Secrets" section in your README that lists each variable, its source (which provider dashboard), its format, and whether it differs between environments.
- Tag secrets with sensitivity levels in your documentation: "critical" (payment keys, database passwords), "standard" (third-party API keys), and "low" (analytics IDs, public keys).
- 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:
- 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.
- 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.
- Identify the exposure source. Was it a committed
.envfile, a log entry, an error message, a screenshot, or a compromised developer machine? - Purge the exposed value. If the key appeared in a Git commit, use
git-filter-repoto remove it from history. If it appeared in logs, purge those log entries. If it was in a Docker image, rebuild and republish. - 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.
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
- Add
.envto.gitignorebefore creating the file. - Validate all required environment variables at application startup.
- Set file permissions to
600on all.envfiles. - Maintain a
.env.examplewith placeholders and comments. - Use pre-commit hooks to catch accidental secret commits.
- Never log environment variable values; log only that they are present.
- Inject secrets through platform-native mechanisms in CI/CD, not
.envfiles. - Have a documented incident response plan for key exposure.
- Rotate every exposed key immediately, investigate after.
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
- Real-World Cryptography — understand the encryption behind secrets management and how environment variables compare to encrypted storage.
- The Web Application Hacker's Handbook — learn how attackers extract secrets from environment variables, logs, and debug output.