jborean93 / pyspnego

Python SPNEGO authentication library
MIT License
52 stars 11 forks source link

Hostname alias (CNAME) not handled properly #75

Closed steelman closed 10 months ago

steelman commented 10 months ago

I am using requests-kerberos on top of pyspnego (and sspilib). The most basic example

import requests
from requests_kerberos import HTTPKerberosAuth, REQUIRED
kerberos_auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, force_preemptive=True)
r = requests.get("https://service.example.org/", auth=kerberos_auth)

gives me errors (i am not entirely sure in which library the real problem is).

DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): service.example.com:443
send: b'GET / HTTP/1.1\r\nHost: service.example.com\r\nUser-Agent: python-requests/2.31.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\n\r\n'
reply: 'HTTP/1.1 401 Unauthorized\r\n'
header: Content-Type: text/html
header: Server: Microsoft-IIS/10.0
header: WWW-Authenticate: Negotiate
header: WWW-Authenticate: NTLM
header: X-Powered-By: ASP.NET
header: Date: Wed, 13 Dec 2023 12:24:48 GMT
header: Content-Length: 1293
DEBUG:urllib3.connectionpool:https://service.example.com:443 "GET / HTTP/1.1" 401 1293
DEBUG:requests_kerberos.kerberos_:handle_401(): Handling: 401
DEBUG:spnego._sspi:SSPI step input:
ERROR:requests_kerberos.kerberos_:generate_request_header(): ctx step failed:
Traceback (most recent call last):
  File "C:\Users\steelman\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\spnego\_context.py", line 68, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\steelman\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\spnego\_sspi.py", line 263, in step
    res = sspilib.raw.initialize_security_context(
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "src\\sspilib\\raw\\_security_context.pyx", line 438, in sspilib.raw._security_context.initialize_security_context

The problem is that service.example.com is an alias for host.example.com (a CNAME DNS record) and there is an SPN only for host.example.com. I have managed to work this problem around changing one line in the above example to

kerberos_auth = HTTPKerberosAuth(hostname_override="host.example.com", mutual_authentication=REQUIRED, sanitize_mutual_error_response=False)

However, I believe this shouldn't be required since the same example (without hostname_override) works fine on Debian with MIT Kerberos.

jborean93 commented 10 months ago

This library just passes through the values given to it to the GSSAPI/SSPI implementation that it is wrapping. Based on the traceback here it's on Windows so it's just using what Windows SSPI does in the background. I know MIT Kerberos has some configurations (like dns_canonicalize_hostname) to be able to do things like have it resolve the target name but I'm unsure if SSPI has any equivalent.

Ultimately name resolution is not something this library handles at all, it just provides the name of the service given to it to the underlying C library. Whether that does a resolution or not is up to that library and it's not something I have any plans on supporting in this library. Ultimately you have four choices here

steelman commented 10 months ago

Thank you for suggestions. Let me add some context and continue this discussion. Even if not to implement any solution in your libraries, then just for future reference.

Resolve the alias in your script before setting the requests hostname,

The thing is the server is configured to work as service.example.net when accessed as host.example.com it shows a "factory default" page. The current setup works perfectly fine with Windows browsers.

Set the hostname override (no guarantee the alias will actually point to that server)

If I resolve the alias and use the value of the A record to set the override then it will work. But this really seems like something that should happen under the hood.

Figure out if SSPI/Windows has a way to resolve the alias for you

That would be the best option. I will investigate it and post my findings here if you don't mind.

Setup an SPN for that alias

Beyond my pay grade (-;

jborean93 commented 10 months ago

The thing is the server is configured to work as service.example.net when accessed as host.example.com it shows a "factory default" page. The current setup works perfectly fine with Windows browsers.

It could potentially be that Windows resolves the alias before it itself called SSPI with the SPN value. Without seeing a trace of the call it would be hard to tell unfortunately. Regardless that would be a question for the library calling this one as we are just in charge of wrapping the C library.

But this really seems like something that should happen under the hood.

Unfortunately it can be dangerous for a few reasons

Granted these are weak reasons but they are still valid in some cases.

Beyond my pay grade (-;

It's not too hard, granted it's also not a simple fix. Essentially you;

steelman commented 10 months ago

It could potentially be that Windows resolves the alias before it itself called SSPI with the SPN value.

Apparently it is applications responsibility to resolve CNAME records. Still, I think there is no point in separate copy of such code in every client. Thus, I've created patches for both requests-kerberos and requests-gssapi.

Unfortunately it can be dangerous for a few reasons

I believe that with Kerberos the worst that can happen is a denial-of-service attack if DNS is attacked.

Beyond my pay grade (-;

It's not too hard, granted it's also not a simple fix.

I may have misused the phrase, but this is literally not my job and I don't have rights to do it.

jborean93 commented 10 months ago

Apparently it is applications responsibility to resolve CNAME records. Still, I think there is no point in separate copy of such code in every client. Thus, I've created patches for both https://github.com/requests/requests-kerberos/pull/185 and https://github.com/pythongssapi/requests-gssapi/pull/50.

It's a tricky question, I personally think this is somewhat irresponsible to say outright, you can't always trust that DNS is correct and you can't always trust that the resolved hostname is going to be who you talk to. It's also not really possible to use in a load balancer situation.

I believe that with Kerberos the worst that can happen is a denial-of-service attack if DNS is attacked.

The worst that can happen is a man in the middle situation where the client thinks they are talking to foo but in reality might be talking to bar. Server authentication is a cornerstone of modern authentication methods, not doing so is dangerous as now the client might be sending sensitive information to bar that should only be for foo.

I may have misused the phrase, but this is literally not my job and I don't have rights to do it.

I sympathize with you here but this is generally the solution when you want to use Kerberos with an alias, especially when that alias can resolve to different hosts over time. It works because now you have a single principal associated with the alias name that will be used to verify the server is who the client thinks it is.

As this library is passing along the values it uses to the underlying C library there's nothing else that I think pyspnego can do here so I'll be closing the issue.