Replace inline upstream-brand mentions in DEPRECATED.md, LLM.md, and a frontend gap description with neutral 'legacy fork' wording. Add a NOTICE file carrying the required upstream MIT attribution. The LICENSE retains the upstream copyright notice as required by the MIT license; that is the only place the upstream name remains. No functional changes. Go backend and frontend build unchanged.
13 KiB
Hanzo KMS
Last Updated: 2026-04-23 Repo: github.com/hanzoai/kms Module: github.com/hanzoai/kms (Go 1.26.1)
Upstream lineage: see LICENSE for the required upstream MIT attribution. The legacy fork's code path was deleted; the current request path is a thin wrapper over github.com/luxfi/kms. The copyright header is retained for legal accuracy, but none of the original upstream code remains on the wire.
What this is
A thin Hanzo-branded wrapper over github.com/luxfi/kms. The Go server,
secret store, MPC client, ZAP secrets server, and S3 replicator all come
from luxfi/kms. This module owns three things:
cmd/kmsd— the daemon. Wires luxfi/kms primitives with Hanzo defaults (port 8443,/data/hanzo-kms, IAM athttps://hanzo.id) and adds Hanzo-specific JWT verification, audit log, version CAS, header hygiene.cmd/kms— the admin CLI (kms put|get|list|rotate|status).pkg/kmsclient— the Go client used by every other Hanzo service to fetch secrets at runtime (HTTP + ZAP fallback).
There is no legacy Node.js fork in this repo. There is no Base/SQLite. There
is no PostgreSQL in the canonical Go module. Everything that used to
live in internal/handler/, internal/store/, internal/server/ has been
deleted in favour of the upstream github.com/luxfi/kms/pkg/{store,keys, mpc,zapserver} packages. The frontend/ and ui/ trees are legacy React
assets shipped as static files; they do not affect the API surface.
Architecture
┌──────────────────────────────┐
│ Hanzo IAM (hanzo.id) │
│ RFC 6749 + OIDC, JWKS │
└──────────────┬───────────────┘
OAuth2 / JWT (RS256)
│
┌───────────────────────────▼──────────────────────────┐
│ kmsd │
│ HTTP :8443 (mux: /v1/kms/* only) │
│ ZAP :9999 (binary, opcodes 0x0040..0x0043) │
│ │
│ cmd/kmsd/main.go ← wiring + routes │
│ cmd/kmsd/auth.go ← JWT verify (RFC 7519) │
│ cmd/kmsd/jwks.go ← JWKS cache + RSA resolve │
│ cmd/kmsd/audit.go ← per-request audit ledger │
│ cmd/kmsd/versioning.go← per-secret version CAS │
│ │
│ github.com/luxfi/kms/pkg/store ← secret CRUD │
│ github.com/luxfi/kms/pkg/keys ← MPC key mgr │
│ github.com/luxfi/kms/pkg/mpc ← MPC client │
│ github.com/luxfi/kms/pkg/zapserver← ZAP transport │
└──────────────┬───────────────────────────────────────┘
│
┌──────────────▼─────────────┐ ┌─────────────────────┐
│ ZapDB (encrypted, LSM) │───▶│ S3 (age-encrypted)│
│ $KMS_DATA_DIR │ │ 1s incremental │
└────────────────────────────┘ └─────────────────────┘
Routes (canonical, exhaustive)
All routes live under /v1/kms. There is no /api/. There are no aliases.
Public
| Method | Path | Notes |
|---|---|---|
| GET | /healthz |
Liveness, no auth |
| POST | /v1/kms/auth/login |
Machine-identity client credentials |
POST /v1/kms/auth/login exchanges clientId+clientSecret for an IAM
access token by proxying to IAM's OAuth token endpoint
(POST $IAM_ENDPOINT/v1/iam/login/oauth/access_token). The response is a
plain {accessToken, expiresIn, tokenType} envelope. Outbound calls to
IAM use /v1/iam/* — not a route this service exposes, and never
/api/* (legacy Casdoor compat surface).
Secrets (per-org, JWT-gated)
| Method | Path |
|---|---|
| GET | /v1/kms/orgs/{org}/secrets/{path...}/{name}?env=… |
| POST | /v1/kms/orgs/{org}/secrets |
| PATCH | /v1/kms/orgs/{org}/secrets/{path...}/{name} |
| DELETE | /v1/kms/orgs/{org}/secrets/{path...}/{name}?env=… |
- Authorization:
Authorization: Bearer <jwt>from Hanzo IAM. Token's verifiedownerclaim must equal the URL{org}segment, OR the token must carry an admin role (superadmin,kms-admin,admin). - POST is upsert: bumps version, no CAS.
- PATCH is update-only and requires version CAS via either the
If-Match: <int>header orbody.version. Missing both → 428. Mismatch → 409 with the current version. Replay defence is structural, not advisory. - DELETE wipes the version record so a recreate restarts from 1.
Admin-only
| Method | Path | Use |
|---|---|---|
| GET | /v1/kms/secrets/{name} |
Process env-var fetch (env-backed) |
| GET | /v1/kms/audit/stats |
Background auditor counters |
These require an admin role claim. The env-backed fetch is intended only for in-cluster bootstrap of services that read their own env vars before KMS is reachable.
MPC keys (only when MPC_VAULT_ID is set)
| Method | Path | Use |
|---|---|---|
| POST | /v1/kms/keys/generate |
DKG a validator key set |
| GET | /v1/kms/keys |
List validator key sets |
| GET | /v1/kms/keys/{id} |
Get one key set |
| POST | /v1/kms/keys/{id}/sign |
Threshold sign (BLS or RT) |
| POST | /v1/kms/keys/{id}/rotate |
Reshare keys |
| GET | /v1/kms/status |
KMS+MPC liveness |
All require admin role. Threshold signing delegates to luxfi/mpc over ZAP
(or HTTP fallback) at MPC_ADDR.
Deploy-mnemonic — one seed, four orgs
kms.hanzo.ai is the single KMS endpoint serving four Lux-derived
chains: hanzo, zoo, pars (plus Lux at kms.lux.network, same
code, separate instance). The same 12-word BIP39 deploy mnemonic is
stored under each org's KMS path:
| Org | Path | Envs |
|---|---|---|
| hanzo | providers/hanzo/deploy-mnemonic | dev, test, main |
| zoo | providers/zoo/deploy-mnemonic | dev, test, main |
| pars | providers/pars/deploy-mnemonic | dev, test, main |
The bytes are identical across orgs (and identical to the bytes Lux
keeps at providers/lux/deploy-mnemonic on kms.lux.network). Each
chain derives its own validator keys via
m/9000'/<networkId>'/<envID>'/<i>. See ~/work/lux/CLAUDE.md
§"Mnemonic + Key Derivation" for the canonical reference.
Jurisdictionally separate white-label tenants do NOT share this
mnemonic — each keeps its own under providers/<tenant>/* in its
own KMS deployment.
Auth contract (cmd/kmsd/auth.go)
Full RFC 7519 enforcement, no escape hatches:
Authorization: Bearer <token>else 401.alg ∈ {RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, EdDSA}. No HS*. Nonone.- Signature verifies against JWKS by
kid. iss == $KMS_EXPECTED_ISSUER.aud⊇ one of$KMS_EXPECTED_AUDIENCE(comma list ok).exp > now, leeway 0.nbf ≤ nowif present.sub(orid) present.
Failure → 401 with body {"message":"unauthorized"}. The structured audit
log records the failure class (alg_not_allowed, expired, wrong_iss,
wrong_aud, sig_invalid, missing_sub, no_bearer, misconfigured).
No claim is echoed to the client body or logs.
KMS_ENV ∈ {dev,devnet,local} is the only mode where missing
KMS_EXPECTED_ISSUER/KMS_EXPECTED_AUDIENCE/KMS_JWKS_URL is tolerated.
In any other mode validateAuthConfigAtBoot refuses to start.
Header hygiene
stripIdentityHeaders deletes every inbound identity header before mux
dispatch. The handler trusts only what the verified JWT says. Killed
headers include X-User-Id, X-Org-Id, X-Roles, X-Hanzo-*,
X-IAM-*, X-Tenant-Id, X-Is-Admin, X-Gateway-*, etc.
methodAllowlist rejects TRACE, CONNECT, OPTIONS at the edge.
Storage
- At rest: ZapDB (Badger-derived LSM) at
$KMS_DATA_DIR. Optional AES-GCM encryption viaKMS_ENCRYPTION_KEY_B64(32 raw bytes b64). - Replication: in-process
badger.Replicatorstreaming age-encrypted incremental + snapshot backups to S3. Off whenREPLICATE_S3_ENDPOINTis empty. - Audit: side-table SQLite at
$KMS_AUDIT_DB(default/tmp/kms-aux.db). Buffered, single writer, never blocks request path.
The luxfi pkg/store.SecretStore handles the on-disk envelope:
per-secret 256-bit DEK, DEK wrapped under master key (AES-256-GCM).
ZAP binary transport
Sub-100µs in-cluster secret CRUD on port KMS_ZAP_PORT (default 9999).
Disabled unless KMS_MASTER_KEY_B64 is set (32 raw bytes b64). Same
authorization model as HTTP — JWT via the same JWKS, identical role
checks. Service discovery via mDNS (_kms._tcp).
Env vars
| Var | Default | Required |
|---|---|---|
KMS_LISTEN |
:8443 |
no |
KMS_ZAP_PORT / KMS_ZAP |
9999 |
no |
KMS_DATA_DIR |
/data/hanzo-kms |
no |
KMS_NODE_ID |
hanzo-kms-0 |
no |
KMS_ENV |
dev |
yes (prod/main) |
KMS_EXPECTED_ISSUER |
— | yes (non-dev) |
KMS_EXPECTED_AUDIENCE |
kms |
yes (non-dev) |
KMS_JWKS_URL |
— | yes (non-dev) |
KMS_ENCRYPTION_KEY_B64 |
— | recommended |
KMS_MASTER_KEY_B64 |
— | required for ZAP |
KMS_AUDIT_DB |
/tmp/kms-aux.db |
no |
IAM_ENDPOINT |
https://hanzo.id |
no |
MPC_ADDR |
mDNS | no (mDNS-discoverable) |
MPC_VAULT_ID |
— | no (key routes off) |
REPLICATE_S3_* |
— | no (replication off) |
Build
make # builds ./kmsd and ./kms
make test # go test ./...
CI: hanzoai/.github/.github/workflows/docker-build.yml@main builds
ghcr.io/hanzoai/kms:<branch> for linux/amd64 + linux/arm64 on
native runners (DO+GKE), no QEMU.
Where things live
cmd/kms/ admin CLI
cmd/kmsd/ daemon (this is the production binary)
pkg/kmsclient/ Go client used by other Hanzo services
sdk/go/ legacy ZK client SDK (separate module)
mpc-node/ standalone MPC node (separate module)
frontend/ legacy React UI (static, served as-is)
ui/ new admin UI (build artefact, optional)
Everything in cmd/kmsd and pkg/kmsclient is the active surface.
Everything in frontend/, ui/, docs/, examples/, mpc-node/, and
sdk/go/ is supporting and not on the request path.
Operator protocol delta (open)
~/work/hanzo/universe/infra/k8s/paas/secrets.yaml ships KMSSecret
resources (secrets.lux.network/v1alpha1) with:
spec.hostAPI: http://kms.hanzo.svc.cluster.local/apispec.authentication.universalAuth.secretsScope.{projectSlug,envSlug,secretsPath}
This shape was minted for the legacy era. The current kmsd only
serves /v1/kms/* and uses org/secrets/{path}/{name}?env=… — there is
no /api/v3/secrets/raw and no projectSlug/envSlug model. The
operator that reconciles KMSSecret resources must be updated to call
the canonical surface; the KMS will not grow a back-compat shim. Tracked
separately — this module does not touch the operator.
Rules
- One canonical path per operation. No aliases.
- Every endpoint requires IAM JWT or an explicit admin role.
KMS_ENVother thandev/devnet/localrefuses to boot without full JWT config.- All secrets at rest are encrypted (envelope DEK + master key).
- Passwords are never stored in this service. Identity lives in IAM. When IAM stores a password, it is bcrypt-hashed (cost ≥ 12).
- No backwards compatibility. No env flags for "use the legacy backend instead."