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

Kerberos auth with round-robin #953

Closed
elijahgagne opened this issue Jun 16, 2021 · 14 comments
Closed

Kerberos auth with round-robin #953

elijahgagne opened this issue Jun 16, 2021 · 14 comments

Comments

@elijahgagne
Copy link

My organization uses AD with round-robin DNS. I am trying to following the example at:

https://ldap3.readthedocs.io/en/latest/bind.html?highlight=ReverseDnsSetting#kerberos

But I'm getting the error:

[egagne:ldap_test] 10s % ./ldap_test.py
Traceback (most recent call last):
  File "/Users/egagne/ldap_test.py", line 14, in <module>
    conn = Connection(server, sasl_credentials=(ReverseDnsSetting.REQUIRE_RESOLVE_ALL_ADDRESSES,), authentication=SASL, sasl_mechanism=KERBEROS, auto_referrals=False)
NameError: name 'ReverseDnsSetting' is not defined

I've tried adding ReverseDnsSetting to my import, this doesn't work:

from ldap3 import Server, Connection, SASL, Tls, KERBEROS, SUBTREE, ReverseDnsSetting
@zorn96
Copy link
Collaborator

zorn96 commented Jun 20, 2021

hi @elijahgagne ! what version of ldap3 are you using?

@elijahgagne
Copy link
Author

[egagne:ldap_test] 4s % pip list | grep ldap3
ldap3        2.9

@zorn96
Copy link
Collaborator

zorn96 commented Jun 20, 2021

it looks like you need to do

from ldap3.core.rdns import ReverseDnsSetting

because I forgot to add ReverseDnsSetting as an import to the top level __init__.py of the package when I added it

@elijahgagne
Copy link
Author

Thanks. That got rid of the import error. Now I'm getting:

conn = Connection(server, sasl_credentials=(ReverseDnsSetting.REQUIRE_RESOLVE_ALL_ADDRESSES,), authentication=SASL, sasl_mechanism=KERBEROS, auto_referrals=False)
conn.bind()
can only concatenate str (not "tuple") to str

I've tried a few other things, but get similar errors about concat an int or if I make it a string: Argument 'cpy' has incorrect type (expected gssapi.raw.creds.Creds, got str)

@zorn96
Copy link
Collaborator

zorn96 commented Jun 25, 2021

hi @elijahgagne ! could you post a full stacktrace potentially?

@elijahgagne
Copy link
Author

[egagne:ldap_test] 7s % python ldap_test.py
Traceback (most recent call last):
  File "/Users/egagne/Dropbox (Dartmouth College)/ldap_test/ldap_test.py", line 19, in <module>
    conn.bind()
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/ldap3/core/connection.py", line 615, in bind
    response = self.do_sasl_bind(controls)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/ldap3/core/connection.py", line 1343, in do_sasl_bind
    result = sasl_gssapi(self, controls)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/ldap3/protocol/sasl/kerberos.py", line 132, in sasl_gssapi
    target_name = gssapi.Name('ldap@' + connection.sasl_credentials[0], gssapi.NameType.hostbased_service)
TypeError: can only concatenate str (not "tuple") to str

@zorn96
Copy link
Collaborator

zorn96 commented Jul 14, 2021

sorry, missed your previous comment. it looks like your connection.sasl_credentials[0] is failing the check isinstance(connection.sasl_credentials[0], ReverseDnsSetting)

the block is

            if connection.sasl_credentials[0] is True:
                hostname = get_hostname_by_addr(connection.socket.getpeername()[0])
                target_name = gssapi.Name('ldap@' + hostname, gssapi.NameType.hostbased_service)
            elif isinstance(connection.sasl_credentials[0], ReverseDnsSetting):
                # all the reverse dns setting stuff
            else:  # string hostname directly provided
                target_name = gssapi.Name('ldap@' + connection.sasl_credentials[0], gssapi.NameType.hostbased_service)

your error is occurring in the else.

@zorn96
Copy link
Collaborator

