Skip to content

Secure Configuration

The most common data breach in Node apps? Hardcoded secrets in source code API keys in GitHub , database passwords in config files , JWT secrets set to "changeme" - I've seen it all in production repos that were supposed to be private Let's fix this shit permanently

Environment Variables with dotenv

# .env - never commit this file
PORT=3000
NODE_ENV=production
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
JWT_SECRET=definitely-not-a-real-secret-change-me
REDIS_URL=redis://:password@localhost:6379
STRIPE_API_KEY=sk_live_abc123def456
SENDGRID_API_KEY=SG.xxxxx
SESSION_SECRET=generate-a-real-random-64-char-string

# .env.example - commit this
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
JWT_SECRET=change-this-in-production
REDIS_URL=redis://localhost:6379
STRIPE_API_KEY=sk_test_xxx
SESSION_SECRET=change-this-in-production
// .gitignore
.env
.env.local
.env.production
.env.*.local

// load in app
require('dotenv').config()

// or with custom path
require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`
})

// access
const dbUrl = process.env.DATABASE_URL

Never commit .env to git : once a secret hits the git history , it's compromised even if you delete it later Use .env.example with placeholder values so new devs know what they need

Secrets Management: Beyond dotenv

For production , environment variables are just the start - you need a real secrets manager

// HashiCorp Vault
const vault = require('node-vault')({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR || 'http://127.0.0.1:8200',
  token: process.env.VAULT_TOKEN
})

async function getSecret(path) {
  const { data } = await vault.read(`secret/data/${path}`)
  return data.data
}

// Usage
const dbCreds = await getSecret('database/production')
const pool = new Pool({
  connectionString: dbCreds.url
})

// AWS Secrets Manager
const { SecretsManager } = require('@aws-sdk/client-secrets-manager')
const client = new SecretsManager({ region: 'us-east-1' })

async function getDbSecret() {
  const data = await client.getSecretValue({
    SecretId: 'prod/database/url'
  })
  return JSON.parse(data.SecretString)
}

// Doppler
const doppler = require('@dopplerhq/node-sdk')
const secrets = doppler.secrets({ project: 'myapp', config: 'prd' })

Use secrets manager in production : env vars are better than hardcoded but still visible in process listings , container logs , and debugging tools

Config Validation with convict

const convict = require('convict')
const schema = {
  env: {
    doc: 'Application environment',
    format: ['production', 'development', 'test'],
    default: 'development',
    env: 'NODE_ENV'
  },
  port: {
    doc: 'HTTP port',
    format: 'port',
    default: 3000,
    env: 'PORT',
    arg: 'port'
  },
  db: {
    url: {
      doc: 'Database connection string',
      format: String,
      default: null,
      env: 'DATABASE_URL',
      sensitive: true  // don't print in toString()
    },
    pool: {
      min: {
        doc: 'Minimum pool connections',
        format: 'nat',
        default: 2,
        env: 'DB_POOL_MIN'
      },
      max: {
        doc: 'Maximum pool connections',
        format: 'nat',
        default: 10,
        env: 'DB_POOL_MAX'
      }
    }
  },
  jwt: {
    secret: {
      doc: 'JWT signing secret',
      format: function(val) {
        if (!val || val.length < 32) {
          throw new Error('JWT secret must be 32+ chars')
        }
      },
      default: null,
      env: 'JWT_SECRET',
      sensitive: true
    },
    expiresIn: {
      doc: 'JWT expiration',
      format: String,
      default: '15m',
      env: 'JWT_EXPIRES_IN'
    }
  },
  redis: {
    url: {
      doc: 'Redis connection string',
      format: String,
      default: null,
      env: 'REDIS_URL',
      sensitive: true
    }
  }
}

const config = convict(schema)
config.validate({ allowed: 'strict' })  // throws if missing required
// config.toString() -> hides sensitive fields

module.exports = config
const config = require('./config')
const app = express()
app.listen(config.get('port'))

Validate config at startup : catching a missing JWT_SECRET when the server starts is infinitely better than catching it when it fails to sign tokens at 3AM

Config Validation with envalid

const { cleanEnv, str, port, url, num } = require('envalid')

const env = cleanEnv(process.env, {
  NODE_ENV: str({ choices: ['development', 'test', 'production'] }),
  PORT: port({ default: 3000 }),
  DATABASE_URL: url(),
  JWT_SECRET: str({ desc: 'JWT signing key , min 32 chars' }),
  REDIS_URL: url(),
  STRIPE_API_KEY: str(),
  LOG_LEVEL: str({ choices: ['debug', 'info', 'warn', 'error'], default: 'info' }),
  MAX_REQUESTS_PER_MINUTE: num({ default: 100 })
})

// env.DATABASE_URL is a validated URL
// env.PORT is a valid port number
// env.STRIPE_API_KEY is a non-empty string
const pool = new Pool({ connectionString: env.DATABASE_URL })

cleanEnv throws on invalid config : it also freezes the env object so nothing can tamper with it later

Security Rules for Configuration

// Rule 1: Validate at startup
function validateConfig() {
  const required = [
    'DATABASE_URL', 'JWT_SECRET', 'REDIS_URL'
  ]
  const missing = required.filter(key => !process.env[key])
  if (missing.length > 0) {
    console.error(`Missing required env vars: ${missing.join(', ')}`)
    process.exit(1)
  }
}
validateConfig()

// Rule 2: Never log secrets
function safeConfig() {
  const safe = { ...process.env }
  const sensitive = ['DATABASE_URL', 'JWT_SECRET', 'API_KEY', 'PASSWORD']
  for (const key of sensitive) {
    if (safe[key]) safe[key] = '***REDACTED***'
  }
  return safe
}
console.log('Config:', safeConfig())

// Rule 3: Use defaults for dev , fail for prod
const dbUrl = process.env.DATABASE_URL || (
  process.env.NODE_ENV === 'production'
    ? (() => { throw new Error('DATABASE_URL required in prod') })()
    : 'postgresql://localhost:5432/dev'
)

Fail fast on missing config in production : a missing secret should crash the process immediately , not silently default to something insecure

Prerequisites

next -> sec_09_logging.md