Following the earlier AdminShell+CollectionCRUD rewrite (PR #14) which already deleted backend/, backend-go/, cloudformation/, helm-charts/, nginx/, sink/, wasm/, upgrade-impact/, e2e/, migration/, package.json and switched the frontend to a 184K Hanzo-first build, complete the cleanup by deleting the remaining Infisical-era residue: - docs/ (1.0G Infisical mintlify docs) - .env.example, .env.dev.example, .env.migration.example, .env.test.example (legacy Postgres/Redis envs; no Go code references them) - .eslintignore, .husky/ (TS toolchain leftovers) - CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md (Infisical repo boilerplate) Working tree drops from ~1.0G to <600K of content (1.5G total is .git/ which `git gc --aggressive --prune=now` shrinks locally; the remote keeps its history shape — anyone pulling gets the lean checkout). Build + test verification: go build ./... (4 cmd binaries: green) go test ./... -count=1 -short (all packages: ok) Kept: cmd/, pkg/, sdk/, frontend/ (AdminShell), schema/, examples/, root Go (audit.go, auth.go, consensus.go, embed.go, jwks.go, mount.go, versioning.go + tests), Dockerfile, Dockerfile.kms-fetch, Makefile, LLM.md, CLAUDE.md, DEPRECATED.md, SOVEREIGN-KMS-ARCHITECTURE.md, TFHE-KMS-ARCHITECTURE.md, LICENSE, README.md, VERSION, .github/ (8 Hanzo workflows: build, build-kms-fetch, check-fe-ts-and-lint, ci, pr-preview, release, validate-pr-title, workflow-sanity).
176 lines
5.7 KiB
Go
176 lines
5.7 KiB
Go
// Copyright (C) 2026, Hanzo AI Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// consensus.go — wires the kmsd ZAP secret-server to its consensus
|
|
// authority. Mirror of luxfi/kms cmd/kms/consensus.go but lives in
|
|
// hanzo/kms because the Hanzo daemon (cmd/kmsd) is the consumer.
|
|
//
|
|
// The ZAP server fails closed: with no validator/operator snapshot, the
|
|
// authorizer cannot be constructed and the ZAP server stays off. The
|
|
// HTTP path (JWT-gated, IAM-issued) is unaffected. The kms-operator is
|
|
// the component that drops the snapshot file onto the pod; when it has
|
|
// shipped, ZAP comes up. Until then HTTP carries the load.
|
|
|
|
package kms
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/kms/pkg/zapserver"
|
|
)
|
|
|
|
const (
|
|
envConsensusValidators = "KMS_CONSENSUS_VALIDATORS"
|
|
envConsensusOperators = "KMS_CONSENSUS_OPERATORS"
|
|
envConsensusFile = "KMS_CONSENSUS_FILE"
|
|
envConsensusTTL = "KMS_CONSENSUS_TTL"
|
|
|
|
defaultConsensusTTL = 30 * time.Second
|
|
)
|
|
|
|
// consensusSnapshot is the wire shape of the JSON file the kms-operator
|
|
// drops into the kmsd container. Identical to the env-var carriage so
|
|
// the operator can pick either delivery.
|
|
type consensusSnapshot struct {
|
|
Validators []string `json:"validators"`
|
|
Operators []string `json:"operators"`
|
|
}
|
|
|
|
// buildConsensusAuthorizer constructs the ConsensusAuthorizer wired at
|
|
// boot. Reads either KMS_CONSENSUS_FILE or KMS_CONSENSUS_VALIDATORS +
|
|
// KMS_CONSENSUS_OPERATORS. Returns (nil, nil) when no source is
|
|
// configured — the caller treats that as "ZAP server off, HTTP only"
|
|
// rather than a fatal. Returns (nil, err) when a source is configured
|
|
// but malformed; the caller refuses to start the ZAP server in that
|
|
// case (fail-closed on bad input).
|
|
func buildConsensusAuthorizer() (zapserver.ConsensusAuthorizer, error) {
|
|
validators, operators, err := loadConsensusSnapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(validators) == 0 && len(operators) == 0 {
|
|
// No snapshot configured. Caller turns off ZAP.
|
|
return nil, nil
|
|
}
|
|
if len(validators) == 0 {
|
|
return nil, errors.New("consensus validator authority is empty (refusing to fail open)")
|
|
}
|
|
if len(operators) == 0 {
|
|
return nil, errors.New("consensus operator authority is empty (refusing to fail open)")
|
|
}
|
|
ttl := defaultConsensusTTL
|
|
if v := strings.TrimSpace(os.Getenv(envConsensusTTL)); v != "" {
|
|
d, err := time.ParseDuration(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", envConsensusTTL, err)
|
|
}
|
|
ttl = d
|
|
}
|
|
az, err := zapserver.NewInProcessAuthorizer(zapserver.InProcessAuthorizerConfig{
|
|
Validators: zapserver.NewStaticAuthorityProvider(validators),
|
|
Operator: zapserver.NewStaticAuthorityProvider(operators),
|
|
CacheTTL: ttl,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Probe both providers once so a malformed snapshot surfaces here
|
|
// (fatal path) rather than on the first inbound request.
|
|
if _, err := az.Authorize(context.Background(), zapserver.Identity{
|
|
NodeID: validators[0],
|
|
}, "self-test", zapserver.OpAuthGet); err != nil {
|
|
return nil, fmt.Errorf("authorizer self-test: %w", err)
|
|
}
|
|
return az, nil
|
|
}
|
|
|
|
// loadConsensusSnapshot returns the (validators, operators) NodeID
|
|
// sets, sourcing from KMS_CONSENSUS_FILE first then falling back to
|
|
// the two env vars.
|
|
func loadConsensusSnapshot() ([]ids.NodeID, []ids.NodeID, error) {
|
|
if path := strings.TrimSpace(os.Getenv(envConsensusFile)); path != "" {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
// Missing file is not fatal at boot — operator hasn't
|
|
// shipped the snapshot yet. Return empty sets so caller
|
|
// disables ZAP rather than crashing.
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, nil, nil
|
|
}
|
|
return nil, nil, fmt.Errorf("%s: %w", envConsensusFile, err)
|
|
}
|
|
var snap consensusSnapshot
|
|
if err := json.Unmarshal(data, &snap); err != nil {
|
|
return nil, nil, fmt.Errorf("%s: %w", envConsensusFile, err)
|
|
}
|
|
validators, err := parseConsensusNodeIDs(snap.Validators)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s validators: %w", envConsensusFile, err)
|
|
}
|
|
operators, err := parseConsensusNodeIDs(snap.Operators)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s operators: %w", envConsensusFile, err)
|
|
}
|
|
return validators, operators, nil
|
|
}
|
|
validators, err := parseConsensusNodeIDs(splitConsensusList(os.Getenv(envConsensusValidators)))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s: %w", envConsensusValidators, err)
|
|
}
|
|
operators, err := parseConsensusNodeIDs(splitConsensusList(os.Getenv(envConsensusOperators)))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s: %w", envConsensusOperators, err)
|
|
}
|
|
return validators, operators, nil
|
|
}
|
|
|
|
// splitConsensusList splits on newlines, commas, and whitespace. Empty
|
|
// elements are dropped. Operators pick whichever delimiter suits their
|
|
// templating.
|
|
func splitConsensusList(raw string) []string {
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, 8)
|
|
field := strings.Builder{}
|
|
for _, r := range raw {
|
|
if r == '\n' || r == ',' || r == ' ' || r == '\t' || r == '\r' {
|
|
if field.Len() > 0 {
|
|
out = append(out, field.String())
|
|
field.Reset()
|
|
}
|
|
continue
|
|
}
|
|
field.WriteRune(r)
|
|
}
|
|
if field.Len() > 0 {
|
|
out = append(out, field.String())
|
|
}
|
|
return out
|
|
}
|
|
|
|
// parseConsensusNodeIDs returns the parsed NodeID slice; any malformed
|
|
// entry is a hard failure (refuses to boot with a typo in the snapshot).
|
|
func parseConsensusNodeIDs(items []string) ([]ids.NodeID, error) {
|
|
out := make([]ids.NodeID, 0, len(items))
|
|
for _, s := range items {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
continue
|
|
}
|
|
id, err := ids.NodeIDFromString(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse %q: %w", s, err)
|
|
}
|
|
out = append(out, id)
|
|
}
|
|
return out, nil
|
|
}
|