Configuration Management and Environment Variables: AI's Secret-Leaking Patterns

You ask your AI assistant to create a database connection. It generates clean, working code that connects perfectly in development. But embedded in that helpful suggestion is your production database password in plain text. Six months later, you discover it's been sitting in your public GitHub repository the entire time.

In 2025, AI-generated code is introducing over 10,000 new security findings per month across studied repositories—a 10x spike in just six months. Among the most dangerous patterns: hardcoded API keys, database credentials in source code, and secrets exposed in client-side bundles.

The problem runs deep: researchers analyzing the Common Crawl dataset used to train large language models discovered 11,908 live API keys, passwords, and credentials embedded in publicly accessible web pages. When AI models train on data containing hardcoded credentials, they learn to reproduce this insecure practice.

The Scope of the Problem

  • 65% of Forbes AI 50 companies had leaked verified secrets on GitHub
  • 10,000+ security findings per month introduced by AI-generated code
  • 11,908 live credentials found in AI training datasets
  • Over 40% of AI-generated code contains some form of vulnerability
  • Secrets in Git history remain accessible forever, even after deletion

Why AI Leaks Secrets

1. Training Data Contains Hardcoded Credentials

AI models learn from billions of lines of public code. Unfortunately, developers frequently commit secrets to public repositories—and the AI learns these patterns as "normal" code.

// This pattern appears thousands of times in AI training data:

// VULNERABLE - AI learned this from training data
const AWS = require('aws-sdk');
AWS.config.update({
    accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
    secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
    region: 'us-east-1'
});

// SECURE - Environment-based configuration
const AWS = require('aws-sdk');
AWS.config.update({
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    region: process.env.AWS_REGION || 'us-east-1'
});

2. AI Prioritizes "Working Code" Over Security

AI tools generate whatever makes the code compile and run. When you ask for a Stripe integration, the AI focuses on functionality—not on keeping your secret key out of version control.

"AI tools generate whatever makes the code compile, happily dropping hardcoded keys into backend logic if that makes the example work. Once committed, it's part of your application in plain sight."

3. No Understanding of Environment Context

AI doesn't understand the difference between development examples and production code. It treats all code generation the same way, often using placeholder values that developers forget to replace.

The Twelve-Factor App: Config Done Right

The twelve-factor app methodology, developed by Heroku engineers, provides the gold standard for configuration management. Its third factor addresses configuration explicitly:

The Litmus Test: Could your codebase be made open source at any moment, without compromising any credentials? If the answer is no, your configuration is wrong.

Core Principles

  • Store config in environment variables: Easy to change between deploys without code changes
  • Never check credentials into version control: Not even encrypted or obfuscated
  • Granular controls: Each env var is independent, not grouped as "environments"
  • Language and OS agnostic: Env vars work everywhere
  • Strict separation: Config varies between deploys, code doesn't

Common AI Configuration Mistakes

1. Hardcoded Database Credentials

// AI-GENERATED (VULNERABLE)
const mysql = require('mysql');
const connection = mysql.createConnection({
    host: 'production-db.amazonaws.com',
    user: 'admin',
    password: 'SuperSecret123!',
    database: 'users_production'
});

// Attack vector: Anyone with repo access has your database
// Result: Full database compromise, data breach

// SECURE VERSION
const mysql = require('mysql');
const connection = mysql.createConnection({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME
});

// With connection URL pattern (preferred)
const connection = mysql.createConnection(process.env.DATABASE_URL);

2. API Keys in Source Code

// AI-GENERATED (VULNERABLE)
const stripe = require('stripe')('sk_live_abc123xyz789...');
const twilio = require('twilio')(
    'AC1234567890abcdef',
    'auth_token_here'
);

// These keys are now:
// - In your Git history forever
// - Visible to anyone with repo access
// - Potentially exposed if repo goes public

// SECURE VERSION
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const twilio = require('twilio')(
    process.env.TWILIO_ACCOUNT_SID,
    process.env.TWILIO_AUTH_TOKEN
);

// Validate required keys at startup
const requiredEnvVars = [
    'STRIPE_SECRET_KEY',
    'TWILIO_ACCOUNT_SID',
    'TWILIO_AUTH_TOKEN'
];

for (const envVar of requiredEnvVars) {
    if (!process.env[envVar]) {
        throw new Error(`Missing required environment variable: ${envVar}`);
    }
}

3. Server Secrets in Client-Side Code

// AI-GENERATED (VULNERABLE) - React component
const PaymentForm = () => {
    const stripe = new Stripe('sk_live_secret_key'); // SERVER KEY IN BROWSER!

    return <form onSubmit={handlePayment}>...</form>;
};

// Browser DevTools: Network tab reveals your secret key
// Result: Anyone can make charges to your Stripe account

