Hanzo
PlatformHanzo KMS

Hanzo KMS

The sealed secret store every Hanzo service reads its credentials from

Hanzo KMS

API reference · Hanzo KMS API → — every endpoint, generated from the OpenAPI spec.

Hanzo KMS is the credential broker for the platform. It holds secrets sealed at rest, hands them to workloads that present a machine identity, and syncs them into Kubernetes through the KMSSecret CRD. That is the whole job — it is a secret store with a login, not a certificate authority, a rotation engine, or a scanner.

Apphttps://kms.hanzo.ai — the console you log into
APIhttps://api.hanzo.ai/v1/kms — one API host for every Hanzo service
AuthMachine identity (clientId/clientSecret) → bearer JWT minted by Hanzo IAM
K8s syncKMSSecret CRD, reconciled by the kms-operator
ImplementationThe luxfi/kms sealed store, embedded in Hanzo Cloud

The whole surface

Seven operations. There is no eighth.

MethodPathAuthPurpose
GET/v1/kms/healthpublicReadiness — 200 only if the store is open and a master key is configured
GET/v1/kms/configpublicConsole runtime config (brand, OIDC issuer, API base, login path)
POST/v1/kms/auth/loginpublicExchange a machine credential for a bearer token
GET/v1/kms/secretsbearerList your org's secret metadata — never values
GET/v1/kms/secrets/{path}/{name}bearerRead one secret's value
POST/v1/kms/secretsbearerCreate or replace one secret
DELETE/v1/kms/secrets/{path}/{name}bearerDelete one secret

Your org is never in the URL. It is read from the validated token's owner claim and folded into the storage path server-side. There is no tenant path segment and no admin override, so over HTTP another org's namespace is not merely refused — it is unaddressable.

Addressing a secret

Three coordinates travel in the request; the fourth comes from your token.

org      ←  the token's owner claim (never in the URL)
path        /providers          a folder beneath your org root
name        OPENAI_API_KEY      the exact key
env         ?env=prod           the environment

env is part of the storage key, not a filter. A value written under env=prod and one written under env=default are two different records.

Operationenv behaviour
POST (write)Required. There is no default — a silently defaulted write lands in a bucket your readers never resolve, so it fails loudly instead
GET / DELETE one secretOptional; falls back to default
GET listOptional; omitted means every environment

The list route also accepts the operator's spellings — environment for env, secretPath for path — so one endpoint serves both callers.

Authenticate

A machine identity is a client-credentials application in Hanzo IAM. KMS brokers its clientId/clientSecret to IAM and returns the bearer you carry on every secret call.

ACCESS_TOKEN=$(curl -sS -X POST https://api.hanzo.ai/v1/kms/auth/login \
  -H 'Content-Type: application/json' \
  -d "{\"clientId\":\"$KMS_CLIENT_ID\",\"clientSecret\":\"$KMS_CLIENT_SECRET\"}" \
  | jq -r .accessToken)
{ "accessToken": "<JWT>", "expiresIn": 3600, "tokenType": "Bearer" }

This is the one public, unauthenticated route, so it is the one route rate-limited per source IP — 60 requests per minute. Failures collapse to a single status with no upstream detail (400 missing field, 401 bad credential, 502 IAM unreachable, 503 no issuer configured); a richer error would be a validity oracle for guessed credentials.

Read, write, list, delete

# Read one value
curl -sS "https://api.hanzo.ai/v1/kms/secrets/providers/OPENAI_API_KEY?env=prod" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# → { "name": "OPENAI_API_KEY", "env": "prod", "value": "<secret value>" }
# Write one value — env is required
curl -sS -X POST https://api.hanzo.ai/v1/kms/secrets \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"path":"/providers","name":"OPENAI_API_KEY","env":"prod","value":"<new value>"}'
# → { "stored": true, "name": "OPENAI_API_KEY", "env": "prod" }
# List metadata — no values come back
curl -sS "https://api.hanzo.ai/v1/kms/secrets?path=/providers&env=prod" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# → { "secrets": [ … ], "total": 2, "names": ["OPENAI_API_KEY","ANTHROPIC_API_KEY"] }
# Delete one
curl -sS -X DELETE "https://api.hanzo.ai/v1/kms/secrets/providers/OPENAI_API_KEY?env=prod" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
# → { "deleted": true, "name": "OPENAI_API_KEY", "env": "prod" }

A secret at the org root omits the folder segment: …/secrets/STRIPE_SECRET_KEY?env=prod.

Listing gives you names, not values. GET /v1/kms/secrets returns each secret's name, path, environment and sealing scheme — no value, no ciphertext. Reading a value is a separate per-secret call, and that response body is the only place the value ever appears: it is not logged and never carried in an error.

What it does not do

Saying so is cheaper than a support thread.

  • No PATCH, no version field, no optimistic concurrency. A write is an upsert; last writer wins.
  • No rotation engine, no PKI or SSH certificate authority, no secret scanning, no dynamic secrets, no third-party secret syncs. None of these are served under /v1/kms.
  • No threshold-signing routes on this surface. GET /v1/kms/health reports a signing field saying whether signing keys are configured for the embedded store; distributed key generation and threshold signatures live in Hanzo MPC, not here.
  • No cross-org read. Cross-org access exists only in-process, inside the cloud binary that holds the master key — never over HTTP.

Failure modes

GET /v1/kms/health answers 200 with {"status":"ok","ready":true,"signing":…,"service":"kms"} only when the store is open and a master key is present. Anything less is 503 with ready:false and the reason — exactly the two states in which the secret routes refuse.

Admission on the secret routes is fail-closed, in this order, all decided before a record is touched:

StatusMeaning
403No validated member on the request
400Malformed org, env, path, or name
503Store open but no master key configured
404Secret not there — a plain 404 that names nothing about the store

Next steps

Log in and read your first secret

How a workload authenticates and refreshes its token

Sync secrets into Kubernetes

The pattern every Hanzo service uses

How is this guide?

On this page