# Secure Environment Variables: Never Leak Credentials in Your App

· 4 min read
# Secure Environment Variables: Never Leak Credentials in Your App

**Primary keywords:** secure environment variables, env var security, secure credentials cloud, prevent env leak, protect api keys hosting
---
Environment variables are the right place for secrets — API keys, database passwords, tokens, and other credentials that should never appear in your source code. But "using env vars" is only half the story. Using them incorrectly can still lead to credential leaks. This guide covers secure practices for managing environment variables in deployed applications.
## Why Environment Variables (and Not Files)
Developers sometimes ask: "Can't I just put secrets in a config file?" Technically yes, but:
- Config files get committed to version control (accidentally or intentionally)
- Config files get included in Docker images
- Config files appear in deployment artifacts and backups
- Anyone with read access to the file system can see them
Environment variables are injected at runtime by the platform, never appear in your codebase, and can be rotated without changing code.
## Setting Secrets Securely with the ApexWeave CLI
```bash
# Set individual secrets
apexweave env:set DATABASE_URL=postgres://user:strongpassword@host/db
apexweave env:set STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxx
fastify app deployment cloud
apexweave env:set JWT_SECRET=$(openssl rand -hex 32)
apexweave env:set SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
# Verify (shows names but NOT values)
apexweave env:list
# Remove a variable
apexweave env:unset OLD_API_KEY
```
The `apexweave env:list` command shows variable names and masked values — you can confirm a variable exists without exposing its value in terminal history.
## What NOT to Do
### Never Commit .env Files
```bash
# .gitignore must include:
.env
.env.local
.env.production
.env.staging
*.env
```
If you've already committed a `.env` file, the secret is in your Git history even after deletion. You must rotate the compromised credentials:
```bash
# Remove from Git history (requires force push)
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty --tag-name-filter cat -- --all
git push origin --force --all
```
Then immediately rotate all credentials that were in the file.
### Never Log Environment Variables
```javascript
// DANGEROUS: Dumps all env vars including secrets to logs
console.log('Environment:', process.env);
app.get('/debug', (req, res) => res.json(process.env)); // Never do this!
node.js cloud hosting platform
// SAFE: Log only specific, non-sensitive values
console.log('Starting in', process.env.NODE_ENV, 'mode');
console.log('Port:', process.env.PORT);
```
### Never Include Secrets in Error Messages
```javascript
// DANGEROUS: Exposes connection string in error
try
await db.connect(process.env.DATABASE_URL);
catch (err)
throw new Error(`Failed to connect to $process.env.DATABASE_URL: $err.message`);
// SAFE: Log detail server-side, send generic message to client
try
await db.connect(process.env.DATABASE_URL);
catch (err)
console.error('Database connection failed:', err.message); // Server logs only
throw new Error('Database connection failed'); // No secret in the error message
```
### Never Hardcode Secrets in Code
git based paas hosting
```javascript
// NEVER
const client = new Stripe('sk_live_AbCdEfGhIjKlMnOpQrStUvWxYz');
// ALWAYS
const client = new Stripe(process.env.STRIPE_SECRET_KEY);
if (!process.env.STRIPE_SECRET_KEY)
throw new Error('STRIPE_SECRET_KEY is required');
```
## Validating Required Variables at Startup
Fail fast when secrets are missing rather than discovering it during a request:
```javascript
// config/required-env.js
const REQUIRED_VARS = [
'DATABASE_URL',
'JWT_SECRET',
'STRIPE_SECRET_KEY',
'REDIS_URL',
];
function validateEnvironment()
const missing = REQUIRED_VARS.filter(name => !process.env[name]);
if (missing.length > 0)
console.error('Missing required environment variables:');
missing.forEach(name => console.error(` - $name`));
process.exit(1);
// Validate format of specific variables
if (process.env.JWT_SECRET && process.env.JWT_SECRET.length < 32)
console.error('JWT_SECRET must be at least 32 characters');
process.exit(1);
if (process.env.STRIPE_SECRET_KEY && !process.env.STRIPE_SECRET_KEY.startsWith('sk_'))
console.error('STRIPE_SECRET_KEY must start with sk_');


process.exit(1);
console.log('Environment validation passed');
module.exports = validateEnvironment;
```
```javascript
// server.js
const validateEnvironment = require('./config/required-env');
validateEnvironment(); // Run BEFORE anything else
```
## Python Environment Validation
```python
import os
import sys
REQUIRED_ENV_VARS =
'DATABASE_URL': 'PostgreSQL connection string',
'SECRET_KEY': 'Django/Flask secret key (min 50 chars)',
'STRIPE_SECRET_KEY': 'Stripe API key',
def validate_environment():
errors = []
for var, description in REQUIRED_ENV_VARS.items():
value = os.environ.get(var)
if not value:
errors.append(f"Missing required variable: var (description)")
# Additional validation
secret_key = os.environ.get('SECRET_KEY', '')
if secret_key and len(secret_key) < 50:
errors.append("SECRET_KEY must be at least 50 characters")
if errors:
for error in errors:
print(f"ERROR: error", file=sys.stderr)
sys.exit(1)
print("Environment validation passed")
validate_environment()
```
## Protecting Secrets in Client-Side Code
For frontend apps (React, Vue, Next.js), some environment variables are exposed to the browser. Treat these carefully:
```javascript
// .env.production
REACT_APP_API_URL=https://api.example.com           # OK — not a secret
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_live_xxx        # OK — publishable keys are public
# NEVER:
REACT_APP_STRIPE_SECRET_KEY=sk_live_xxx             # NEVER — exposed in browser!
REACT_APP_DATABASE_URL=postgres://...               # NEVER — exposed in browser!
```
For Next.js:
```javascript
// Public (browser-safe) — prefix with NEXT_PUBLIC_
NEXT_PUBLIC_API_URL=https://api.example.com
deploy express app production
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
// Server-only (never sent to browser) — no prefix
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_xxx
JWT_SECRET=...
```
Apex Weave
## Rotating Compromised Credentials
If you suspect a secret has been leaked:
1. **Immediately revoke the old credential** at the source (Stripe dashboard, database admin, etc.)
2. **Generate a new credential**
3. **Update the env var:**
```bash
apexweave env:set STRIPE_SECRET_KEY=sk_live_newkey
apexweave deploy
```
4. **Audit your logs** for unauthorized use of the old credential
5. **Search your Git history** for the leaked credential:
```bash
git log -p --all | grep -i "sk_live_"
```
## Secret Scanning in CI/CD
Prevent accidentally committing secrets with pre-commit hooks:
```bash
# Install gitleaks
brew install gitleaks
# Scan for secrets before commit
gitleaks git --pre-commit
```
Add to `.git/hooks/pre-commit`:
```bash
#!/bin/sh
gitleaks git --staged --verbose
if [ $? -ne 0 ]; then
echo "Potential secrets detected. Commit aborted."
exit 1
fi
```
Or use GitHub's built-in secret scanning for your repositories (available in Security settings).
## Environment Variables vs. Secret Management Services
For high-security applications, a dedicated secrets manager adds features like:
- Audit logging (who accessed which secret, when)
- Automatic rotation
- Fine-grained access control
- Secret versioning
Options: AWS Secrets Manager, HashiCorp Vault, Doppler. These integrate with ApexWeave via environment variables — the secrets manager provides the value, ApexWeave injects it into your app.
ApexWeave's environment variable management keeps your credentials out of your codebase and out of your deployment artifacts. Start with the free 7-day trial at [apexweave.io](https://apexweave.io).