Skip to main content

secrets-source-of-truth

Why It Exists​

A portfolio of services each needs the same kinds of secrets (database URLs, signing keys, API tokens) in several places: a hosting platform's env, a parameter store, CI, and a local .env file. Without a rule, the "real" value ends up wherever someone last edited it, and the copies drift silently.

The rule this skill encodes: 1Password is the single human-authored source of truth; every platform is a runtime mirror, filled from 1Password by an env-sync CLI (diff, push, validate) driven by a per-repo secrets.manifest.yml. The skill answers one question: where does secret X live, and how do I get it into the platform that needs it? How an app then reads the variable is a different skill (runtime-env-config).

It was hardened after a real credential-exfiltration finding in its CapRover adapter (below).

What It Does​

  • A naming convention for 1Password items, <repo>/<service>/<env>, split into an app-secrets vault for runtime values and an infra-access vault for credentials that reach a store.
  • The env-sync CLI and one adapter per platform (SSM, Vercel, Render, CapRover, GitHub Actions, dotenv), all implementing the same readCurrent / pushValues / describeTarget interface.
  • A decision tree for "where does this secret live", a rotation procedure, and a capture procedure for secrets that only exist on a platform today.
  • Invariants that keep it honest, for example: never hand-edit a platform value; never pass --yes to override a confirmation refusal; never print a secret, only a fingerprint.

How To Use It​

Run from the consuming repo's root, where secrets.manifest.yml lives:

env-sync validate --service <name> --env <env> # item exists, every target reachable
env-sync diff --service <name> --env <env> # compare 1Password to the mirrors
env-sync push --service <name> --env <env> # preview first; add --apply to write

If a manifest has a caprover target, export the allowlist first:

export CAPROVER_ADMIN_PASSWORD=... # from the infra-access vault
export CAPROVER_ALLOWED_DOMAINS=captain.example.com # comma-separate multiple

Gotchas & Invariants​

A manifest must not choose where the admin password goes​

The CapRover adapter logs in by POSTing CAPROVER_ADMIN_PASSWORD to https://<captainDomain>/api/v2/login, and captainDomain comes from the manifest, which is repo content a reviewer may never read. A hostile or compromised manifest on a default branch could therefore redirect the full admin password to a host the attacker controls.

Two changes close it, and both are breaking for existing local setups:

  1. Fail closed on an allowlist. Before the password is even read, captainDomain must be a bare hostname (letters, digits, . and -; no scheme, port, path or @) that exactly matches, case-insensitively, an entry of CAPROVER_ALLOWED_DOMAINS. Unset, empty or blank refuses. No wildcards, no suffix matching. The error names the variable and shows the export line, so a sudden failure explains itself.
  2. Refuse redirects. fetch follows redirects by default, and a 307 or 308 re-sends the POST body and custom headers to the new location. The allowlist only vets the first host, so every CapRover request is made with redirect: "error". A captain that legitimately redirects (http to https, apex to www) fails with caprover login failed: fetch failed (unexpected redirect): list its final hostname.

The standalone Go and TypeScript ports of env-sync (0.4.0 and later) enforce the same rules.

Every secret-shaped log line is a bug​

Error messages are built only from an HTTP status and the response's description, never from a request body or a response data field, which is where env var values live. This is why a failed push can be pasted into a ticket.

How it ships​

This skill is not an npm package. It reaches you through the repository's git-based channels (the skills CLI with --skill skills/secrets-source-of-truth, or the plugin marketplace entry), so update your installed copy to pick up a fix.