bolkedebruin / rdpgw

Remote Desktop Gateway in Go for deploying on Linux/BSD/Kubernetes
Apache License 2.0
693 stars 117 forks source link

Support for NTLM authentication added #109

Closed m7913d closed 2 months ago

m7913d commented 2 months ago

To support NTLM authentication, a database is added as an authentication source. Currently, only the configuration file is supported as a database. Database authentication supports Basic and NTLM authentication protcols.

ServerConfig.BasicAuthEnabled renamed to LocalEnabled as Basic auth can be used with Database or Local.

Advantage of this new database authentication option is that it supports the default windows client without the need to setup OpenID Connect or Kerberos.

m7913d commented 2 months ago

Thanks for your review.

database option

Regarding the name database authentication, I think most users are interested in the source used to validate the credentials, not the protocol (e.g. Basic or NTLM) used to communicate between the client and the gateway. In fact, most users would expect the gateway to work with any compliant RDP client (especially the standard Windows client), so not supporting NTLM could be considered a bug in this regard. For consistency, I think the user should choose either the source (local or database) or the protocol (Basic or NTLM). Nevertheless, if you want to rename database to NTLM, that's fine by me.

Architecture

So, you want to move the NTLM validation to rdpgw-auth? Or to a new helper program, e.g. rdpgw-ntlm?

Which part of the code do you want to move to this helper program?

Which threat model do you take into account?

License

Where do you want to include it?

Maintainability

Ideally, I would like to push my changes to an upstream. The staaldraad repo (from which I pulled the source code) or the Psiphon project (from which I included some bug fixes) might be better suited as maintainers, but I don't know yet if they are interested.

If you want to be the maintainer yourself, that's fine by me too.

Testing

I will have a look at it.

bolkedebruin commented 2 months ago

On "database", I think we differ of opinion here :-). We can (eventually) enable all authentication mechanisms at the same time and they are all backed up by a database some way. So let's set this as ntlm please.

Also because I'd like to be able to have basic and ntlm authentication at the same time "rdpgw-auth" is the right place.

I consider the "database" that needs to be read as sensitive as /etc/shadow. Hence accessing it in a separate process with a separate uid. The rpc protocol can be extended to support the challenge response mechanism. Essentially relaying between the client and rdpgw-auth. I guess this is option three but I don't see why this would require knowledge of the go-ntlm internals.

For the license I think putting it in licenses naming it "thomson-reuters" and including the line "this software..." When running rdpgw-auth without arguments or with -h should be okay. Open to suggestions here of course. Just kind of making this up

m7913d commented 2 months ago

Architecture

If you split the generation of the Challenge and the validation of the Authenticate message to a different process process (=option 3), we need to pass at least the Server challenge (which I consider a go-ntlm internal as it is currently not exposed in the interface) and maybe other things (such as the negotiated flags) to rdpgw-auth.

The go-ntlm library rather assumes that a single ServerSession instance is used to generate the Challenge and validate the Authenticate message. See https://github.com/m7913d/go-ntlm?tab=readme-ov-file#sample-usage-as-ntlm-server

However, if you say "The rpc protocol can be extended to support the challenge response mechanism", it gives me the intention that you want to generate the Challenge by rdpgw-auth, which is option 1.

bolkedebruin commented 2 months ago

Architecture

However, if you say "The rpc protocol can be extended to support the challenge response mechanism", it gives me the intention that you want to generate the Challenge by rdpgw-auth, which is option 1.

Yep :-)

m7913d commented 2 months ago

I refactored the code as requested, except for:

m7913d commented 2 months ago

Concerning the maintainability of the go-ntlm library, I contacted some projects using the library (psiphon and sematext) and recent contributors (staaldraad), but only staaldraad responded so far.

I'm fine with reviewing PR to the go-ntlm library, but I will not actively contribute to the further development. It may be better to host it under your own account.

