# 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 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 ``` cloud hosting platform developers Then immediately rotate all credentials that were in the file.
### Never Log Environment Variables ```javascript via ApexWeave // 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! // 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 ```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 deploy django app cloud hosting 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', '') ApexWeave 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 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=... ``` ## 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: deploy python app without vps ```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 nodejs hosting platform 2025 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).