mirror of
https://github.com/luxfi/kms.git
synced 2026-07-27 03:38:31 +00:00
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>
149 lines
4.0 KiB
Go
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
|
|
}
|