Files
zeekayandHanzo Dev bf57162556 fix(secrets): a listing must name what a get can fetch
A list is a subtree query — it spans sub-paths, and with no env filter it
spans environments. The wire answered with bare NAMES, and a bare name is not
an address in a store keyed by (path, env, name): the only thing a caller can
do with one is re-join it at the path it happened to query. For any record one
segment deeper, or in another environment, that names a key which does not
exist. So the list named a secret and the matching get missed it — and
LoadEnv, whose whole job is "fetch everything here and set it", exits the
process on the first such record.

The list now answers with COORDINATES on both framings, and the coordinate
type moves to its own leaf package: the client needed to name a record and
could not import the server's storage package to do it without dragging the
database into every binary that reads one secret. One coordinate, one place,
imported by store, zapserver, zapclient and the HTTP face alike.

GetSecretsWith reads each record at its own coordinate. It still keys the
result by name, because os.Setenv does — an environment is a flat namespace —
so two records under the subtree that share a name now ERROR naming both
coordinates instead of one silently winning. A process that boots with the
wrong DATABASE_URL and never learns is the failure being avoided.

Find also bounds itself (10000 rows) and SAYS when it did. An enumeration is
authenticated, but one request that pulls the whole keyspace into memory and
onto the wire is still an amplification lever — and a cap that did not report
itself would reintroduce the very defect this surface was hardened against: a
short list that reads as the whole truth.

Whitespace in an env or name is refused on write for the same reason: a list
query trims its filter, so a stored "prod " could never be selected by
?env=prod and would answer empty, which reads as an empty store.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 15:39:23 -07:00

49 lines
2.4 KiB
Go

// Package secret holds the ADDRESS of a stored secret — the coordinate, never
// the value.
//
// It exists because both ends of the wire need to agree on what a secret is
// named, and neither may depend on the other to say so: pkg/store owns the
// ZapDB keyspace, pkg/zapclient runs inside every consumer, and a client that
// had to import the server's storage package to name a record would drag the
// database into every binary that reads one secret. One coordinate type, in one
// leaf package, imported by store, zapserver, zapclient and the HTTP face
// alike — so "which record" means exactly the same thing everywhere.
//
// Nothing here can carry a value. That is the point: enumeration is a
// coordinate operation, and the type system, not a code review, is what keeps a
// listing from ever returning plaintext.
package secret
// Ref is a stored secret's full coordinate — the complete answer to "what is in
// this store". It has no value field, so an enumeration is structurally
// incapable of returning a secret, and it carries the path and env alongside
// the name so a listed record is never mistaken for a different environment's
// record of the same name.
//
// A Ref feeds straight back into Get and Delete. That round trip is the
// invariant: whatever a listing names, a read of that same coordinate must
// find. A list that answers with bare names cannot honor it, because a name
// alone does not say which path or which environment it came from.
type Ref struct {
Path string `json:"path"`
Env string `json:"env"`
Name string `json:"name"`
}
// Query selects a set of stored secrets. The ZERO Query selects every record:
// a store you cannot ask "what is in you" cannot be audited, rotated, or
// checked for coverage, so "everything" must be expressible.
type Query struct {
// Path is a subtree root. It selects the secrets stored at this path AND
// at every path beneath it; "" selects the whole store. Matching is on the
// segment boundary, so "deploy" reaches "deploy/ci" but never "deployfoo".
Path string
// Env restricts the result to one environment. "" means EVERY environment.
// There is deliberately no default env here: env is a component of the
// storage key, so a listing that silently picked one would report an empty
// store while another env held every record — indistinguishable, from the
// outside, from a store that is genuinely empty.
Env string
}