As NTLM is considered rather an old/being phased out authentication mechanism, I don't expect many new features will be added to the library, but I think supporting the NTLM protocol is still very relevant for the rdpgw project to support the default Microsoft remote desktop client.

bolkedebruin commented 2 months ago

Awesome! I probably will move away from storing user credentials into the config file. I'd rather have something like /etc/smbpasswd that is encrypted with a key from the environment. But for now looks good.

m7913d commented 2 months ago

Awesome! I probably will move away from storing user credentials into the config file. I'd rather have something like /etc/smbpasswd that is encrypted with a key from the environment. But for now looks good.

Encrypting using a reversible algorithm would be possible. We can also store the NTLM hash of the user credentials (i.e. NTOWFv2, see go-ntlm), instead of the plain password. Just take into account that this hash is a password equivalent, i.e. knowing this hash is enough to authenticate, but it still prevents leaking the original password in case the user reused it for another application.

m7913d commented 2 months ago

Thanks for reviewing/merging!

ryanblenis commented 2 months ago

@m7913d Nice addition! I don't have deep knowledge of the NTLM inner-workings, but took a quick look at what you did here. Because what I'd really like to see is if the NTLM challenge can be passed directly to an LDAP server for authentication. It would make either direct LDAP access possible or (in my case) a Duo LDAP auth server integration with MFA (which I'm currently accomplishing via PAM with RADIUS auth, but it's based on basic auth, which requires certain RDP clients.

I'm fumbling my way around how everything is passed around, but interested in your thoughts into adding that into the mix.

Other than that, I'm not sure how "stacking" basic and ntlm auths is supposed to work. If I enabled both, it seems that only NTLM would be attempted and basic would never be used, so all users had to be in the NTLM DB for it to work. Not sure if that's an error in this implementation, or the fact that clients may try NTLM first so it always defaults to that, but has no way to "fall back" to basic auth.

m7913d commented 2 months ago

@ryanblenis I'm not very familiar with LDAP. If you could retrieve the NT and LM hash from LDAP, then it should be relatively easy to to support NTLM auth with LDAP as backend.

Stacking authentication protocols only means that the rdpgw server indicates to the client that it supports both authentication protocols, but I think most (or all) clients will not fall back to Basic if the server indicates that it also supports NTLM, as the client probably assumes that if the password is rejected using NTLM, it will also be rejected using Basic auth. I think it's somewhat uncommon to use different password backends depending on the authentication protocol used with the client, but this is partly the result of the authentication protocols (between client and gateway server) not being compatible with all password backends (i.e. for local auth, the client needs to pass the plain password to the gateway server, which NTLM doesn't do).

So, to make stacking work, you should:

Note that I don't use stacking at the moment.

Disclaimer: I'm not the maintainer of this library. So, information may be inaccurate.

ryanblenis commented 2 months ago

@m7913d Thanks for the quick response. I don't believe you can retrieve the hashes from LDAP, but I believe you can bind with LDAP via hash (at least this function would have me believe https://github.com/go-ldap/ldap/blob/master/bind.go#L465 ), I just can't seem to find out where the appropriate hash is accessible for this purpose.

m7913d commented 2 months ago

@ryanblenis I guess that this it the NT or LM password hash as stored in the LDAP database as I don't see any challenge/response mechanism present. If this is indeed the case, it is not possible to use this function as the client hashes this password hash again with a randomly generated challenge. Basically, you have

plain text password/username => (a) NT and LM hash == (hashing using server + client challenge) ==> (b) NT and LM response.

Hashes are irreversible. (a) is static, (b) changes for every authentication due to random client+server challenge. Server + client should perform same operation (starting from the plain password or (a)) and server should compare if computed value corresponds with (b) provided by the client. Interface requiring (a) can not be used the server as it only receives (b) from the client.

So, if you cannot retrieve (a) or plain password from LDAP, LDAP should support the full challenge/response mechanism (i.e. generate a server challenge).