diff --git a/README.rst b/README.rst index 6c787ca..da45737 100644 --- a/README.rst +++ b/README.rst @@ -229,6 +229,39 @@ To enable delegation of credentials to a server that requests delegation, pass Be careful to only allow delegation to servers you trust as they will be able to impersonate you using the delegated credentials. +Hostname canonicalization +------------------------- + +When one or more services run on a single host and CNAME records are employed +to point at the host's A or AAAA records, and there is an SPN only for +the canonical name of the host, different hostname needs to be used for +an HTTP request and differnt for authentication. To enable canonical name +resolution call ``dns_canonicalize_hostname(True)`` on an ``HTTPSPNEGOAuth`` +object. Optionally, if ``use_reverse_dns(True)`` is called, an additional +reverse DNS lookup will be used to obtain the canonical name. + + + >>> import requests + >>> from requests_gssapi import HTTPSPNEGOAuth + >>> gssapi_auth = HTTPSPNEGOAuth() + >>> gssapi_auth.dns_canonicalize_hostname(True) + >>> gssapi_auth.use_reverse_dns(True) + >>> r = requests.get("http://example.org", auth=gssapi_auth) + ... + +.. warning::: + Using an insecure DNS queries for principal name canonicalization can + result in risc of a man-in-the-middle attack. Strictly speaking such + queries are in violation of RFC 4120. Alas misconfigured realms exist + and client libraries like MIT Kerberos provide means to canonicalize + principal names via DNS queries. Be very careful when using thi option. + +.. seealso::: + `RFC 4120 ` + `RFC 6808 ` + `Kerberos configuration known issues, Kerberos authentication and DNS CNAMEs ` + `krb5.conf ` + Logging ------- diff --git a/src/requests_gssapi/compat.py b/src/requests_gssapi/compat.py index 0ae4ca8..99d93c7 100644 --- a/src/requests_gssapi/compat.py +++ b/src/requests_gssapi/compat.py @@ -2,6 +2,7 @@ Compatibility library for older versions of python and requests_kerberos """ +import socket import sys import gssapi @@ -32,6 +33,8 @@ def __init__( principal=None, hostname_override=None, sanitize_mutual_error_response=True, + dns_canonicalize_hostname=False, + use_reverse_dns=False ): # put these here for later self.principal = principal @@ -46,12 +49,27 @@ def __init__( opportunistic_auth=force_preemptive, creds=None, sanitize_mutual_error_response=sanitize_mutual_error_response, + dns_canonicalize_hostname=dns_canonicalize_hostname, + use_reverse_dns=use_reverse_dns ) def generate_request_header(self, response, host, is_preemptive=False): # This method needs to be shimmed because `host` isn't exposed to # __init__() and we need to derive things from it. Also, __init__() # can't fail, in the strictest compatability sense. + canonhost = host + if self.dns_canonicalize_hostname: + try: + ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME) + canonhost = ai[0][3] + + if self.use_reverse_dns: + ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD) + canonhost = ni[0] + + except socket.gaierror as e: + if e.errno == socket.EAI_MEMORY: + raise e try: if self.principal is not None: gss_stage = "acquiring credentials" @@ -64,7 +82,7 @@ def generate_request_header(self, response, host, is_preemptive=False): # name-based HTTP hosting) if self.service is not None: gss_stage = "initiating context" - kerb_host = host + kerb_host = canonhost if self.hostname_override: kerb_host = self.hostname_override diff --git a/src/requests_gssapi/gssapi_.py b/src/requests_gssapi/gssapi_.py index 0d3c9f6..342dce1 100644 --- a/src/requests_gssapi/gssapi_.py +++ b/src/requests_gssapi/gssapi_.py @@ -1,5 +1,6 @@ import logging import re +import socket from base64 import b64decode, b64encode import gssapi @@ -128,6 +129,47 @@ def __init__( self.creds = creds self.mech = mech if mech else SPNEGO self.sanitize_mutual_error_response = sanitize_mutual_error_response + self._dns_canonicalize_hostname = False + self._use_reverse_dns = False + + def dns_canonicalize_hostname(self, value=None): + """ + Enables canonical hostname resolution via CNAME records. + + >>> import requests + >>> from requests_gssapi import HTTPSPNEGOAuth + >>> gssapi_auth = HTTPSPNEGOAuth() + >>> gssapi_auth.dns_canonicalize_hostname(True) + >>> gssapi_auth.use_reverse_dns(True) + >>> r = requests.get("http://example.org", auth=gssapi_auth) + + .. warning::: + Using an insecure DNS queries for principal name + canonicalization can result in risc of a man-in-the-middle + attack. Strictly speaking such queries are in violation of + RFC 4120. Alas misconfigured realms exist and client libraries + like MIT Kerberos provide means to canonicalize principal + names via DNS queries. Be very careful when using thi option. + + .. seealso::: + `RFC 4120 ` + `RFC 6808 ` + """ + if isinstance(value, bool): + self._dns_canonicalize_hostname = value + return self._dns_canonicalize_hostname + + def use_reverse_dns(self, value=None): + """ + Use rev-DNS query to resolve canonical host name when DNS + canonicalization is enabled. + + .. seealso:: + See `dns_canonicalize_hostname` for further details and warnings. + """ + if isinstance(value, bool): + self._use_reverse_dns = value + return self._use_reverse_dns def generate_request_header(self, response, host, is_preemptive=False): """ @@ -144,12 +186,26 @@ def generate_request_header(self, response, host, is_preemptive=False): if self.mutual_authentication != DISABLED: gssflags.append(gssapi.RequirementFlag.mutual_authentication) + canonhost = host + if self._dns_canonicalize_hostname and type(self.target_name) != gssapi.Name: + try: + ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME) + canonhost = ai[0][3] + + if self._use_reverse_dns: + ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD) + canonhost = ni[0] + + except socket.gaierror as e: + if e.errno == socket.EAI_MEMORY: + raise e + try: gss_stage = "initiating context" name = self.target_name if type(name) != gssapi.Name: if "@" not in name: - name = "%s@%s" % (name, host) + name = "%s@%s" % (name, canonhost) name = gssapi.Name(name, gssapi.NameType.hostbased_service) self.context[host] = gssapi.SecurityContext(