Files
zeekay 9baabd6022 Invalidate the token that failed, not a key nothing is stored under
The token cache is keyed on sha256(clientID:clientSecret@host). The reconciler
invalidated it with `InvalidateToken(host, "", "")`, which hashes to a key
nothing was ever stored under — so the delete matched nothing and the stale
entry survived. Every later reconcile reused a token the server had stopped
accepting.

Measured in production: 133 of 147 KMSSecrets failing, every read answering
403 "authentication required", for weeks. The credentials were never the
problem — logging in by hand with the same clientId and secret returns a token,
and a GET with that token returns 200. The operator was simply presenting a
dead one. The token's exp minus iat is 604800 seconds, and both operator pods
were seven days old.

The reconciler cannot pass the credentials because it does not have them:
AuthenticationDetails carries BearerToken and Host and deliberately not the
client secret, which is the right shape and should stay. So invalidation now
takes what the caller does hold — InvalidateBearer(host, token) drops whichever
entry issued that token.

Three tests. One pins that the old empty-credential call is a no-op, so the bug
cannot come back wearing its original face. One proves InvalidateBearer forces a
fresh login. One proves an unknown token evicts nothing, because an invalidation
that clears live entries is its own outage.

The existing InvalidateToken test passed throughout: it called the function with
real credentials while production called it with empty ones. A function proven
correct and a call site that never used it that way.
2026-08-20 17:44:42 -07:00

91 lines
3.3 KiB
Go

package util
import (
"context"
"testing"
secretsv1 "github.com/hanzoai/kms-operator/api/v1"
"github.com/hanzoai/kms-operator/packages/kmsapi"
)
// fakeTransport serves canned values keyed by secret name.
type fakeTransport struct {
values map[string]string
}
func (f *fakeTransport) Login(context.Context, string, string, string) (kmsapi.LoginResponse, error) {
return kmsapi.LoginResponse{AccessToken: "t"}, nil
}
func (f *fakeTransport) LoginCached(context.Context, string, string, string) (string, error) {
return "t", nil
}
func (f *fakeTransport) InvalidateToken(string, string, string) {}
func (f *fakeTransport) InvalidateBearer(_, _ string) {}
func (f *fakeTransport) GetSecret(_ context.Context, _, _, _, _, _, name string) (kmsapi.SecretResponse, error) {
v, ok := f.values[name]
if !ok {
return kmsapi.SecretResponse{}, kmsapi.ErrNotFound
}
return kmsapi.SecretResponse{Value: v, Version: 1}, nil
}
func (f *fakeTransport) CreateSecret(context.Context, string, string, string, string, string, string, string) (int64, error) {
return 1, nil
}
func (f *fakeTransport) UpdateSecret(context.Context, string, string, string, string, string, string, string, int64) (int64, error) {
return 1, nil
}
func (f *fakeTransport) DeleteSecret(context.Context, string, string, string, string, string, string) error {
return nil
}
// TestB64KeysDecodeOnProjection pins the binary convention: a key named
// "X.b64" is fetched by that name, base64-decoded, and projected as "X"
// with the exact raw bytes — so managed Secrets keep the byte contract
// their consumers expect (e.g. a raw 32-byte BLS signer key).
func TestB64KeysDecodeOnProjection(t *testing.T) {
raw := []byte{0x00, 0x01, 0x1f, 0xff, 0x7f, 0x80, 0x0a} // control bytes + high bytes
ft := &fakeTransport{values: map[string]string{
"staker-0.crt": "-----BEGIN CERTIFICATE-----\nAA==\n-----END CERTIFICATE-----\n",
"signer-0.key.b64": "AAEf/3+ACg==", // base64 of raw
}}
scope := secretsv1.MachineIdentityScopeInWorkspace{
ProjectSlug: "lux",
EnvSlug: "testnet",
SecretsPath: "/staking",
Keys: []string{"staker-0.crt", "signer-0.key.b64"},
}
out, err := GetPlainTextSecretsViaMachineIdentity(context.Background(), ft, "https://kms.example", "tok", scope)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := map[string]string{}
for _, s := range out {
got[s.Key] = s.Value
}
if _, exists := got["signer-0.key.b64"]; exists {
t.Fatalf(".b64 suffix must be stripped on projection")
}
if got["signer-0.key"] != string(raw) {
t.Fatalf("signer-0.key bytes mangled: got %q want %q", got["signer-0.key"], raw)
}
if got["staker-0.crt"] == "" {
t.Fatalf("plain key must pass through unchanged")
}
}
// TestB64KeyInvalidBase64FailsClosed pins fail-closed: a .b64 value that
// does not decode fails the whole fetch rather than projecting garbage.
func TestB64KeyInvalidBase64FailsClosed(t *testing.T) {
ft := &fakeTransport{values: map[string]string{"signer-0.key.b64": "not!!base64"}}
scope := secretsv1.MachineIdentityScopeInWorkspace{
ProjectSlug: "lux",
EnvSlug: "testnet",
SecretsPath: "/staking",
Keys: []string{"signer-0.key.b64"},
}
if _, err := GetPlainTextSecretsViaMachineIdentity(context.Background(), ft, "https://kms.example", "tok", scope); err == nil {
t.Fatalf("expected error for invalid base64, got nil")
}
}