Files
zeekay 893119b82a api: serve secrets.lux.network at v1
The six kinds (KMSSecret, KMSDynamicSecret, KMSPushSecret, ClusterGenerator,
Password, UUID) move from v1alpha1 to v1. One package of Go types, renamed
api/v1alpha1 -> api/v1, so there is one definition of each kind rather than a
copy per version.

The CRDs declare conversion: None, so the API server relabels apiVersion and
returns the same bytes. The operator therefore needs to watch only the version
the CRD serves; a second Go package would add drift and buy nothing. The v1
schema is v1alpha1's, unchanged -- controller-gen's output moves by exactly one
line per CRD, the version name.

Call sites import the package as secretsv1 because
packages/controllerhelpers already binds bare v1 to k8s.io/api/apps/v1.

RBAC is group- and resource-scoped, so it is unaffected.
2026-08-13 17:11:10 -07:00

97 lines
3.4 KiB
Go

// Package util/secrets.go: read-side helpers that turn a successful
// auth + scope into a slice of plaintext key/value pairs.
//
// The canonical luxfi/kms surface has no list endpoint — every value
// MUST be addressed by exact name. The caller therefore has to
// enumerate the expected keys on the CR via secretsScope.keys, and
// this helper fans out one GET per name. NotFound is treated as
// fatal: empty-fetch fail-closed, the controller refuses to project
// an empty Secret.
package util
import (
"context"
"encoding/base64"
"errors"
"fmt"
"strings"
secretsv1 "github.com/hanzoai/kms-operator/api/v1"
"github.com/hanzoai/kms-operator/packages/kmsapi"
"github.com/hanzoai/kms-operator/packages/model"
)
// GetPlainTextSecretsViaMachineIdentity fetches every key listed in
// secretsScope.keys from the canonical /v1/kms/orgs/{org}/secrets/...
// surface and returns them as a flat slice for the projection layer.
//
// Validation:
// - secretsScope.keys MUST be non-empty (no list endpoint exists).
// - Per-input bounds enforced via kmsapi.validateScope.
// - Each value is scanned for control bytes; NUL or C0 fails the
// entire reconcile rather than truncate downstream.
func GetPlainTextSecretsViaMachineIdentity(
ctx context.Context,
kmsClient kmsapi.Transport,
host string,
bearerToken string,
scope secretsv1.MachineIdentityScopeInWorkspace,
) ([]model.SingleEnvironmentVariable, error) {
if kmsClient == nil {
return nil, errors.New("GetPlainTextSecretsViaMachineIdentity: kms client is nil")
}
if bearerToken == "" {
return nil, errors.New("GetPlainTextSecretsViaMachineIdentity: empty bearer token")
}
if scope.ProjectSlug == "" {
return nil, errors.New("secretsScope.projectSlug is required")
}
if scope.EnvSlug == "" {
return nil, errors.New("secretsScope.envSlug is required")
}
if len(scope.Keys) == 0 {
return nil, errors.New(
"secretsScope.keys is required (luxfi/kms has no list endpoint — enumerate explicitly)",
)
}
if len(scope.Keys) > kmsapi.MaxKeysPerCR {
return nil, fmt.Errorf("secretsScope.keys exceeds %d entries", kmsapi.MaxKeysPerCR)
}
scopePath := kmsapi.NormaliseScopePath(scope.SecretsPath)
out := make([]model.SingleEnvironmentVariable, 0, len(scope.Keys))
for _, key := range scope.Keys {
if key == "" {
return nil, errors.New("secretsScope.keys contains an empty entry")
}
resp, err := kmsClient.GetSecret(ctx, host, bearerToken, scope.ProjectSlug, scope.EnvSlug, scopePath, key)
if err != nil {
return nil, fmt.Errorf("fetch secret %q: %w", key, err)
}
// Binary convention: KMS values are strings and the transport
// rejects control bytes, so raw binary material (e.g. a BLS
// signer key) is stored base64-encoded under a name ending in
// ".b64". On projection the suffix is stripped and the value
// decoded, so the managed Secret keeps the exact byte contract
// its consumers expect. One transform, one place.
name, value := key, resp.Value
if stripped, ok := strings.CutSuffix(key, ".b64"); ok {
raw, decErr := base64.StdEncoding.DecodeString(strings.TrimSpace(resp.Value))
if decErr != nil {
return nil, fmt.Errorf("secret %q: value is not valid base64: %w", key, decErr)
}
name, value = stripped, string(raw)
}
out = append(out, model.SingleEnvironmentVariable{
Key: name,
Value: value,
Type: "shared",
ID: "", // canonical surface has no separate ID
SecretPath: scope.SecretsPath,
})
}
return out, nil
}