Merge pull request #137523 from enj/enj/f/constrained_impersonation_latency_metrics

KEP-5284: add impersonation latency tracking
This commit is contained in:
Kubernetes Prow Robot 2026-03-10 19:29:36 +05:30 committed by GitHub
commit 2757a872ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 332 additions and 24 deletions

View file

@ -51,12 +51,17 @@ func WithConstrainedImpersonation(handler http.Handler, a authorizer.Authorizer,
recordAuthorizationCall: metrics.RecordImpersonationAuthorizationCall,
}
recordAttempt := func(ctx context.Context, mode, decision string, duration time.Duration) {
metrics.RecordImpersonationAttempt(mode, decision, duration)
request.TrackImpersonationLatency(ctx, duration)
}
return &constrainedImpersonationHandler{
handler: handler,
tracker: newImpersonationModesTracker(ma),
s: s,
recordAttempt: metrics.RecordImpersonationAttempt,
recordAttempt: recordAttempt,
metricsAuthorizer: ma,
}
}
@ -67,7 +72,7 @@ type constrainedImpersonationHandler struct {
s runtime.NegotiatedSerializer
// to allow unit tests to override metrics recording
recordAttempt func(mode, decision string, duration time.Duration)
recordAttempt func(ctx context.Context, mode, decision string, duration time.Duration)
metricsAuthorizer *metricsAuthorizer
}
@ -98,12 +103,12 @@ func (c *constrainedImpersonationHandler) ServeHTTP(w http.ResponseWriter, req *
impersonatedUser, err := c.tracker.getImpersonatedUser(ctx, wantedUser, attributes)
duration := time.Since(start)
if err != nil {
c.recordAttempt("", "denied", duration)
c.recordAttempt(ctx, "", "denied", duration)
klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "err", err)
responsewriters.RespondWithError(w, req, err, c.s)
return
}
c.recordAttempt(modeFromConstraint(impersonatedUser.constraint), "allowed", duration)
c.recordAttempt(ctx, modeFromConstraint(impersonatedUser.constraint), "allowed", duration)
req = req.WithContext(request.WithUser(ctx, impersonatedUser.user))
httplog.LogOf(req, w).Addf("%v is impersonating %v", userString(requestor), userString(impersonatedUser.user))

View file

