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 14, 2017
1 parent bb745a7 commit c22f456
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 2 deletions.
132 changes: 132 additions & 0 deletions pkg/cmd/server/origin/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package origin

import (
"bufio"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"

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

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

const AUDIT_TRIGGERED = "audittriggered"

var _ http.ResponseWriter = &auditResponseWriter{}

type auditResponseWriter struct {
http.ResponseWriter
requestContextMapper apirequest.RequestContextMapper
req *http.Request
out io.Writer
}

func (a *auditResponseWriter) WriteHeader(code int) {
ctx, ok := a.requestContextMapper.Get(a.req)
if !ok {
glog.Errorf("Unable to get RequestContextMapper and read context data!")
return
}
// if the original audit handler triggered there's no need to do anything
triggeredValue := ctx.Value(AUDIT_TRIGGERED)
if triggered, ok := triggeredValue.(bool); ok && triggered {
return
}
// else we log simulating original audit lines
id := uuid.NewRandom().String()
line := fmt.Sprintf("%s AUDIT: id=%q ip=%q method=%q user=<???> uri=%q\n",
time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(a.req), a.req.Method, 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{}

// withAuditWrapper decorates a http.Handler with audit logging information for all the
// requests coming to the server. If out is nil, no decoration takes place.
func withAuditWrapper(handler http.Handler, requestContextMapper apirequest.RequestContextMapper, out io.Writer) http.Handler {
if out == nil {
return handler
}
// enable the original audit handler
handler = apifilters.WithAudit(handler, requestContextMapper, out)
// and wrap it with the piece of code responsible for injecting flag into the
// context which will tell the AuthnAudit handler if he should or not log
// information about failed Authn operation.
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx, ok := requestContextMapper.Get(req)
if !ok {
apiresponsewriters.InternalError(w, req, errors.New("no context found for request"))
return
}
apirequest.WithValue(ctx, AUDIT_TRIGGERED, true)
handler.ServeHTTP(w, req)
})
}

// withAuthnAudit decorates a http.Handler with audit logging ONLY information
// related to failed Authn requests.
func withAuthnAudit(handler http.Handler, requestContextMapper 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, requestContextMapper, out)
handler.ServeHTTP(respWriter, req)
})
}

func decorateResponseWriter(responseWriter http.ResponseWriter, req *http.Request,
requestContextMapper apirequest.RequestContextMapper, out io.Writer) http.ResponseWriter {
delegate := &auditResponseWriter{
ResponseWriter: responseWriter,
req: req,
requestContextMapper: requestContextMapper,
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
}
9 changes: 7 additions & 2 deletions 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 @@ -199,8 +199,9 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
// backwards compatible writer to regular log
writer = cmdutil.NewGLogWriterV(0)
}
handler = apifilters.WithAudit(handler, contextMapper, writer)
handler = withAuditWrapper(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 +217,10 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
}
}

if c.Options.AuditConfig.Enabled {
handler = withAuthnAudit(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 c22f456

Please sign in to comment.