Skip to content
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

server: add support for message authentication/authorization #46

Closed
wants to merge 2 commits into from

Conversation

rlbdv
Copy link
Contributor

@rlbdv rlbdv commented Sep 1, 2018

No description provided.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 1, 2018

I could easily imagine the current approach might not be acceptable, but I'd be happy to make adjustments. Please see the commit message for additional explanation and rationale.

I haven't added any tests yet, but "lein test" should still work, if a bit noisily, and the noise can be suppressed:

NREPL_SUPPRESS_DEFAULT_AUTH_WARNING=true lein test

In any case, assuming we can settle on an initial approach, I'll be happy to add tests and other desired enhancements.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 1, 2018

Oh, and I'd imagine before we're finished, this should include some updates to the manual.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 1, 2018

Oh, and in case it's not completely clear, the intention is that tools like lein might start specifying an auth-wrapper, i.e.

(start-server ... :auth-wrapper (file-auth-wrapper (slurp "./nrepl-auth-token"))

@bbatsov
Copy link
Contributor

bbatsov commented Sep 4, 2018

I'll add a bit of feedback inline and I'll invite @nrepl/nrepl members to share their opinions as well.

@@ -64,6 +64,31 @@
s))
% %))))

(defn no-auth-wrapper
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the purpose of this wrapper. I guess that if we don't want to use authorization we can simply pass nil or something along those lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming I understand, that'd be fine too. Were you saying that you just wanted to special case the "no auth" case at the point of use, i.e. put this code there guarded by a check whether the default-auth-wrapper is set, or are you saying that you don't want to warn at all when there's no protection?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not warn at all by default given the fact that now there are no clients supporting authentication and this warning is just going to be something annoying for the end users. But obviously selecting explicitly a no-auth option works in solving this as well, because if you chose it, then you're fine with it being absent. Generally I don't like no-something functionality - I think just need an auth-wrapper or middleware that can be configured additionally would be the best approach.

(println "Suppress warning with NREPL_SUPPRESS_DEFAULT_AUTH_WARNING=true")))
handle-message)

(defn file-auth-wrapper
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's named file-auth-wrapper, but I don't see it doing anything with files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right, that's vestigial -- I'll fix it in the next revision.

@bbatsov
Copy link
Contributor

bbatsov commented Sep 4, 2018

On a general note - won't it be better to handle authorization with some built-in middleware? Seems to me that'd be more in line with the spirit of how we've been doing things so far.

Overall the suggested approach seems to be simple, which is good. I definitely don't want to over-engineer things. I feel that we can't be emitting warnings in the absence of an auth token, as most clients won't support this for a while and that's going to be just annoying, even if it can be turned off. Generally in the beginning whatever form of authorization gets added should be opt-in and only way down the road we can think about turning this on by default (maybe).

I hope this makes sense.

@@ -107,8 +132,9 @@
* :port — defaults to 0, which autoselects an open port
* :bind — bind address, by default \"::\" (falling back to \"localhost\"
if \"::\" isn't resolved by the underlying network stack)
* :auth-wrapper - defaults to the result of `(default-auth-wrapper)`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially something like this should also be configurable via the command-line interface. Same goes for auth-token.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable -- I can investigate. If it's not straightforward, is that something you'd want in the first iteration, or is it possible that could be added later? (I agree that if it's feasible, it'd be better to have it now.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a look at the nrepl.cmdline ns - I think you'll find that adding this is going to be pretty simple. (but it's not something that necessarily has to be part of the first iteration)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i pursue it, what were you contemplating here? I could see doing something like what's done for --handler/--middleware (depending on how we resolve the middleware-related questions), but that still leaves the token. I assume we'll need to read it from an environment variable, file, stdin, or similar so that it's not visible to others on the system (via ps -au, etc.), which might suggest adding a custom auth handler to cmdline.

I suppose we could also consider defining a "standard", e.g. to look for ~/.nrepl-auth-token (or whatever) for a candidate token, as a default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this type of authentication yes. On the server side everything's easy with respect to auth token and auth schemes. The real question is how to teach clients about different schemes. Guess the idea is to just hardcode the support for whatever auth schemes are known in the various clients.

(let [port (or port 0)
auth-wrapper (or auth-wrapper default-auth-wrapper)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default should certainly be some no-op function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, perhaps that answers my other question above. And while it may not have to be addressed in tools.nrepl. I definitely think that the ecosystem should be secure by default, and so it should eventually require some effort to end up with a backdoor that bypasses all local authentication. At a minimum that might suggest ensuring that eventually all "front ends", i.e. leiningen, cider, the nrepl command line, etc. default to some kind of authentication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it might be plausible to start with no changes to the defaults and/or external behavior (as you suggest), but add something to the README recommending authentication, and mention the desire to eventually change the default. Then at some point later, we could start warning to err about the impending change of the default (under the assumption that most consumers won't be parsing err, and won't be disrupted, but people may see it), and then finally, after that's been the norm for a while, change the default, along with an appropriate version change -- i.e. perhaps as a "breaking" change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's exactly the approach I'd take. Big changes should happen gradually and need to be tested extensively first. We also need to give time to clients to catch up with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With respect to middleware -- assuming I understand your comments correctly, I treated the authentication independently under the supposition that authentication is somewhat different. i.e. it's something that should "go first", so that nothing else can be added before it that accidentally or intentionally acts on incoming, unauthenticated data. Treating it independently was also a simplification, trying to make it easier for someone to convince themselves that they (or someone else) hadn't accidentally or intentionally, broken or bypassed intended restrictions.

Along those same lines, I was (rightly or wrongly) thinking about the decision to accept a message as potentially independent of the decision about what to do with it once it's been accepted (a la ssh/pam, etc.).

I suppose the alternative might be to just make authentication a normal wrapper, and add loud documentation about always "tacking it back on to the front" if you use your own middleware, or if you add your own wrappers in front of the default wrapper. (Of course in the latter case, you'd end up authenticating the same message multiple times.)

And one reason I'd provided the default-auth-wrapper (even if the default were identity for now) is so that people could easily wrap the default handler with their own additional policies.

@bbatsov
Copy link
Contributor

bbatsov commented Sep 16, 2018

With respect to middleware -- assuming I understand your comments correctly, I treated the authentication independently under the supposition that authentication is somewhat different. i.e. it's something that should "go first", so that nothing else can be added before it that accidentally or intentionally acts on incoming, unauthenticated data. Treating it independently was also a simplification, trying to make it easier for someone to convince themselves that they (or someone else) hadn't accidentally or intentionally, broken or bypassed intended restrictions.

Yeah, I see this point. Generally I think that in practice that's unlikely to be a problem as you can obviously intercept any message in an authentication middleware, so unless someone explicitly disables it we're in business. But I'm not insisting on a middleware approach. I just seems to me in line with everything we've done some far (e.g. even sessions are middleware and they could have easily been something special).

Along those same lines, I was (rightly or wrongly) thinking about the decision to accept a message as potentially independent of the decision about what to do with it once it's been accepted (a la ssh/pam, etc.).

You'll have to elaborate on this a bit. We still have to authenticate every message regardless of the approach taken, so I'm a bit puzzled by this comment.

I suppose the alternative might be to just make authentication a normal wrapper, and add loud documentation about always "tacking it back on to the front" if you use your own middleware, or if you add your own wrappers in front of the default wrapper. (Of course in the latter case, you'd end up authenticating the same message multiple times.)

For the middleware approach this seems perfectly fine to me. If we go with the wrapper approach probably we can just make this some var you can pass on server-init. I'm thinking we can also have a configurable middleware that could support different auth schemes, but that would probably be an overkill initially.

Long story short - I try to be really careful with the approach we take, so that we're certain we select something that's going to be flexible, get the job done and relatively consistent with how we've operated so far.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 16, 2018

You'll have to elaborate on this a bit. We still have to authenticate every message regardless of the approach taken, so I'm a bit puzzled by this comment.

I just meant that I did this:

(auth-wrapper (or handler (default-handler)))

so that it was really clear that authentication happens before anything else, and you can't side-step it.

Whether or not that's what we want, is of course, another question, but that's what I was thinking at the time. (The vague (possibly inappropriate) ssh analog being that you can't run any commands until you've been allowed access, perhaps passed the whole pam stack, etc.).

And with respect to your other suggestions, I have a suspicion I don't completely understand the arrangement you're contemplating. I was wondering if you might be able to sketch it out a bit more. i.e. What would the approach(es) you've been thinking of look like?

Thanks

@bbatsov
Copy link
Contributor

bbatsov commented Sep 16, 2018

so that it was really clear that authentication happens before anything else, and you can't side-step it.

I see.

And with respect to your other suggestions, I have a suspicion I don't completely understand the arrangement you're contemplating. I was wondering if you might be able to sketch it out a bit more. i.e. What would the approach(es) you've been thinking of look like?

What I was thinking about was:

  • you've got auth middleware
  • the behaviour of the middleware is configured on server startup by passing to the server some var (implementation of an auth schema)
  • When using clj you can also pass this var (or something along those lines) via the command line interface
  • This middleware positions itself before everything in the middleware stack and checks each message for the necessary auth data.

I think that's pretty similar to what you're proposing with the slight difference it's a middleware instead of something hardcoded in the server. Once again - I'm not 100% sure which approach would be better. At least superficially your idea seems to be a bit simpler, but on the other hand - not very consistent with with how most functionality has been implemented so far.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 16, 2018 via email

@bbatsov
Copy link
Contributor

bbatsov commented Sep 17, 2018

And so while tools.nrepl could be involved

Btw, it's just nREPL (nrepl.) now. The tools. part was dropped when we decided to leave Clojure Contrib. Normally we refer to tools.nrepl as the legacy nREPL at this point.

So I'd planned to badger technomancy (and the cider devs)

I guess you don't know that I'm the author of CIDER. 😉 That's why from the very beginning I've been thinking about this end-to-end. And I happen to be quite familiar with most of the other nREPL clients out there. :-)

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 23, 2018

I guess you don't know that I'm the author of CIDER. wink That's why from the very beginning I've been thinking about this end-to-end. And I happen to be quite familiar with most of the other nREPL clients out there. :-)

Hah, no I didn't. And well, that's certainly convenient.

So backing up a bit. I'll be ecstatic if we just make it possible for people to enable some form of solid authentication for lein/cider/etc. Anything more I'd just consider a bonus -- and with respect to a lot of the details, "if you're happy, I'm happy".

Given that, what would you like this first version to look like? Would it resolve most of the questions if I were to change the auth implementation to be a middleware, assuming I understand what we mean by that, and then provide a default auth-middleware stack that's empty, along with a nrepl-auth-token-wrapper (or whatever) that people can include if they want to. And perhaps that wrapper would implement something like scheme described here? #46 (comment)

Then, once we're happy with that, we can perhaps ping technomancy, if he's not already paying attention...

@bbatsov
Copy link
Contributor

bbatsov commented Sep 27, 2018

Given that, what would you like this first version to look like? Would it resolve most of the questions if I were to change the auth implementation to be a middleware, assuming I understand what we mean by that, and then provide a default auth-middleware stack that's empty, along with a nrepl-auth-token-wrapper (or whatever) that people can include if they want to. And perhaps that wrapper would implement something like scheme described here? #46 (comment)

Yep, that'd be a good start. I'm guessing that with the middleware approach we can have some param as part of the server init that controls wether the middleware does something or is an no-op.

Then, once we're happy with that, we can perhaps ping technomancy, if he's not already paying attention...

Sounds like a plan to me. There's also my small https://github.com/nrepl/lein-nrepl where we can add support for this before it makes it to upstream leiningen. Changes there certainly take a while to happen, so this might be a good stop-gap measure.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 29, 2018

Regarding the middleware approach, were you just thinking that we might start with

(defn default-auth-middlewares []
  [])

then maybe later change the default to something like:

(defn default-auth-middlewares []
  [#'nrepl.middleware.auth/require-nrepl-auth-token])

or were you imagining the more complicated arrangement we have on the default-middlewares side, i.e. linearization, etc.? (If the latter, I'm a bit wary of any more complexity than we really have to have when auth is involved.)

@bbatsov
Copy link
Contributor

bbatsov commented Sep 30, 2018

Yeah, your suggestion makes sense to me. I'm assuming you plan to make the auth-middlewares run before every other middleware, right?

Btw, it might be good to use authentication instead of auth in the code, as some people also brought up the issue of authorization and auth can be interpreted as referring to both.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 30, 2018 via email

@bbatsov
Copy link
Contributor

bbatsov commented Sep 30, 2018

Well, seems to me that'd be better - especially given the fact that i called it authentication and you called it authorization. :D I just assumed that we'd consider the token just an authentication mechanism and if you are authenticated (have the right token) you're authorized to do anything, but I guess your perspective on this is different - the presence of a token authorizes you to do anything.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 30, 2018 via email

@bbatsov
Copy link
Contributor

bbatsov commented Sep 30, 2018

I think blanket authorization will be fine, so let's not overthink this. After all any form of authorization is going to be infinitely better than none. :-) In an ideal world it would have been nice to be able to optionally tie auth tokens to the ability to execute only certain middleware ops, but that might be a bit excessive, so let's go with something simple.

@rlbdv
Copy link
Contributor Author

rlbdv commented Sep 30, 2018 via email

@bbatsov
Copy link
Contributor

bbatsov commented Oct 1, 2018

Sounds good. I wondered about keeping the shorter :auth for the key
since it'll be in every message, but probably not significant either
way. I'm fine with whatever you prefer.

I'm fine with the shorter name as well, provided the middleware is well documented.

Also, right now I have an additional :explanation key for errors. Is
that reasonable, or should it be something else?

We always put errors in the status set, so I guess you can have there :unauthorized :no-auth-token, :unauthorized :invalid-auth-token.

@rlbdv
Copy link
Contributor Author

rlbdv commented Oct 2, 2018 via email

@rlbdv
Copy link
Contributor Author

rlbdv commented Oct 2, 2018 via email

@bbatsov
Copy link
Contributor

bbatsov commented Oct 2, 2018

That seems reasonable to me.

@rlbdv
Copy link
Contributor Author

rlbdv commented Oct 5, 2018

How is the existing --handler argument typically used from the command line, i.e. what's an sample argument value? (Does it rely on reader evaluation?)

@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 12, 2018 via email

@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 12, 2018 via email

@rlbdv rlbdv force-pushed the rlbdv/add-nrepl-auth branch 2 times, most recently from 2e5de10 to 67d7c06 Compare November 12, 2018 21:13
@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 12, 2018

OK, I've pushed an update, but I haven't changed the authorization code much yet (with respect to our current discussion). Instead, I've broken the unrelated changes out into separate commits, before the the auth changes, and I've rebased onto current master.

Since the unrelated changes had accumulated, I thought it'd be better to consider them on their own merits. Please let me know if you'd like me to change or omit any of them, or cherry-pick them if you like, and I'll rebase again.

@rlbdv rlbdv changed the title server: add support for message authentication server: add support for message authorization Nov 17, 2018
@rlbdv rlbdv changed the title server: add support for message authorization server: add support for message authentication/authorization Nov 17, 2018
@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 17, 2018

I've rebased onto current master and split the auth work into two commits. The first only adds the :auth-wrapper, which I think we may have mostly resolved? And the second contains the require-nrepl-authorization-token wrapper that I think we're less certain about.

Oh, and I switched to :auth-wrapper from :authorization-wrapper because that option itself could specify a handler that does authentication or authorization, i.e. it's a mechanism without policy.

@@ -157,7 +157,7 @@
"Require and resolve `thing`
`thing` can be a string or a symbol."
[thing]
(let [thing (symbol thing)]
(let [thing (edn/read-string thing)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this blow up if thing is a symbol?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks, should be fixed.

@bbatsov
Copy link
Contributor

bbatsov commented Nov 18, 2018

I've rebased onto current master and split the auth work into two commits. The first only adds the :auth-wrapper, which I think we may have mostly resolved? And the second contains the require-nrepl-authorization-token wrapper that I think we're less certain about.

My only real concern about this is the use of the env variable there. If you only move the configuration to rely only on nrepl.config I'd be (mostly) fine with its current state. On a broader note - there's the philosophical question of whether middleware should really read configuration directly or should always get the configuration passed to it via some other means.

Oh, and I switched to :auth-wrapper from :authorization-wrapper because that option itself could specify a handler that does authentication or authorization, i.e. it's a mechanism without policy.

That's fine. I was thinking we might call this message-filter or something along those lines, as it's actually even more generic that auth-wrapper gives it credit. :-) Ultimately we can advertise this as a way to go to the very bottom of the middleware stack.

@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 18, 2018 via email

@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 18, 2018

But if the code to implement the policy were simple enough (I'd certainly hope so), we could just provide it for people to copy -- I'm not remotely excited by the prospect of a library containing a 10 line function.

@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 18, 2018

(...and honestly, a system-level /etc based token file seems unlikely to be all that useful in this case.)

@bbatsov
Copy link
Contributor

bbatsov commented Nov 22, 2018

that support for :message-filter (or whatever), while possibly
convenient, and perhaps a touch potentially safer (i.e. harder to
avoid), may not really be the key thing.

That is we could (of course) do what's needed with just the existing
:handler or :middleware support, and some policy that all the relevant
parties can agree to (perhaps you were suggesting this earlier).  Given
a policy, could also provide a middleware function that implements it,
so that clojure-based tools can just use it.

But at least right, now, I suspect that the policy's going to end up
including environment variables (since it's one of the safe ways to
provide a value directly -- as opposed to the command line), and for
clojure-based tools, I imagine it'd be a lot more convenient to just
rely on some function that we provide, say:```

Great points! The more I think about this, the more I realize how careful we have to be with its design. I'd still prefer to use nREPL's config files over env variables, though, but that's a relatively small implementation detail. 

You know what - can you fire up a separate PR that just includes the cmdline improvements you made, so I can merge those (I'm too lazy to cherry pick the commits from this PR 😄 ) and we can continue the conversation just focused on the filter/policy stuff. 

Add a server :auth-wrapper option that can specify a wrapper for the
existing :handler, supporting access control checks that cannot easily
be bypassed, intentionally or accidentally.  Allow specification of
the wrapper from the command line via --auth-wrapper some/thing.

The intent is for all tools to eventually specify some suitable
wrapper, and perhaps even to change the server to default to a
specific :auth-wrapper that requires some form of authorization (in a
backward-incompatible release), since running an nrepl server
currently leaves your host open to local exploits, bypassing all
system authentication (password or otherwise) for your account[1].

[1] If you're lucky, they'll run xmeltdown; if you're not, rm -rf
    "$HOME"
Provide a require-nrepl-authorization-token wrapper that requires all
incoming messages to have an :auth key whose value matches a token
retrieved as:

  - the value of NREPL_AUTHORIZATION_TOKEN in the environment, or

  - the content of the file indicated by of
    NREPL_AUTHORIZATION_TOKEN_FILE in the environment, or

  - the content of ~/.nrepl/authorization-token

See the docstring for additional information.
@rlbdv
Copy link
Contributor Author

rlbdv commented Nov 24, 2018 via email

@jwhitlark
Copy link

I hate to pollute the thread, but since it's related, can I ask where SSL/TLS support would be discussed?

@rlbdv
Copy link
Contributor Author

rlbdv commented Dec 27, 2018 via email

@bbatsov
Copy link
Contributor

bbatsov commented Dec 27, 2018

@rlbdv Don't worry - in the mean time Lein 2.8.3 came out and it bundles nREPL 0.5.3, which is a great victory as now Lein natively supports the new nREPL configuration files.

We've also had @cgrand join our team and he has been working on some really great improvements for the next release.

Not sure -- I'm not planning to try to address that now. Perhaps a new
issue or something similar?

Yeah, this seems unrelated. I guess we can have some encrypted transports, but that's definitely not something high priority for me right now. In the mean time using the HTTPS transport (Drawbridge) seems like a decent workaround, or just tunnelling a normal unencrypted connection over SSH.

@MalloZup
Copy link
Contributor

MalloZup commented Aug 5, 2019

autogenerated with https://github.com/MalloZup/doghub: pr inactive since 200 days. Please update the PR or close it

@rlbdv
Copy link
Contributor Author

rlbdv commented Jul 23, 2021

Just as an update, I set this aside to pursue #204 first, and may (or may not) return to this once that one's resolved.

@bbatsov
Copy link
Contributor

bbatsov commented Mar 26, 2023

@rlbdv Should we close this one? It has been dormant for quite a while.

@rlbdv
Copy link
Contributor Author

rlbdv commented Mar 30, 2023

I think the filesystem socket support we added is sufficient for me for now, so perhaps -- I'm not likely to pursue it further right now, though I suppose it might still be interesting for situations where filesystem sockets aren't sufficient.

@rlbdv rlbdv closed this Mar 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants