Skip to content

Commit b9872bc

Browse files
committed
Add audit filter that will be able to catch authn failures
1 parent 8203145 commit b9872bc

File tree

2 files changed

+209
-1
lines changed

2 files changed

+209
-1
lines changed

pkg/cmd/server/origin/audit.go

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package origin
2+
3+
import (
4+
"bufio"
5+
"encoding/base64"
6+
"errors"
7+
"fmt"
8+
"net"
9+
"net/http"
10+
"strings"
11+
"sync"
12+
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
15+
auditinternal "k8s.io/apiserver/pkg/apis/audit"
16+
"k8s.io/apiserver/pkg/audit"
17+
"k8s.io/apiserver/pkg/audit/policy"
18+
"k8s.io/apiserver/pkg/endpoints/filters"
19+
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
20+
"k8s.io/apiserver/pkg/endpoints/request"
21+
)
22+
23+
// WithAuthFallbackAudit decorates a http.Handler with a fallback audit, logging
24+
// information only when the original one did was not triggered.
25+
// This needs to be used with WithAuditTriggeredMarker, which wraps the original
26+
// audit filter.
27+
func WithAuthFallbackAudit(handler http.Handler, requestContextMapper request.RequestContextMapper,
28+
sink audit.Sink, policy policy.Checker) http.Handler {
29+
if sink == nil || policy == nil {
30+
return handler
31+
}
32+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
33+
respWriter := decorateResponseWriter(w, getUsername(req), req, requestContextMapper, sink, policy)
34+
handler.ServeHTTP(respWriter, req)
35+
})
36+
}
37+
38+
// decorateResponseWriter is a copy method from the upstream audit, adapted
39+
// to work with the fallback audit mechanism.
40+
func decorateResponseWriter(responseWriter http.ResponseWriter, username string, req *http.Request,
41+
requestContextMapper request.RequestContextMapper, sink audit.Sink, policy policy.Checker) http.ResponseWriter {
42+
delegate := &auditResponseWriter{
43+
ResponseWriter: responseWriter,
44+
username: username,
45+
request: req,
46+
requestContextMapper: requestContextMapper,
47+
sink: sink,
48+
policy: policy,
49+
}
50+
// check if the ResponseWriter we're wrapping is the fancy one we need
51+
// or if the basic is sufficient
52+
_, cn := responseWriter.(http.CloseNotifier)
53+
_, fl := responseWriter.(http.Flusher)
54+
_, hj := responseWriter.(http.Hijacker)
55+
if cn && fl && hj {
56+
return &fancyResponseWriterDelegator{delegate}
57+
}
58+
return delegate
59+
}
60+
61+
var _ http.ResponseWriter = &auditResponseWriter{}
62+
63+
// auditResponseWriter intercepts WriteHeader, sets it in the event.
64+
type auditResponseWriter struct {
65+
http.ResponseWriter
66+
once sync.Once
67+
username string
68+
request *http.Request
69+
requestContextMapper request.RequestContextMapper
70+
sink audit.Sink
71+
policy policy.Checker
72+
}
73+
74+
func (a *auditResponseWriter) Write(bs []byte) (int, error) {
75+
a.processCode(http.StatusOK) // the Go library calls WriteHeader internally if no code was written yet. But this will go unnoticed for us
76+
return a.ResponseWriter.Write(bs)
77+
}
78+
79+
func (a *auditResponseWriter) WriteHeader(code int) {
80+
a.processCode(code)
81+
a.ResponseWriter.WriteHeader(code)
82+
}
83+
84+
func (a *auditResponseWriter) processCode(code int) {
85+
a.once.Do(func() {
86+
ctx, ok := a.requestContextMapper.Get(a.request)
87+
if !ok {
88+
responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("no context found for request"))
89+
return
90+
}
91+
92+
// if there already exists an audit event in the context we don't need to do anything
93+
if ae := request.AuditEventFrom(ctx); ae != nil {
94+
return
95+
}
96+
97+
// otherwise, we need to create the event by ourselves and log the auth error
98+
// the majority of this code is copied from upstream WithAudit filter
99+
attribs, err := filters.GetAuthorizerAttributes(ctx)
100+
if err != nil {
101+
utilruntime.HandleError(fmt.Errorf("failed to GetAuthorizerAttributes: %v", err))
102+
responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("failed to parse request"))
103+
return
104+
}
105+
106+
level := a.policy.Level(attribs)
107+
audit.ObservePolicyLevel(level)
108+
if level == auditinternal.LevelNone {
109+
// Don't audit.
110+
return
111+
}
112+
113+
ev, err := audit.NewEventFromRequest(a.request, level, attribs)
114+
if err != nil {
115+
utilruntime.HandleError(fmt.Errorf("failed to complete audit event from request: %v", err))
116+
responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("failed to update context"))
117+
return
118+
}
119+
120+
// since user is not set at this point, we need to read it manually
121+
ev.User.Username = a.username
122+
ctx = request.WithAuditEvent(ctx, ev)
123+
if err := a.requestContextMapper.Update(a.request, ctx); err != nil {
124+
utilruntime.HandleError(fmt.Errorf("failed to attach audit event to the context: %v", err))
125+
responsewriters.InternalError(a.ResponseWriter, a.request, errors.New("failed to update context"))
126+
return
127+
}
128+
129+
ev.ResponseStatus = &metav1.Status{}
130+
ev.ResponseStatus.Code = int32(code)
131+
ev.Stage = auditinternal.StageResponseStarted
132+
processEvent(a.sink, ev)
133+
})
134+
}
135+
136+
// getUsername returns username or information on the authn method being used.
137+
func getUsername(req *http.Request) string {
138+
auth := strings.TrimSpace(req.Header.Get("Authorization"))
139+
140+
// check basic auth
141+
const basicScheme string = "Basic "
142+
if strings.HasPrefix(auth, basicScheme) {
143+
const basic = "<basic>"
144+
str, err := base64.StdEncoding.DecodeString(auth[len(basicScheme):])
145+
if err != nil {
146+
return basic
147+
}
148+
cred := strings.SplitN(string(str), ":", 2)
149+
if len(cred) < 2 {
150+
return basic
151+
}
152+
return cred[0]
153+
}
154+
155+
// check bearer token
156+
parts := strings.Split(auth, " ")
157+
if len(parts) > 1 && strings.ToLower(parts[0]) == "bearer" {
158+
return "<bearer>"
159+
}
160+
161+
// other tokens
162+
token := strings.TrimSpace(req.URL.Query().Get("access_token"))
163+
if len(token) > 0 {
164+
return "<token>"
165+
}
166+
167+
// cert authn
168+
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
169+
return "<x509>"
170+
}
171+
172+
return ""
173+
}
174+
175+
// processEvent triggers save on an event and updates stats
176+
func processEvent(sink audit.Sink, ev *auditinternal.Event) {
177+
audit.ObserveEvent()
178+
sink.ProcessEvents(ev)
179+
}
180+
181+
// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and
182+
// http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc)
183+
// working.
184+
type fancyResponseWriterDelegator struct {
185+
*auditResponseWriter
186+
}
187+
188+
func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool {
189+
return f.ResponseWriter.(http.CloseNotifier).CloseNotify()
190+
}
191+
192+
func (f *fancyResponseWriterDelegator) Flush() {
193+
f.ResponseWriter.(http.Flusher).Flush()
194+
}
195+
196+
func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) {
197+
return f.ResponseWriter.(http.Hijacker).Hijack()
198+
}
199+
200+
var _ http.CloseNotifier = &fancyResponseWriterDelegator{}
201+
var _ http.Flusher = &fancyResponseWriterDelegator{}
202+
var _ http.Hijacker = &fancyResponseWriterDelegator{}