// SECURE VERSION - Only publishable key client-side
const PaymentForm = () => {
    // NEXT_PUBLIC_ prefix = safe for browser
    const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);

    const handlePayment = async () => {
        // Call YOUR server, which has the secret key
        const response = await fetch('/api/create-payment-intent', {
            method: 'POST',
            body: JSON.stringify({ amount: 1000 })
        });
    };

    return <form onSubmit={handlePayment}>...</form>;
};

4. JWT Secrets and Encryption Keys

// AI-GENERATED (VULNERABLE)
const jwt = require('jsonwebtoken');
const SECRET = 'my-super-secret-jwt-key-123';

app.post('/login', (req, res) => {
    const token = jwt.sign({ userId: user.id }, SECRET);
    res.json({ token });
});

// Anyone with this secret can:
// - Forge authentication tokens
// - Impersonate any user
// - Bypass all authentication

// SECURE VERSION
const jwt = require('jsonwebtoken');

// Use cryptographically strong secret (256+ bits)
// Generated with: openssl rand -base64 32
const SECRET = process.env.JWT_SECRET;

if (!SECRET || SECRET.length < 32) {
    throw new Error('JWT_SECRET must be at least 32 characters');
}

app.post('/login', (req, res) => {
    const token = jwt.sign(
        { userId: user.id },
        SECRET,
        { expiresIn: '1h', algorithm: 'HS256' }
    );
    res.json({ token });
});

Type-Safe Configuration with Zod

Runtime validation ensures your application fails fast with clear error messages instead of mysterious production failures:

// src/config/env.ts
import { z } from 'zod';

// Define your environment schema
const envSchema = z.object({
    // Database
    DATABASE_URL: z.string().url(),

    // Authentication
    JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
    SESSION_SECRET: z.string().min(32),

    // Third-party APIs
    STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
    STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
    TWILIO_ACCOUNT_SID: z.string().startsWith('AC'),
    TWILIO_AUTH_TOKEN: z.string().min(32),

    // AWS
    AWS_ACCESS_KEY_ID: z.string().regex(/^AKIA[0-9A-Z]{16}$/),
    AWS_SECRET_ACCESS_KEY: z.string().min(40),
    AWS_REGION: z.string().default('us-east-1'),

    // Application
    NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
    PORT: z.coerce.number().min(1).max(65535).default(3000),
    LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),

    // Feature flags (optional)
    ENABLE_DEBUG_MODE: z.coerce.boolean().default(false),
});

// Parse and validate
const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
    console.error('Invalid environment variables:');
    console.error(JSON.stringify(parsed.error.flatten().fieldErrors, null, 2));
    process.exit(1);
}

export const env = parsed.data;

// Type-safe access throughout your app
// env.DATABASE_URL  // string
// env.PORT          // number
// env.NODE_ENV      // 'development' | 'staging' | 'production'

Secrets Scanning with Gitleaks

Prevent secrets from ever being committed with pre-commit hooks:

# .pre-commit-config.yaml
repos:
  # Gitleaks - Detect hardcoded secrets
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.24.2
    hooks:
      - id: gitleaks

  # detect-secrets by Yelp
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  # git-secrets by AWS Labs
  - repo: https://github.com/awslabs/git-secrets
    rev: master
    hooks:
      - id: git-secrets

Setup and usage:

# Install pre-commit
pip install pre-commit

# Install the git hooks
pre-commit install

# Run against entire repo (first time)
pre-commit run gitleaks --all-files

# Generate baseline for existing (intentional) secrets
detect-secrets scan > .secrets.baseline

GitHub Actions Integration

# .github/workflows/secrets-scan.yml
name: Secrets Scanning

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Scan full history

      - name: Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_NOTIFY_USER_LIST: '@security-team'

  trufflehog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: TruffleHog
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
          extra_args: --only-verified

Client vs Server Environment Variables

Modern frameworks have specific conventions for client-safe vs server-only variables:

Critical Rule: Never expose server-side secrets (API keys, database passwords, JWT secrets) to client-side code. Browser JavaScript can be inspected by anyone using DevTools.

// Framework-specific patterns

// NEXT.JS
// Server-only (not exposed to browser)
process.env.DATABASE_URL
process.env.STRIPE_SECRET_KEY

// Client-safe (bundled into browser JS)
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
process.env.NEXT_PUBLIC_API_URL

// VITE
// Server-only
import.meta.env.DATABASE_URL  // undefined in browser

// Client-safe (VITE_ prefix)
import.meta.env.VITE_API_URL
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY

// CREATE-REACT-APP
// Client-safe only (REACT_APP_ prefix)
process.env.REACT_APP_API_URL
// No server-side support - it's purely a client bundler

Server-Side API Route Pattern

// pages/api/create-payment.ts (Next.js)
import Stripe from 'stripe';

// This code ONLY runs on the server
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2023-10-16'
});

