Skip to content

Commit 42e6065

Browse files
committed
Add audit filter that will be able to catch authn failures
1 parent bb745a7 commit 42e6065

File tree

3 files changed

+167
-6
lines changed

3 files changed

+167
-6
lines changed

Diff for: pkg/cmd/server/handlers/authentication.go

+14-5
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@ import (
99
apirequest "k8s.io/apiserver/pkg/endpoints/request"
1010
)
1111

12+
const (
13+
AuditAuthnUser = "audit-authn-user"
14+
AuditAuthnError = "audit-authn-error"
15+
)
16+
1217
// AuthenticationHandlerFilter creates a filter object that will enforce authentication directly
1318
func AuthenticationHandlerFilter(handler http.Handler, authenticator authenticator.Request, contextMapper apirequest.RequestContextMapper) http.Handler {
1419
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
20+
ctx, ok := contextMapper.Get(req)
21+
if !ok {
22+
http.Error(w, "Unable to find request context", http.StatusInternalServerError)
23+
return
24+
}
25+
1526
user, ok, err := authenticator.AuthenticateRequest(req)
1627
if err != nil || !ok {
28+
ctx = apirequest.WithValue(ctx, AuditAuthnUser, user)
29+
ctx = apirequest.WithValue(ctx, AuditAuthnError, err)
30+
contextMapper.Update(req, ctx)
1731
http.Error(w, "Unauthorized", http.StatusUnauthorized)
1832
return
1933
}
2034

21-
ctx, ok := contextMapper.Get(req)
22-
if !ok {
23-
http.Error(w, "Unable to find request context", http.StatusInternalServerError)
24-
return
25-
}
2635
if err := contextMapper.Update(req, apirequest.WithUser(ctx, user)); err != nil {
2736
glog.V(4).Infof("Error setting authenticated context: %v", err)
2837
http.Error(w, "Unable to set authenticated request context", http.StatusInternalServerError)

Diff for: pkg/cmd/server/origin/audit.go

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package origin
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"net"
9+
"net/http"
10+
"time"
11+
12+
"github.com/golang/glog"
13+
"github.com/pborman/uuid"
14+
15+
utilnet "k8s.io/apimachinery/pkg/util/net"
16+
"k8s.io/apiserver/pkg/authentication/user"
17+
apiresponsewriters "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
18+
apirequest "k8s.io/apiserver/pkg/endpoints/request"
19+
20+
serverhandlers "github.com/openshift/origin/pkg/cmd/server/handlers"
21+
)
22+
23+
const AuditTriggered = "audit-triggered"
24+
25+
var _ http.ResponseWriter = &auditResponseWriter{}
26+
27+
// auditResponseWriter is responsible for serving as a fallback audit, responsible
28+
// for logging failed authn events.
29+
type auditResponseWriter struct {
30+
http.ResponseWriter
31+
contextMapper apirequest.RequestContextMapper
32+
req *http.Request
33+
out io.Writer
34+
}
35+
36+
func (a *auditResponseWriter) WriteHeader(code int) {
37+
ctx, ok := a.contextMapper.Get(a.req)
38+
if !ok {
39+
apiresponsewriters.InternalError(a.ResponseWriter, a.req, errors.New("no context found for request"))
40+
return
41+
}
42+
// if the original audit handler triggered there's no need to do anything
43+
triggeredValue := ctx.Value(AuditTriggered)
44+
if triggered, ok := triggeredValue.(bool); ok && triggered {
45+
a.ResponseWriter.WriteHeader(code)
46+
return
47+
}
48+
// else we log simulating original audit lines
49+
authnUserValue := ctx.Value(serverhandlers.AuditAuthnUser)
50+
username := "<none>"
51+
if userInfo, ok := authnUserValue.(user.Info); ok {
52+
username = userInfo.GetName()
53+
}
54+
authnErrorValue := ctx.Value(serverhandlers.AuditAuthnUser)
55+
authnError, ok := authnErrorValue.(error)
56+
if !ok {
57+
authnError = errors.New("<none>")
58+
}
59+
id := uuid.NewRandom().String()
60+
line := fmt.Sprintf("%s AUDIT: id=%q ip=%q method=%q user=%q error=%q uri=%q\n",
61+
time.Now().Format(time.RFC3339Nano), id, utilnet.GetClientIP(a.req), a.req.Method, username, authnError, a.req.URL)
62+
if _, err := fmt.Fprint(a.out, line); err != nil {
63+
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err)
64+
}
65+
line = fmt.Sprintf("%s AUDIT: id=%q response=\"%d\"\n", time.Now().Format(time.RFC3339Nano), id, code)
66+
if _, err := fmt.Fprint(a.out, line); err != nil {
67+
glog.Errorf("Unable to write audit log: %s, the error is: %v", line, err)
68+
}
69+
70+
a.ResponseWriter.WriteHeader(code)
71+
}
72+
73+
// fancyResponseWriterDelegator implements http.CloseNotifier, http.Flusher and
74+
// http.Hijacker which are needed to make certain http operation (e.g. watch, rsh, etc)
75+
// working.
76+
type fancyResponseWriterDelegator struct {
77+
*auditResponseWriter
78+
}
79+
80+
func (f *fancyResponseWriterDelegator) CloseNotify() <-chan bool {
81+
return f.ResponseWriter.(http.CloseNotifier).CloseNotify()
82+
}
83+
84+
func (f *fancyResponseWriterDelegator) Flush() {
85+
f.ResponseWriter.(http.Flusher).Flush()
86+
}
87+
88+
func (f *fancyResponseWriterDelegator) Hijack() (net.Conn, *bufio.ReadWriter, error) {
89+
return f.ResponseWriter.(http.Hijacker).Hijack()
90+
}
91+
92+
var _ http.CloseNotifier = &fancyResponseWriterDelegator{}
93+
var _ http.Flusher = &fancyResponseWriterDelegator{}
94+
var _ http.Hijacker = &fancyResponseWriterDelegator{}
95+
96+
// WithAuditTriggeredMarker is responsible for marking that the audit did actually
97+
// took place and fallback audit should not trigger.
98+
func WithAuditTriggeredMarker(handler http.Handler, contextMapper apirequest.RequestContextMapper, out io.Writer) http.Handler {
99+
if out == nil {
100+
return handler
101+
}
102+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
103+
ctx, ok := contextMapper.Get(req)
104+
if !ok {
105+
apiresponsewriters.InternalError(w, req, errors.New("no context found for request"))
106+
return
107+
}
108+
contextMapper.Update(req, apirequest.WithValue(ctx, AuditTriggered, true))
109+
handler.ServeHTTP(w, req)
110+
})
111+
}
112+
113+
// WithAuthFallbackAudit decorates a http.Handler with a fallback audit, logging
114+
// information only when the original one did was not triggered.
115+
// This needs to be used with WithAuditTriggeredMarker, which wraps the original
116+
// audit filter.
117+
func WithAuthFallbackAudit(handler http.Handler, contextMapper apirequest.RequestContextMapper, out io.Writer) http.Handler {
118+
if out == nil {
119+
return handler
120+
}
121+
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
122+
respWriter := decorateResponseWriter(w, req, contextMapper, out)
123+
handler.ServeHTTP(respWriter, req)
124+
})
125+
}
126+
127+
// decorateResponseWriter is a copy method from the upstream audit, adapted
128+
// to work with the fallback audit mechanism.
129+
func decorateResponseWriter(responseWriter http.ResponseWriter, req *http.Request,
130+
contextMapper apirequest.RequestContextMapper, out io.Writer) http.ResponseWriter {
131+
delegate := &auditResponseWriter{
132+
ResponseWriter: responseWriter,
133+
req: req,
134+
contextMapper: contextMapper,
135+
out: out,
136+
}
137+
// check if the ResponseWriter we're wrapping is the fancy one we need
138+
// or if the basic is sufficient
139+
_, cn := responseWriter.(http.CloseNotifier)
140+
_, fl := responseWriter.(http.Flusher)
141+
_, hj := responseWriter.(http.Hijacker)
142+
if cn && fl && hj {
143+
return &fancyResponseWriterDelegator{delegate}
144+
}
145+
return delegate
146+
}