@ -34,6 +34,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/audit/policy"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
@ -49,12 +51,20 @@ type constrainedImpersonationTest struct {
constrainedImpersonationHandler *constrainedImpersonationHandler
checkedAttrs []authorizer.Attributes
echoCalled bool
auditEvents []*auditinternal.Event
attemptMode string
attemptDecision string
authorizationMetrics map[string]int // "mode/decision" -> count
}
func (c *constrainedImpersonationTest) ProcessEvents(evs ...*auditinternal.Event) bool {
for _, e := range evs {
c.auditEvents = append(c.auditEvents, e.DeepCopy()) // event is mutated in place so capture its current state
}
return true
}
func (c *constrainedImpersonationTest) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
c.checkedAttrs = append(c.checkedAttrs, a)
@ -174,9 +184,11 @@ func (c *constrainedImpersonationTest) handler() http.Handler {
addImpersonation := WithConstrainedImpersonation(c.echoUserInfoHandler(), c, serializer.NewCodecFactory(s))
c.constrainedImpersonationHandler = addImpersonation.(*constrainedImpersonationHandler)
c.constrainedImpersonationHandler.recordAttempt = func(mode, decision string, _ time.Duration) {
recordAttempt := c.constrainedImpersonationHandler.recordAttempt
c.constrainedImpersonationHandler.recordAttempt = func(ctx context.Context, mode, decision string, duration time.Duration) {
c.attemptMode = mode
c.attemptDecision = decision
recordAttempt(ctx, mode, decision, duration)
}
c.constrainedImpersonationHandler.metricsAuthorizer.recordAuthorizationCall = func(mode, decision string, _ time.Duration) {
if c.authorizationMetrics == nil {
@ -185,9 +197,20 @@ func (c *constrainedImpersonationTest) handler() http.Handler {
c.authorizationMetrics[mode+"/"+decision]++
}
addAuthentication := c.authenticationHandler(addImpersonation)
addRequestInfo := c.requestInfoHandler(addAuthentication)
return addRequestInfo
fakeRuleEvaluator := policy.NewFakePolicyRuleEvaluator(auditinternal.LevelRequestResponse, nil)
// follow the same handler chain order as DefaultBuildHandlerChain
addAudit := filters.WithAudit(addImpersonation, c, fakeRuleEvaluator, nil)
addAuthentication := c.authenticationHandler(addAudit)
addLatencyTrackers := filters.WithLatencyTrackers(addAuthentication)
addRequestInfo := c.requestInfoHandler(addLatencyTrackers)
// set received timestamp >500ms in the past so audit annotations are emitted
addReceivedTimestamp := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req = req.WithContext(request.WithReceivedTimestamp(req.Context(), time.Now().Add(-time.Hour)))
addRequestInfo.ServeHTTP(w, req)
})
addAuditInit := filters.WithAuditInit(addReceivedTimestamp)
return addAuditInit
}
type testRoundTripper struct {
@ -250,6 +273,25 @@ func comparableAttributes(attributes authorizer.Attributes) authorizer.Attribute
}
}
func comparableAuditUser(u *authenticationv1.UserInfo) *user.DefaultInfo {
if u == nil {
return nil
}
var extra map[string][]string
if len(u.Extra) > 0 {
extra = make(map[string][]string, len(u.Extra))
for k, v := range u.Extra {
extra[k] = v
}
}
return &user.DefaultInfo{
Name: u.Username,
UID: u.UID,
Groups: u.Groups,
Extra: extra,
}
}
func comparableUser(u user.Info) *user.DefaultInfo {
return &user.DefaultInfo{
Name: u.GetName(),
@ -280,6 +322,36 @@ func (c *constrainedImpersonationTest) assertMetrics(r testRequest) {
require.Equal(c.t, r.expectedAuthorizationMetrics, authorizationMetrics, "unexpected authorization metrics")
}
func (c *constrainedImpersonationTest) assertAuditEvents(r testRequest) {
c.t.Helper()
events := c.auditEvents
c.auditEvents = nil
require.Len(c.t, events, 2)
requestReceived := events[0]
responseComplete := events[1]
require.Empty(c.t, requestReceived.Annotations["apiserver.latency.k8s.io/impersonation"])
require.Regexp(c.t, "^[0-9.]+[µnm]s$", responseComplete.Annotations["apiserver.latency.k8s.io/impersonation"])
require.Nil(c.t, requestReceived.ImpersonatedUser)
require.Equal(c.t, r.expectedImpersonatedUser, comparableAuditUser(responseComplete.ImpersonatedUser))
require.Nil(c.t, requestReceived.ResponseStatus)
require.Equal(c.t, r.expectedMessage, responseComplete.ResponseStatus.Message)
require.Nil(c.t, requestReceived.AuthenticationMetadata)
var expectedAuthenticationMetadata *auditinternal.AuthenticationMetadata
if r.expectedCode == http.StatusOK && r.expectedAttemptMode != "legacy" {
expectedAuthenticationMetadata = &auditinternal.AuthenticationMetadata{
ImpersonationConstraint: "impersonate:" + r.expectedAttemptMode,
}
}
require.Equal(c.t, expectedAuthenticationMetadata, responseComplete.AuthenticationMetadata)
}
func (c *constrainedImpersonationTest) assertCache(r testRequest) {
rr := require.New(c.t)
@ -1222,6 +1294,7 @@ func TestConstrainedImpersonationFilter(t *testing.T) {
test.assertAttributes(r)
test.assertCache(r)
test.assertMetrics(r)
test.assertAuditEvents(r)
}
})
}

View file

