diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init.go index 7c9ca8e92665e..cd7d1d7de17b6 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init.go @@ -17,6 +17,8 @@ limitations under the License. package filters import ( + "encoding/binary" + "math/rand/v2" "net/http" "k8s.io/apimachinery/pkg/types" @@ -26,13 +28,33 @@ import ( "github.com/google/uuid" ) +// nonCryptoRandReader is an io.Reader backed by math/rand/v2 for generating +// audit IDs without going through crypto/rand (which is expensive in FIPS mode). +type nonCryptoRandReader struct{} + +func (nonCryptoRandReader) Read(b []byte) (int, error) { + for i := 0; i+8 <= len(b); i += 8 { + binary.NativeEndian.PutUint64(b[i:], rand.Uint64()) + } + // handle trailing bytes + if tail := len(b) % 8; tail > 0 { + var buf [8]byte + binary.NativeEndian.PutUint64(buf[:], rand.Uint64()) + copy(b[len(b)-tail:], buf[:tail]) + } + return len(b), nil +} + // WithAuditInit initializes the audit context and attaches the Audit-ID associated with a request. // // a. If the caller does not specify a value for Audit-ID in the request header, we generate a new audit ID // b. We echo the Audit-ID value to the caller via the response Header 'Audit-ID'. func WithAuditInit(handler http.Handler) http.Handler { + // Audit IDs are correlation tokens, not security-sensitive. Use non-crypto + // randomness to avoid expensive FIPS-mode crypto/rand on every request. + reader := nonCryptoRandReader{} return withAuditInit(handler, func() string { - return uuid.New().String() + return uuid.Must(uuid.NewRandomFromReader(reader)).String() }) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init_test.go index c12b2879177bc..4f4ebd00d4889 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init_test.go @@ -26,6 +26,44 @@ import ( "k8s.io/apiserver/pkg/audit" ) +func TestWithAuditInitGeneratesValidUUIDv4(t *testing.T) { + var auditIDGot string + handler := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) { + v, ok := audit.AuditIDFrom(req.Context()) + if !ok { + t.Fatal("expected AuditIDFrom to return true") + } + auditIDGot = string(v) + }) + + wrapped := WithAuditInit(handler) + + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/api/v1/namespaces", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + // No Audit-ID header set — force generation. + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + parsed, err := uuid.Parse(auditIDGot) + if err != nil { + t.Fatalf("generated audit ID %q is not a valid UUID: %v", auditIDGot, err) + } + if parsed.Version() != 4 { + t.Errorf("expected UUID version 4, got %d", parsed.Version()) + } + if parsed.Variant() != uuid.RFC4122 { + t.Errorf("expected RFC 4122 variant, got %v", parsed.Variant()) + } + + echoed := w.Header().Get("Audit-ID") + if echoed != auditIDGot { + t.Errorf("Audit-ID response header %q doesn't match context value %q", echoed, auditIDGot) + } +} + func TestWithAuditID(t *testing.T) { largeAuditID := fmt.Sprintf("%s-%s", uuid.New().String(), uuid.New().String()) tests := []struct {