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

Implement DNS hostname canonicalization #50

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://datatracker.ietf.org/doc/html/rfc4120>`
`RFC 6808 <https://datatracker.ietf.org/doc/html/rfc6806>`
`Kerberos configuration known issues, Kerberos authentication and DNS CNAMEs <https://learn.microsoft.com/en-us/previous-versions/office/sharepoint-server-2010/gg502606(v=office.14)?redirectedfrom=MSDN#kerberos-authentication-and-dns-cnames>`
`krb5.conf <https://web.mit.edu/kerberos/krb5-1.21/doc/admin/conf_files/krb5_conf.html>`

Logging
-------

Expand Down
20 changes: 19 additions & 1 deletion src/requests_gssapi/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Compatibility library for older versions of python and requests_kerberos
"""

import socket
import sys

import gssapi
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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

Expand Down
58 changes: 57 additions & 1 deletion src/requests_gssapi/gssapi_.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import re
import socket
from base64 import b64decode, b64encode

import gssapi
Expand Down Expand Up @@ -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 <https://datatracker.ietf.org/doc/html/rfc4120>`
`RFC 6808 <https://datatracker.ietf.org/doc/html/rfc6806>`
"""
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):
"""
Expand All @@ -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(
Expand Down