@ -165,6 +165,10 @@ type LatencyTrackers struct {
// When called multiple times, the latency incurred inside to
// decode func each time will be summed up.
DecodeTracker DurationTracker
// ImpersonationTracker tracks the latency incurred in resolving impersonation.
// This includes mode selection, authorization checks, and cache lookups.
ImpersonationTracker DurationTracker
}
type latencyTrackersKeyType int
@ -193,6 +197,7 @@ func WithLatencyTrackersAndCustomClock(parent context.Context, c clock.Clock) co
SerializationTracker: newSumLatencyTracker(c),
ResponseWriteTracker: newSumLatencyTracker(c),
DecodeTracker: newSumLatencyTracker(c),
ImpersonationTracker: newSumLatencyTracker(c),
})
}
@ -286,6 +291,14 @@ func TrackDecodeLatency(ctx context.Context, d time.Duration) {
}
}
// TrackImpersonationLatency is used to track latency incurred
// in resolving impersonation for the request.
func TrackImpersonationLatency(ctx context.Context, d time.Duration) {
if tracker, ok := LatencyTrackersFrom(ctx); ok {
tracker.ImpersonationTracker.TrackDuration(d)
}
}
// AuditAnnotationsFromLatencyTrackers will inspect each latency tracker
// associated with the request context and return a set of audit
// annotations that can be added to the API audit entry.
@ -301,6 +314,7 @@ func AuditAnnotationsFromLatencyTrackers(ctx context.Context) map[string]string
apfQueueWaitLatencyKey = "apiserver.latency.k8s.io/apf-queue-wait"
authenticationLatencyKey = "apiserver.latency.k8s.io/authentication"
authorizationLatencyKey = "apiserver.latency.k8s.io/authorization"
impersonationLatencyKey = "apiserver.latency.k8s.io/impersonation"
)
tracker, ok := LatencyTrackersFrom(ctx)
@ -339,5 +353,8 @@ func AuditAnnotationsFromLatencyTrackers(ctx context.Context) map[string]string
if latency := tracker.AuthorizationTracker.GetLatency(); latency != 0 {
annotations[authorizationLatencyKey] = latency.String()
}
if latency := tracker.ImpersonationTracker.GetLatency(); latency != 0 {
annotations[impersonationLatencyKey] = latency.String()
}
return annotations
}

View file

