Environment Variable Security: Avoiding Common Secrets Exposure Pitfalls | SoniNow Blog

Limited TimeLearn More

secretsenvironment variablessecurityconfigurationdevops

Environment Variable Security: Avoiding Common Secrets Exposure Pitfalls

Published

2026-06-23

Read Time

5 mins

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.