Skip to content

Commit e2f57be

Browse files
authored
Merge pull request kubernetes#77824 from roycaihw/webhook-trace
mutating webhook: audit log mutation existence and patch
2 parents 4480499 + 98ad20c commit e2f57be

File tree

20 files changed

+955
-119
lines changed

20 files changed

+955
-119
lines changed

staging/src/k8s.io/apiserver/pkg/admission/attributes.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"k8s.io/apimachinery/pkg/runtime"
2525
"k8s.io/apimachinery/pkg/runtime/schema"
2626
"k8s.io/apimachinery/pkg/util/validation"
27+
auditinternal "k8s.io/apiserver/pkg/apis/audit"
2728
"k8s.io/apiserver/pkg/authentication/user"
2829
)
2930

@@ -42,12 +43,17 @@ type attributesRecord struct {
4243

4344
// other elements are always accessed in single goroutine.
4445
// But ValidatingAdmissionWebhook add annotations concurrently.
45-
annotations map[string]string
46+
annotations map[string]annotation
4647
annotationsLock sync.RWMutex
4748

4849
reinvocationContext ReinvocationContext
4950
}
5051

52+
type annotation struct {
53+
level auditinternal.Level
54+
value string
55+
}
56+
5157
func NewAttributesRecord(object runtime.Object, oldObject runtime.Object, kind schema.GroupVersionKind, namespace, name string, resource schema.GroupVersionResource, subresource string, operation Operation, operationOptions runtime.Object, dryRun bool, userInfo user.Info) Attributes {
5258
return &attributesRecord{
5359
kind: kind,
@@ -111,7 +117,7 @@ func (record *attributesRecord) GetUserInfo() user.Info {
111117

112118
// getAnnotations implements privateAnnotationsGetter.It's a private method used
113119
// by WithAudit decorator.
114-
func (record *attributesRecord) getAnnotations() map[string]string {
120+
func (record *attributesRecord) getAnnotations(maxLevel auditinternal.Level) map[string]string {
115121
record.annotationsLock.RLock()
116122
defer record.annotationsLock.RUnlock()
117123

@@ -120,26 +126,36 @@ func (record *attributesRecord) getAnnotations() map[string]string {
120126
}
121127
cp := make(map[string]string, len(record.annotations))
122128
for key, value := range record.annotations {
123-
cp[key] = value
129+
if value.level.Less(maxLevel) || value.level == maxLevel {
130+
cp[key] = value.value
131+
}
124132
}
125133
return cp
126134
}
127135

136+
// AddAnnotation adds an annotation to attributesRecord with Metadata audit level
128137
func (record *attributesRecord) AddAnnotation(key, value string) error {
138+
return record.AddAnnotationWithLevel(key, value, auditinternal.LevelMetadata)
139+
}
140+
141+
func (record *attributesRecord) AddAnnotationWithLevel(key, value string, level auditinternal.Level) error {
129142
if err := checkKeyFormat(key); err != nil {
130143
return err
131144
}
132-
145+
if level.Less(auditinternal.LevelMetadata) {
146+
return fmt.Errorf("admission annotations are not allowed to be set at audit level lower than Metadata, key: %q, level: %s", key, level)
147+
}
133148
record.annotationsLock.Lock()
134149
defer record.annotationsLock.Unlock()
135150

136151
if record.annotations == nil {
137-
record.annotations = make(map[string]string)
152+
record.annotations = make(map[string]annotation)
138153
}
139-
if v, ok := record.annotations[key]; ok && v != value {
140-
return fmt.Errorf("admission annotations are not allowd to be overwritten, key:%q, old value: %q, new value:%q", key, record.annotations[key], value)
154+
annotation := annotation{level: level, value: value}
155+
if v, ok := record.annotations[key]; ok && v != annotation {
156+
return fmt.Errorf("admission annotations are not allowd to be overwritten, key:%q, old value: %v, new value: %v", key, record.annotations[key], annotation)
141157
}
142-
record.annotations[key] = value
158+
record.annotations[key] = annotation
143159
return nil
144160
}
145161

staging/src/k8s.io/apiserver/pkg/admission/attributes_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"testing"
2121

2222
"github.com/stretchr/testify/assert"
23+
auditinternal "k8s.io/apiserver/pkg/apis/audit"
2324
)
2425

2526
func TestAddAnnotation(t *testing.T) {
@@ -28,13 +29,13 @@ func TestAddAnnotation(t *testing.T) {
2829
// test AddAnnotation
2930
attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/validate-policy", "privileged")
3031
attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/admit-policy", "privileged")
31-
annotations := attr.getAnnotations()
32+
annotations := attr.getAnnotations(auditinternal.LevelMetadata)
3233
assert.Equal(t, annotations["podsecuritypolicy.admission.k8s.io/validate-policy"], "privileged")
3334

3435
// test overwrite
3536
assert.Error(t, attr.AddAnnotation("podsecuritypolicy.admission.k8s.io/validate-policy", "privileged-overwrite"),
3637
"admission annotations should not be allowd to be overwritten")
37-
annotations = attr.getAnnotations()
38+
annotations = attr.getAnnotations(auditinternal.LevelMetadata)
3839
assert.Equal(t, annotations["podsecuritypolicy.admission.k8s.io/validate-policy"], "privileged", "admission annotations should not be overwritten")
3940

4041
// test invalid plugin names
@@ -47,7 +48,7 @@ func TestAddAnnotation(t *testing.T) {
4748
for name, invalidKey := range testCases {
4849
err := attr.AddAnnotation(invalidKey, "value-foo")
4950
assert.Error(t, err)
50-
annotations = attr.getAnnotations()
51+
annotations = attr.getAnnotations(auditinternal.LevelMetadata)
5152
assert.Equal(t, annotations[invalidKey], "", name+": invalid pluginName is not allowed ")
5253
}
5354

staging/src/k8s.io/apiserver/pkg/admission/audit.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,18 @@ func ensureAnnotationGetter(a Attributes) error {
8585
}
8686

8787
func (handler auditHandler) logAnnotations(a Attributes) {
88+
if handler.ae == nil {
89+
return
90+
}
8891
switch a := a.(type) {
8992
case privateAnnotationsGetter:
90-
audit.LogAnnotations(handler.ae, a.getAnnotations())
93+
for key, value := range a.getAnnotations(handler.ae.Level) {
94+
audit.LogAnnotation(handler.ae, key, value)
95+
}
9196
case AnnotationsGetter:
92-
audit.LogAnnotations(handler.ae, a.GetAnnotations())
97+
for key, value := range a.GetAnnotations(handler.ae.Level) {
98+
audit.LogAnnotation(handler.ae, key, value)
99+
}
93100
default:
94101
// this will never happen, because we have already checked it in ensureAnnotationGetter
95102
}

staging/src/k8s.io/apiserver/pkg/admission/interfaces.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"k8s.io/apimachinery/pkg/runtime"
2424
"k8s.io/apimachinery/pkg/runtime/schema"
25+
auditinternal "k8s.io/apiserver/pkg/apis/audit"
2526
"k8s.io/apiserver/pkg/authentication/user"
2627
)
2728

@@ -62,8 +63,15 @@ type Attributes interface {
6263
// "podsecuritypolicy" is the name of the plugin, "admission.k8s.io" is the name of the organization, "admit-policy" is the key name.
6364
// An error is returned if the format of key is invalid. When trying to overwrite annotation with a new value, an error is returned.
6465
// Both ValidationInterface and MutationInterface are allowed to add Annotations.
66+
// By default, an annotation gets logged into audit event if the request's audit level is greater or
67+
// equal to Metadata.
6568
AddAnnotation(key, value string) error
6669

70+
// AddAnnotationWithLevel sets annotation according to key-value pair with additional intended audit level.
71+
// An Annotation gets logged into audit event if the request's audit level is greater or equal to the
72+
// intended audit level.
73+
AddAnnotationWithLevel(key, value string, level auditinternal.Level) error
74+
6775
// GetReinvocationContext tracks the admission request information relevant to the re-invocation policy.
6876
GetReinvocationContext() ReinvocationContext
6977
}
@@ -86,13 +94,13 @@ type ObjectInterfaces interface {
8694

8795
// privateAnnotationsGetter is a private interface which allows users to get annotations from Attributes.
8896
type privateAnnotationsGetter interface {
89-
getAnnotations() map[string]string
97+
getAnnotations(maxLevel auditinternal.Level) map[string]string
9098
}
9199

92100
// AnnotationsGetter allows users to get annotations from Attributes. An alternate Attribute should implement
93101
// this interface.
94102
type AnnotationsGetter interface {
95-
GetAnnotations() map[string]string
103+
GetAnnotations(maxLevel auditinternal.Level) map[string]string
96104
}
97105

98106
// ReinvocationContext provides access to the admission related state required to implement the re-invocation policy.

staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/BUILD

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ go_library(
2929
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
3030
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library",
3131
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/util:go_default_library",
32+
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
3233
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
3334
"//vendor/github.com/evanphx/json-patch:go_default_library",
3435
"//vendor/k8s.io/klog:go_default_library",
@@ -38,13 +39,18 @@ go_library(
3839

3940
go_test(
4041
name = "go_default_test",
41-
srcs = ["plugin_test.go"],
42+
srcs = [
43+
"dispatcher_test.go",
44+
"plugin_test.go",
45+
],
4246
embed = [":go_default_library"],
4347
deps = [
4448
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
4549
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
4650
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
4751
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:go_default_library",
52+
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
53+
"//vendor/github.com/evanphx/json-patch:go_default_library",
4854
"//vendor/github.com/stretchr/testify/assert:go_default_library",
4955
],
5056
)

staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,23 @@ import (
4141
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
4242
webhookrequest "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
4343
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
44+
auditinternal "k8s.io/apiserver/pkg/apis/audit"
4445
webhookutil "k8s.io/apiserver/pkg/util/webhook"
4546
utiltrace "k8s.io/utils/trace"
4647
)
4748

49+
const (
50+
// PatchAuditAnnotationPrefix is a prefix for persisting webhook patch in audit annotation.
51+
// Audit handler decides whether annotation with this prefix should be logged based on audit level.
52+
// Since mutating webhook patches the request body, audit level must be greater or equal to Request
53+
// for the annotation to be logged
54+
PatchAuditAnnotationPrefix = "patch.webhook.admission.k8s.io/"
55+
// MutationAuditAnnotationPrefix is a prefix for presisting webhook mutation existence in audit annotation.
56+
MutationAuditAnnotationPrefix = "mutation.webhook.admission.k8s.io/"
57+
)
58+
59+
var encodingjson = json.CaseSensitiveJsonIterator()
60+
4861
type mutatingDispatcher struct {
4962
cm *webhookutil.ClientManager
5063
plugin *Plugin
@@ -77,7 +90,7 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
7790
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
7891
}()
7992
var versionedAttr *generic.VersionedAttributes
80-
for _, hook := range hooks {
93+
for i, hook := range hooks {
8194
attrForCheck := attr
8295
if versionedAttr != nil {
8396
attrForCheck = versionedAttr
@@ -116,8 +129,11 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
116129
}
117130

118131
t := time.Now()
119-
120-
changed, err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o)
132+
round := 0
133+
if reinvokeCtx.IsReinvoke() {
134+
round = 1
135+
}
136+
changed, err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o, round, i)
121137
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name)
122138
if changed {
123139
// Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation.
@@ -162,7 +178,11 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
162178

163179
// note that callAttrMutatingHook updates attr
164180

165-
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) (bool, error) {
181+
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces, round, idx int) (bool, error) {
182+
configurationName := invocation.Webhook.GetConfigurationName()
183+
annotator := newWebhookAnnotator(attr, round, idx, h.Name, configurationName)
184+
changed := false
185+
defer func() { annotator.addMutationAnnotation(changed) }()
166186
if attr.Attributes.IsDryRun() {
167187
if h.SideEffects == nil {
168188
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
@@ -182,7 +202,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
182202
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
183203
}
184204
trace := utiltrace.New("Call mutating webhook",
185-
utiltrace.Field{"configuration", invocation.Webhook.GetConfigurationName()},
205+
utiltrace.Field{"configuration", configurationName},
186206
utiltrace.Field{"webhook", h.Name},
187207
utiltrace.Field{"resource", attr.GetResource()},
188208
utiltrace.Field{"subresource", attr.GetSubresource()},
@@ -240,6 +260,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
240260
if err != nil {
241261
return false, apierrors.NewInternalError(err)
242262
}
263+
243264
if len(patchObj) == 0 {
244265
return false, nil
245266
}
@@ -284,10 +305,103 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
284305
return false, apierrors.NewInternalError(err)
285306
}
286307

287-
changed := !apiequality.Semantic.DeepEqual(attr.VersionedObject, newVersionedObject)
308+
changed = !apiequality.Semantic.DeepEqual(attr.VersionedObject, newVersionedObject)
288309
trace.Step("Patch applied")
310+
annotator.addPatchAnnotation(patchObj, result.PatchType)
289311
attr.Dirty = true
290312
attr.VersionedObject = newVersionedObject
291313
o.GetObjectDefaulter().Default(attr.VersionedObject)
292314
return changed, nil
293315
}
316+
317+
type webhookAnnotator struct {
318+
attr *generic.VersionedAttributes
319+
patchAnnotationKey string
320+
mutationAnnotationKey string
321+
webhook string
322+
configuration string
323+
}
324+
325+
func newWebhookAnnotator(attr *generic.VersionedAttributes, round, idx int, webhook, configuration string) *webhookAnnotator {
326+
return &webhookAnnotator{
327+
attr: attr,
328+
patchAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", PatchAuditAnnotationPrefix, round, idx),
329+
mutationAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", MutationAuditAnnotationPrefix, round, idx),
330+
webhook: webhook,
331+
configuration: configuration,
332+
}
333+
}
334+
335+
func (w *webhookAnnotator) addMutationAnnotation(mutated bool) {
336+
if w.attr == nil || w.attr.Attributes == nil {
337+
return
338+
}
339+
value, err := mutationAnnotationValue(w.configuration, w.webhook, mutated)
340+
if err != nil {
341+
klog.Warningf("unexpected error composing mutating webhook annotation: %v", err)
342+
return
343+
}
344+
if err := w.attr.Attributes.AddAnnotation(w.mutationAnnotationKey, value); err != nil {
345+
klog.Warningf("failed to set mutation annotation for mutating webhook key %s to %s: %v", w.mutationAnnotationKey, value, err)
346+
}
347+
}
348+
349+
func (w *webhookAnnotator) addPatchAnnotation(patch interface{}, patchType admissionv1.PatchType) {
350+
if w.attr == nil || w.attr.Attributes == nil {
351+
return
352+
}
353+
var value string
354+
var err error
355+
switch patchType {
356+
case admissionv1.PatchTypeJSONPatch:
357+
value, err = jsonPatchAnnotationValue(w.configuration, w.webhook, patch)
358+
if err != nil {
359+
klog.Warningf("unexpected error composing mutating webhook JSON patch annotation: %v", err)
360+
return
361+
}
362+
default:
363+
klog.Warningf("unsupported patch type for mutating webhook annotation: %v", patchType)
364+
return
365+
}
366+
if err := w.attr.Attributes.AddAnnotationWithLevel(w.patchAnnotationKey, value, auditinternal.LevelRequest); err != nil {
367+
// NOTE: we don't log actual patch in kube-apiserver log to avoid potentially
368+
// leaking information
369+
klog.Warningf("failed to set patch annotation for mutating webhook key %s; confugiration name: %s, webhook name: %s", w.patchAnnotationKey, w.configuration, w.webhook)
370+
}
371+
}
372+
373+
// MutationAuditAnnotation logs if a webhook invocation mutated the request object
374+
type MutationAuditAnnotation struct {
375+
Configuration string `json:"configuration"`
376+
Webhook string `json:"webhook"`
377+
Mutated bool `json:"mutated"`
378+
}
379+
380+
// PatchAuditAnnotation logs a patch from a mutating webhook
381+
type PatchAuditAnnotation struct {
382+
Configuration string `json:"configuration"`
383+
Webhook string `json:"webhook"`
384+
Patch interface{} `json:"patch,omitempty"`
385+
PatchType string `json:"patchType,omitempty"`
386+
}
387+
388+
func mutationAnnotationValue(configuration, webhook string, mutated bool) (string, error) {
389+
m := MutationAuditAnnotation{
390+
Configuration: configuration,
391+
Webhook: webhook,
392+
Mutated: mutated,
393+
}
394+
bytes, err := encodingjson.Marshal(m)
395+
return string(bytes), err
396+
}
397+
398+
func jsonPatchAnnotationValue(configuration, webhook string, patch interface{}) (string, error) {
399+
p := PatchAuditAnnotation{
400+
Configuration: configuration,
401+
Webhook: webhook,
402+
Patch: patch,
403+
PatchType: string(admissionv1.PatchTypeJSONPatch),
404+
}
405+
bytes, err := encodingjson.Marshal(p)
406+
return string(bytes), err
407+
}

0 commit comments

Comments
 (0)