There's a line of code that appears in countless web applications, and it's responsible for more security breaches than almost any other mistake. It looks something like this:
python SECRET_KEY = 'my-super-secret-key-12345'
Or this:
javascript const apiKey = 'sk_live_abc123xyz789';
It's a secret, hardcoded directly in the source code, committed to version control, and pushed to a repository where it sits forever, waiting to be discovered.
This is the most common security mistake in web development. And the fix is simple, well-established, and has been best practice for decades: environment variables.
Let's talk about why hardcoded secrets are so dangerous, how environment variables solve the problem, and how to implement this pattern correctly across different frameworks.
Why Hardcoded Secrets Are Dangerous
When you hardcode a secret in your source code, several bad things happen simultaneously.
**The secret enters version control.** Every commit, every branch, every fork, every clone contains the secret. It's not just in your current code, it's in the entire history of your repository. Even if you delete it in a later commit, it remains in previous commits, discoverable by anyone who looks.
**The secret is shared with everyone who touches the codebase.** Every developer who clones the repo has it. Every CI/CD system that runs your tests has it. Every deployment tool, every code review tool, every IDE plugin that reads your files. The secret proliferates beyond your control.
**Automated bots are scanning for leaked secrets.** GitHub, GitLab, and third-party services continuously scan repositories for exposed credentials. So do malicious actors. There are documented cases of AWS keys being exploited within minutes of being committed to a public repo.
**The secret can't be rotated without a code change.** If you need to change the secret, you have to modify the source code, commit the change, push it, and deploy. This makes rotation slow and painful, which means it happens less often than it should.
**The same secret is used everywhere.** If the same code runs in development, staging, and production, the same secret is used in all three. A leak in any environment compromises all of them.
How Environment Variables Solve This
Environment variables are settings that exist outside your code, in the runtime environment. They're set by the operating system, the deployment platform, or a configuration file that's not part of the source code.
Here's why this matters:
**The secret never appears in source code.** Your code references the variable name, not the value. The actual value lives in the environment, separate from the codebase.
**The secret is not in version control.** Since it's not in the code, it's not in Git. It can't be accidentally committed, pushed, or leaked through the repository.
**Different environments can have different values.** Development, staging, and production each set their own environment variables. The same code runs in all three, but with different secrets. A development leak doesn't compromise production.
**Rotation is trivial.** Change the environment variable, restart the application, and the new secret takes effect. No code changes, no commits, no deployments.
**Access can be controlled.** In production, only the deployment system and the application runtime need access to the environment variables. Developers don't need to know production secrets.
The .env File Pattern
For local development, environment variables are typically managed through a .env file. This is a simple text file that lives in your project directory and contains key-value pairs:
SECRET_KEY=your-generated-secret-key-here DATABASE_URL=postgresql://user:pass@localhost:5432/db API_KEY=sk_test_abc123
The key rule: **never commit .env to version control.** Add it to .gitignore immediately:
# .gitignore .env .env.local .env.*.local
Most frameworks and languages have libraries to load .env files. In Python, python-dotenv is the standard:
python from dotenv import load_dotenv import os
load_dotenv() secret_key = os.environ.get('SECRET_KEY')
In Node.js, the dotenv package does the same:
javascript require('dotenv').config(); const secretKey = process.env.SECRET_KEY;
Instead of .env, you should commit a .env.example file with placeholder values:
# .env.example SECRET_KEY=generate-a-secret-key-here DATABASE_URL=postgresql://user:pass@localhost:5432/db API_KEY=your-api-key-here
This tells other developers what variables they need to set without exposing actual values.
Implementation Examples
### Django
Django's default settings.py includes a hardcoded SECRET_KEY. Replace it with:
python import os SECRET_KEY = os.environ.get('SECRET_KEY')
For more robust handling, add a check that fails loudly if the key is missing:
python import os from django.core.exceptions import ImproperlyConfigured
SECRET_KEY = os.environ.get('SECRET_KEY') if not SECRET_KEY: raise ImproperlyConfigured('SECRET_KEY environment variable is not set')
This ensures the application never starts without a properly configured key. For a deeper look at what this key does, read our guide on Django's SECRET_KEY explained.
### Flask
Flask is similar. Instead of hardcoding:
python import os from flask import Flask
app = Flask(__name__) app.secret_key = os.environ.get('SECRET_KEY')
### Node.js / Express
javascript require('dotenv').config(); const express = require('express'); const app = express();
const secretKey = process.env.SECRET_KEY; if (!secretKey) { console.error('SECRET_KEY environment variable is not set'); process.exit(1); }
### Next.js
Next.js has built-in support for environment variables. Create a .env.local file:
NEXT_PUBLIC_API_URL=https://api.example.com SECRET_KEY=your-secret-key-here
Variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Everything else is server-side only.
Production Secret Management
In production, .env files are not the right approach. They're a development convenience. For production, use your platform's secret management system.
**Heroku:** Use config vars set through the dashboard or CLI: heroku config:set SECRET_KEY=your-key
**AWS:** Use AWS Secrets Manager or Parameter Store. These provide encrypted storage, access control, and rotation capabilities.
**Google Cloud:** Use Secret Manager, which provides versioned, encrypted secret storage with IAM-based access control.
**Kubernetes:** Use Kubernetes Secrets, which can be mounted as environment variables or files.
**Docker:** Pass secrets through environment variables in your docker-compose.yml or Docker run command. For sensitive secrets, use Docker Secrets in swarm mode.
The principle is the same across all platforms: the secret lives outside the code, in a managed system that provides encryption, access control, and audit logs.
Common Mistakes
**Committing the .env file.** This is the most common mistake. Double-check your .gitignore. Verify that .env is listed before you make your first commit.
**Using the same secret across environments.** Generate a unique secret for each environment. Development, staging, and production should all have different values.
**Not failing fast on missing secrets.** If a secret is missing, your application should refuse to start, not silently use a default or empty value. Fail loudly so the problem is noticed immediately.
**Logging secrets.** Be careful not to log environment variables. If you log your environment for debugging, redact secrets. A common mistake is logging the entire config object, which includes the secret.
**Putting secrets in client-side code.** Environment variables that start with NEXT_PUBLIC_ (Next.js), VITE_ (Vite), or REACT_APP_ (Create React App) are embedded in the client-side bundle. Anyone can read them. Only use this prefix for non-sensitive values.
The Bottom Line
Storing secrets in environment variables is not a new or exotic practice. It's the baseline standard for secure application development. It's been best practice for decades, and every major framework and platform supports it.
The pattern is simple: generate a strong secret, store it in an environment variable, load it at runtime, never commit it. Your secrets stay out of version control, out of your codebase, and out of the hands of anyone who doesn't need them.
If you're currently hardcoding secrets, fix it today. Generate a new secret, move it to an environment variable, and rotate the old one. Future you will be grateful.
To generate a cryptographically secure secret key right now, use our Django Key Generator. And for a comprehensive look at what these keys protect, read our Django SECRET_KEY guide.