if cls._zone_keys.get(zonename, None):
cls.secureZone(confdir, zonename, cls._zone_keys.get(zonename))
+ @classmethod
+ def waitForTCPSocket(cls, ipaddress, port):
+ for try_number in range(0, 100):
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(1.0)
+ sock.connect((ipaddress, port))
+ sock.close()
+ return
+ except Exception as err:
+ if err.errno != errno.ECONNREFUSED:
+ print(f'Error occurred: {try_number} {err}', file=sys.stderr)
+ time.sleep(0.1)
+
@classmethod
def startAuth(cls, confdir, ipaddress):
cls._auths[ipaddress] = subprocess.Popen(authcmd, close_fds=True,
stdout=fdLog, stderr=fdLog,
env=cls._auth_env)
-
- time.sleep(2)
+ cls.waitForTCPSocket(ipaddress, cls._authPort)
if cls._auths[ipaddress].poll() is not None:
- try:
- cls._auths[ipaddress].kill()
- except OSError as e:
- if e.errno != errno.ESRCH:
- raise
- with open(logFile, 'r') as fdLog:
- print(fdLog.read())
- sys.exit(cls._auths[ipaddress].returncode)
+ print(f"\n*** startAuth log for {logFile} ***")
+ with open(logFile, 'r') as fdLog:
+ print(fdLog.read())
+ print(f"*** End startAuth log for {logFile} ***")
+ raise AssertionError('%s failed (%d)' % (authcmd, cls._auths[ipaddress].returncode))
@classmethod
def setUpSockets(cls):
cls.tearDownAuth()
@classmethod
- def tearDownAuth(cls):
- if 'PDNSRECURSOR_FAST_TESTS' in os.environ:
- delay = 0.1
- else:
- delay = 1.0
+ def killProcess(cls, p):
+ # Don't try to kill it if it's already dead
+ if p.poll() is not None:
+ return
+ try:
+ p.terminate()
+ for count in range(10):
+ x = p.poll()
+ if x is not None:
+ break
+ time.sleep(0.1)
+ if x is None:
+ print("kill...", p, file=sys.stderr)
+ p.kill()
+ p.wait()
+ except OSError as e:
+ # There is a race-condition with the poll() and
+ # kill() statements, when the process is dead on the
+ # kill(), this is fine
+ if e.errno != errno.ESRCH:
+ raise
+ @classmethod
+ def tearDownAuth(cls):
for _, auth in cls._auths.items():
- try:
- auth.terminate()
- if auth.poll() is None:
- time.sleep(delay)
- if auth.poll() is None:
- auth.kill()
- auth.wait()
- except OSError as e:
- if e.errno != errno.ESRCH:
- raise
+ cls.killProcess(auth)
@classmethod
def sendUDPQuery(cls, query, timeout=2.0, decode=True, fwparams=dict()):
--- /dev/null
+echo commands to run:
+echo Passwords enterd shoudl match those in kerberos-server setup script
+echo rm -f kt.keytab
+echo ktutil
+echo add_entry -password -p testuser1@EXAMPLE.COM -k 1 -e aes256-cts-hmac-sha1-96
+echo add_entry -password -p testuser2@EXAMPLE.COM -k 1 -e aes256-cts-hmac-sha1-96
+echo add_entry -password -p DNS/ns1.example.net@EXAMPLE.COM -k 1 -e aes256-cts-hmac-sha1-96
+echo wkt kt.keytab
+echo quit
--- /dev/null
+[libdefaults]
+ default_realm = EXAMPLE.COM
+
+[realms]
+ EXAMPLE.COM = {
+ kdc = 127.0.0.1:1188
+ admin_server = 127.0.0.1:1749
+ }
+
--- /dev/null
+function updatepolicy(arg)
+ princ = arg:getPeerPrincipal()
+ return princ == "testuser2@EXAMPLE.COM"
+end
--- /dev/null
+FROM debian:bullseye
+
+EXPOSE 749 88
+
+ENV DEBIAN_FRONTEND noninteractive
+# The -qq implies --yes
+RUN apt-get -qq update
+RUN apt-get -qq install locales krb5-kdc krb5-admin-server
+RUN apt-get -qq clean
+
+#RUN locale-gen "en_US.UTF-8"
+#RUN echo "LC_ALL=\"en_US.UTF-8\"" >> /etc/default/locale
+
+ENV REALM ${REALM:-EXAMPLE.COM}
+ENV SUPPORTED_ENCRYPTION_TYPES ${SUPPORTED_ENCRYPTION_TYPES:-aes256-cts-hmac-sha1-96:normal}
+ENV KADMIN_PRINCIPAL ${KADMIN_PRINCIPAL:-kadmin/admin}
+ENV KADMIN_PASSWORD ${KADMIN_PASSWORD:-MITiys4K5}
+
+COPY kerberos-init.sh /tmp/
+CMD /tmp/kerberos-init.sh
--- /dev/null
+version: "2"
+services:
+ kerberos:
+ build: .
+ ports:
+ - "1188:88"
+ - "1749:749"
+ volumes:
+ # This is needed otherwise there won't be enough entropy to generate a new kerberos realm
+ - /dev/urandom:/dev/random
--- /dev/null
+#!/bin/bash
+
+KADMIN_PRINCIPAL_FULL=$KADMIN_PRINCIPAL@$REALM
+
+echo "REALM: $REALM"
+echo "KADMIN_PRINCIPAL_FULL: $KADMIN_PRINCIPAL_FULL"
+echo "KADMIN_PASSWORD: $KADMIN_PASSWORD"
+echo ""
+
+KDC_KADMIN_SERVER=$(hostname -f)
+tee /etc/krb5.conf <<EOF
+[libdefaults]
+ default_realm = $REALM
+
+[realms]
+ $REALM = {
+ kdc_ports = 88,750
+ kadmind_port = 749
+ kdc = $KDC_KADMIN_SERVER
+ admin_server = $KDC_KADMIN_SERVER
+ }
+EOF
+echo ""
+
+tee /etc/krb5kdc/kdc.conf <<EOF
+[realms]
+ $REALM = {
+ acl_file = /etc/krb5kdc/kadm5.acl
+ max_renewable_life = 7d 0h 0m 0s
+ supported_enctypes = $SUPPORTED_ENCRYPTION_TYPES
+ default_principal_flags = +preauth
+ }
+EOF
+
+tee /etc/krb5kdc/kadm5.acl <<EOF
+$KADMIN_PRINCIPAL_FULL *
+EOF
+
+MASTER_PASSWORD=$(tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1)
+# This command also starts the krb5-kdc and krb5-admin-server services
+krb5_newrealm <<EOF
+$MASTER_PASSWORD
+$MASTER_PASSWORD
+EOF
+
+kadmin.local -q "delete_principal -force $KADMIN_PRINCIPAL_FULL"
+kadmin.local -q "addprinc -pw $KADMIN_PASSWORD $KADMIN_PRINCIPAL_FULL"
+
+kadmin.local -q "delete_principal -force testuser1@$REALM"
+kadmin.local -q "delete_principal -force testuser2@$REALM"
+kadmin.local -q "delete_principal -force DNS/ns1.example.net@$REALM"
+kadmin.local -q "addprinc -pw pw1 testuser1@$REALM"
+kadmin.local -q "addprinc -pw pw2 testuser2@$REALM"
+kadmin.local -q "addprinc -pw pw3 DNS/ns1.example.net@$REALM"
+
+krb5kdc
+kadmind -nofork
fi
done
-set -e
if [ "${PDNS_DEBUG}" = "YES" ]; then
set -x
fi
-nosetests --with-xunit $@
+ignore="-I test_GSSTSIG.py"
+if [ "${WITHKERBEROS}" = "YES" ]; then
+ ignore=""
+ (cd kerberos-server && docker-compose up --detach --build)
+fi
+
+nosetests --with-xunit $ignore $@
+ret=$?
+
+if [ "${WITHKERBEROS}" = "YES" ]; then
+ (cd kerberos-server && docker-compose stop || exit 0)
+fi
+exit $ret
--- /dev/null
+#!/usr/bin/env python
+import dns
+import os
+import subprocess
+
+from authtests import AuthTest
+
+
+class GSSTSIGBase(AuthTest):
+ _config_template_default = """
+module-dir=../regression-tests/modules
+daemon=no
+socket-dir={confdir}
+cache-ttl=0
+negquery-cache-ttl=0
+query-cache-ttl=0
+log-dns-queries=yes
+log-dns-details=yes
+loglevel=9
+distributor-threads=1"""
+
+ _config_template = """
+launch=gsqlite3
+gsqlite3-database=configs/auth/powerdns.sqlite
+gsqlite3-pragma-foreign-keys=yes
+gsqlite3-dnssec=yes
+enable-gss-tsig=yes
+allow-dnsupdate-from=0.0.0.0/0
+dnsupdate=yes
+"""
+ _auth_env = {'KRB5_CONFIG' : './kerberos-client/krb5.conf',
+ 'KRB5_KTNAME' : './kerberos-client/kt.keytab'
+ }
+
+ @classmethod
+ def setUpClass(cls):
+ super(GSSTSIGBase, cls).setUpClass()
+ os.system("$PDNSUTIL --config-dir=configs/auth delete-zone example.net")
+ os.system("$PDNSUTIL --config-dir=configs/auth delete-zone noacceptor.net")
+ os.system("$PDNSUTIL --config-dir=configs/auth delete-zone wrongacceptor.net")
+ os.system("$PDNSUTIL --config-dir=configs/auth create-zone example.net")
+ os.system("$PDNSUTIL --config-dir=configs/auth create-zone noacceptor.net")
+ os.system("$PDNSUTIL --config-dir=configs/auth create-zone wrongacceptor.net")
+
+ os.system("$PDNSUTIL --config-dir=configs/auth add-record example.net . SOA 3600 'ns1.example.net otto.example.net 2022010403 10800 3600 604800 3600'")
+ os.system("$PDNSUTIL --config-dir=configs/auth add-record noacceptor.net . SOA 3600 'ns1.noacceptor.net otto.example.net 2022010403 10800 3600 604800 3600'")
+ os.system("$PDNSUTIL --config-dir=configs/auth add-record wrongacceptor.net . SOA 3600 'ns1.wrongacceptor.net otto.example.net 2022010403 10800 3600 604800 3600'")
+
+ os.system("$PDNSUTIL --config-dir=configs/auth set-meta example.net GSS-ACCEPTOR-PRINCIPAL DNS/ns1.example.net@EXAMPLE.COM")
+ os.system("$PDNSUTIL --config-dir=configs/auth set-meta wrongacceptor.net GSS-ACCEPTOR-PRINCIPAL DNS/ns1.example.net@EXAMPLE.COM")
+ os.system("$PDNSUTIL --config-dir=configs/auth set-meta example.net TSIG-ALLOW-DNSUPDATE testuser1@EXAMPLE.COM")
+
+ def kinit(self, user):
+ ret = subprocess.run(["kinit", "-Vt", "./kerberos-client/kt.keytab", user], env=self._auth_env)
+ self.assertEqual(ret.returncode, 0)
+
+ def nsupdate(self, commands, expected=0):
+ full = "server 127.0.0.1 %s\n" % self._authPort
+ full += commands + "\nsend\nquit\n"
+ ret = subprocess.run(["nsupdate", "-g"], input=full, env=self._auth_env, capture_output=True, text=True)
+ self.assertEqual(ret.returncode, expected)
+
+ def checkInDB(self, zone, record):
+ ret = os.system("$PDNSUTIL --config-dir=configs/auth list-zone %s | egrep -q %s" % (zone, record))
+ self.assertEqual(ret, 0)
+
+ def checkNotInDB(self, zone, record):
+ ret = os.system("$PDNSUTIL --config-dir=configs/auth list-zone %s | fgrep -q %s" % (zone, record))
+ self.assertNotEqual(ret, 0)
+
+class TestBasicGSSTSIG(GSSTSIGBase):
+
+ _config_template = """
+launch=gsqlite3
+gsqlite3-database=configs/auth/powerdns.sqlite
+gsqlite3-pragma-foreign-keys=yes
+gsqlite3-dnssec=yes
+enable-gss-tsig=yes
+allow-dnsupdate-from=0.0.0.0/0
+dnsupdate=yes
+"""
+ def testAllowedUpdate(self):
+ self.checkNotInDB('example.net', 'inserted1.example.net')
+ self.kinit("testuser1")
+ self.nsupdate("add inserted1.example.net 10 A 1.2.3.1")
+ self.checkInDB('example.net', '^inserted1.example.net.*10.*IN.*A.*1.2.3.1$')
+
+ def testDisallowedUpdate(self):
+ self.kinit("testuser2")
+ self.nsupdate("add inserted2.example.net 10 A 1.2.3.2", 2)
+ self.checkNotInDB('example.net', 'inserted2.example.net')
+
+ def testNoAcceptor(self):
+ self.kinit("testuser1")
+ self.nsupdate("add inserted3.noacceptor.net 10 A 1.2.3.3", 2)
+ self.checkNotInDB('example.net', 'inserted3.example.net')
+
+ def testWrongAcceptor(self):
+ self.kinit("testuser1")
+ self.nsupdate("add inserted4.wrongacceptor.net 10 A 1.2.3.4", 2)
+ self.checkNotInDB('example.net', 'inserted4.example.net')
+
+class TestLuaGSSTSIG(GSSTSIGBase):
+
+ _config_template = """
+launch=gsqlite3
+gsqlite3-database=configs/auth/powerdns.sqlite
+gsqlite3-pragma-foreign-keys=yes
+gsqlite3-dnssec=yes
+enable-gss-tsig=yes
+allow-dnsupdate-from=0.0.0.0/0
+dnsupdate=yes
+lua-dnsupdate-policy-script=kerberos-client/update-policy.lua
+"""
+ def testDisallowedByLuaUpdate(self):
+ self.kinit("testuser1")
+ self.nsupdate("add inserted10.example.net 10 A 1.2.3.10", 0) # Lua deny is still a NOERROR
+ self.checkNotInDB('example.net', 'inserted10.example.net')
+
+ def testAllowedByLuaUpdate(self):
+ self.kinit("testuser2")
+ self.nsupdate("add inserted11.example.net 10 A 1.2.3.11")
+ self.checkInDB('example.net', '^inserted11.example.net.*10.*IN.*A.*1.2.3.11$')
+
+
+ def testNoAcceptor(self):
+ self.kinit("testuser1")
+ self.nsupdate("add inserted12.noacceptor.net 10 A 1.2.3.12", 2)
+ self.checkNotInDB('example.net', 'inserted12.example.net')
+
+ def testWrongAcceptor(self):
+ self.kinit("testuser1")
+ self.nsupdate("add inserted13.wrongacceptor.net 10 A 1.2.3.13", 2)
+ self.checkNotInDB('example.net', 'inserted13.example.net')
+
'curl',
'default-jre-headless',
'dnsutils',
+ 'docker-compose',
'faketime',
'gawk',
+ 'krb5-user',
'ldnsutils',
'libboost-serialization1.71.0',
'libcdb1',
if backend == 'authpy':
with c.cd('regression-tests.auth-py'):
- c.run(f'PDNS=/opt/pdns-auth/sbin/pdns_server PDNS2=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig NOTIFY=/opt/pdns-auth/bin/pdns_notify NSEC3DIG=/opt/pdns-auth/bin/nsec3dig SAXFR=/opt/pdns-auth/bin/saxfr ZONE2SQL=/opt/pdns-auth/bin/zone2sql ZONE2LDAP=/opt/pdns-auth/bin/zone2ldap ZONE2JSON=/opt/pdns-auth/bin/zone2json PDNSUTIL=/opt/pdns-auth/bin/pdnsutil PDNSCONTROL=/opt/pdns-auth/bin/pdns_control PDNSSERVER=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig GMYSQLHOST=127.0.0.1 GMYSQL2HOST=127.0.0.1 MYSQL_HOST="127.0.0.1" PGHOST="127.0.0.1" PGPORT="5432" ./runtests')
+ c.run(f'PDNS=/opt/pdns-auth/sbin/pdns_server PDNS2=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig NOTIFY=/opt/pdns-auth/bin/pdns_notify NSEC3DIG=/opt/pdns-auth/bin/nsec3dig SAXFR=/opt/pdns-auth/bin/saxfr ZONE2SQL=/opt/pdns-auth/bin/zone2sql ZONE2LDAP=/opt/pdns-auth/bin/zone2ldap ZONE2JSON=/opt/pdns-auth/bin/zone2json PDNSUTIL=/opt/pdns-auth/bin/pdnsutil PDNSCONTROL=/opt/pdns-auth/bin/pdns_control PDNSSERVER=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig GMYSQLHOST=127.0.0.1 GMYSQL2HOST=127.0.0.1 MYSQL_HOST="127.0.0.1" PGHOST="127.0.0.1" PGPORT="5432" WITHKERBEROS=YES ./runtests')
return
with c.cd('regression-tests'):