{
  "uid": "cogitave.learn.configuration-management.secrets-and-environments",
  "kind": "moduleUnit",
  "href": "/modules/configuration-management/secrets-and-environments/",
  "title": "Secrets and environments",
  "summary": "",
  "type": null,
  "products": [],
  "roles": [],
  "levels": [],
  "subjects": [],
  "headings": [
    "Three planes, never conflated",
    "Secrets are referenced, never embedded",
    "The dev-env approach: dotenvx"
  ],
  "source": "Config can be committed and reviewed in the open. Secrets cannot. The\n[secrets & environment standard](../../../../standards/docs/standards/secrets-and-env.md)\nand [ADR-0008](../../../../standards/docs/decisions/0008-secrets-management-and-dotenvx.md)\ndraw a hard line, and the rule at the top of it is simple.\n\n> [!IMPORTANT]\n> **No plaintext secret in git, ever** - not in code, history, CI logs, issues, or\n> Terraform state. The *only* secret-shaped thing that may be committed is config\n> that is **encrypted at rest**.\n\n## Three planes, never conflated\n\nThe standard separates secret handling into three planes. Conflating them is the\nmistake that leaks credentials.\n\n| Plane | What | Where |\n| --- | --- | --- |\n| **Runtime secrets** | DB creds, API and OAuth tokens, signing keys | A secret store (Vault-class or cloud SM), rotated and leased |\n| **Config and dev env** | app `.env`, non-prod config, k8s manifests | **Encrypted-at-rest, committed** (dotenvx / SOPS), decrypted at runtime |\n| **CI / platform** | cloud access for pipelines | **GitHub OIDC to cloud**, keyless, per-run |\n\nThe dev-env plane is a safe *transport* for low- and medium-sensitivity config.\nIt is **not** a substitute for the store: high-sensitivity and production\ncredentials always come from the store via workload identity, because dev-env\nfiles are not dynamic, not auto-rotated, and not leased.\n\n## Secrets are referenced, never embedded\n\nThis is where configuration and secrets meet. Config carries a **reference**, and\nthe app resolves the **value** at runtime.\n\n- A secret field's type is **`SecretRef`** - a URI such as `vault://...` or\n  `cog-secrets://...` - never a `String` holding a value. A literal-looking secret\n  in config is a **parse error**.\n- The loader resolves `SecretRef` to a value at startup; if it cannot resolve, the\n  service **fails closed**, exactly as it does for any invalid config.\n- The config-to-secret edges form an auditable dependency graph: which service may\n  resolve which secret.\n\n## The dev-env approach: dotenvx\n\nPlaintext `.env` is the single largest source of leaks, so the standard mandates\n**[dotenvx](https://dotenvx.com/)**: encrypted `.env` files that are safe to\ncommit.\n\n- A **`DOTENV_PUBLIC_KEY`** sits at the top of the encrypted `.env`, so anyone on\n  the team can **add and encrypt** values.\n- The matching **`DOTENV_PRIVATE_KEY`** is the only thing that can decrypt. It\n  lives in `.env.keys`, which is **gitignored**. At runtime `dotenvx run` decrypts\n  and injects.\n- **Key custody is the important part:** the private keys are themselves *runtime\n  secrets*. In production they live in the secret store, never in `.env.keys`; in\n  CI/CD the key is fetched via **GitHub OIDC to the store**, not pasted as a static\n  repo secret. The encrypted `.env` is committed; the key is not.\n\nFor structured files and Kubernetes/GitOps, the reference tool is **SOPS + age**,\nwhich encrypts only the *values* so manifests still diff cleanly.\n\n> [!CAUTION]\n> On a leak, the order is fixed: **revoke or rotate the credential first**, then\n> purge history with `git filter-repo`, and handle it as a security incident.\n> Purging history without rotating leaves a live, exposed credential - and around\n> 80% of leaked secrets are still valid when found.\n",
  "partOf": "cogitave.learn.configuration-management",
  "durationInMinutes": 7,
  "quiz": null
}