mirror of
https://github.com/luxfi/kms.git
synced 2026-07-27 03:38:31 +00:00
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:
+19
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user