diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/ddog.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/ddog.go new file mode 100644 index 0000000000000..b74a5b990d6cc --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/ddog.go @@ -0,0 +1,47 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filters + +import ( + "crypto/tls" + "k8s.io/apiserver/pkg/audit" + "net/http" +) + +// WithDDOGAudits adds additional information about the request to the audit logs. +// This is useful for debugging and troubleshooting. +// TLS Cipher for fips +// Content-Type of the response to track JSON vs protobuf +func WithDDOGAudits(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + // Set the content type annotation if it is set + if _, ok := w.Header()["Content-Type"]; ok { + audit.AddAuditAnnotation(ctx, "audit.datadoghq.com/contentType", w.Header().Get("Content-Type")) + } + + // Set the TLS Cipher annotation if TLS and CipherSuite are set + if req.TLS != nil { + if req.TLS.CipherSuite > 0 { + audit.AddAuditAnnotation(ctx, "audit.datadoghq.com/cipher", tls.CipherSuiteName(req.TLS.CipherSuite)) + } + } + + handler.ServeHTTP(w, req) + }) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/ddog_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/ddog_test.go new file mode 100644 index 0000000000000..3a513a905b7d0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/ddog_test.go @@ -0,0 +1,130 @@ +package filters + +import ( + "crypto/tls" + "crypto/x509" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit/policy" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "net/http" + "net/http/httptest" + "testing" +) + +func TestWithDDOGAudits(t *testing.T) { + handler := func(http.ResponseWriter, *http.Request) {} + shortRunningPath := "/api/v1/namespaces/default/pods/foo" + + for _, test := range []struct { + desc string + tlsCipher uint16 + contentType string + expected []auditinternal.Event + }{ + { + "TLS Cipher set & Content Type set", + tls.TLS_RSA_WITH_RC4_128_SHA, + "application/json", + []auditinternal.Event{ + { + Stage: auditinternal.StageResponseComplete, + Verb: "get", + RequestURI: shortRunningPath, + ResponseStatus: &metav1.Status{Code: 200}, + Annotations: map[string]string{ + "audit.datadoghq.com/cipher": "TLS_RSA_WITH_RC4_128_SHA", + "audit.datadoghq.com/contentType": "application/json", + }, + }, + }, + }, { + "TLS Cipher set & Content Type unset", + tls.TLS_RSA_WITH_RC4_128_SHA, + "", + []auditinternal.Event{ + { + Stage: auditinternal.StageResponseComplete, + Verb: "get", + RequestURI: shortRunningPath, + ResponseStatus: &metav1.Status{Code: 200}, + Annotations: map[string]string{ + "audit.datadoghq.com/cipher": "TLS_RSA_WITH_RC4_128_SHA", + }, + }, + }, + }, { + "TLS Cipher unset & Content Type unset", + 0, + "", + []auditinternal.Event{ + { + Stage: auditinternal.StageResponseComplete, + Verb: "get", + RequestURI: shortRunningPath, + ResponseStatus: &metav1.Status{Code: 200}, + }, + }, + }, { + "TLS Cipher unset & Content Type set", + 0, + "application/json", + []auditinternal.Event{ + { + Stage: auditinternal.StageResponseComplete, + Verb: "get", + RequestURI: shortRunningPath, + ResponseStatus: &metav1.Status{Code: 200}, + Annotations: map[string]string{ + "audit.datadoghq.com/contentType": "application/json", + }, + }, + }, + }, + } { + t.Run(test.desc, func(t *testing.T) { + sink := &fakeAuditSink{} + fakeRuleEvaluator := policy.NewFakePolicyRuleEvaluator(auditinternal.LevelRequestResponse, []auditinternal.Stage{auditinternal.StageRequestReceived, auditinternal.StageResponseStarted}) + handler := WithAudit(http.HandlerFunc(handler), sink, fakeRuleEvaluator, func(r *http.Request, ri *request.RequestInfo) bool { return true }) + handler = WithAuditInit(handler) + handler = WithDDOGAudits(handler) + + req, _ := http.NewRequest("GET", shortRunningPath, nil) + req.RemoteAddr = "127.0.0.1" + req = withTestContext(req, &user.DefaultInfo{Name: "admin"}, nil) + res := httptest.NewRecorder() + + if test.tlsCipher > 0 { + req.TLS = &tls.ConnectionState{PeerCertificates: []*x509.Certificate{{}}, CipherSuite: test.tlsCipher} + } + if test.contentType != "" { + res.Header().Set("Content-Type", test.contentType) + } + + func() { + defer func() { + recover() + }() + handler.ServeHTTP(res, req) + }() + + events := sink.Events() + t.Logf("audit log: %v", events) + if len(events) != len(test.expected) { + t.Fatalf("Unexpected amount of lines in audit log: %d", len(events)) + } + for i, _ := range test.expected { + event := events[i] + if len(event.Annotations) != len(test.expected[i].Annotations) { + t.Errorf("[%s] expected %d annotations, got %d", test.desc, len(test.expected[i].Annotations), len(event.Annotations)) + } + for y, _ := range event.Annotations { + if test.expected[i].Annotations[y] != event.Annotations[y] { + t.Errorf("[%s] expected annotation %s to be %s, got %s", test.desc, y, test.expected[i].Annotations[y], event.Annotations[y]) + } + } + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index a326e7335ae78..5dc9dfd3fb10d 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -1069,6 +1069,7 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler { handler = genericapifilters.WithMuxAndDiscoveryComplete(handler, c.lifecycleSignals.MuxAndDiscoveryComplete.Signaled()) handler = genericfilters.WithPanicRecovery(handler, c.RequestInfoResolver) handler = genericapifilters.WithAuditInit(handler) + handler = genericapifilters.WithDDOGAudits(handler) return handler }