Files
zeekayandHanzo Dev 7116f60fc6 fix(kms): delete unwrapped /v1/kms/secrets/{name} env-leak route (HIGH/CRITICAL)
The route was registered on the bare mux with NO auth middleware and did
os.Getenv(name), returning the raw value. A caller reaching :8080 past the
network boundary (NetworkPolicy gap, port-forward, pod compromise, SSRF) could
`curl .../v1/kms/secrets/KMS_MASTER_KEY_B64` with no Authorization header and
exfiltrate the root REK protecting every per-secret DEK — plus MPC_TOKEN,
KMS_ENCRYPTION_KEY_B64, and (k8s/ variant) the S3 backup keys. On the live
lux-kms-go/kms statefulset KMS_MASTER_KEY_B64 is populated, so this was live
master-key exposure gated only by the network → CRITICAL.

Deleted, not gated: zero callers (kms-operator + kms client read via the
org-scoped path/ZAP; process env is never a secret source). The three
org-scoped secret routes are extracted into registerSecretRoutes so the exact
exposed route set is testable in isolation. Secrets now flow ONLY through
/v1/kms/orgs/{org}/secrets/* (requireOrgJWT) and the ZAP wire.

Regression TestSecretRoutes_NoEnvVarLeak: unauth -> 404, admin -> 404, the
process-env canary is never in the body. Corrected the stale kms.go doc
comment that referenced the route. Full cmd/kms suite green (CGO_ENABLED=0).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-21 09:30:20 -07:00

149 lines
4.0 KiB
Go

// Package kms is the canonical Go client for KMS.
//
// Transport: native luxfi/zap binary protocol on port 9999. There is no
// HTTP fallback in this package — services that need cross-cluster reach use
// the org-scoped HTTP surface (GET /v1/kms/orgs/{org}/secrets/{path}/{name}
// with an IAM bearer), but in-cluster Go is always ZAP.
//
// Defaults read from environment:
//
// KMS_ADDR host:port (default zap.kms.svc.cluster.local:9999)
// KMS_PATH secret path prefix (default "/")
// KMS_ENV secret environment (default "default")
//
// Bootstrap pattern — populate os.Setenv at process start, then read with
// the standard library:
//
// func main() {
// kms.LoadEnv()
// db := os.Getenv("DATABASE_URL")
// run(db)
// }
//
// Programmatic pattern:
//
// v, err := kms.Get(ctx, "DATABASE_URL")
// all, err := kms.GetSecrets(ctx)
package kms
import (
"context"
"fmt"
"log"
"os"
"github.com/luxfi/kms/pkg/zapclient"
)
const (
defaultAddr = "zap.kms.svc.cluster.local:9999"
defaultPath = "/"
defaultEnv = "default"
)
// Config overrides the env-var defaults. The zero value works in-cluster
// and is the recommended way to call.
type Config struct {
Addr string // KMS host:port
Path string // secret path prefix
Env string // secret environment slug
}
func (c Config) resolve() Config {
if c.Addr == "" {
c.Addr = envOr("KMS_ADDR", defaultAddr)
}
if c.Path == "" {
c.Path = envOr("KMS_PATH", defaultPath)
}
if c.Env == "" {
c.Env = envOr("KMS_ENV", defaultEnv)
}
return c
}
// Get fetches a single secret value at the configured path/env.
func Get(ctx context.Context, name string) (string, error) {
return GetWith(ctx, Config{}, name)
}
// GetWith fetches a single secret value with explicit configuration.
func GetWith(ctx context.Context, cfg Config, name string) (string, error) {
cfg = cfg.resolve()
c, err := zapclient.Dial(ctx, cfg.Addr, cfg.Path)
if err != nil {
return "", fmt.Errorf("kms: dial %s: %w", cfg.Addr, err)
}
defer c.Close()
v, err := c.GetAt(ctx, cfg.Path, name, cfg.Env)
if err != nil {
return "", fmt.Errorf("kms: get %s/%s@%s: %w", cfg.Path, name, cfg.Env, err)
}
return v, nil
}
// GetSecrets fetches every secret at the configured path/env.
func GetSecrets(ctx context.Context) (map[string]string, error) {
return GetSecretsWith(ctx, Config{})
}
// GetSecretsWith fetches every secret with explicit configuration.
func GetSecretsWith(ctx context.Context, cfg Config) (map[string]string, error) {
cfg = cfg.resolve()
c, err := zapclient.Dial(ctx, cfg.Addr, cfg.Path)
if err != nil {
return nil, fmt.Errorf("kms: dial %s: %w", cfg.Addr, err)
}
defer c.Close()
names, err := c.ListAt(ctx, cfg.Path, cfg.Env)
if err != nil {
return nil, fmt.Errorf("kms: list %s@%s: %w", cfg.Path, cfg.Env, err)
}
out := make(map[string]string, len(names))
for _, n := range names {
v, err := c.GetAt(ctx, cfg.Path, n, cfg.Env)
if err != nil {
return nil, fmt.Errorf("kms: get %s/%s@%s: %w", cfg.Path, n, cfg.Env, err)
}
out[n] = v
}
return out, nil
}
// LoadEnv fetches every secret at the configured path/env and writes each
// to os.Setenv. Fails fast (log.Fatalf) on error so the process exits
// before silently running with missing config.
func LoadEnv() {
if err := LoadEnvCtx(context.Background()); err != nil {
log.Fatalf("kms: LoadEnv: %v", err)
}
}
// LoadEnvCtx is the context-aware form of LoadEnv. Use this from tests or
// when you want to control the timeout.
func LoadEnvCtx(ctx context.Context) error {
return LoadEnvWith(ctx, Config{})
}
// LoadEnvWith populates os.Setenv with secrets fetched using the given config.
func LoadEnvWith(ctx context.Context, cfg Config) error {
secrets, err := GetSecretsWith(ctx, cfg)
if err != nil {
return err
}
for k, v := range secrets {
if err := os.Setenv(k, v); err != nil {
return fmt.Errorf("kms: setenv %s: %w", k, err)
}
}
return nil
}
func envOr(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}