Search code examples
pythondnskerberosdnspython

Generate TSIG keyring (as encoded byte string) for DNS Update


I am trying to use python DNS module (dnspython) to create (add) new DNS record.

Documentation specifies how to create update http://www.dnspython.org/examples.html :

import dns.tsigkeyring
import dns.update
import sys

keyring = dns.tsigkeyring.from_text({
    'host-example.' : 'XXXXXXXXXXXXXXXXXXXXXX=='
})

update = dns.update.Update('dyn.test.example', keyring=keyring)
update.replace('host', 300, 'a', sys.argv[1])

But it does not precise, how to actually generate keyring string that can be passed to dns.tsigkeyring.from_text() method in the first place.

What is the correct way to generate the key? I am using krb5 at my organization.


Solution

  • Server is running on Microsoft AD DNS with GSS-TSIG.

    TSIG and GSS-TSIG are different beasts – the former uses a static preshared key that can be simply copied from the server, but the latter uses Kerberos (GSSAPI) to negotiate a session key for every transaction.

    At the time when this thread was originally posted, dnspython 1.x did not have any support for GSS-TSIG whatsoever.

    (The handshake does not result in a static key that could be converted to a regular TSIG keyring; instead the GSSAPI library itself must be called to build an authenticator – dnspython 1.x could not do that, although dnspython 2.1 finally can.)

    If you are trying to update an Active Directory DNS server, BIND's nsupdate command-line tool supports GSS-TSIG (and sometimes it even works). You should be able to run it through subprocess and simply feed the necessary updates via stdin.

    cmds = [f'zone {dyn_zone}\n',
            f'del {fqdn}\n',
            f'add {fqdn} 60 TXT "{challenge}"\n',
            f'send\n']
    subprocess.run(["nsupdate", "-g"],
                   input="".join(cmds).encode(),
                   check=True)
    

    As with most Kerberos client applications, nsupdate expects the credentials to be already present in the environment (that is, you need to have already obtained a TGT using kinit beforehand; or alternatively, if a recent version of MIT Krb5 is used, you can point $KRB5_CLIENT_KTNAME to the keytab containing the client credentials).

    Update: dnspython 2.1 finally has the necessary pieces for GSS-TSIG, but creating the keyring is currently a very manual process – you have to call the GSSAPI library and process the TKEY negotiation yourself. The code for doing so is included at the bottom.

    (The Python code below can be passed a custom gssapi.Credentials object, but otherwise it looks for credentials in the environment just like nsupdate does.)

    import dns.rdtypes.ANY.TKEY
    import dns.resolver
    import dns.update
    import gssapi
    import socket
    import time
    import uuid
    
    def _build_tkey_query(token, key_ring, key_name):
        inception_time = int(time.time())
        tkey = dns.rdtypes.ANY.TKEY.TKEY(dns.rdataclass.ANY,
                                         dns.rdatatype.TKEY,
                                         dns.tsig.GSS_TSIG,
                                         inception_time,
                                         inception_time,
                                         3,
                                         dns.rcode.NOERROR,
                                         token,
                                         b"")
    
        query = dns.message.make_query(key_name,
                                       dns.rdatatype.TKEY,
                                       dns.rdataclass.ANY)
        query.keyring = key_ring
        query.find_rrset(dns.message.ADDITIONAL,
                         key_name,
                         dns.rdataclass.ANY,
                         dns.rdatatype.TKEY,
                         create=True).add(tkey)
        return query
    
    def _probe_server(server_name, zone):
        gai = socket.getaddrinfo(str(server_name),
                                 "domain",
                                 socket.AF_UNSPEC,
                                 socket.SOCK_DGRAM)
        for af, sf, pt, cname, sa in gai:
            query = dns.message.make_query(zone, "SOA")
            res = dns.query.udp(query, sa[0], timeout=2)
            return sa[0]
    
    def gss_tsig_negotiate(server_name, server_addr, creds=None):
        # Acquire GSSAPI credentials
        gss_name = gssapi.Name(f"DNS@{server_name}",
                               gssapi.NameType.hostbased_service)
        gss_ctx = gssapi.SecurityContext(name=gss_name,
                                         creds=creds,
                                         usage="initiate")
    
        # Name generation tips: https://tools.ietf.org/html/rfc2930#section-2.1
        key_name = dns.name.from_text(f"{uuid.uuid4()}.{server_name}")
        tsig_key = dns.tsig.Key(key_name, gss_ctx, dns.tsig.GSS_TSIG)
    
        key_ring = {key_name: tsig_key}
        key_ring = dns.tsig.GSSTSigAdapter(key_ring)
    
        token = gss_ctx.step()
        while not gss_ctx.complete:
            tkey_query = _build_tkey_query(token, key_ring, key_name)
            response = dns.query.tcp(tkey_query, server_addr, timeout=5)
            if not gss_ctx.complete:
                # Original comment:
                # https://github.com/rthalley/dnspython/pull/530#issuecomment-658959755
                # "this if statement is a bit redundant, but if the final token comes
                # back with TSIG attached the patch to message.py will automatically step
                # the security context. We dont want to excessively step the context."
                token = gss_ctx.step(response.answer[0][0].key)
    
        return key_name, key_ring
    
    def gss_tsig_update(zone, update_msg, creds=None):
        # Find the SOA of our zone
        answer = dns.resolver.resolve(zone, "SOA")
        soa_server = answer.rrset[0].mname
        server_addr = _probe_server(soa_server, zone)
    
        # Get the GSS-TSIG key
        key_name, key_ring = gss_tsig_negotiate(soa_server, server_addr, creds)
    
        # Dispatch the update
        update_msg.use_tsig(keyring=key_ring,
                            keyname=key_name,
                            algorithm=dns.tsig.GSS_TSIG)
        response = dns.query.tcp(update_msg, server_addr)
        return response