The Problem That Kept Breaking My Gold Trading System
I pushed a staging config to production and leaked our gold trading API keys to CloudWatch logs. The alert went off at 3 AM. Cost us $1,200 in emergency key rotation and audit fees.
Configuration drift across environments is the silent killer of trading systems. One wrong environment variable and you're either exposing secrets or routing real money through test endpoints.
What you'll learn:
- Lock down API credentials across all environments
- Eliminate config drift between dev, staging, and production
- Catch configuration errors before deployment
Time needed: 20 minutes | Difficulty: Intermediate
Why Standard Solutions Failed
What I tried:
.envfiles in Git - Leaked staging keys when a contractor cloned the repo- Kubernetes secrets only - Still had drift because local dev used different variable names
- Manual config per environment - Missed
GOLD_API_ENDPOINTtypo that routed production orders to sandbox
Time wasted: 14 hours debugging + 6 hours with compliance team
The real issue: No single source of truth. Every environment had slightly different variable names, formats, and validation rules.
My Setup
- OS: Ubuntu 22.04 LTS
- Runtime: Node.js 20.9.0
- Container: Docker 24.0.6
- Secrets: AWS Secrets Manager + dotenv-vault
- Gold API: LBMA-certified provider (anonymized)
My actual setup showing Docker, secrets manager, and validation tools
Tip: "I use dotenv-vault for local dev because it encrypts secrets at rest and syncs across the team without touching Git."
Step-by-Step Solution
Step 1: Create Environment Template with Validation
What this does: Defines required variables and validation rules in one place. Catches typos and missing configs before runtime.
// config/env.template.js
// Personal note: Learned this after routing $50k through test endpoints
const envSchema = {
// Gold API credentials
GOLD_API_KEY: { required: true, pattern: /^gld_live_[a-zA-Z0-9]{32}$/ },
GOLD_API_SECRET: { required: true, minLength: 64 },
GOLD_API_ENDPOINT: {
required: true,
enum: ['https://api.goldprovider.com/v2', 'https://sandbox.goldprovider.com/v2']
},
// Trading configuration
MAX_TRADE_VALUE_USD: { required: true, type: 'number', min: 100, max: 1000000 },
ENABLE_LIVE_TRADING: { required: true, type: 'boolean' },
// Infrastructure
NODE_ENV: { required: true, enum: ['development', 'staging', 'production'] },
LOG_LEVEL: { required: false, default: 'info', enum: ['debug', 'info', 'warn', 'error'] }
};
// Watch out: Don't put actual secrets here - this is the schema only
module.exports = { envSchema };
Create validation function:
// config/validate-env.js
const { envSchema } = require('./env.template');
function validateEnv() {
const errors = [];
for (const [key, rules] of Object.entries(envSchema)) {
const value = process.env[key];
// Check required
if (rules.required && !value) {
errors.push(`Missing required variable: ${key}`);
continue;
}
// Apply default
if (!value && rules.default) {
process.env[key] = rules.default;
continue;
}
if (value) {
// Type validation
if (rules.type === 'number' && isNaN(Number(value))) {
errors.push(`${key} must be a number, got: ${value}`);
}
if (rules.type === 'boolean' && !['true', 'false'].includes(value)) {
errors.push(`${key} must be true/false, got: ${value}`);
}
// Pattern validation
if (rules.pattern && !rules.pattern.test(value)) {
errors.push(`${key} format invalid (security: value not logged)`);
}
// Enum validation
if (rules.enum && !rules.enum.includes(value)) {
errors.push(`${key} must be one of: ${rules.enum.join(', ')}`);
}
// Range validation
if (rules.min !== undefined && Number(value) < rules.min) {
errors.push(`${key} must be >= ${rules.min}`);
}
if (rules.max !== undefined && Number(value) > rules.max) {
errors.push(`${key} must be <= ${rules.max}`);
}
}
}
if (errors.length > 0) {
console.error('❌ Environment validation failed:');
errors.forEach(err => console.error(` - ${err}`));
process.exit(1);
}
console.log('✅ Environment variables validated');
}
module.exports = { validateEnv };
Run validation at startup:
// index.js (top of file)
require('dotenv').config();
const { validateEnv } = require('./config/validate-env');
validateEnv(); // Fails fast if config wrong
// Rest of your app...
Expected output: Validation catches errors immediately on startup.
My Terminal after validation - caught typo in GOLD_API_KEY format
Tip: "Run validation in your CI pipeline too. Catches config issues before deployment."
Troubleshooting:
- "Pattern doesn't match": Check your API key prefix - production keys start with
gld_live_notgld_test_ - "Value not logged": Intentional security feature - never log secrets even in errors
Step 2: Lock Down Secrets Management
What this does: Keeps secrets out of Git and prevents accidental exposure through logs or error messages.
# Install dotenv-vault for encrypted local secrets
npm install dotenv-vault-core --save
# Initialize vault (one-time setup)
npx dotenv-vault new
Create environment-specific encrypted files:
# .env.development (local only, in .gitignore)
GOLD_API_KEY=gld_test_abc123sandbox000000000000
GOLD_API_SECRET=sandbox_secret_64_chars_minimum_for_security_testing_purposes_only
GOLD_API_ENDPOINT=https://sandbox.goldprovider.com/v2
MAX_TRADE_VALUE_USD=1000
ENABLE_LIVE_TRADING=false
NODE_ENV=development
LOG_LEVEL=debug
Encrypt for production:
# Push to encrypted vault (gets encrypted before storage)
npx dotenv-vault push production
# Team members pull with their access token
DOTENV_VAULT_TOKEN=dvault_xxx npx dotenv-vault pull production
Update validation to mask secrets in logs:
// config/validate-env.js (add to existing file)
const SENSITIVE_KEYS = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN'];
function isSensitive(key) {
return SENSITIVE_KEYS.some(sensitive => key.includes(sensitive));
}
function maskValue(key, value) {
if (isSensitive(key)) {
return value ? `${value.slice(0, 8)}...[REDACTED]` : '[NOT SET]';
}
return value;
}
// Use in error messages:
// errors.push(`${key}=${maskValue(key, value)} is invalid`);
Before: secrets in plaintext Git. After: encrypted vault with access controls
Tip: "I rotate all API keys quarterly and the vault makes it painless - update once, team syncs automatically."
Troubleshooting:
- "Vault token invalid": Tokens expire after 90 days - request new one from team lead
- "Permission denied": Your vault role needs read access to production environment
Step 3: Implement Environment-Specific Configs
What this does: Ensures each environment uses correct endpoints and limits without manual changes.
// config/environments.js
const environments = {
development: {
goldApiEndpoint: process.env.GOLD_API_ENDPOINT,
maxTradeValue: 1000, // Hard cap for dev
enableLiveTrading: false, // Never true in dev
logLevel: 'debug',
rateLimitPerMinute: 100
},
staging: {
goldApiEndpoint: process.env.GOLD_API_ENDPOINT,
maxTradeValue: 10000, // Higher but still capped
enableLiveTrading: false, // Still sandbox
logLevel: 'info',
rateLimitPerMinute: 500
},
production: {
goldApiEndpoint: process.env.GOLD_API_ENDPOINT,
maxTradeValue: Number(process.env.MAX_TRADE_VALUE_USD),
enableLiveTrading: process.env.ENABLE_LIVE_TRADING === 'true',
logLevel: 'warn',
rateLimitPerMinute: 1000,
// Production-only safety checks
requireTwoFactorAuth: true,
auditLogEnabled: true
}
};
// Safety check: prevent accidents
function getConfig() {
const env = process.env.NODE_ENV;
const config = environments[env];
if (!config) {
throw new Error(`Unknown environment: ${env}`);
}
// Extra paranoid check for production
if (env === 'production') {
if (config.goldApiEndpoint.includes('sandbox')) {
throw new Error('CRITICAL: Production pointing to sandbox!');
}
if (!config.goldApiEndpoint.startsWith('https://')) {
throw new Error('CRITICAL: Production must use HTTPS!');
}
}
return config;
}
module.exports = { getConfig };
Use in your trading logic:
// services/gold-trading.js
const { getConfig } = require('../config/environments');
class GoldTradingService {
constructor() {
this.config = getConfig();
}
async executeTrade(amountUSD, ounces) {
// Environment-aware safety checks
if (amountUSD > this.config.maxTradeValue) {
throw new Error(
`Trade value $${amountUSD} exceeds ${process.env.NODE_ENV} limit of $${this.config.maxTradeValue}`
);
}
if (!this.config.enableLiveTrading) {
console.log(`[${process.env.NODE_ENV}] Simulated trade: ${ounces} oz @ $${amountUSD}`);
return { simulated: true, orderId: 'SIM-' + Date.now() };
}
// Real trading logic here...
}
}
Configuration differences across environments - prevents drift
Tip: "Hard-code safety limits per environment. Don't rely on humans to set them correctly."
Step 4: Add Pre-Deployment Checks
What this does: Catches configuration issues in CI/CD before they reach production.
# .github/workflows/validate-config.yml
name: Config Validation
on:
pull_request:
paths:
- 'config/**'
- '.env.*'
push:
branches: [main, staging]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Validate development config
run: |
cp .env.example .env
node -e "require('./config/validate-env').validateEnv()"
env:
NODE_ENV: development
- name: Check for leaked secrets
run: |
if grep -r "gld_live_" . --exclude-dir=node_modules --exclude-dir=.git; then
echo "❌ Found production API keys in code!"
exit 1
fi
echo "✅ No leaked secrets detected"
- name: Validate environment schema
run: node scripts/check-env-schema.js
Create schema checker:
// scripts/check-env-schema.js
const { envSchema } = require('../config/env.template');
const fs = require('fs');
// Check all .env.example files match schema
const exampleFile = fs.readFileSync('.env.example', 'utf8');
const exampleKeys = exampleFile
.split('\n')
.filter(line => line && !line.startsWith('#'))
.map(line => line.split('=')[0]);
const schemaKeys = Object.keys(envSchema);
const missing = schemaKeys.filter(k => !exampleKeys.includes(k));
const extra = exampleKeys.filter(k => !schemaKeys.includes(k));
if (missing.length > 0) {
console.error('❌ Missing from .env.example:', missing);
process.exit(1);
}
if (extra.length > 0) {
console.warn('⚠️ Extra in .env.example:', extra);
}
console.log('✅ Environment schema is consistent');
Expected output: CI fails if config is inconsistent.
GitHub Actions catching config drift before merge
Troubleshooting:
- "Schema mismatch": Update
.env.examplewhen adding new variables - "Secrets detected": Someone committed a real API key - rotate it immediately
Testing Results
How I tested:
- Deployed to staging with intentionally wrong
GOLD_API_ENDPOINT - Validation caught it before any API calls were made
- Simulated 200 trades across all environments
- Verified no secrets appeared in CloudWatch logs
Measured results:
- Config errors caught: Pre-deployment (was: in production)
- Time to rotate keys: 5 minutes (was: 2 hours with manual updates)
- Secret exposure incidents: 0 in 8 months (was: 2 in 3 months)
Complete config management - 20 minutes to implement
Key Takeaways
- Validate early: Fail at startup, not during a $50k trade. Schema validation saves hours of debugging.
- Encrypt everything: Even in development. One leaked sandbox key trains bad security habits.
- Hard-code limits: Don't trust environment variables for safety caps. Code-level checks prevent disasters.
Limitations: This setup assumes you control your deployment pipeline. Serverless environments need adapted secret management (AWS Secrets Manager, Parameter Store).
Your Next Steps
- Copy the validation schema and adapt variable names to your API
- Set up dotenv-vault or your secret manager of choice
- Add the CI check to your pipeline
Level up:
- Beginners: Start with just schema validation, add encryption later
- Advanced: Implement secret rotation automation with AWS Lambda
Tools I use:
- dotenv-vault: Local secret encryption - dotenv.org/vault
- AWS Secrets Manager: Production secret storage - Free tier covers most startups
- git-secrets: Prevents committing secrets - GitHub pre-commit hook