# Production secrets are platform-managed and the repo documents where they live

- **Pattern:** `ab-002626` (`soc2-readiness.logical-access.production-secrets-platform-managed`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-07-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002626
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002626)

## Why it matters

SOC 2 CC6.1 (logical access security software, infrastructure, and architectures) expects credentials that gate production systems to live in a managed store with controlled access, not in flat files anyone with repo read access can copy. This check evidences CC6.1: an auditor's first secrets question is "where do production credentials live and who can read them", and the answer must be a platform (Vercel or Netlify encrypted env vars, AWS Secrets Manager, Google Secret Manager, Vault), not a tracked `.env`. AI-generated and vibe-coded apps typically fail this because coding assistants scaffold `dotenv` plus a working `.env`, the app runs, and nothing ever forces the move to a managed store or the writing of a `.env.example`, so the team cannot even enumerate which secrets exist. This is partly an evidence-pointer check: for dashboard-managed platform env vars it verifies committed evidence of the control (a `.env.example`, platform config, or ops doc naming the store), which is exactly the artifact a SOC 2 auditor will ask for. It deliberately does not grep for leaked literal values; the sibling no-hardcoded-secrets check covers that. This one checks management posture and documentation.

## Severity rationale

High because undocumented or flat-file secret management means production credential access is untracked and unrevocable per person, and a single repo-access grant silently becomes production access, which is the exact access-control architecture failure CC6.1 targets.

## Remediation

Move every production credential into your deploy platform's encrypted env vars (Vercel, Netlify, Railway) or a secrets service (AWS Secrets Manager, Google Secret Manager, Vault), read them via `process.env` only, and commit a `.env.example` that enumerates each expected variable with placeholder values (`STRIPE_SECRET_KEY=sk_live_xxx_set_in_vercel_dashboard`). If any tracked file currently holds live values, rotate those credentials, then `git rm --cached .env` and add it to `.gitignore`. Add a short ops note (for example `docs/secrets.md` or a README section) naming the store so the location is auditable from the repo.

## Detection

- **ID:** `production-secrets-platform-managed`
- **Severity:** `high`
- **What to look for:** First determine whether this is a deployable app: a web/server framework in `package.json` (`next`, `express`, `fastify`, `hono`, `remix`, `@sveltejs/kit`, `nuxt`), a server entrypoint, or deploy config (`vercel.json`, `netlify.toml`, `fly.toml`, `render.yaml`, `app.yaml`, `Dockerfile` with a serving CMD); Python/Go equivalents: `django`/`flask`/`fastapi` in `requirements.txt`/`pyproject.toml`, `net/http` server in `main.go`. If pure library or CLI with no deployment surface, skip. Otherwise gather two evidence legs. Leg 1, env-based reads: `process.env.X` (JS/TS), `os.environ` / `pydantic BaseSettings` (Python), `os.Getenv` (Go). Leg 2, a committed pointer to the secret store, any ONE of: (a) `.env.example` or `.env.template` with placeholder values enumerating expected vars; (b) platform config file (`vercel.json`, `netlify.toml`, `fly.toml`, `render.yaml`, `app.yaml`); (c) IaC secret-manager resources (`aws_secretsmanager_secret`, `google_secret_manager_secret`, `vault_generic_secret` / other `vault_*` resources) in `*.tf` or CDK/Pulumi sources; (d) CI secret references (`${{ secrets.X }}` in `.github/workflows/*.yml`, or GitLab/CircleCI equivalents); (e) an ops doc (`README.md`, `docs/**`, `SECURITY.md`, `secrets.md`) naming the secret store. Also check `git ls-files` output (or the tracked tree) for flat secret files: tracked `.env`, `.env.production`, `.env.local`, `config/production.json`, `secrets.yml` containing live-looking values (long random strings, `sk_live_`, `AKIA`, real connection strings), and confirm `.gitignore` covers `.env*`. Platform-encrypted env vars managed in a Vercel/Netlify/Railway dashboard are a fully compliant store; the committed `.env.example`, platform config, or doc is the evidence this check verifies, so never fail solely because the store itself is dashboard-side.
- **Pass criteria:** Code reads secrets from the environment (Leg 1) AND at least one Leg 2 pointer exists with placeholder-only values, AND no tracked flat file carries live-looking production credential values.
- **Fail criteria:** Any tracked flat file contains real-looking production credential values (tracked `.env`/`.env.production` with live keys, `config/production.json` with secrets), even if a `.env.example` also exists; OR the app is deployable and there is zero committed evidence of where production secrets live (no example file, no platform config, no IaC secret resources, no CI secret refs, no ops doc); OR the only "evidence" is a sneaky variant: a `.env.example` whose values are themselves live-looking credentials (a renamed real env file), an ops doc that is a stub or TODO placeholder, an IaC secret resource block that is fully commented out, or `dotenv` wired to a tracked `.env`.
- **Skip (N/A) when:** No deployable app surface (pure library, CLI, or docs repo with no server entrypoint and no deploy config). Quote: `"No web framework, server entrypoint, or deploy config — project has no deployable app surface"`.
- **Before evaluating, quote:** One `process.env` (or `os.environ` / `os.Getenv`) read site with file path, plus the Leg 2 evidence: the `.env.example` path with 2-3 placeholder lines, or the platform/IaC/CI file path and the specific secret reference, or the ops doc path and the sentence naming the store. On fail, quote the tracked flat-file path and describe (never reproduce) the live-looking values found.
- **Report even on pass:** `"Secrets read via process.env; store evidence: <path> (<env-example | platform-config | iac-secret-manager | ci-secret-refs | ops-doc>); no tracked flat files with live values"`.
- **Detail on fail:** `".env.production is git-tracked and contains live-looking values (sk_live_ prefix, 40+ char random strings) — rotate these credentials, git rm --cached, and commit a placeholder-only .env.example"` or `"Next.js app deploys via Dockerfile but repo has no .env.example, platform config, IaC secret resources, CI secret refs, or ops doc naming the secret store — commit evidence of where production secrets live"`.
- **Remediation:** Move production credentials into platform-encrypted env vars or a secrets service, read via `process.env`, and commit a placeholder-only `.env.example` enumerating every expected variable (e.g. `DATABASE_URL=postgres://user:pass@host/db_set_in_vercel`). If live values are tracked, rotate them first, then `git rm --cached .env` and gitignore `.env*`. Add `docs/secrets.md` naming the store. Literal leaked-credential grepping is covered by the Stack Scan's `no-hardcoded-secrets` check in the `project-snapshot` audit.

## External references

- soc2 CC6.1
- iso-27001 A.5.17
- nist IA-5

Taxons: cryptography-and-secrets, regulatory-conformance

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