zorn96 commented Jul 14, 2021

oh i see. this is actually a bug 😬
i originally made ReverseDnsSetting an enum, which worked for this check. but it got changed to an object because enums broke python 2 compatibility in the library, and now it doesn't work because ReverseDnsSetting.REQUIRE_RESOLVE_ALL_ADDRESSES is just an integer rather than an instance of the enum

let me fix this

@zorn96
Copy link
Collaborator

zorn96 commented Jul 15, 2021

created #967 to fix

@zorn96
Copy link
Collaborator

zorn96 commented Jul 15, 2021

fix is merged into dev and will be released with 2.9.1

@zorn96
Copy link
Collaborator

zorn96 commented Jul 16, 2021

@elijahgagne we should be releasing 2.9.1 soon, but in the meanwhile you can use True instead of ReverseDnsSetting .REQUIRE_RESOLVE_ALL_ADDRESSES and you should get the same behavior. it was originally a boolean (and boolean still works for backwards compatibility), and ReverseDnsSetting was added to give users more flexibility when mixing hostnames and IP addresses

with True and ReverseDnsSetting .REQUIRE_RESOLVE_ALL_ADDRESSES, all IPs specified in servers will be resolved to hostnames using reverse dns, and all hostnames specified in servers that we bind to will be resolved to IPs and then resolved to hostnames again using reverse dns. this is useful for setups with all IPs (since kerberos needs fqdns) or where the hostnames are aliases (e.g. you use mountpoint.local in scripts and that maps through to either mountpoint.staging.mydomain.com or mountpoint.prod.mydomain.com depending on which datacenter the script runs in using the underlying dns)

the new reverse dns settings let you mix and match, and make reverse dns optional. like you might have 2 servers in your pool in your script fastest.local and master-server.mydomain.com. You might use this to run a script in lots of places - fastest.local can be mapped via dns nameservers throughout the datacenter to an ip close to where the script instance is running in the datacenter, and then that reverse dns maps back to the proper kerberos name, while master-server.mydomain.com will always work but is slower due to being further away, so it's there as a fallback if the nearest site goes down.

if your use case has everything as IPs or everything possessing reverse dns mappings, then you can use True for now

@elijahgagne
Copy link
Author

elijahgagne commented Jul 18, 2021

Thanks! I haven't figured it out yet, but I'll keep trying. Here are some details where I've changed IPs and hostnames to protect the innocent.

[egagne:ldap_test] % nslookup subdomain.college.edu
Server:		111.222.17.4
Address:	111.222.17.4#53

Name:	subdomain.college.edu
Address: 10.214.0.31
Name:	subdomain.college.edu
Address: 10.194.0.30
Name:	subdomain.college.edu
Address: 10.174.12.5
Name:	subdomain.college.edu
Address: 10.174.140.4
Name:	subdomain.college.edu
Address: 10.173.2.197
Name:	subdomain.college.edu
Address: 10.172.4.103
Name:	subdomain.college.edu
Address: 10.172.2.190
Name:	subdomain.college.edu
Address: 10.173.4.117
Name:	subdomain.college.edu
Address: 10.174.12.4
Name:	subdomain.college.edu
Address: 10.204.0.30
#reverse_dns.py
from ldap3 import Server, Connection, Tls, SASL, KERBEROS
import ssl

tls = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
server = Server('subdomain.college.edu', use_ssl=True, tls=tls)
c = Connection(server, sasl_credentials=(True,), authentication=SASL, sasl_mechanism=KERBEROS)
c.bind()
print(c.extend.standard.who_am_i())
[egagne:ldap_test] % kinit elijah@SUBDOMAIN.COLLEGE.EDU
elijah@SUBDOMAIN.COLLEGE.EDU's password:
[egagne:ldap_test] 1 % klist
Credentials cache: API:28E63F08-3DBD-446C-BE6D-09A9C8E2B1A4
        Principal: elijah@SUBDOMAIN.COLLEGE.EDU

  Issued                Expires               Principal
