-
Notifications
You must be signed in to change notification settings - Fork 45
authentication skeleton #314
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
Conversation
This is great. Really urgently needed. Still going through it in detail. |
}, | ||
} | ||
} else { | ||
SchemeResult::NotRequested |
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.
Trying to understand NotRequested
. Here it's being returned when there is no header at all. In a real world case that might be something like: you can't fail HTTP auth if there's no auth header. Is that right? But whether that's a failure might be contextual. If an endpoint expect auth to happen by that method, then that header being missing should be a failure, right? Maybe that's not a thing we need.
The name makes me also think about calling code trying to skip a scheme, for example if a given endpoint is only allowed to auth by certain schemes. In that case it would be the endpoint that would "not request" auth by this scheme. Is that a possibility with the current setup? From what I can tell that would require different endpoints having their own Authenticator
, since that's where the list of allowed schemes is collected.
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.
Trying to understand NotRequested. Here it's being returned when there is no header at all. In a real world case that might be something like: you can't fail HTTP auth if there's no auth header. Is that right? But whether that's a failure might be contextual. If an endpoint expect auth to happen by that method, then that header being missing should be a failure, right? Maybe that's not a thing we need.
My assumption is that an endpoint doesn't really care how you're authenticated -- it cares about what privileges you have (authorization). Basically, the way I expect this will work is: endpoints will use the authn system to get back "Authenticated" or "Unauthenticated", then they will do privilege checks like "can this Actor read information about this Project?" And for unauthenticated contexts, the answer will usually be "no". But there are some operations that might still succeed. This might not be that important for us but I could see allowing unauthenticated users to list the supported versions of the API, for example, or maybe fetch metadata about publicly-available images.
The name makes me also think about calling code trying to skip a scheme, for example if a given endpoint is only allowed to auth by certain schemes. In that case it would be the endpoint that would "not request" auth by this scheme. Is that a possibility with the current setup? From what I can tell that would require different endpoints having their own Authenticator, since that's where the list of allowed schemes is collected.
That's right. Like I said I was assuming endpoints don't care how you're authenticated. NotRequested
is never exposed outside the little authn subsystem -- it's only if all schemes return that, then we return Unauthenticated.
Is there a use case you're thinking of where the endpoint would care how you authenticated?
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.
The only thing that comes to mind would be an endpoint that needs a particular auth scheme from the client so it can make another request using that same auth. But in that case
a) the allowed schemes would have to consist of only that scheme
b) it might make more sense to put the auth info in the request payload if the request needs to use it
c) rather than pulling the needed auth info from the request, once the user is authenticated, the endpoint could pull the needed info from somewhere else
But most importantly no, I can’t think of a concrete use case for this.
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.
The thought I was meandering toward was this: if the logic that produces NotRequested
is part of the scheme, then if you want to use the same general “scheme” in two places with different NotRequested
conditions, then you’d have to make two Scheme
impls (maybe with some shared code). But that’s perfectly fine.
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.
@davepacheco , it's great to see you start tackling this! It all looks very reasonable to me. I expect we can iterate as needed without much problem given how generic this is.
/// Client successfully authenticated | ||
Authenticated(Details), | ||
/// Client did not attempt to authenticate | ||
Unauthenticated, |
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.
Is it not useful to provide the Actor for an unauthenticated request as well? I'm thinking of things like logging which actor fails the most. This could be an attacker going after a specific actor for some reason.
Or am I just off base, because failed requests wouldn't know the actor?
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.
Yeah, if the requested is unauthenticated, then the user didn't even try, so there is no actor.
If the user attempts to authenticate and fails, that produces an authn::Error rather than an authn::Context. That could include the actor for the variants of authn::Reason
that have an actor (which won't be all of them) and we could log that.
Of course, there are other ways to model this: you could get back an Unauthenticated context for this rather than an Error, but I thought that would make it way too easy to accidentally proceed with a request that failed authn.
// TODO-security Does this leak too much information, to say that | ||
// the header itself was malformed? It doesn't feel like it, and as | ||
// a user it's _really_ helpful to know if you've just, like, | ||
// encoded it wrong. |
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.
Take this with a grain of salt, as I could be completely wrong here with regards to security analysis.
If the format is already well-defined because callers must know how to make proper requests, then there is no problem as the information is already public. In this case the only time the format will be off is a programmer error, in which case we want to return this so the user can correct their issues, or an automated attack script that is trying to find the right format. If our system is actually secure the latter should not be a problem, since getting the format correct doesn't trigger authentication.
Now, with that being said, there's possibly some side channel (timing?) exposed from this, but I'm skeptical it's a real problem. By this point you are already returning an error, so any additional information would have to match to something discernable in the side channel output. As an example, if a different format error for some reason took longer to reply with then another this could be an attack vector.
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.
That's my take as well (that this information is already public -- we're not leaking any new information about the implementation).
I figured I'd leave this in because at some point I expect we'll do a deeper security audit and we'll want to look more carefully at this.
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 looks like a great start to me. FWIW I think the security TODOs have appropriate current states and could be changed later as we learn of further requirements
/// The credentials were syntactically valid, but semantically invalid | ||
/// (e.g., a cryptographic signature did not match) | ||
#[error("bad credentials: {source:#}")] | ||
BadCredentials { |
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.
Should we include what actor this failure was for? This seems useful for internal logs, though I guess we'll want to think through what a system for tracking authentication failures would look like for us
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 think that's a good idea. @andrewjstone alluded to this, too.
I'm not sure if we'll always have this, but we do today for "spoof" and we can always make it an Option
later.
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.
Fixed in 98e72c0.
nexus/src/authn/external/spoof.rs
Outdated
@@ -0,0 +1,156 @@ | |||
//! Implements a custom, test-only authn scheme that blindly trusts the client |
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.
Perhaps "scheme that trusts the client without verification" to avoid the idiom
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.
Fixed in 98e72c0.
|
||
let server_state = { | ||
let authn = omicron_nexus::authn::external::Authenticator::new( | ||
authn_schemes_configured, |
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.
If I understand correctly, the authenticator with its set of authn schemes is configured at the Dropshot server level. This means that all the endpoints in a given server support the same authn schemes, and conversely if we wanted two sets of endpoints to support different schemes we would have to put them in different servers.
I wonder whether it would be worth making it easier to specify sets of allowed schemes per endpoint. For example, if there are a couple of endpoints that are really only intended for the console, we might only want to support cookie auth (or whatever) on those.
Or maybe that's better handled by putting those endpoints in their own server, as the current implementation requires. One advantage is that it makes it pretty hard to proliferate lists of auth schemes. Ideally there are as few lists in operation as possible.
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.
Well, the new authenticator appears in the server-wide context, that's true. But there's no reason that all endpoints have to use that authenticator. (Today, none of them does.) We can always add a second authenticator in the server-wide context and use that from the console-only endpoints. I'm not sure if that's what we want, but I think that's a "later" question because we don't have any console-only routes yet, 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.
Oh yeah, I see. The use of the authenticator is an explicit call in each handler. And yes, there are no console specific routes yet.
) -> SchemeResult { | ||
let headers = request.headers(); | ||
authn_spoof(headers.get(HTTP_HEADER_OXIDE_AUTHN_SPOOF)) | ||
} |
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.
looks like a cookie auth scheme would look almost the same
let headers = request.headers();
// split on ';' and call `Cookie::parse` from the cookie crate on each chunk
let cookies = parse_cookies(headers.get("Cookie"));
authn_cookie(cookies.get(COOKIE_NAME_OXIDE_AUTHN_COOKIE))
the biggest difference being that in authn_cookie
you have to look up the cookie value in a sessions table that associates session with actors.
Thanks @david-crespo @teisenbe @andrewjstone for taking a look! I think I've addressed everything. |
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.
Happy to see this merged. I want to start using it to experiment with console auth.
In preparation for prototyping authorization, I found I needed some basic kind of authentication in Nexus. This change includes a small (albeit overengineered) authentication subsystem for this. It doesn't support any useful authentication yet, but it has enough to test itself and to get started with authorization.
This change is kind of big. I'm happy to do an interactive walk-through if that's helpful.
Here's my long-term thinking on this:
This change only gets us started. It includes:
authn::Context
), authentication errors, and related typesauthn::external::Authenticator
) that's pluggable using a pretty simple trait (authn::external::HttpAuthnScheme
). (I'm using the word "scheme" as a sort of generalization of HTTP authentication schemes. A "scheme" is any way of producing an authentication context, usually by looking at an incoming request and verifying some credentials.)This change does not do a whole bunch of other things we'll eventually need to do: