Files
zeekayandHanzo Dev 0eb123c085 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers: promoted-field keys in struct literals, maps.Copy, any and
range over int. omitzero is not applied, since it changes what the JSON
says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:46:02 -07:00

266 lines
8.9 KiB
Go

// Package util/auth.go owns the controller-side authentication logic.
//
// The canonical luxfi/kms surface (~/work/hanzo/kms cmd/kmsd) accepts a
// single auth shape: clientId/clientSecret -> POST /v1/kms/auth/login ->
// bearer token. Every other auth method that the legacy SDK exposed
// (service tokens, service accounts, K8s/AWS/Azure/GCP machine
// identities) has no equivalent on the canonical server, so this
// package now models exactly one strategy: UniversalAuth.
//
// Forward break only — there are no fallbacks, no toggles, no
// "legacy-support" knobs. Every reconciler MUST present a
// universalAuth.credentialsRef pointing at a Kubernetes Secret that
// holds clientId / clientSecret.
package util
import (
"context"
"errors"
"fmt"
secretsv1 "github.com/hanzoai/kms-operator/api/v1"
"github.com/hanzoai/kms-operator/packages/kmsapi"
authenticationv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
"github.com/aws/smithy-go/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// AuthStrategyType identifies the authentication strategy resolved at
// reconcile time. Only UNIVERSAL_MACHINE_IDENTITY is supported.
type AuthStrategyType string
// AuthStrategy enumerates the supported authentication strategies. The
// surface has been frozen to one entry on purpose; do not add new
// strategies without a corresponding canonical-server route.
var AuthStrategy = struct {
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
}{
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
}
// SecretCrdType identifies which CRD owns a reconcile call. Used for
// type-safe casts in the auth handler.
type SecretCrdType string
var SecretCrd = struct {
KMS_SECRET SecretCrdType
KMS_PUSH_SECRET SecretCrdType
}{
KMS_SECRET: "KMS_SECRET",
KMS_PUSH_SECRET: "KMS_PUSH_SECRET",
}
// SecretAuthInput carries the CR + its kind into the auth handler.
type SecretAuthInput struct {
Secret any
Type SecretCrdType
}
// AuthenticationDetails is the result of HandleUniversalAuth: a fresh
// bearer token, the host that minted it, and the scope (org / env /
// path / keys) the caller asked for. Reconcilers consume this directly
// without re-resolving credentials.
type AuthenticationDetails struct {
AuthStrategy AuthStrategyType
BearerToken string
Host string
MachineIdentityScope secretsv1.MachineIdentityScopeInWorkspace
SecretType SecretCrdType
}
// ErrAuthNotApplicable signals that the supplied CR does not configure
// the strategy under inspection. The reconciler retries the next
// strategy in its list. With only one strategy left, this just means
// "no universalAuth.credentialsRef configured".
var ErrAuthNotApplicable = errors.New("authentication not applicable")
// HandleUniversalAuth resolves credentials, performs login (cached),
// and returns a bearer ready to be used against the canonical surface.
//
// Hardening:
// - credentialsRef MUST live in the same namespace as the
// reconciling CR. Cross-namespace reads are refused — the operator
// SA's RBAC may grant it cluster-wide secret reads, and a tenant CR
// in namespace A must not be able to spec a credentialsRef in a
// privileged namespace it does not own.
// - clientId / clientSecret are scanned for control bytes before
// they leave the cluster. NUL is the only way a malicious CR-author
// who controls the credentials Secret could smuggle a payload past
// a downstream log scrubber, so we reject it here.
//
// A nil kmsClient falls back to a fresh HTTP-only on-demand instance —
// the production code path always supplies one. The ZAP transport
// (kmszap.Client) is opaque behind the Transport interface; callers
// pick the concrete implementation at ResourceVariables construction.
func HandleUniversalAuth(
ctx context.Context,
reconcilerClient client.Client,
secretCrd SecretAuthInput,
host string,
kmsClient kmsapi.Transport,
) (AuthenticationDetails, error) {
var (
credRef secretsv1.KubeSecretReference
scope secretsv1.MachineIdentityScopeInWorkspace
)
switch secretCrd.Type {
case SecretCrd.KMS_SECRET:
kmsSecret, ok := secretCrd.Secret.(secretsv1.KMSSecret)
if !ok {
return AuthenticationDetails{}, errors.New("HandleUniversalAuth: not a KMSSecret")
}
credRef = kmsSecret.Spec.Authentication.UniversalAuth.CredentialsRef
scope = kmsSecret.Spec.Authentication.UniversalAuth.SecretsScope
case SecretCrd.KMS_PUSH_SECRET:
kmsPushSecret, ok := secretCrd.Secret.(secretsv1.KMSPushSecret)
if !ok {
return AuthenticationDetails{}, errors.New("HandleUniversalAuth: not a KMSPushSecret")
}
credRef = kmsPushSecret.Spec.Authentication.UniversalAuth.CredentialsRef
default:
return AuthenticationDetails{}, fmt.Errorf("HandleUniversalAuth: unsupported CRD type %q", secretCrd.Type)
}
if credRef.SecretName == "" || credRef.SecretNamespace == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
// Same-namespace enforcement. The CR's namespace is the only place
// the operator will read credentials from.
crNS := crNamespace(secretCrd)
if crNS != "" && credRef.SecretNamespace != crNS {
return AuthenticationDetails{}, fmt.Errorf(
"credentialsRef cross-namespace refused: CR in '%s' cannot read Secret from '%s'",
crNS, credRef.SecretNamespace,
)
}
creds, err := GetKMSUniversalAuthFromKubeSecret(ctx, reconcilerClient, credRef)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("HandleUniversalAuth: load credentialsRef: %w", err)
}
if creds.ClientId == "" || creds.ClientSecret == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
if kmsapi.ContainsUnsafeControl(creds.ClientId) || kmsapi.ContainsUnsafeControl(creds.ClientSecret) {
return AuthenticationDetails{}, errors.New("HandleUniversalAuth: credentialsRef contains control bytes")
}
if kmsClient == nil {
newCli, newErr := kmsapi.New(kmsapi.Config{})
if newErr != nil {
return AuthenticationDetails{}, fmt.Errorf("HandleUniversalAuth: build kms client: %w", newErr)
}
kmsClient = newCli
}
// On the ZAP transport, NormaliseHost would strip the "zap://"
// scheme; preserve the raw host string from the CR so the
// underlying transport can route correctly.
resolvedHost := host
if !kmsapi.IsZAPHost(host) {
resolvedHost = kmsapi.NormaliseHost(host)
}
_ = resolvedHost // used below in AuthenticationDetails
token, err := kmsClient.LoginCached(ctx, host, creds.ClientId, creds.ClientSecret)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("HandleUniversalAuth: login: %w", err)
}
return AuthenticationDetails{
AuthStrategy: AuthStrategy.UNIVERSAL_MACHINE_IDENTITY,
BearerToken: token,
Host: resolvedHost,
MachineIdentityScope: scope,
SecretType: secretCrd.Type,
}, nil
}
func crNamespace(secretCrd SecretAuthInput) string {
switch secretCrd.Type {
case SecretCrd.KMS_SECRET:
if v, ok := secretCrd.Secret.(secretsv1.KMSSecret); ok {
return v.Namespace
}
case SecretCrd.KMS_PUSH_SECRET:
if v, ok := secretCrd.Secret.(secretsv1.KMSPushSecret); ok {
return v.Namespace
}
}
return ""
}
// ── Service-account token issuance retained for K8s integrations ──
//
// The serviceaccount-token issuance flow below is still used by other
// in-tree controllers (ClusterGenerator etc.) and is independent of
// the KMS auth flow. Left in place to avoid churn unrelated to the
// luxfi/kms port.
func GetServiceAccountToken(
k8sClient client.Client,
namespace string,
serviceAccountName string,
autoCreateServiceAccountToken bool,
serviceAccountTokenAudiences []string,
) (string, error) {
if autoCreateServiceAccountToken {
restClient, err := GetRestClientFromClient()
if err != nil {
return "", fmt.Errorf("failed to get REST client: %w", err)
}
tokenRequest := &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
ExpirationSeconds: ptr.Int64(600),
},
}
if len(serviceAccountTokenAudiences) > 0 {
tokenRequest.Spec.Audiences = serviceAccountTokenAudiences
}
result := &authenticationv1.TokenRequest{}
err = restClient.
Post().
Namespace(namespace).
Resource("serviceaccounts").
Name(serviceAccountName).
SubResource("token").
Body(tokenRequest).
Do(context.Background()).
Into(result)
if err != nil {
return "", fmt.Errorf("failed to create token: %w", err)
}
return result.Status.Token, nil
}
serviceAccount := &corev1.ServiceAccount{}
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: serviceAccountName, Namespace: namespace}, serviceAccount)
if err != nil {
return "", err
}
if len(serviceAccount.Secrets) == 0 {
return "", fmt.Errorf("no secrets found for service account %s", serviceAccountName)
}
secretName := serviceAccount.Secrets[0].Name
secret := &corev1.Secret{}
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: secretName, Namespace: namespace}, secret)
if err != nil {
return "", err
}
return string(secret.Data["token"]), nil
}