export default async function handler(req, res) {
    if (req.method !== 'POST') {
        return res.status(405).json({ error: 'Method not allowed' });
    }

    try {
        const paymentIntent = await stripe.paymentIntents.create({
            amount: req.body.amount,
            currency: 'usd'
        });

        // Only send client-safe data
        res.json({
            clientSecret: paymentIntent.client_secret
        });
    } catch (error) {
        res.status(500).json({ error: 'Payment failed' });
    }
}

Enterprise Secrets Management

HashiCorp Vault

// Using Vault with Node.js
import Vault from 'node-vault';

const vault = Vault({
    endpoint: process.env.VAULT_ADDR,
    token: process.env.VAULT_TOKEN
});

async function getSecrets() {
    const { data } = await vault.read('secret/data/myapp');
    return {
        dbPassword: data.data.DB_PASSWORD,
        apiKey: data.data.API_KEY
    };
}

// Dynamic secrets - Vault generates credentials on-demand
async function getDatabaseCredentials() {
    const { data } = await vault.read('database/creds/myapp-role');
    return {
        username: data.username,  // Temporary, auto-rotated
        password: data.password   // Expires after lease duration
    };
}

AWS Secrets Manager

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'us-east-1' });

async function getSecret(secretName: string): Promise<Record<string, string>> {
    const command = new GetSecretValueCommand({ SecretId: secretName });
    const response = await client.send(command);

    if (response.SecretString) {
        return JSON.parse(response.SecretString);
    }
    throw new Error('Secret not found');
}

// Usage with caching
let cachedSecrets: Record<string, string> | null = null;

export async function getAppSecrets() {
    if (!cachedSecrets) {
        cachedSecrets = await getSecret('myapp/production');
        // Refresh cache every hour
        setTimeout(() => { cachedSecrets = null; }, 60 * 60 * 1000);
    }
    return cachedSecrets;
}

.gitignore Patterns for Secrets

# .gitignore - Environment and secrets files
.env
.env.local
.env.*.local
.env.development
.env.production
.env.test

# IDE and editor secrets
.idea/
.vscode/settings.json

# AWS credentials
.aws/credentials

# SSH keys
*.pem
*.key
id_rsa
id_ed25519

# Certificates
*.crt
*.cert
*.p12
*.pfx

# Database dumps (may contain sensitive data)
*.sql
*.dump

# Terraform state (contains secrets in plain text)
*.tfstate
*.tfstate.*
.terraform/

# Docker secrets
docker-compose.override.yml

# Kubernetes secrets
*-secret.yaml
*-secrets.yaml

What To Do When Secrets Are Exposed

  1. Rotate immediately: Generate new credentials before doing anything else
  2. Revoke the exposed secret: Disable the old API key/password in the provider's dashboard
  3. Audit access logs: Check if the secret was used maliciously
  4. Remove from Git history: Use BFG Repo-Cleaner or git filter-branch
  5. Force push: Update all branches and tags
  6. Notify affected users: If customer data may have been exposed
  7. Post-mortem: Understand how it happened and prevent recurrence
# Remove secret from entire Git history
# WARNING: This rewrites history - coordinate with your team

# Using BFG Repo-Cleaner (faster, recommended)
bfg --replace-text secrets.txt my-repo.git

# Using git filter-repo (built into Git)
git filter-repo --invert-paths --path secrets.env

# Force push to update remote
git push origin --force --all
git push origin --force --tags

# All team members must re-clone or reset their local repos

Key Takeaways

Configuration Security Essentials

  • Never trust AI with secrets: Over 40% of AI-generated code is vulnerable; always audit configuration
  • Follow twelve-factor app: Store config in environment variables, never in code
  • Use Zod for validation: Type-safe environment variables catch errors at startup
  • Implement secrets scanning: Gitleaks pre-commit hooks prevent secrets from ever being committed
  • Separate client and server vars: Only NEXT_PUBLIC_ / VITE_ prefixed vars reach the browser
  • Use secrets managers for production: Vault for multi-cloud, AWS SM for AWS-native workloads
  • Rotate exposed secrets immediately: If a secret is committed, consider it compromised
  • The litmus test: Could your codebase be open-sourced without exposing credentials?
  • Scan Git history: Deleted files still exist in Git history and remain accessible

Conclusion

AI coding assistants are remarkably good at writing functional code, but they're trained on datasets filled with security anti-patterns. The same training data that teaches AI to write elegant solutions also teaches it to hardcode database passwords and expose API keys.

The solution isn't to stop using AI assistants—it's to build configuration security into your development workflow. Validate environment variables with Zod. Block secrets at commit time with Gitleaks. Use the twelve-factor methodology as your north star. And always ask yourself: could this codebase be open-sourced right now without compromising credentials?

Remember: a secret committed to Git is a secret compromised. Even if you delete it in the next commit, it lives in your repository's history forever—accessible to anyone who clones your repo. Prevention is the only reliable cure.

In our next article, we'll explore Building AI-Resistant Codebases, examining how to structure your code so AI assistants generate better, safer suggestions.