Context / need
A service platform means dozens of credentials: database passwords, API
tokens, registrar API keys for TLS. The risk is not exotic: a secret
committed by mistake, an environment variable leaking into a log or a
docker inspect, a token stored in plain text on the server.
Constraints
- Everything is versioned. That is the strength of the setup (declarative, reproducible configuration), and it forces a clean boundary between Git and secrets.
- A single maintainer. The solution must be easy to apply every time, without heroics.
Decision & why
Rather than heavy tooling, systematic hygiene, applied everywhere.
- Nothing in Git. Environment files and secret directories are ignored
from the repository’s creation. Committed templates (
.env.example) document the expected variables, without values. - File-based injection rather than environment variables when the image
allows it (e.g.
POSTGRES_PASSWORD_FILE). The secret appears neither indocker inspectnor in the environment inherited by processes. - Never a plain-text token on the server. API tokens, like those of the Hub, are stored hashed with SHA-256. A database leak then discloses no reusable credential.
- A self-hosted Vaultwarden vault, reachable from the tailnet only, as the source of truth for human secrets: generation, storage, sharing.
Accepted tradeoff
No centralized server-side secrets manager (Vault or sops/age). At the scale of a single maintainer, file discipline and the vault cover the need without adding one more critical component. If the platform grows (several operators, automatic rotation), this choice will be revisited.
Outcome
- Secure: a public repository or a compromised database discloses no exploitable secret. The leak surface (logs, inspect, environment) is reduced at the source.
- Maintainable: simple rules, identical everywhere. Any service on the platform follows the same contract.
Posture: audit, find, fix
Hygiene is not a state, it is a practice. Revisit the existing setup regularly, look for stray secrets (Git history, configurations, logs), fix, and revoke whatever was exposed, even briefly. A leaked secret stays compromised until it is replaced.
A lived example, July 2026. A voluntary security audit of the platform’s
repositories revealed a WireGuard private key committed in plain text in
a docker-compose, since the repository’s first commit, meaning several
weeks of potential exposure. Doctrine applied: an exposed secret is a
compromised secret. The key was removed from the file, then made permanently
obsolete (the VPN it served was decommissioned). And since Git history keeps
everything, the publication rule was hardened: a repository made public
starts from a fresh history.
The audit remains manual today; its tooling (automated history scanning, pre-commit hooks) is a roadmap project.