Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package filters

import (
"encoding/binary"
"math/rand/v2"
"net/http"

"k8s.io/apimachinery/pkg/types"
Expand All @@ -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()
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down