Skip to content

Commit

Permalink
dns-rfc2136: GSS-TSIG support
Browse files Browse the repository at this point in the history
  • Loading branch information
grawity committed Nov 27, 2022
1 parent 56a6e5f commit 2ea8f69
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 7 deletions.
17 changes: 17 additions & 0 deletions certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,21 @@
};
};
Kerberos (GSS-TSIG) usage
'''''''''''''''''''''''''
This plugin supports authenticating DNS updates using Kerberos (GSS-TSIG),
which can be used with Active Directory DNS servers or with BIND 9.
- ``dns_rfc2136_algorithm`` must be set to ``GSS-TSIG``.
- ``dns_rfc2136_name`` changes its meaning; instead of a TSIG key name, it
must contain the FQDN of the DNS server (which will be translated into a
Kerberos principal).
- ``dns_rfc2136_secret`` must be set to ``None`` in order to use default
Kerberos credentials from the environment. Alternatively it may contain
parameters ``ccache=`` and/or ``client_keytab=`` to select a non-default
ticket cache or to automatically acquire tickets from a keytab file.
"""
85 changes: 79 additions & 6 deletions certbot-dns-rfc2136/certbot_dns_rfc2136/_internal/dns_rfc2136.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""DNS Authenticator using RFC 2136 Dynamic Updates."""
import logging
import time
from typing import Any
from typing import Callable
from typing import Optional
Expand All @@ -10,6 +11,7 @@
import dns.query
import dns.rdataclass
import dns.rdatatype
import dns.rdtypes.ANY.TKEY
import dns.tsig
import dns.tsigkeyring
import dns.update
Expand All @@ -31,6 +33,7 @@ class Authenticator(dns_common.DNSAuthenticator):
"""

ALGORITHMS = {
'GSS-TSIG': dns.tsig.GSS_TSIG,
'HMAC-MD5': dns.tsig.HMAC_MD5,
'HMAC-SHA1': dns.tsig.HMAC_SHA1,
'HMAC-SHA224': dns.tsig.HMAC_SHA224,
Expand Down Expand Up @@ -73,8 +76,8 @@ def _setup_credentials(self) -> None:
'credentials',
'RFC 2136 credentials INI file',
{
'name': 'TSIG key name',
'secret': 'TSIG key secret',
'name': 'TSIG key name (for HMAC) or server name (for GSS-TSIG)',
'secret': 'TSIG key secret (for HMAC) or credential parameters (for GSS_TSIG)',
'server': 'IP address of the target DNS server'
},
self._validate_credentials
Expand Down Expand Up @@ -105,11 +108,79 @@ def __init__(self, server: str, port: int, key_name: str, key_secret: str,
key_algorithm: dns.name.Name, timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:
self.server = server
self.port = port
self.keyring = dns.tsigkeyring.from_text({
key_name: key_secret
})
self.algorithm = key_algorithm
self._default_timeout = timeout
if self.algorithm == dns.tsig.GSS_TSIG:
# For GSS-TSIG we expect 'key_name' to contain the server's FQDN, which is mandatory to
# obtain Kerberos tickets for that server (somewhat similar to validating TLS hostname).
self.keyring, self.keyname = self._negotiate_gss_keyring(key_name, key_secret)
else:
self.keyring = dns.tsigkeyring.from_text({key_name: key_secret})
self.keyname = key_name

def _build_tkey_query(self, 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 _negotiate_gss_keyring(self, server_name: str, key_secret: str) -> dns.tsig.GSSTSigAdapter:
import gssapi
import uuid

# By default GSSAPI will take credentials from environment (KRB5CCNAME for the ticket cache
# and optionally KRB5_CLIENT_KTNAME for a keytab to automatically acquire tickets with),
# but recent MIT Krb5 allows specifying this per-context using "credential store extensions",
# which we use if "ccache=" and/or "client_keytab=" parameters are specified, e.g.
#
# dns_rfc2136_secret = ccache=FILE:/tmp/krb5cc_certbot client_keytab=FILE:/etc/krb5.keytab
if key_secret and key_secret != 'None':
cred_params = {k: v for k, v in kvp.split('=', 1)}
gss_cred = gssapi.Credentials(usage='initiate', store=cred_params)
else:
gss_cred = None

# Initialize GSSAPI context
gss_name = gssapi.Name('DNS@{0}'.format(server_name), gssapi.NameType.hostbased_service)
#gss_ctx = gssapi.SecurityContext(name=gss_name, usage='initiate')
gss_ctx = gssapi.SecurityContext(name=gss_name, creds=gss_cred, usage='initiate')

# Name generation tips: https://tools.ietf.org/html/rfc2930#section-2.1
key_name = dns.name.from_text('{0}.{1}'.format(uuid.uuid4(), server_name))
tsig_key = dns.tsig.Key(key_name, gss_ctx, dns.tsig.GSS_TSIG)
key_ring = dns.tsig.GSSTSigAdapter({key_name: tsig_key})

# Perform GSSAPI negotiation via TKEY
in_token = None
while not gss_ctx.complete:
out_token = gss_ctx.step(in_token)
if not out_token:
break
request = self._build_tkey_query(out_token, key_ring, key_name)
try:
response = dns.query.tcp(request, self.server, self._default_timeout, self.port)
except (OSError, dns.exception.Timeout) as e:
logger.debug('TCP query failed, fallback to UDP: %s', e)
response = dns.query.udp(request, self.server, self._default_timeout, self.port)
in_token = response.answer[0][0].key

return key_ring, key_name

def add_txt_record(self, record_name: str, record_content: str, record_ttl: int) -> None:
"""
Expand All @@ -130,6 +201,7 @@ def add_txt_record(self, record_name: str, record_content: str, record_ttl: int)
update = dns.update.Update(
domain,
keyring=self.keyring,
keyname=self.keyname,
keyalgorithm=self.algorithm)
update.add(rel, record_ttl, dns.rdatatype.TXT, record_content)

Expand Down Expand Up @@ -165,6 +237,7 @@ def del_txt_record(self, record_name: str, record_content: str) -> None:
update = dns.update.Update(
domain,
keyring=self.keyring,
keyname=self.keyname,
keyalgorithm=self.algorithm)
update.delete(rel, dns.rdatatype.TXT, record_content)

Expand Down Expand Up @@ -217,7 +290,7 @@ def _query_soa(self, domain_name: str) -> bool:
# Turn off Recursion Desired bit in query
request.flags ^= dns.flags.RD
# Use our TSIG keyring
request.use_tsig(self.keyring, algorithm=self.algorithm) # type: ignore[attr-defined]
request.use_tsig(self.keyring, self.keyname, algorithm=self.algorithm) # type: ignore[attr-defined]

try:
try:
Expand Down
2 changes: 1 addition & 1 deletion certbot-dns-rfc2136/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
version = '2.0.0'

install_requires = [
'dnspython>=1.15.0',
'dnspython>=2.1.0',
'setuptools>=41.6.0',
]

Expand Down

0 comments on commit 2ea8f69

Please sign in to comment.