fix(kms): re-tag go.sum for keys@v1.1.0 + org sub-scope auth (lux→lux-infra)

Build (Build KMS / Dockerfile, -mod=mod) failed 17 days at 'go mod
download': luxfi/keys@v1.1.0 was re-tagged (force-moved to a different
commit) after kms's go.sum was written, so CI's direct-from-GitHub
fetch (GOPRIVATE) computed h1:D3s754vT… while go.sum pinned the stale
h1:a4UkVVg6… → 'SECURITY ERROR: checksum mismatch'. Regenerated the one
go.sum line to the authoritative current hash (no security bypass).

The keys↔kms 'cycle' (keys@v1.1.0 → kms@v1.9.12, a tag that does not
exist) does NOT block this build: the package graph is acyclic
(go list -deps ./pkg/zapclient has zero luxfi/keys; pkg/envelope is
interface-decoupled, keys appears only in its _test files), and MVS
resolves the kms self-module from the local tree, never fetching the
phantom v1.9.12. The production server binary (cmd/kms) and operator
(cmd/kms-operator) compile ZERO external luxfi/keys (cmd/kms uses the
in-repo pkg/keys; only the cmd/smoke-replay dev tool pulls external
keys) — so no keys content enters the shipped images.

org sub-scope auth: the lux-operator queries KMS with org=lux-infra
(its Infisical projectSlug) but the lux-kms token's org claim is 'lux'
→ strict equality returned 403. orgAuthorizes() now admits a requested
org that equals the token org OR is a hyphen-delimited sub-scope of it
(lux authorizes lux-infra/lux-mainnet/…; the '-' boundary keeps lux
from leaking into luxx-infra). One function, one call site.

Tests: cmd/kms ok (14 TestRequireOrgJWT incl. 2 new sub-scope cases),
pkg/{envelope,zapclient,zapserver} ok.
This commit is contained in:
zeekay
2026-06-24 10:14:53 -07:00
parent 7b37424326
commit 1889442402
3 changed files with 91 additions and 2 deletions
+19 -1
View File
@@ -77,6 +77,24 @@ func (c *orgClaims) orgs() []string {
return out
}
// orgAuthorizes reports whether a token org slug authorizes access to a
// requested path org. The IAM `owner`/`name` claim carries the parent
// org (e.g. "lux"), but operators address project-scoped vaults under
// that org via a longer slug (e.g. the lux-operator queries KMS with
// org="lux-infra", its Infisical projectSlug). A token for org "lux"
// must reach "lux-infra", "lux-mainnet", … — every project under it —
// without minting per-project tokens.
//
// Match rule (boundary-safe): the requested org is authorized when it
// equals the token org, OR the requested org is a sub-scope, i.e. it
// begins with `tokenOrg + "-"`. The '-' separator prevents a token for
// "lux" from leaking into an unrelated org like "luxx" — only true
// hyphen-delimited sub-scopes match.
func orgAuthorizes(tokenOrg, requested string) bool {
return requested == tokenOrg ||
strings.HasPrefix(requested, tokenOrg+"-")
}
// orgJWTAuth verifies tokens against the IAM JWKS. Issuer is checked
// (must equal expectedIssuer). Audience is NOT enforced because IAM
// client_credentials grants don't pin an audience to the resource
@@ -216,7 +234,7 @@ func (a *orgJWTAuth) requireOrgJWT(next http.HandlerFunc) http.HandlerFunc {
}
allowed := false
for _, o := range claims.orgs() {
if o == org {
if orgAuthorizes(o, org) {
allowed = true
break
}
+71
View File
@@ -132,6 +132,77 @@ func TestRequireOrgJWT_crossOrgRejected(t *testing.T) {
}
}
func TestRequireOrgJWT_subScopeAuthorized(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
// Token for parent org "lux"; request for the project sub-scope
// "lux-infra" (the lux-operator's projectSlug) → 200. A token for
// the parent org reaches every hyphen-delimited project under it.
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "lux-kms",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "lux",
})
called := false
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/lux-infra/secrets/lux/devnet/staking", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("got %d want 200 (lux authorizes lux-infra); body=%s", rec.Code, rec.Body.String())
}
if !called {
t.Error("inner handler not called for authorized sub-scope")
}
}
func TestRequireOrgJWT_subScopeBoundaryRejected(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
defer iam.Close()
auth := newOrgJWTAuth(iam.URL, "")
// Token for org "lux"; request for "luxx-infra" → 403. The '-'
// boundary in the prefix rule prevents a token for "lux" from
// leaking into an unrelated org whose slug merely shares a prefix.
tok := signOrgClaims(t, signer, orgClaims{
Claims: jwt.Claims{
Issuer: iam.URL,
Subject: "lux-kms",
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
Owner: "lux",
})
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/kms/orgs/{org}/secrets/{rest...}", auth.requireOrgJWT(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("inner handler must NOT be called: lux must not authorize luxx-infra")
}))
req := httptest.NewRequest(http.MethodGet, "/v1/kms/orgs/luxx-infra/secrets/X", nil)
req.Header.Set("Authorization", "Bearer "+tok)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("got %d want 403 (boundary); body=%s", rec.Code, rec.Body.String())
}
}
func TestRequireOrgJWT_kmsAdminCrossesOrgs(t *testing.T) {
signer, jwks := newTestSigner(t)
iam := httptest.NewServer(jwksHandler(jwks))
+1 -1
View File
@@ -158,7 +158,7 @@ github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/ids v1.2.15 h1:omE+E4+0Poj9DzM11ejSFgteaSQ3KDHi5g54iH6jcxI=
github.com/luxfi/ids v1.2.15/go.mod h1:Fj73K5xcblvdE0SxU/ip+jE8VqNdu+80548su5KJ7xI=
github.com/luxfi/keys v1.1.0 h1:a4UkVVg6G09XC7vPtXKxEGwVt50GNPjEvq2pkjYZW2k=
github.com/luxfi/keys v1.1.0 h1:D3s754vT+01G6TSdxkwb+skwsIqCgPuJizPypjzj78I=
github.com/luxfi/keys v1.1.0/go.mod h1:U3tZNDmv3nXkPoZwLtq9RNjwyN0XyoN29worigfT+c0=
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=