Provably-dead exported accessor: no in-repo callers (only cmd/kmsd and
mount.go consume the root package) and no external importer of
github.com/hanzoai/kms across the tree uses it. Its backing httpAddr
field was write-only and its comment was wrong (stored the raw config
addr, never a resolved listener address). Removes field, assignment,
and accessor. Build+vet green.
env is a first-class component of the storage key
(kms/secrets/{path}/{env}/{name}); a POST/PATCH that omitted env used to
silently default to "default", committing the write to a bucket that
project/env/path readers (kms-operator, cluster syncs) never resolve. That
split landed an IAM z-password in env=default while prod kept serving the
stale value. POST/PATCH now fail loud with 400 when env is omitted;
GET/DELETE/LIST keep a compat default for legacy readers (a read/delete
can't plant a value another reader trusts).
Root cause on the client: sdk/go/kmsclient dropped env on every HTTP op
(c.env was used only on the ZAP path), so a client configured for env=prod
still wrote to env=default over HTTP. The HTTP path now sends c.env on the
write body and as ?env= on read/delete, matching the ZAP path. Built
in-tree via a local replace so cmd/kms and cmd/kms-fetch ship the fix; the
two Dockerfiles stage sdk/go/go.mod so `go mod download` resolves the
replaced module.
Regression tests: write without env -> 400; write env=prod is readable via
the operator's project/env/path resolution (sha256 round-trip, values never
printed) and is not visible in env=default.
Claude-Session: https://claude.ai/code/session_01D4FSvT3UfhrFJNQjrctjEj
Co-authored-by: hanzo-dev <dev@hanzo.ai>
IAM moved its OAuth token endpoint to /v1/iam/oauth/token; the login proxy
(and the Go SDK client) still called the retired /v1/iam/login/oauth/access_token,
which now 404s — so /v1/kms/auth/login returned 401 for every valid machine
identity. This broke fresh KMSSecret-operator logins and the hanzoai/ci deploy-
cred fetch. Point both at /v1/iam/oauth/token (form/body unchanged); update LLM.md.
Note: requires a KMS image release + redeploy (ghcr.io/luxfi/kms) to take effect.
The kms.hanzo.ai "/api/v1/* returns 200" signal was a false positive: the
frontend catch-all (registerFrontend) served index.html for any unmatched
GET, including /api/v1/auth/universal-auth/login. No /api/ route is
registered anywhere in this repo — the server is already 100% /v1/kms/*
native (Go mux, both Go SDKs, and the React frontend all use /v1/kms).
The catch-all's defense-in-depth bail-out excluded /v1/, /healthz, /health
but not /api/, so an /api/-shaped GET fell through to the SPA fallback and
answered 200. This made a nonexistent legacy /api/ backend look live.
Add /api/ (and bare /api) to the bail-out so it 404s at the wire. The ONE
canonical prefix is /v1/kms; there is no /api/, and now the server says so
unambiguously. No alias, no rewrite, no backward compat.
frontend_spa_test.go locks the invariant: /api/* → 404, /v1/ and /health →
404, /healthz → 200 (real handler wins), client routes + assets → 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a bare list endpoint so an operator console can browse an org's secret
KEYS without ever exposing a value. Structurally incapable of leaking a value:
the scan iterates ZapDB KEYS ONLY (PrefetchValues=false) under
kms/secrets/<brand/org>/ and the row type has no value field — there is no
code path from this handler to a secret value blob.
Verified against the store: secret path/name/env are PLAINTEXT in the ZapDB
key kms/secrets/{path}/{env}/{name}; only the value blob is at-rest encrypted.
So names are honestly listable; values are never touched.
- Authz: identical authorize()+canActOnOrg() gate as get-one — never weaker.
Structurally confined to the org's brand/<org>/ namespace (stricter: cannot
enumerate another tenant's names). Optional ?prefix= (safePath + org
containment) and ?env= filters.
- Routing: bare GET coexists with the {rest...} wildcard (Go 1.22 ServeMux);
the bare path hits list, .../secrets/<path>/<name> still returns its value.
- updatedTime sourced from a new kms/mtimes/ sibling index (mirrors
versioning.go), written on put/patch, deleted on delete — never from the
value blob. Records predating this carry no mtime (updatedTime omitted),
never faked.
- TestRed3 traversal heuristic tightened: '/secrets/foo/..' path-cleans to
'/secrets' (the list), an authorized empty {secrets:[]} with no value; the
test now asserts on an actual value payload, not the substring 'secret'.
Tests: 9 new TestList_* (no-value-leak, authz fail-closed, routing
precedence, prefix injection-safety, env filter, empty org, mtime RFC3339).
VERSION 2.5.2 -> 0.159.4 (patch, v0.x server line; tag cut on merge, not now).