Diff for: pkg/cmd/server/origin/master.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
186186
handler = serverhandlers.ImpersonationFilter(handler, c.Authorizer, c.GroupCache, contextMapper)
187187

188188
// audit handler must comes before the impersonationFilter to read the original user
189+
var writer io.Writer
189190
if c.Options.AuditConfig.Enabled {
190-
var writer io.Writer
191191
if len(c.Options.AuditConfig.AuditFilePath) > 0 {
192192
writer = &lumberjack.Logger{
193193
Filename: c.Options.AuditConfig.AuditFilePath,
@@ -200,7 +200,9 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
200200
writer = cmdutil.NewGLogWriterV(0)
201201
}
202202
handler = apifilters.WithAudit(handler, contextMapper, writer)
203+
handler = WithAuditTriggeredMarker(handler, contextMapper, writer)
203204
}
205+
204206
handler = serverhandlers.AuthenticationHandlerFilter(handler, c.Authenticator, contextMapper)
205207
handler = namespacingFilter(handler, contextMapper)
206208
handler = cacheControlFilter(handler, "no-store") // protected endpoints should not be cached
@@ -216,6 +218,10 @@ func (c *MasterConfig) buildHandlerChain(assetConfig *AssetConfig) (func(http.Ha
216218
}
217219
}
218220

221+
if c.Options.AuditConfig.Enabled {
222+
handler = WithAuthFallbackAudit(handler, contextMapper, writer)
223+
}
224+
219225
handler, err := assetConfig.WithAssets(handler)
220226
if err != nil {
221227
glog.Fatalf("Failed to setup serving of assets: %v", err)

0 commit comments

Comments
 (0)