-
Notifications
You must be signed in to change notification settings - Fork 40k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow audit to log authorization failures #51119
Conversation
The only remaining part is tests for the new bits. |
} | ||
|
||
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/upstream//
ev.ResponseStatus = &metav1.Status{} | ||
ev.ResponseStatus.Code = int32(code) | ||
ev.Stage = auditinternal.StageResponseStarted | ||
processEvent(a.sink, ev) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as we are in another file now, maybe rename this to processAuditEvent
b20b905
to
2c8c5c0
Compare
@sttts I've added tests and run bazel, ptal |
@destijl Could you please take a look? /cc @tallclair |
lgtm. But two more eyes on it wouldn't harm. |
if err != nil { | ||
return basic | ||
} | ||
cred := strings.SplitN(string(str), ":", 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to give an unauthenticated attacker complete control over the username field? Potential XSS vector if downstream consumers are displaying the contents?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This piece of code is taken directly from basic auth, so the same problem applies there. If I'm able to address other comment of yours, this should not be a problem anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/cc @liggitt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any user can fake a user-name and this name is extracted here to be added to the audit event on authn failure. This by itself is expected and intended. But I agree that allowing any kind of binary data might open up an attack vector on buggy consumers. What is best practice here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same vector applies to resource name parsed from the url, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For resource names at least the charset is restricted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For resource names at least the charset is restricted.
not by request parsing, the restrictions are per resource
} | ||
|
||
// getUsername returns username or information on the authn method being used. | ||
func getUsername(req *http.Request) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AIUI we get an authentication failure, then handover the failed request to this code to try and extract some context from the request. Is there something stopping us logging a more meaningful failure at the actual authentication failure time? Seems like the point where we determine the password/cert/etc is bad is the place where we should create the log.
This hard-codes some assumptions about the auth methods we accept which would seem to better belong in a single library (or libraries, one for each method) so that we can be careful about how we handle this untrusted input and reason about code changes needed to deprecate authentication methods if necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm... I'm not a big fan of it either, but it was the first thing I could think of. I'll dig into the code once more and see how we can do a better job here.
|
||
// check basic auth | ||
const basicScheme string = "Basic " | ||
if strings.HasPrefix(auth, basicScheme) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
// other tokens | ||
token := strings.TrimSpace(req.URL.Query().Get("access_token")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this, this method is not supported
if len(cred) < 2 { | ||
return basic | ||
} | ||
return cred[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that putting unverified data about users in audit logs is fraught
2c8c5c0
to
ca8fb73
Compare
} | ||
|
||
// if there already exists an audit event in the context we don't need to do anything | ||
if ae := request.AuditEventFrom(ctx); ae != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left this check here, but it shouldn't be needed, since we hook in failed authn specifically.
@liggitt @sttts @destijl I've addressed your concerns, can I get a quick review on this one? |
} | ||
|
||
func (m *fakeRequestContextMapper) Get(req *http.Request) (request.Context, bool) { | ||
ctx := request.NewContext() | ||
if m.ctx == nil { | ||
m.ctx = request.NewContext() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes fakeRequestContextMapper non-thread safe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this is just test, for that particular usecase. I doubt fakeRequestContextMapper
will be use anywhere else. Same for the next call, as well.
} | ||
|
||
resolver := newTestRequestInfoResolver() | ||
info, err := resolver.NewRequestInfo(req) | ||
if err == nil { | ||
ctx = request.WithRequestInfo(ctx, info) | ||
m.ctx = request.WithRequestInfo(m.ctx, info) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and this makes it not safe to be called multiple times
@@ -63,6 +63,9 @@ func WithAuthentication(handler http.Handler, mapper genericapirequest.RequestCo | |||
if err != nil { | |||
glog.Errorf("Unable to authenticate the request due to an error: %v", err) | |||
} | |||
if auditFailedAuthn != nil { | |||
w = auditFailedAuthn.Decorate(w, user, req) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are you passing user? when err != nil || !ok
, user
is unusable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and if you don't need user, then auditFailedAuthn.Decorate
has the same args as http.Handler
, and you can simple pass a wrapped failed http.Handler in here that logs, then handles
8310c21
to
c2443ab
Compare
@@ -48,37 +48,17 @@ func WithAudit(handler http.Handler, requestContextMapper request.RequestContext | |||
return | |||
} | |||
|
|||
attribs, err := GetAuthorizerAttributes(ctx) | |||
ev, err := createAuditEvent(ctx, policy, requestContextMapper, req, w) | |||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this still lead to http 500?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ic, createAuditEvent writes the error. I would prefer to see it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or at least a good comment here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll move the error handling up, it'll be more explicit.
return nil, err | ||
} | ||
|
||
ctx = request.WithAuditEvent(ctx, ev) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is unexpected in a create*
function as a side-effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
either move that our or call the func createAuditEventAndAttachToCtx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved context extraction also to that method and went with createAuditEventAndAttachToContext
.
return | ||
} | ||
|
||
ev, err := createAuditEvent(ctx, policy, requestContextMapper, req, w) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this attaches an event to the context. The WithAudit
filter will override that. Correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above.
ev.ResponseStatus.Message = getAuthMethods(req) | ||
ev.Stage = auditinternal.StageResponseStarted | ||
|
||
rw := decorateResponseWriter(w, ev, sink) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes the rw.WriteHeader to process an event. The WithAudit
also decorates. So if an endpoint handler writes the header, the WithAudit
decorator will send an event to the audit backend, then your new outer decorator will the same. Moreover, you ev
here is another one than the one of WithAudit
. So yours will be pretty empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No double decoration will happen. he reason for that is that WithFailedAuthenticationAudit
decorates only the failed handler that is passed to WithAuthentication
. At that point no request will ever reach regular WithAudit
because the processing is passed through a different handler chain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it. Please call the parameter failedHandler
here. Then it's clearer.
c2443ab
to
4e5f11f
Compare
|
||
// WithFailedAuthenticationAudit 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't find "WithAuditTriggeredMarker" in other places.
Maybe this doc string is out of date?
auditinternal "k8s.io/apiserver/pkg/apis/audit" | ||
"k8s.io/apiserver/pkg/audit/policy" | ||
// import to call webhook's init() function to register audit.Event to schema | ||
_ "k8s.io/apiserver/plugin/pkg/audit/webhook" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tested that it's not necessary to import this in this unit test.
4e5f11f
to
6e0c511
Compare
Updated with addressed comments from @CaoShuFeng |
lgtm after the |
6e0c511
to
9fef244
Compare
/approve no-issue |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: soltysh, sttts Associated issue requirement bypassed by: sttts The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these OWNERS Files:
You can indicate your approval by writing |
/retest |
@soltysh: The following test failed, say
Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here. |
/retest Review the full test history for this PR. |
Automatic merge from submit-queue (batch tested with PRs 50832, 51119, 51636, 48921, 51712) |
|
||
ev.ResponseStatus = &metav1.Status{} | ||
ev.ResponseStatus.Message = getAuthMethods(req) | ||
ev.Stage = auditinternal.StageResponseStarted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am OK with auditinternal.StageResponseStarted
here.
But why StageResponseStarted
is better than StageRequestReceived
?
What this PR does / why we need it:
This PR extends our current audit mechanism allowing to audit failed login attempts.
Release note: