LPsLux Proposals
Wallets & Identity
LP-7110

Machine Identity

Draft

A service proves who it is with one OIDC client_credentials call to hanzo.id; the org in its token authorizes every hyphen-delimited sub-scope under it.

Category
Core
Created
2026-06-24

Abstract

A non-human caller — a validator, an operator, a venue daemon — proves who it is with one call: OIDC client_credentials to hanzo.id. The returned JWT carries an org. That org is the whole authority. A token for org lux reaches every sub-scope written lux-<x>lux-infra, lux-mainnet — and nothing else.

This is not DID (LP-10093, an on-chain identity a user owns) and not KMS (LP-7326, which holds the secrets). It is only the answer to "who is this service, and what may it touch."

Invariant

A machine authenticates one way — client_credentials against hanzo.id. Its token's org O authorizes scope S when S == O or S begins with O + "-". No other prefix matches.

Why (first principles)

A service has no password to type and no browser to redirect. The OAuth2 client_credentials grant is exactly this case: a clientId/clientSecret pair exchanged for a bearer token, no user in the loop. One grant, one path — a second auth path is a second thing to secure and a second place to be wrong.

The org boundary is a value, not a list. KMS never stores "lux may read lux-infra, lux-mainnet, …"; it derives the answer from the token's org and the requested scope. Add a project lux-staking and the same lux token reaches it with no new grant. The hyphen is what keeps the value safe: lux authorizes lux-infra but not luxx, because only O + "-" extends the scope — a bare prefix would leak across orgs.

Enforcement

The grant is one handler. POST /v1/kms/auth/login takes a clientId/clientSecret, posts grant_type=client_credentials to hanzo.id, and returns the access token — "Machine identity auth via IAM" (kms/cmd/kms/main.go:197-229). The token endpoint is the canonical /v1/iam/oauth/token, one env-overridable default (kms/cmd/kms/main.go:117).

The boundary is one function. orgAuthorizes(tokenOrg, requested) returns true only when requested == tokenOrg || strings.HasPrefix(requested, tokenOrg+"-") — the hyphen "prevents a token for lux from leaking into an unrelated org like luxx" (kms/cmd/kms/auth.go:93-96). Every secrets request runs through it: the request's {org} path param must be authorized by some org the token resolves to, or the request is 403 (kms/cmd/kms/auth.go:201-250).

The org a token resolves to comes from its IAM claims, fail-closed: a client_credentials application token carries owner admin, so its org is the prefix of its <org>-<service> name; a user token carries the org in owner. A token with no resolvable org is rejected (kms/cmd/kms/auth.go:64-78, :187-189).

Copyright and related rights waived via CC0.