@ -509,7 +509,7 @@ func Test_ValidateAnnotationsAndWarnings(t *testing.T) {
checkExpectedError(t, err, testcase.err)
checkFailureReason(t, err, testcase.failureReason)
checkExpectedWarnings(t, warnHandler, testcase.warnings)
checkAuditEvents(t, logFile, expectedAuditEvents(testcase.auditAnnotations, ns, code), auditAnnotationFilter)
checkAuditEvents(t, logFile, expectedAuditEvents(testcase.auditAnnotations, ns, testcase.err, code), auditAnnotationFilter)
})
}
}
@ -3412,7 +3412,7 @@ func (w *warningHandler) HandleWarningHeader(code int, _ string, message string)
w.warnings.Insert(message)
}
func expectedAuditEvents(auditAnnotations map[string]string, ns string, code int32) []utils.AuditEvent {
func expectedAuditEvents(auditAnnotations map[string]string, ns, msg string, code int32) []utils.AuditEvent {
return []utils.AuditEvent{
{
Level: auditinternal.LevelRequest,
@ -3420,6 +3420,7 @@ func expectedAuditEvents(auditAnnotations map[string]string, ns string, code int
RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/configmaps", ns),
Verb: "create",
Code: code,
StatusMessage: msg,
User: "system:apiserver",
ImpersonatedUser: testReinvocationClientUsername,
ImpersonatedGroups: "system:authenticated",

View file

@ -53,6 +53,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/group"
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
@ -62,7 +64,9 @@ import (
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
unionauthz "k8s.io/apiserver/pkg/authorization/union"
genericrequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server"
utilfeature "k8s.io/apiserver/pkg/util/feature"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
@ -81,6 +85,7 @@ import (
"k8s.io/kubernetes/test/integration"
"k8s.io/kubernetes/test/integration/authutil"
"k8s.io/kubernetes/test/integration/framework"
testutils "k8s.io/kubernetes/test/utils"
"k8s.io/kubernetes/test/utils/ktesting"
)
@ -936,12 +941,17 @@ func TestImpersonateIsForbidden(t *testing.T) {
}
func TestImpersonateWithUID(t *testing.T) {
auditPolicyFile, auditLogFile := setupImpersonationAuditFiles(t)
server := kubeapiservertesting.StartTestServerOrDie(
t,
nil,
[]string{
"--authorization-mode=RBAC",
"--anonymous-auth",
"--audit-policy-file", auditPolicyFile,
"--audit-log-path", auditLogFile,
"--audit-log-version", "audit.k8s.io/v1",
"--audit-log-mode", "blocking",
},
framework.SharedEtcd(),
)
@ -1003,6 +1013,10 @@ func TestImpersonateWithUID(t *testing.T) {
if diff := cmp.Diff(expectedCsrSpec, actualCsrSpec); diff != "" {
t.Fatalf("CSR spec was different than expected, -got, +want:\n %s", diff)
}
withUID := allowedImpersonationEvent("create", http.StatusCreated, "alice", "system:authenticated", "certificatesigningrequests", nil)
withUID.ImpersonatedUID = "1234"
assertImpersonationAuditEvents(t, auditLogFile, user.APIServerUser, withUID)
})
t.Run("impersonation with only UID fails", func(t *testing.T) {
@ -1026,6 +1040,7 @@ func TestImpersonateWithUID(t *testing.T) {
})
t.Run("impersonating UID without authorization fails", func(t *testing.T) {
truncateAuditLog(t, auditLogFile)
adminClient := clientset.NewForConfigOrDie(server.ClientConfig)
authutil.GrantUserAuthorization(t, ctx, adminClient, "system:anonymous",
@ -1056,6 +1071,10 @@ func TestImpersonateWithUID(t *testing.T) {
); diff != "" {
t.Fatalf("forbidden error different than expected, -got, +want:\n %s", diff)
}
assertImpersonationAuditEvents(t, auditLogFile, "system:anonymous",
deniedImpersonationEvent("list", `uids.authentication.k8s.io "1234" is forbidden: User "system:anonymous" cannot impersonate resource "uids" in API group "authentication.k8s.io" at the cluster scope`, "nodes"),
)
})
}
@ -1087,12 +1106,17 @@ func TestConstrainedImpersonation(t *testing.T) {
t.Cleanup(cancel)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConstrainedImpersonation, true)
var auditLogFile string
_, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
opts.Authorization.Modes = []string{"RBAC"}
auditLogFile = setupImpersonationAudit(t, opts)
},
ModifyServerConfig: func(config *controlplane.Config) {
config.ControlPlane.Generic.Authentication.Authenticator = authenticator
config.ControlPlane.Generic.RequestInfoResolver = &slowImpersonationRequests{
delegate: server.NewRequestInfoResolver(config.ControlPlane.Generic),
}
},
})
t.Cleanup(tearDownFn)
@ -1113,6 +1137,7 @@ func TestConstrainedImpersonation(t *testing.T) {
t.Run("bob impersonating alice", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
truncateAuditLog(t, auditLogFile)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "bob"
@ -1186,10 +1211,17 @@ func TestConstrainedImpersonation(t *testing.T) {
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} FP`,
})
assertImpersonationAuditEvents(t, auditLogFile, "bob",
deniedImpersonationEvent("list", `pods is forbidden: User "bob" cannot impersonate-on:user-info:list resource "pods" in API group "" at the cluster scope`, "pods"),
deniedImpersonationEvent("list", `pods is forbidden: User "bob" cannot impersonate-on:user-info:list resource "pods" in API group "" at the cluster scope`, "pods"),
allowedImpersonationEvent("list", http.StatusOK, "alice", "system:authenticated", "pods", new("impersonate:user-info")),
deniedImpersonationEvent("watch", `pods is forbidden: User "bob" cannot impersonate-on:user-info:watch resource "pods" in API group "" at the cluster scope`, "pods"),
)
})
t.Run("bob impersonating a node", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
truncateAuditLog(t, auditLogFile)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "bob"
@ -1263,10 +1295,16 @@ func TestConstrainedImpersonation(t *testing.T) {
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
})
assertImpersonationAuditEvents(t, auditLogFile, "bob",
deniedImpersonationEvent("list", `pods is forbidden: User "bob" cannot impersonate-on:arbitrary-node:list resource "pods" in API group "" at the cluster scope`, "pods"),
deniedImpersonationEvent("list", `nodes.authentication.k8s.io "node1" is forbidden: User "bob" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope`, "pods"),
allowedImpersonationEvent("list", http.StatusOK, "system:node:node1", "system:authenticated,system:nodes", "pods", new("impersonate:arbitrary-node")),
)
})
t.Run("impersonating scheduled node", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
truncateAuditLog(t, auditLogFile)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "serviceaccount2"
@ -1337,10 +1375,17 @@ func TestConstrainedImpersonation(t *testing.T) {
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} FP`,
})
assertImpersonationAuditEvents(t, auditLogFile, "system:serviceaccount:default:sa2",
deniedImpersonationEvent("list", `pods is forbidden: User "system:serviceaccount:default:sa2" cannot impersonate-on:arbitrary-node:list resource "pods" in API group "" at the cluster scope`, "pods"),
)
assertImpersonationAuditEvents(t, auditLogFile, "system:serviceaccount:default:sa1",
allowedImpersonationEvent("list", http.StatusOK, "system:node:node1", "system:authenticated,system:nodes", "pods", new("impersonate:associated-node")),
)
})
t.Run("fallback to legacy impersonation", func(t *testing.T) {
resetAllMetrics(t, ctx, superuserClient)
truncateAuditLog(t, auditLogFile)
impersonatorClientConfig := rest.CopyConfig(kubeConfig)
impersonatorClientConfig.BearerToken = "bob"
@ -1375,6 +1420,9 @@ func TestConstrainedImpersonation(t *testing.T) {
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} FP`,
`apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} FP`,
})
assertImpersonationAuditEvents(t, auditLogFile, "bob",
allowedImpersonationEvent("list", http.StatusOK, "alice", "system:authenticated", "pods", nil),
)
})
}
@ -1419,6 +1467,147 @@ func assertImpersonationMetrics(t *testing.T, ctx context.Context, client client
}
}
const impersonationAuditPolicy = `
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
- level: Metadata
`
func setupImpersonationAuditFiles(t *testing.T) (policyFilePath, logFilePath string) {
t.Helper()
dir := t.TempDir()
policyFilePath = filepath.Join(dir, "audit-policy.yaml")
if err := os.WriteFile(policyFilePath, []byte(impersonationAuditPolicy), 0644); err != nil {
t.Fatalf("failed to write audit policy: %v", err)
}
logFilePath = filepath.Join(dir, "audit.log")
return policyFilePath, logFilePath
}
func setupImpersonationAudit(t *testing.T, opts *options.ServerRunOptions) string {
t.Helper()
policyFilePath, logFilePath := setupImpersonationAuditFiles(t)
opts.Audit.PolicyFile = policyFilePath
opts.Audit.LogOptions.Path = logFilePath
opts.Audit.LogOptions.GroupVersionString = "audit.k8s.io/v1"
opts.Audit.LogOptions.BatchOptions.Mode = "blocking"
return logFilePath
}
func truncateAuditLog(t *testing.T, logFilePath string) {
t.Helper()
if err := os.Truncate(logFilePath, 0); err != nil {
t.Fatalf("failed to truncate audit log: %v", err)
}
}
func getAuditEvents(t *testing.T, logFilePath string) []testutils.AuditEvent {
t.Helper()
stream, err := os.Open(logFilePath)
if err != nil {
t.Fatalf("failed to open audit log: %v", err)
}
defer func() {
if err := stream.Close(); err != nil {
t.Errorf("failed to close audit log: %v", err)
}
}()
report, err := testutils.CheckAuditLinesFiltered(stream, nil, auditv1.SchemeGroupVersion, func(_, _ string) bool {
return true // get all audit annotations
})
if err != nil {
t.Fatalf("failed to parse audit log: %v", err)
}
return report.AllEvents
}
func assertImpersonationAuditEvents(t *testing.T, logFilePath, wantUser string, wantEvents ...testutils.AuditEvent) {
t.Helper()
latencyPattern := regexp.MustCompile("^[0-9.]+[µnm]s$")
var matched []testutils.AuditEvent
for _, event := range getAuditEvents(t, logFilePath) {
if event.Stage != auditinternal.StageResponseComplete {
continue
}
if event.User != wantUser {
continue
}
if len(event.ImpersonatedUser) == 0 && !strings.Contains(event.StatusMessage, "impersonate") {
continue
}
matched = append(matched, event)
}
if len(matched) != len(wantEvents) {
t.Fatalf("expected %d audit event(s) from user %q with impersonation, got %d: %v", len(wantEvents), wantUser, len(matched), matched)
}
for i, event := range matched {
got := testutils.AuditEvent{
Verb: event.Verb,
Code: event.Code,
StatusMessage: event.StatusMessage,
ImpersonatedUser: event.ImpersonatedUser,
ImpersonatedUID: event.ImpersonatedUID,
ImpersonatedGroups: event.ImpersonatedGroups,
Resource: event.Resource,
Namespace: event.Namespace,
AuthorizeDecision: event.AuthorizeDecision,
ImpersonationConstraint: event.ImpersonationConstraint,
}
if diff := cmp.Diff(wantEvents[i], got); len(diff) > 0 {
t.Errorf("audit event[%d] mismatch (-want +got): %s", i, diff)
}
if event.Verb != "watch" && utilfeature.DefaultFeatureGate.Enabled(features.ConstrainedImpersonation) {
latency := event.CustomAuditAnnotations["apiserver.latency.k8s.io/impersonation"]
if !latencyPattern.MatchString(latency) {
t.Errorf("audit event[%d] expected valid impersonation latency annotation, got %q", i, latency)
}
}
}
}
func allowedImpersonationEvent(verb string, code int32, impersonatedUser, impersonatedGroups, resource string, constraint *string) testutils.AuditEvent {
return testutils.AuditEvent{
Verb: verb,
Code: code,
ImpersonatedUser: impersonatedUser,
ImpersonatedGroups: impersonatedGroups,
Resource: resource,
AuthorizeDecision: "allow",
ImpersonationConstraint: constraint,
}
}
func deniedImpersonationEvent(verb, statusMessage, resource string) testutils.AuditEvent {
return testutils.AuditEvent{
Verb: verb,
Code: http.StatusForbidden,
StatusMessage: statusMessage,
Resource: resource,
}
}
type slowImpersonationRequests struct {
delegate genericrequest.RequestInfoResolver
}
func (s *slowImpersonationRequests) NewRequestInfo(req *http.Request) (*genericrequest.RequestInfo, error) {
if len(req.Header.Get(authenticationv1.ImpersonateUserHeader)) > 0 {
time.Sleep(505 * time.Millisecond) // force latency audit annotations to be emitted for impersonation requests
}
return s.delegate.NewRequestInfo(req)
}
// TestConstrainedImpersonationDisabled tests the impersonation behavior when the
// ConstrainedImpersonation feature gate is disabled. In this mode, the legacy
// impersonation behavior is expected, where a user only needs the "impersonate"
@ -1441,9 +1630,11 @@ func TestConstrainedImpersonationDisabled(t *testing.T) {
t.Cleanup(cancel)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConstrainedImpersonation, false)
var auditLogFile string
_, kubeConfig, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
opts.Authorization.Modes = []string{"RBAC"}
auditLogFile = setupImpersonationAudit(t, opts)
},
ModifyServerConfig: func(config *controlplane.Config) {
config.ControlPlane.Generic.Authentication.Authenticator = authenticator
@ -1466,6 +1657,8 @@ func TestConstrainedImpersonationDisabled(t *testing.T) {
})
t.Run("bob impersonating alice", func(t *testing.T) {
truncateAuditLog(t, auditLogFile)
authutil.GrantUserAuthorization(t, ctx, superuserClient, "bob", rbacv1.PolicyRule{
Verbs: []string{"impersonate:user-info"},
APIGroups: []string{"authentication.k8s.io"},
@ -1507,9 +1700,15 @@ func TestConstrainedImpersonationDisabled(t *testing.T) {
if err != nil {
t.Fatalf("expected no error, got %T %v", err, err)
}
assertImpersonationAuditEvents(t, auditLogFile, "bob",
deniedImpersonationEvent("list", `users "alice" is forbidden: User "bob" cannot impersonate resource "users" in API group "" at the cluster scope`, "pods"),
allowedImpersonationEvent("list", http.StatusOK, "alice", "system:authenticated", "pods", nil),
)
})
t.Run("serviceaccount impersonating a node", func(t *testing.T) {
truncateAuditLog(t, auditLogFile)
authutil.GrantUserAuthorization(t, ctx, superuserClient, "system:serviceaccount:default:sa1", rbacv1.PolicyRule{
Verbs: []string{"impersonate:associated-node"},
APIGroups: []string{"authentication.k8s.io"},
@ -1558,6 +1757,11 @@ func TestConstrainedImpersonationDisabled(t *testing.T) {
if err != nil {
t.Fatalf("expected no error, got %T %v", err, err)
}
assertImpersonationAuditEvents(t, auditLogFile, "system:serviceaccount:default:sa1",
deniedImpersonationEvent("list", `users "system:node:node1" is forbidden: User "system:serviceaccount:default:sa1" cannot impersonate resource "users" in API group "" at the cluster scope`, "pods"),
allowedImpersonationEvent("list", http.StatusOK, "system:node:node1", "system:authenticated", "pods", nil),
)
})
}

View file

@ -34,20 +34,23 @@ import (
// AuditEvent is a simplified representation of an audit event for testing purposes
type AuditEvent struct {
ID types.UID
Level auditinternal.Level
Stage auditinternal.Stage
RequestURI string
Verb string
Code int32
User string
ImpersonatedUser string
ImpersonatedGroups string
Resource string
Namespace string
RequestObject bool
ResponseObject bool
AuthorizeDecision string
ID types.UID
Level auditinternal.Level
Stage auditinternal.Stage
RequestURI string
Verb string
Code int32
StatusMessage string
User string
ImpersonatedUser string
ImpersonatedUID string
ImpersonatedGroups string
ImpersonationConstraint *string
Resource string
Namespace string
RequestObject bool
ResponseObject bool
AuthorizeDecision string
// The Check functions in this package takes ownerships of these maps. You should
// not reference these maps after calling the Check functions.
@ -147,6 +150,7 @@ func testEventFromInternalFiltered(e *auditinternal.Event, customAnnotationsFilt
}
if e.ResponseStatus != nil {
event.Code = e.ResponseStatus.Code
event.StatusMessage = e.ResponseStatus.Message
}
if e.ResponseObject != nil {
event.ResponseObject = true
@ -156,9 +160,13 @@ func testEventFromInternalFiltered(e *auditinternal.Event, customAnnotationsFilt
}
if e.ImpersonatedUser != nil {
event.ImpersonatedUser = e.ImpersonatedUser.Username
event.ImpersonatedUID = e.ImpersonatedUser.UID
sort.Strings(e.ImpersonatedUser.Groups)
event.ImpersonatedGroups = strings.Join(e.ImpersonatedUser.Groups, ",")
}
if e.AuthenticationMetadata != nil {
event.ImpersonationConstraint = &e.AuthenticationMetadata.ImpersonationConstraint
}
event.AuthorizeDecision = e.Annotations["authorization.k8s.io/decision"]
for k, v := range e.Annotations {
if strings.HasPrefix(k, mutating.PatchAuditAnnotationPrefix) {