Jul 18 16:50:29 2021  Jul 19 02:50:26 2021  krbtgt/SUBDOMAIN.COLLEGE.EDU@SUBDOMAIN.COLLEGE.EDU
[egagne:ldap_test] 3s % python reverse_dns.py
Traceback (most recent call last):
  File "/Users/egagne/Dropbox/Notes/ldap_test/main.py", line 9, in <module>
    c.bind()
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/ldap3/core/connection.py", line 615, in bind
    response = self.do_sasl_bind(controls)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/ldap3/core/connection.py", line 1343, in do_sasl_bind
    result = sasl_gssapi(self, controls)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/ldap3/protocol/sasl/kerberos.py", line 149, in sasl_gssapi
    out_token = ctx.step(in_token)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/gssapi/_utils.py", line 155, in check_last_err
    return func(self, *args, **kwargs)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/gssapi/_utils.py", line 128, in catch_and_return_token
    return func(self, *args, **kwargs)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/gssapi/sec_contexts.py", line 519, in step
    return self._initiator_step(token=token)
  File "/Users/egagne/.pyenv/versions/3.9.4/Python.framework/Versions/3.9/lib/python3.9/site-packages/gssapi/sec_contexts.py", line 535, in _initiator_step
    res = rsec_contexts.init_sec_context(self._target_name, self._creds,
  File "gssapi/raw/sec_contexts.pyx", line 245, in gssapi.raw.sec_contexts.init_sec_context
gssapi.raw.misc.GSSError: Major (851968):  Miscellaneous failure (see text), Minor (2529638919): Server not found in Kerberos database

I'm testing mostly on my Mac, but I also tried it from a domain joined Linux box and got the same error. I can hardcode a domain controller hostname in the Python code and get Kerberos authentication to work. My hope is that I can avoid that and specify subdomain.college.edu so that I can use DNS to find the domain controller.

This hardcoded example works for me with only some minor tweaks from reverse_dns.py

from ldap3 import Server, Connection, Tls, SASL, KERBEROS
import ssl

tls = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
server = Server('server.subdomain.college.edu', use_ssl=True, tls=tls)
c = Connection(server, authentication=SASL, sasl_mechanism=KERBEROS)
c.bind()
print(c.extend.standard.who_am_i())

@zorn96
Copy link
Collaborator

zorn96 commented Jul 22, 2021

ah this is the right use case for reverse dns but there might be some other issues with this approach.
technically, not all of the ips under subdomain.college.edu will be LDAP-capable domain controllers, and not all are guaranteed to have functioning reverse DNS. since the ServerPool strategy is to use the first server that responds, this might cause some flakey behavior that's outside your control with this approach

AD actually uses special DNS entries for a domain to indicate which controllers are LDAP-capable, and also to indicate their proper kerberos hostnames.
if you do nslookup -type=SRV _ldap._tcp.dc._msdcs.subdomain.college.edu with the appropriate domain piece used instead of subdomain.college.edu you'll see these records. you can query them from python using one of the dns libraries available, and that might be moreso what you want (and you can then make a serverpool of them). with this approach, reverse dns isn't even necessary

@elijahgagne
Copy link
Author

Thanks for the help. When I do nslookup -type=SRV _ldap._tcp.dc._msdcs.subdomain.college.edu I find the same 10 servers that I get back when I do nslookup subdomain.college.edu. But your comment about server pools made me read up on that and it works perfectly for me. I have three domain controllers in the same region so my code looks like

from ldap3 import Server, Connection, Tls, SASL, KERBEROS
import ssl

tls = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
server1 = Server('dc1.subdomain.college.edu', use_ssl=True, tls=tls)
server2 = Server('dc2.subdomain.college.edu', use_ssl=True, tls=tls)
server3 = Server('dc3.subdomain.college.edu', use_ssl=True, tls=tls)
c = Connection([server1, server2, server3], authentication=SASL, sasl_mechanism=KERBEROS)
c.bind()
print(c.extend.standard.who_am_i())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants