What Go-side secret-handling convention do you mandate fleet-wide, and what do you leave to teams?
answer
- standardise only what fails silently
- one rule about arrival, one about type
- the API is the enforcement, not the wiki
- name the holes or you sell false confidence
basics
~20 sMandate the two things that fail silently otherwise: secrets arrive as file contents read with os.ReadFile rather than as environment values, and every secret is a named type whose String method masks it. Leave storage backends and per-service layout to teams.
solid answer
~50 sI standardise the two decisions whose failure mode is invisible, and only those. First, delivery: the value arrives in a file with restrictive permissions and the environment carries only the path, because an environment value is a `string` every package and every child process can read, while `os.ReadFile` gives a `[]byte` gated by file permissions. Second, representation: a secret is never a bare `string` in a config struct — it is a named type with a value-receiver `String` that masks it, a `MarshalJSON` that does the same, and one conspicuous accessor for the plaintext. I enforce both in a shared config package rather than in a document. What I leave to teams is where secrets are stored and how they are scoped; what I concede is a recorded exemption list for legacy services and vendor binaries that only accept an environment variable.
go deeper
Know the two habits behind the convention: read a secret from a file into a []byte rather than from an environment value, and give it a type whose String method prints a mask instead of the value.
Be able to explain, mechanism by mechanism, why an environment value is more exposed than a file read and where a String method stops working — unexported fields, %#v, encoding/json.
Argue the tradeoffs for a real service and show the enforcement: a shared config package that hands back the safe type, one conspicuous accessor, and a test that formats the config and finds no plaintext.
Own the fleet decision and its politics: what you standardise versus leave to teams, how you phase the migration, what exemptions you grant and record, and who gets to overrule you.
## Which decisions are actually fleet-wide Most of what people want to standardise about secrets is local: which store, which naming scheme, which scope per service. Those differ legitimately between teams and cost little when they differ. Two decisions do not, because they are the ones whose failure is silent and whose blast radius is the whole fleet: 1. **How a secret reaches the process** — an environment value or a file the process reads. 2. **What type it has once inside** — a bare `string` or a type that cannot be printed by accident. Both are cheap to get right at the start of a service and expensive to retrofit across two hundred of them, which is the definition of a platform decision rather than a team one. ## The delivery call, argued in Go terms An environment-delivered secret has properties that follow from Go's own API surface. `os.Environ()` hands *any* package in the binary — including a transitive dependency and any crash reporter linked into it — a copy of every variable. `os/exec` gives every child process the parent's environment unless someone remembers to set `Cmd.Env`. The value is a `string`, which Go cannot overwrite. And on Linux the initial block stays visible through `/proc/<pid>/environ` regardless of what `os.Unsetenv` does afterwards. A file-delivered secret inverts each of those: `os.ReadFile` returns a `[]byte` that can be zeroed with `clear` when the process is done with it, access is gated by the file mode rather than by process ancestry, there is no enumeration API that walks all of them, and nothing inherits it implicitly. The cost is real and worth stating: local development gets slightly more ceremony, some vendor binaries only accept environment variables, and a file needs a mount or a writer. That is why the standard is *path in the environment, value in the file* — deployments keep configuring processes the way they already do. ## The type call, and its honest limits Mandating a named secret type is the higher-leverage half, because the incidents almost never come from someone printing a password on purpose. They come from a config struct dumped whole into an error with `%+v`, a debug handler serialising it, or a panic path that formats state. A type with a value-receiver `String` returning a constant mask stops the default path. The mandate has to be stated with its holes visible, or it produces false confidence, which is worse than no convention: - `fmt` skips `String` for **unexported** fields, so the convention has to be "the field's *type* is a secret type", not "the field is unexported". - `%#v` asks for `GoString`, not `String`. - `encoding/json` uses `MarshalJSON`; a `String` method means nothing to it. - Structured logging has its own hook, `slog.LogValuer`. - None of it touches a core file or a heap dump, where the plaintext is still greppable. So the mandated type implements the formatting hooks the codebase actually uses, and the mandate is explicitly about *accidental* exposure. ## Enforcement, which is where conventions live or die A convention documented in a wiki page decays; a convention expressed as the only available API does not. The enforcement point is a shared configuration package that returns the secret type and offers no way to obtain a bare `string` except through one conspicuously named accessor. That makes each real use of the plaintext a single greppable call site, which is what code review and a CI analyzer built on the standard analysis tooling can both watch. Everything else — a lint rule flagging `os.Getenv` for names matching a secret-ish pattern, a test that formats the config struct and asserts the mask appears — is a cheap backstop. ## What I concede, and who overrules me I cannot rewrite the fleet at once, so the boundary is: new services and the shared config package comply now; existing services comply when they next touch configuration; a written exemption list carries the vendor binaries and the legacy paths, with an owner and a review date on each entry. If the security team decides the exemption list is too long or that a particular service's exposure is unacceptable, they overrule the schedule — that is their call to make, and having the list is what makes the conversation possible at all. The measure of whether the convention worked is not compliance percentage. It is whether the next postmortem about a leaked credential says "it was printed by a code path nobody thought about", because that is precisely the failure the two mandates exist to remove.
- A vendor binary you must run only accepts its credential in an environment variable. What do you do?Grant a recorded exemption rather than pretending it complies. Load the secret from the file in the entrypoint, build that child's environment explicitly with `Cmd.Env` so it carries that one variable and nothing else, keep the parent's own copy in a secret type, and put the exemption on the list with an owner and a review date. The scope of the exception is then one process, not the fleet.
- How do you keep the mandate from becoming false confidence?State its limits in the same document that states the rule: it stops accidental formatting, not deliberate printing, and it does nothing for a core file or a heap dump. Make the secret type implement every formatting hook the codebase actually uses — `String`, `MarshalJSON`, and the structured-logging hook — and add a test that formats a populated config struct and asserts no plaintext appears.
- How do you measure whether this convention is working?By the shape of incidents rather than a compliance number. Track how many credential-exposure postmortems name a diagnostic path — an error, a debug endpoint, an inherited environment — and whether the exemption list is shrinking. A high compliance percentage with the same incidents recurring means the mandate is aimed at the wrong path.
saying these in an interview costs you the question
- Standardises everything, including team-local storage choices
- Presents a String method as making secrets safe everywhere
- Mandates a rule with no enforcement point in code
- Refuses any exemption and drives teams to work around the standard
- Cannot say which failure each half of the convention prevents