# ESP API credentials stored in environment variables

- **Pattern:** `ab-002454` (`sending-pipeline-infrastructure.esp-integration.credentials-secure`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002454
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002454)

## Why it matters

A hardcoded SendGrid or Mailgun API key in a committed source file is a credential that any engineer with repository read access — and any attacker who gains access to the repo, a build artifact, or a public GitHub fork — can use immediately to send email at your expense, under your sending domain, to any recipient list. This maps to OWASP A02 (Cryptographic Failures) and CWE-798 (Use of Hard-coded Credentials). The damage is not theoretical: repositories are leaked, build logs are captured, and Docker image layers are extracted. Committing credentials is an irreversible exposure until the key is rotated.

## Severity rationale

Critical because a committed API key grants immediate unauthorized send access to the account and cannot be made safe after the fact without key rotation.

## Remediation

Load all ESP credentials via `process.env` and fail loudly at startup if the variable is absent, in `lib/esp-client.ts`:

```ts
const apiKey = process.env.SENDGRID_API_KEY
if (!apiKey) {
  throw new Error('SENDGRID_API_KEY environment variable is required')
}
sgMail.setApiKey(apiKey)
```

Audit `.env.example` to confirm it contains only placeholder values like `your-key-here`. Rotate any key that has ever appeared in a committed file, even in a branch that was never merged.

## Detection

- **ID:** `credentials-secure`
- **Severity:** `critical`
- **What to look for:** Examine how the ESP client is initialized. Count all credential references (API keys, SMTP passwords, sending tokens) across the codebase — there must be 0 hardcoded string literals. Look in the ESP client initialization file, any configuration files committed to the repository, and check `.env.example` to confirm actual keys are not there. Verify the real credentials are loaded via `process.env.*` or a secrets manager (AWS Secrets Manager, Vault, Doppler).
- **Pass criteria:** All ESP API keys and credentials are loaded from environment variables or a secrets manager at runtime. Count all credential references in the codebase — fewer than 1 hardcoded value must exist (i.e., zero). `.env.example` contains only placeholder values. Must not pass when even 1 credential string literal exists in any committed source file.
- **Fail criteria:** An API key, SMTP password, or other credential is hardcoded as a string literal in any source file. Any committed configuration file contains a live credential.
- **Skip (N/A) when:** Never — all credential handling must be checked.
- **Cross-reference:** The Operational Resilience (Email) Audit's failure-recovery category verifies ESP failover — this check verifies the foundational credential security that failover depends on.
- **Detail on fail:** Describe the credential exposure. Do not include the actual key value. Example: `"SendGrid API key hardcoded as a string literal in lib/email.ts — will be committed to source control"` or `"SMTP password hardcoded in nodemailer transport config in config/mail.js"`
- **Remediation:** Load credentials from environment variables at module initialization:

  ```ts
  // lib/esp-client.ts
  import sgMail from '@sendgrid/mail'

  const apiKey = process.env.SENDGRID_API_KEY
  if (!apiKey) {
    throw new Error('SENDGRID_API_KEY environment variable is required')
  }

  sgMail.setApiKey(apiKey)
  export { sgMail }
  ```

  For multiple environments, use a secrets manager to inject variables:

  ```ts
  // For AWS Secrets Manager
  import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'

  const client = new SecretsManagerClient({ region: 'us-east-1' })
  const { SecretString } = await client.send(new GetSecretValueCommand({ SecretId: 'prod/email-service/sendgrid' }))
  const { apiKey } = JSON.parse(SecretString!)
  ```

## External references

- cwe CWE-798
- cwe CWE-312
- owasp A02

Taxons: cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-002454
