-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add audit filter that will be able to catch authn failures
- Loading branch information
Showing
2 changed files
with
209 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters