Day 152 — Secrets & 12-factor config¶
Month 6 · Week 2 · ⬅ Day 151 · Day 153 ➡ · Journal index
🎯 Learning Objective¶
Apply the 12-factor config discipline in Go: read all config from the environment, fail fast on invalid values, source secrets from files (not inline env), and never log them.
📚 Topics¶
- 12-factor III: strict separation of config from code
os.LookupEnv, typed parsing, accumulate-and-join validation- The
*_FILEsecret pattern; redaction; never committing secrets
📖 Reading / Sources¶
- The Twelve-Factor App — III. Config
-
os.LookupEnv·errors.Join -
strconv·time.ParseDuration - Docker secrets (the
*_FILEconvention)
📝 Notes¶
- Factor III: config that varies between deploys lives in the environment, not in code or a committed file. The same binary then runs in dev/staging/prod by swapping env → [[12-factor]].
- Parse env into a typed
Configstruct once at startup; the rest of the program depends on the struct, never scatteredos.Getenvcalls → [[config-struct]]. - Use
os.LookupEnv, notos.Getenv, when you must distinguish "unset" from "empty".Getenvreturns""for both;LookupEnvreturns theokboolean. - Fail fast and accumulate. Validate every field, collect problems, and return them together with
errors.Join. A misconfigured deploy then reports all issues at once instead of one-restart-at-a-time → [[fail-fast]]. - Secrets are not config. Inject them as files (
<NAME>_FILE→ path), because env vars leak viadocker inspect,/proc/<pid>/environ, child processes, and crash dumps → [[secrets]]. Docker/K8s mount secrets at paths like/run/secrets/db_password. - Never log a secret. Redact (mask all but a few chars) before printing, and keep secrets out of error messages and structured-log fields. Pair with
slog'sReplaceAttrto drop sensitive keys. - Never commit secrets. Use
.gitignore, scanning (gitleaks), and rotate anything that lands in history.
💻 Code Examples¶
// Load builds a typed Config from the environment and reports EVERY problem.
func Load() (Config, error) {
var problems []error
cfg := Config{}
port := getenv("PORT", "8080") // helper: LookupEnv with a fallback
if n, err := strconv.Atoi(port); err != nil {
problems = append(problems, fmt.Errorf("PORT %q not an int: %w", port, err))
} else {
cfg.Port = n
}
if cfg.DatabaseURL = getenv("DATABASE_URL", ""); cfg.DatabaseURL == "" {
problems = append(problems, errors.New("DATABASE_URL is required"))
}
return cfg, errors.Join(problems...) // one error wrapping all causes
}
Full code:
examples/month-06/config/main.go· Run:go run ./examples/month-06/configSecret resolution (
*_FILEbeats env) + redaction:examples/month-06/secrets/main.go· Run:go run ./examples/month-06/secrets
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Typed env config with accumulate-and-join validation | ✅ | exercises/month-06/week-2/envconfig |
*_FILE-first secret resolver + redaction |
✅ | exercises/month-06/week-2/secretsource |
🐛 Mistakes Made¶
- Used
os.Getenv("FEATURE_X")and couldn't tell "unset" from "explicitly empty". Switched toos.LookupEnvto get theokflag. - Returned the first config error and stopped; ops had to fix-and-restart five times. Switched to
errors.Jointo surface all problems at once.
❓ Open Questions¶
- For real secret stores (Vault, AWS Secrets Manager), is the
*_FILEpattern still the integration point? (Often yes — a sidecar/CSI driver mounts the secret as a file the app reads unchanged.)
🧠 Active Recall (answer without looking)¶
- Q: Why prefer mounting a secret as a file over passing it in an environment variable?
A
Env vars leak: they appear in docker inspect, /proc/<pid>/environ, are inherited by child processes, and can land in crash dumps. A file (mode 0600) is read only by the process and isn't broadcast through the environment.
2. Q: What does errors.Join give you in a config loader, and how do callers still inspect causes? A
It combines many validation errors into one value (so you report all problems at once). errors.Is/errors.As still match any of the joined causes, since Join builds an error whose Unwrap() []error exposes them.
🪶 Feynman Reflection¶
12-factor config is "build once, configure per-deploy." The binary knows how to run; the environment tells it where (DB, ports) and the mounted files tell it the secrets. Validate it all at the door, shout about everything that's wrong, and never write a secret into a log.
🕳️ Knowledge Gaps¶
- Hot-reloading config (SIGHUP, file-watch) vs restart-to-reload — touches [[day-153]] (signals).
✅ Summary¶
I can load a typed config from the environment, fail fast with joined errors, source secrets from files via the *_FILE pattern, and redact them so they never hit the logs.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 153: handle SIGINT/SIGTERM and shut the server down gracefully.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦🟦⬜ |
Suggested commit: docs(journal): secrets & 12-factor config (day 152)