diff --git a/pkg/cmd/server/origin/audit.go b/pkg/cmd/server/origin/audit.go new file mode 100644 index 000000000000..1ec7ffa5a99e --- /dev/null +++ b/pkg/cmd/server/origin/audit.go @@ -0,0 +1,202 @@ +package origin + +import ( + "bufio" + "encoding/base64" + "errors" + "fmt" + "net" + "net/http" + "strings" + "sync" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/audit/policy" + "k8s.io/apiserver/pkg/endpoints/filters" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/request" +) + +// WithAuthFallbackAudit decorates a http.Handler with a fallback audit, logging +// information only when the original one did was not triggered. +// This needs to be used with WithAuditTriggeredMarker, which wraps the original +// audit filter. +func WithAuthFallbackAudit(handler http.Handler, requestContextMapper request.RequestContextMapper, + sink audit.Sink, policy policy.Checker) http.Handler { + if sink == nil || policy == nil { + return handler + } + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + respWriter := decorateResponseWriter(w, getUsername(req), req, requestContextMapper, sink, policy) + handler.ServeHTTP(respWriter, req) + }) +} + +// decorateResponseWriter is a copy method from the upstream audit, adapted +// to work with the fallback audit mechanism. +func decorateResponseWriter(responseWriter http.ResponseWriter, username string, req *http.Request, + requestContextMapper request.RequestContextMapper, sink audit.Sink, policy policy.Checker) http.ResponseWriter { + delegate := &auditResponseWriter{ + ResponseWriter: responseWriter, + username: username, + request: req, + requestContextMapper: requestContextMapper, + sink: sink, + policy: policy, + } + // check if the ResponseWriter we're wrapping is the fancy one we need + // or if the basic is sufficient + _, cn := responseWriter.(http.CloseNotifier) + _, fl := responseWriter.(http.Flusher) + _, hj := responseWriter.(http.Hijacker) + if cn && fl && hj { + return &fancyResponseWriterDelegator{delegate} + } + return delegate +} + +var _ http.ResponseWriter = &auditResponseWriter{} + +// auditResponseWriter intercepts WriteHeader, sets it in the event. +type auditResponseWriter struct { + http.ResponseWriter + once sync.Once + username string + request *http.Request + requestContextMapper request.RequestContextMapper + sink audit.Sink + policy policy.Checker +} + +func (a *auditResponseWriter) Write(bs []byte) (int, error) { + a.processCode(http.StatusOK) // the Go library calls WriteHeader internally if no code was written yet. But this will go unnoticed for us + return a.ResponseWriter.Write(bs) +} + +func (a *auditResponseWriter) WriteHeader(code int) { + a.processCode(code) + a.ResponseWriter.WriteHeader(code) +} + +func (a *auditResponseWriter) processCode(code int) { + a.once.Do(func() { + ctx, ok := a.requestContextMapper.Get(a.request) + if !ok { + responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("no context found for request")) + return + } + + // if there already exists an audit event in the context we don't need to do anything + if ae := request.AuditEventFrom(ctx); ae != nil { + return + } + + // otherwise, we need to create the event by ourselves and log the auth error + // the majority of this code is copied from upstream WithAudit filter + attribs, err := filters.GetAuthorizerAttributes(ctx) + if err != nil { + utilruntime.HandleError(fmt.Errorf("failed to GetAuthorizerAttributes: %v", err)) + responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("failed to parse request")) + return + } + + level := a.policy.Level(attribs) + audit.ObservePolicyLevel(level) + if level == auditinternal.LevelNone { + // Don't audit. + return + } + + ev, err := audit.NewEventFromRequest(a.request, level, attribs) + if err != nil { + utilruntime.HandleError(fmt.Errorf("failed to complete audit event from request: %v", err)) + responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("failed to update context")) + return + } + + // since user is not set at this point, we need to read it manually + ev.User.Username = a.username + ctx = request.WithAuditEvent(ctx, ev) + if err := a.requestContextMapper.Update(a.request, ctx); err != nil { + utilruntime.HandleError(fmt.Errorf("failed to attach audit event to the context: %v", err)) + responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("failed to update context")) + return + } + + ev.ResponseStatus = &metav1.Status{} + ev.ResponseStatus.Code = int32(code) + ev.Stage = auditinternal.StageResponseStarted + processEvent(a.sink, ev) + }) +} + +// getUsername returns username or information on the authn method being used. +func getUsername(req *http.Request) string { + auth := strings.TrimSpace(req.Header.Get("Authorization")) + + // check basic auth + const basicScheme string = "Basic " + if strings.HasPrefix(auth, basicScheme) { + const basic = "" + str, err := base64.StdEncoding.DecodeString(auth[len(basicScheme):]) + if err != nil { + return basic + } + cred := strings.SplitN(string(str), ":", 2) + if len(cred) < 2 { + return basic + } + return cred[0] + } + + // check bearer token + parts := strings.Split(auth, " ") + if len(parts) > 1 && strings.ToLower(parts[0]) == "bearer" { + return "" + } + + // other tokens + token := strings.TrimSpace(req.URL.Query().Get("access_token")) + if len(token) > 0 { + return "" + } + + // cert authn + if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 { + return "" + } + + return "" +} + +// processEvent triggers save on an event and updates stats +func processEvent(sink audit.Sink, ev *auditinternal.Event) { + audit.ObserveEvent() + sink.ProcessEvents(ev) +} + +// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and +// http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc) +// working. +type fancyResponseWriterDelegator struct { + *auditResponseWriter +} + +func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool { + return f.ResponseWriter.(http.CloseNotifier).CloseNotify() +} + +func (f *fancyResponseWriterDelegator) Flush() { + f.ResponseWriter.(http.Flusher).Flush() +} + +func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return f.ResponseWriter.(http.Hijacker).Hijack() +} + +var _ http.CloseNotifier = &fancyResponseWriterDelegator{} +var _ http.Flusher = &fancyResponseWriterDelegator{} +var _ http.Hijacker = &fancyResponseWriterDelegator{} diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 1ef7716125ea..7b431fe06b9b 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -286,6 +286,7 @@ func (c *MasterConfig) buildHandlerChain(assetServerHandler http.Handler) func(a handler = serverhandlers.ImpersonationFilter(handler, c.Authorizer, cache.NewGroupCache(c.UserInformers.User().InternalVersion().Groups()), contextMapper) // audit handler must comes before the impersonationFilter to read the original user + var auditPolicyChecker auditpolicy.Checker if c.Options.AuditConfig.Enabled { var writer io.Writer if len(c.Options.AuditConfig.AuditFilePath) > 0 { @@ -300,13 +301,14 @@ func (c *MasterConfig) buildHandlerChain(assetServerHandler http.Handler) func(a writer = cmdutil.NewGLogWriterV(0) } c.AuditBackend = auditlog.NewBackend(writer) - auditPolicyChecker := auditpolicy.NewChecker(&auditinternal.Policy{ + auditPolicyChecker = auditpolicy.NewChecker(&auditinternal.Policy{ // This is for backwards compatibility maintaining the old visibility, ie. just // raw overview of the requests comming in. Rules: []auditinternal.PolicyRule{{Level: auditinternal.LevelMetadata}}, }) handler = apifilters.WithAudit(handler, contextMapper, c.AuditBackend, auditPolicyChecker, kc.LongRunningFunc) } + handler = serverhandlers.AuthenticationHandlerFilter(handler, c.Authenticator, contextMapper) handler = namespacingFilter(handler, contextMapper) handler = cacheControlFilter(handler, "no-store") // protected endpoints should not be cached @@ -322,6 +324,10 @@ func (c *MasterConfig) buildHandlerChain(assetServerHandler http.Handler) func(a } } + if c.Options.AuditConfig.Enabled { + handler = WithAuthFallbackAudit(handler, contextMapper, c.AuditBackend, auditPolicyChecker) + } + if c.WebConsoleEnabled() { handler = WithAssetServerRedirect(handler, c.Options.AssetConfig.PublicURL) }