pkg/cmd/server/origin/master.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ func (c *MasterConfig) buildHandlerChain(assetServerHandler http.Handler) func(a
285285
handler = serverhandlers.ImpersonationFilter(handler, c.Authorizer, cache.NewGroupCache(c.UserInformers.User().InternalVersion().Groups()), genericConfig.RequestContextMapper)
286286

287287
// audit handler must comes before the impersonationFilter to read the original user
288+
var auditPolicyChecker auditpolicy.Checker
288289
if c.Options.AuditConfig.Enabled {
289290
var writer io.Writer
290291
if len(c.Options.AuditConfig.AuditFilePath) > 0 {
@@ -299,13 +300,14 @@ func (c *MasterConfig) buildHandlerChain(assetServerHandler http.Handler) func(a
299300
writer = cmdutil.NewGLogWriterV(0)
300301
}
301302
c.AuditBackend = auditlog.NewBackend(writer)
302-
auditPolicyChecker := auditpolicy.NewChecker(&auditinternal.Policy{
303+
auditPolicyChecker = auditpolicy.NewChecker(&auditinternal.Policy{
303304
// This is for backwards compatibility maintaining the old visibility, ie. just
304305
// raw overview of the requests comming in.
305306
Rules: []auditinternal.PolicyRule{{Level: auditinternal.LevelMetadata}},
306307
})
307308
handler = apifilters.WithAudit(handler, genericConfig.RequestContextMapper, c.AuditBackend, auditPolicyChecker, genericConfig.LongRunningFunc)
308309
}
310+
309311
handler = serverhandlers.AuthenticationHandlerFilter(handler, c.Authenticator, genericConfig.RequestContextMapper)
310312
handler = namespacingFilter(handler, genericConfig.RequestContextMapper)
311313
handler = cacheControlFilter(handler, "no-store") // protected endpoints should not be cached
@@ -321,6 +323,10 @@ func (c *MasterConfig) buildHandlerChain(assetServerHandler http.Handler) func(a
321323
}
322324
}
323325

326+
if c.Options.AuditConfig.Enabled {
327+
handler = WithAuthFallbackAudit(handler, genericConfig.RequestContextMapper, c.AuditBackend, auditPolicyChecker)
328+
}
329+
324330
handler = apiserverfilters.WithCORS(handler, c.Options.CORSAllowedOrigins, nil, nil, nil, "true")
325331
handler = apiserverfilters.WithTimeoutForNonLongRunningRequests(handler, genericConfig.RequestContextMapper, genericConfig.LongRunningFunc)
326332
// TODO: MaxRequestsInFlight should be subdivided by intent, type of behavior, and speed of

0 commit comments

Comments
 (0)