Files
zeekayandGitHub 6ab565e2ba feat(transport): add ZAP-native transport alongside HTTP (#2)
Introduces a kmsapi.Transport interface so the reconciler can talk to
the canonical luxfi/kms surface over either JSON-over-HTTP or the
binary ZAP wire (opcodes 0x0040..0x0043) on the same KMS daemon.

Why: sub-100µs in-cluster secret CRUD for the operator, on the same
authoritative store as the HTTP path. The HTTP transport stays the
default — ZAP is opt-in per KMSSecret/KMSPushSecret via host scheme:
  http(s)://…           → kmsapi.Client (today's path, unchanged)
  zap://host:port       → kmszap.Client (new, NodeID-bound)
  zap+mdns://_kms._tcp  → kmszap.Client (mDNS discovery)

Auth model — important divergence from HTTP, documented in
packages/kmszap/client.go: ZAP authorises by peer NodeID + ACL
(KMS_ZAP_ACL), not by IAM bearer token. Operator deployments wanting
ZAP MUST register their NodeID with the kmsd ACL. clientId/clientSecret
on the CR's universalAuth credentialsRef are ignored on the ZAP path
(the call still works because Login() returns a synthetic
"zap:<nodeID>" token kept by the existing token cache).

Files:
  packages/kmsapi/transport.go      — Transport interface, IsZAPHost
  packages/kmszap/client.go         — ZAP-backed kmsapi.Transport impl
  packages/util/transport.go        — shared BuildTransport factory
  controllers/kmssecret  + kmspushsecret — use util.BuildTransport
  packages/util/{auth,secrets,models}.go — signatures use Transport
2026-05-30 20:23:15 -07:00

61 lines
2.1 KiB
Go

// Package util/transport.go: shared KMS transport selector.
//
// Both reconcilers (KMSSecret, KMSPushSecret) need to pick between the
// HTTP and ZAP transports at ResourceVariables construction time. The
// selection rule is identical, so the factory lives here instead of
// being duplicated per package.
package util
import (
"os"
"strings"
"github.com/hanzoai/kms-operator/packages/kmsapi"
"github.com/hanzoai/kms-operator/packages/kmszap"
)
// BuildTransport returns either a kmsapi.Client (HTTP) or a
// kmszap.Client (ZAP) depending on the host scheme.
//
// HTTP: any URL beginning with http:// or https:// (default).
// ZAP: any URL beginning with zap:// (direct address) or zap+mdns://
// (mDNS discovery).
//
// ZAP requires the operator to register its NodeID in the kmsd ACL
// (KMS_ZAP_ACL) with the appropriate role + path prefix; misconfigured
// ACL surfaces as forbidden on the first GetSecret call. The NodeID
// itself is read from KMS_OPERATOR_ZAP_NODE_ID, with a safe-by-default
// fallback that only works when the server runs in open mode.
//
// Returns kmsapi.Transport unconditionally — there is no nil-return
// path. CA-bundle parse failures fall back to a system-roots-only
// client; the subsequent request will surface the underlying problem
// via the controller's normal error reporting.
func BuildTransport(host, caCertPEM, userAgent string) kmsapi.Transport {
if kmsapi.IsZAPHost(host) {
return kmszap.New(kmszap.Config{
NodeID: zapNodeIDFromEnv(),
})
}
cli, err := kmsapi.New(kmsapi.Config{
CACertPEM: caCertPEM,
UserAgent: userAgent,
})
if err != nil {
cli, _ = kmsapi.New(kmsapi.Config{UserAgent: userAgent})
}
return cli
}
// zapNodeIDFromEnv resolves the NodeID this operator presents on
// outbound ZAP connections. The ACL on the KMS server side binds this
// NodeID to (role, path-prefix). Defaults to "kms-operator" when the
// env var is unset — only safe when KMS_ZAP_ACL is also unset (open
// mode); production deployments MUST set both.
func zapNodeIDFromEnv() string {
if id := strings.TrimSpace(os.Getenv("KMS_OPERATOR_ZAP_NODE_ID")); id != "" {
return id
}
return "kms-operator"
}