Environment Variable Security: Avoiding Common Secrets Exposure Pitfalls

Environment variables are the standard mechanism for injecting configuration into applications, but they are also one of the most common sources of secrets exposure. A leaked API key, database password, or signing secret can lead to a complete compromise of your application and infrastructure.
.env File Discipline
The .env file pattern is convenient for local development but creates serious risks when mismanaged. The cardinal rule: .env files must never be committed to version control.
# .gitignore — ensure .env files are never committed
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*.env
!/.env.example
Maintain a .env.example file in the repository that documents all required environment variables with placeholder values. Include types and descriptions so new team members know what to configure:
# .env.example
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/soninow_db
DB_POOL_MAX=20
# Authentication
JWT_ACCESS_SECRET=change-me-to-a-random-64-char-string
JWT_REFRESH_SECRET=change-me-to-another-random-string
JWT_ALGORITHM=RS256
# External Services
STRIPE_API_KEY=pk_test_...
SENDGRID_API_KEY=SG....
# Feature Flags
ENABLE_BETA_FEATURES=false
CI/CD Secrets Injection
CI/CD pipelines are a primary vector for secrets exposure — through build logs, cached artifacts, and misconfigured environment variables. Use the platform's native secrets management rather than embedding secrets in configuration files or repository variables.
# GitHub Actions: Use secrets, not env vars
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
# Inject secrets securely — masked in logs automatically
- name: Deploy to VPS
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
# SSH_PRIVATE_KEY is never printed, never stored on disk
echo "$SSH_PRIVATE_KEY" | ssh-add -
rsync -az --delete ./dist/ deploy@server:/app/
ssh deploy@server 'docker compose pull && docker compose up -d'
# Docker Compose: Never hardcode secrets
# docker-compose.yml — reference env vars, don't embed values
services:
app:
image: soninow/app:latest
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt # Only on server, never committed
For Docker secrets, use the built-in secrets mechanism (Swarm or Compose) rather than environment variables for highly sensitive values. The secret file is mounted as a temporary filesystem in /run/secrets/, not passed as an environment variable.
Preventing Secrets in Logs and Error Output
Secrets frequently leak through application logs, error messages, and debugging output. Implement automated redaction.
// Structured logging with automatic secret redaction
import pino from 'pino';
const SENSITIVE_KEYS = new Set([
'password', 'secret', 'token', 'api_key', 'apiKey',
'authorization', 'cookie', 'jwt', 'credit_card',
'ssn', 'access_token', 'refresh_token',
]);
const logger = pino({
redact: {
paths: ['req.headers.authorization', 'req.body.password',
'req.body.token', 'req.body.secret'],
censor: '[REDACTED]',
},
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
// Exclude sensitive query parameters
query: req.query ? sanitizeQueryParams(req.query) : undefined,
}),
},
});
For environment variables, never dump them in debugging output. Configure Node.js to hide secrets from error stack traces and debug logs:
# Node.js — hide environment details in error output
NODE_OPTIONS="--no-deprecation --unhandled-rejections=strict"
# Never expose NODE_DEBUG which prints env
Secrets Rotation and Expiration
Static secrets are a liability — every day they remain unchanged increases the window of exposure. Implement automated rotation.
# Rotate database password and update application
#!/bin/bash
# 1. Generate new password
NEW_DB_PASSWORD=$(openssl rand -base64 32)
# 2. Update database user password
psql -h $DB_HOST -U admin -c \
"ALTER USER app_user WITH PASSWORD '${NEW_DB_PASSWORD}';"
# 3. Update secrets manager
aws secretsmanager put-secret-value \
--secret-id soninow/db/password \
--secret-string "$NEW_DB_PASSWORD"
# 4. Trigger application redeployment
# The application reads the new secret on restart
kubectl rollout restart deployment/app
# 5. Verify new password works before removing old
if curl -s -o /dev/null -w "%{http_code}" https://soninow.com/health | grep -q 200; then
echo "Rotation successful"
else
echo "Rollback required" && exit 1
fi
Use short-lived credentials where possible. AWS IAM roles, GCP service accounts, and database IAM authentication eliminate static credentials entirely by issuing time-limited tokens.
Secrets Manager Usage
For production, use a dedicated secrets manager rather than environment variables for the most sensitive credentials. HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, and Azure Key Vault provide encrypted storage, access audit logs, and automated rotation.
// AWS Secrets Manager integration
import {
SecretsManagerClient,
GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getDatabaseCredentials(): Promise<DbCredentials> {
const response = await client.send(
new GetSecretValueCommand({
SecretId: 'soninow/production/db',
})
);
return JSON.parse(response.SecretString!);
}
// Cache credentials with automatic refresh before expiry
class RotatingSecret<T> {
private cached: T | null = null;
private expiresAt: number = 0;
async get(): Promise<T> {
if (Date.now() < this.expiresAt && this.cached) {
return this.cached;
}
this.cached = await fetchSecret<T>();
this.expiresAt = Date.now() + 3600000; // 1 hour cache
return this.cached;
}
}
Audit Logging for Secrets Access
Track who accesses secrets and when. Both your secrets manager and application should log secret access attempts:
// Log secret access for audit
async function getSecretWithAudit(name: string, userId: string): Promise<string> {
logger.info('Secret access', {
secret: name,
user: userId,
timestamp: new Date().toISOString(),
source: req.ip,
});
const secret = await secretsClient.get(name);
return secret;
}
Set up alerts for unusual secret access patterns — a CI pipeline accessing a production database secret during a non-deployment hour, or an application instance querying a secret it never previously accessed.
Scanning for Leaked Secrets
Use automated tools to scan code repositories and CI artifacts for accidentally committed secrets. Integrate pre-commit hooks and CI scanners.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
args: ['--verbose']
# Scan entire repository history for secrets
gitleaks detect --source . --verbose --no-git
trufflehog git file://. --results=verified,unknown
For CI, add a secrets scan step that fails the build if any potential secrets are detected. If a secret is confirmed leaked, immediately rotate it and investigate access logs.
Proper secrets management prevents one of the most common attack vectors. Our <a href="/services/devops-server-management">devops and infrastructure services</a> include secrets management architecture, rotation automation, and security audits. Contact SoniNow to protect your application secrets.
Related Insights

API Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window
A guide to implementing API rate limiting including token bucket, leaky bucket, sliding window, and distributed rate limiting with Redis for production APIs.

Authentication Patterns in Modern Web Apps: JWT, OAuth, and Session Management
A guide to authentication patterns for web applications including JWT implementation, OAuth 2.0 flows, refresh tokens, session management, and secure storage.

Authentication Patterns in Modern Web Apps: JWT, Sessions, and Passkeys
A guide to modern authentication patterns comparing JWT, session-based auth, and passkeys including implementation strategies, security considerations, and user experience.