Secrets Management

Store and retrieve secrets under envelope encryption

Lux KMS stores secrets under envelope encryption and serves them over HTTP and the native ZAP wire. A secret is addressed by four values — org, path, name and env — and holds exactly one value per address.

Addressing

PartMeaning
orgTenant scope. Also the scope of the token that reads it.
pathA grouping string you choose, e.g. ci, myservice/local.
nameThe secret's name, e.g. DATABASE_URL.
envdev, test, main (or default).

There is nothing to create first — no project, no workspace, no folder object. Writing to a path brings it into being.

env is a field, not a hostname. One KMS per org serves every environment; there are no kms.dev.* / kms.test.* hosts.

Storing and reading

# create or replace — one upsert, not two endpoints
curl -X POST https://kms.lux.cloud/v1/kms/orgs/lux/secrets \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"path": "myapp", "name": "db-password", "env": "main", "value": "s3cureP@ss"}'

# read
curl -H "Authorization: Bearer $TOKEN" \
  "https://kms.lux.cloud/v1/kms/orgs/lux/secrets/myapp/db-password?env=main"

# list names under a path
curl -H "Authorization: Bearer $TOKEN" \
  "https://kms.lux.cloud/v1/kms/orgs/lux/secrets?path=myapp&env=main"

In the URL, path and name are one segment list split at the last slash: .../secrets/a/b/C is path a/b, name C. Escape each segment on its own — escaping the joined string encodes the separators and the server reads one long name.

Encryption at rest

Read this before choosing a transport — the two planes do not store the same bytes.

Over ZAP, each secret gets its own random 256-bit DEK; the value is sealed with AES-256-GCM under that DEK, and the DEK is wrapped under the deployment's Root Encryption Key. The AAD binds path, name and env, so a sealed value cannot be replayed at a different address.

Over HTTP, the value is stored as given and returned as given. The per-secret envelope is not applied on this path. What protects it is whole-database encryption at rest (KMS_ENCRYPTION_KEY_B64, applied by the storage engine over the entire keyspace) plus org-scoped IAM JWT auth and TLS in transit. That is meaningfully weaker than the envelope: there is no per-secret key and no address binding.

Two consequences that will bite if you mix transports against one store:

  • A secret written over ZAP and read over HTTP returns the raw sealed bytes, not the value. HTTP does not unseal.
  • A secret written over HTTP and read over ZAP fails to unseal — there is no wrapped DEK to unwrap.

Pick one transport per secret and stay on it. The Go client uses ZAP exclusively.

SchemeUse
aead+mlkemAES-256-GCM with an ML-KEM-encapsulated DEK (ZAP path).
tfheThreshold reveal.
ckksEncrypted ML compute.

The Root Encryption Key can be sourced from a luxfi/mpc threshold cluster (MPC_REK_ENDPOINT) rather than a static K8s Secret, in which case the unwrapped REK exists only in the process heap between boot and shutdown. A deployment configured that way fails closed — if the REK cannot be fetched, it refuses to start rather than falling back. A deployment with no REK at all still serves the HTTP secrets plane, since that plane never needs one.

Transports

The same store is served two ways, with the same question set:

HTTPZAP op
GET .../secrets/{path}/{name}OpSecretGet 0x0040read
POST .../secretsOpSecretPut 0x0041create or replace
GET .../secrets?path=&env=OpSecretList 0x0042list names
DELETE .../secrets/{path}/{name}OpSecretDelete 0x0043delete

ZAP is the in-cluster path (binary, over luxfi/zap); the Go client uses it exclusively and has no HTTP fallback.

What this service does not do

The following are not implemented — if you need them, they are your application's job, not the KMS's:

  • Versioning. One value per (org, path, name, env). A write replaces the previous value; there is no history and no rollback, and no ?version= selector.
  • Automatic rotation. There are no rotation providers or intervals. Rotating a credential means writing the new value.
  • Point-in-time recovery. Durability comes from encrypted replication of the whole store to S3, not from per-secret history.
  • Tags and folders as objects. path is a plain string, not a folder record you create or list.

Earlier revisions of this page described all four as features. They were inherited from a different product this service replaced, and never shipped here.

Durability

The embedded store replicates to S3 continuously — incremental every second, full snapshot hourly — with every replicated object encrypted under age before it leaves the process.

s3://<bucket>/kms/{node-id}/
  ├── snap/{timestamp}.zap.age
  └── inc/{version}.zap.age