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 Jun 16, 2017
1 parent a68db68 commit a008a21
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 1 deletion.
179 changes: 179 additions & 0 deletions pkg/cmd/server/origin/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package origin

import (
"bufio"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"

"github.com/golang/glog"
"github.com/pborman/uuid"

utilnet "k8s.io/apimachinery/pkg/util/net"
apiresponsewriters "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
apirequest "k8s.io/apiserver/pkg/endpoints/request"
)

const AuditTriggered = "audit-triggered"

var _ http.ResponseWriter = &auditResponseWriter{}

// auditResponseWriter is responsible for serving as a fallback audit, responsible
// for logging failed authn events.
type auditResponseWriter struct {
http.ResponseWriter
contextMapper apirequest.RequestContextMapper
req *http.Request
out io.Writer
}

func (a *auditResponseWriter) WriteHeader(code int) {
ctx, ok := a.contextMapper.Get(a.req)
if !ok {
apiresponsewriters.InternalError(a.ResponseWriter, a.req, errors.New("no context found for request"))
return
}
// if the original audit handler triggered there's no need to do anything
triggeredValue := ctx.Value(AuditTriggered)
if triggered, ok := triggeredValue.(bool); ok && triggered {
a.ResponseWriter.WriteHeader(code)
return
}
id := uuid.NewRandom().String()
line := fmt.Sprintf("%s AUDIT: id=%q ip=%q method=%q user=%q uri=%q\n",
time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(a.req), a.req.Method, getUsername(a.req), a.req.URL)
if _, err := fmt.Fprint(a.out, line); err != nil {
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err)
}
line = fmt.Sprintf("%s AUDIT: id=%q response=\"%d\"\n", time.Now().Format(time.RFC3339Nano), id, code)
if _, err := fmt.Fprint(a.out, line); err != nil {
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err)
}

a.ResponseWriter.WriteHeader(code)
}

// 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{}

// WithAuditTriggeredMarker is responsible for marking that the audit did actually
// took place and fallback audit should not trigger.
func WithAuditTriggeredMarker(handler http.Handler, contextMapper apirequest.RequestContextMapper, out io.Writer) http.Handler {
if out == nil {
return handler
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx, ok := contextMapper.Get(req)
if !ok {
apiresponsewriters.InternalError(w, req, errors.New("no context found for request"))
return
}
contextMapper.Update(req, apirequest.WithValue(ctx, AuditTriggered, true))
handler.ServeHTTP(w, req)
})
}

// 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, contextMapper apirequest.RequestContextMapper, out io.Writer) http.Handler {
if out == nil {
return handler
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
respWriter := decorateResponseWriter(w, req, contextMapper, out)
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, req *http.Request,
contextMapper apirequest.RequestContextMapper, out io.Writer) http.ResponseWriter {
delegate := &auditResponseWriter{
ResponseWriter: responseWriter,
req: req,
contextMapper: contextMapper,
out: out,
}
// 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
}

// getUsername returns username or information on the authn method being used.
func getUsername(req *http.Request) string {
auth := strings.TrimSpace(req.Header.Get("Authorization"))
if auth == "" {
return "<none>"
}

// check basic auth
const basicScheme string = "Basic "
if strings.HasPrefix(auth, basicScheme) {
const basicInvalid = "<basic_invalid>"
str, err := base64.StdEncoding.DecodeString(auth[len(basicScheme):])
if err != nil {
return basicInvalid
}

cred := strings.SplitN(string(str), ":", 2)
if len(cred) < 2 {
return basicInvalid
}

return cred[0]
}

// check bearer token
parts := strings.Split(auth, " ")
if len(parts) > 1 && strings.ToLower(parts[0]) == "bearer" {
token := parts[1]
// Empty bearer tokens aren't valid
if len(token) == 0 {
return "<bearer_invalid>"
}

return "<bearer>"
}

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

return "<unknown>"
}
8 changes: 7 additions & 1 deletion pkg/cmd/server/origin/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
handler = serverhandlers.ImpersonationFilter(handler, c.Authorizer, c.GroupCache, contextMapper)

// audit handler must comes before the impersonationFilter to read the original user
var writer io.Writer
if c.Options.AuditConfig.Enabled {
var writer io.Writer
if len(c.Options.AuditConfig.AuditFilePath) > 0 {
writer = &lumberjack.Logger{
Filename: c.Options.AuditConfig.AuditFilePath,
Expand All @@ -200,7 +200,9 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
writer = cmdutil.NewGLogWriterV(0)
}
handler = apifilters.WithAudit(handler, contextMapper, writer)
handler = WithAuditTriggeredMarker(handler, contextMapper, writer)
}

handler = serverhandlers.AuthenticationHandlerFilter(handler, c.Authenticator, contextMapper)
handler = namespacingFilter(handler, contextMapper)
handler = cacheControlFilter(handler, "no-store") // protected endpoints should not be cached
Expand All @@ -216,6 +218,10 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
}
}

if c.Options.AuditConfig.Enabled {
handler = WithAuthFallbackAudit(handler, contextMapper, writer)
}

handler, err := assetConfig.WithAssets(handler)
if err != nil {
glog.Fatalf("Failed to setup serving of assets: %v", err)
Expand Down

0 comments on commit a008a21

Please sign in to comment.