Skip to content

Commit

Permalink
Add audit filter that will be able to catch authn failures
Browse files Browse the repository at this point in the history
  • Loading branch information
soltysh committed Aug 14, 2017
1 parent 0ef1289 commit 7442b22
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 1 deletion.
202 changes: 202 additions & 0 deletions pkg/cmd/server/origin/audit.go
Original file line number Diff line number Diff line change
@@ -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 = "<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 "<bearer>"
}

// other tokens
token := strings.TrimSpace(req.URL.Query().Get("access_token"))
if len(token) > 0 {
return "<token>"
}

// cert authn
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
return "<x509>"
}

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{}
8 changes: 7 additions & 1 deletion pkg/cmd/server/origin/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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)
}
Expand Down

0 comments on commit 7442b22

Please sign in to comment.