Browse Source

feat(nsmaster): TSIG-secured AXFR replication to external secondaries

Peter Thomassen 3 years ago
parent
commit
05e36156d8

+ 3 - 0
.env.default

@@ -6,6 +6,7 @@ DESECSTACK_NS=ns1.example.com ns2.example.com
 DESECSTACK_IPV4_REAR_PREFIX16=172.16
 DESECSTACK_IPV6_SUBNET=fda8:7213:9e5e:1::/80
 DESECSTACK_IPV6_ADDRESS=fda8:7213:9e5e:1::0642:ac10:0080
+DESECSTACK_PORT_XFR=53
 
 # certificates
 DESECSTACK_WWW_CERTS=./certs
@@ -33,9 +34,11 @@ DESECSTACK_NSLORD_DEFAULT_TTL=3600
 
 # nsmaster-related
 DESECSTACK_DBMASTER_PASSWORD_pdns=
+DESECSTACK_NSMASTER_ALSO_NOTIFY=
 DESECSTACK_NSMASTER_APIKEY=
 DESECSTACK_NSMASTER_CARBONSERVER=
 DESECSTACK_NSMASTER_CARBONOURNAME=
+DESECSTACK_NSMASTER_TSIGKEY=
 
 # monitoring
 DESECSTACK_WATCHDOG_SECONDARIES=ns1.example.org ns2.example.net

+ 4 - 1
.github/workflows/main.yml

@@ -26,11 +26,14 @@ env:
   DESECSTACK_DB_PASSWORD_pdnsmaster: 9Fn33T5yGukjwelt
   DESECSTACK_NSLORD_APIKEY: 9Fn33T5yGukjekwjew
   DESECSTACK_NSLORD_DEFAULT_TTL: 1234
-  DESECSTACK_NSMASTER_APIKEY: LLq1orOQuXCINUz4TV
   DESECSTACK_DBMASTER_PORT: 13306
+  DESECSTACK_NSMASTER_ALSO_NOTIFY:
+  DESECSTACK_NSMASTER_APIKEY: LLq1orOQuXCINUz4TV
+  DESECSTACK_NSMASTER_TSIGKEY: +++undefined/undefined/undefined/undefined/undefined/undefined/undefined/undefined+++A==
   DESECSTACK_IPV4_REAR_PREFIX16: 172.16
   DESECSTACK_IPV6_SUBNET: bade:affe:dead:beef:b011::/80
   DESECSTACK_IPV6_ADDRESS: bade:affe:dead:beef:b011:0642:ac10:0080
+  DESECSTACK_PORT_XFR: 12353
   DESECSTACK_WWW_CERTS: ./certs
   DESECSTACK_MINIMUM_TTL_DEFAULT: 3600
   DESECSTACK_PROMETHEUS_PASSWORD: Je9NNkqbULsg

+ 3 - 0
README.md

@@ -39,6 +39,7 @@ Although most configuration is contained in this repository, some external depen
         need to manually update persisted data structures such as the MySQL grant tables! Better don't do it.
       - `DESECSTACK_IPV6_SUBNET`: IPv6 net, ideally /80 (see below)
       - `DESECSTACK_IPV6_ADDRESS`: IPv6 address of frontend container, ideally 0642:ac10:0080 in within the above subnet (see below)
+      - `DESECSTACK_PORT_XFR`: Port over which XFRs are performed with secondaries
     - certificates
       - `DESECSTACK_WWW_CERTS`: `./path/to/certificates` for `www` container. This directory is monitored for changes so that nginx can reload when new keys/certificates are provided. **Note:** The reload is done any time something changes in the directory. The relevant files are **not** watched individually.
     - API-related
@@ -62,9 +63,11 @@ Although most configuration is contained in this repository, some external depen
       - `DESECSTACK_NSLORD_DEFAULT_TTL`: TTL to use by default, including for default NS records
     - nsmaster-related
       - `DESECSTACK_DBMASTER_PASSWORD_pdns`: mysql password for pdns on nsmaster
+      - `DESECSTACK_NSMASTER_ALSO_NOTIFY`: Comma-separated list of additional IP addresses to notify of zone updates
       - `DESECSTACK_NSMASTER_APIKEY`: pdns API key on nsmaster (required so that we can execute zone deletions on nsmaster, which replicates to the secondaries)
       - `DESECSTACK_NSMASTER_CARBONSERVER`: pdns `carbon-server` setting on nsmaster (optional)
       - `DESECSTACK_NSMASTER_CARBONOURNAME`: pdns `carbon-ourname` setting on nsmaster (optional)
+      - `DESECSTACK_NSMASTER_TSIGKEY`: Base64-encoded value of the default TSIG key used for talking to external secondaries (algorithm: HMAC-SHA256)
     - monitoring-related
       - `DESECSTACK_WATCHDOG_SECONDARIES`: space-separated list of secondary hostnames; used to check correct replication of recent DNS changes
       - `DESECSTACK_PROMETHEUS_PASSWORD`: basic auth password for user `prometheus` at `https://${DESECSTACK_DOMAIN}/prometheus/`

+ 2 - 1
api/desecapi/pdns_change_tracker.py

@@ -113,7 +113,8 @@ class PDNSChangeTracker:
                 {
                     'name': self.domain_name_normalized,
                     'kind': 'SLAVE',
-                    'masters': [socket.gethostbyname('nslord')]
+                    'masters': [socket.gethostbyname('nslord')],
+                    'master_tsig_key_ids': ['default'],
                 }
             )
 

+ 7 - 1
docker-compose.test-e2e2.yml

@@ -41,7 +41,7 @@ services:
   nslord:
     networks:
       front:
-        ipv4_address: ${DESECSTACK_IPV4_REAR_PREFIX16}.0.129 # make nslord available for test-e2e
+        ipv4_address: ${DESECSTACK_IPV4_REAR_PREFIX16}.0.129  # make available for test-e2e
     environment:
     - DESECSTACK_NSLORD_CACHE_TTL=0
     # faketime setup
@@ -54,6 +54,11 @@ services:
     volumes:
     - faketime:/etc/faketime/:ro
 
+  nsmaster:
+    networks:
+      front:
+        ipv4_address: ${DESECSTACK_IPV4_REAR_PREFIX16}.0.130  # make available for test-e2e
+
   test-e2e2:
     build: test/e2e2
     restart: "no"
@@ -64,6 +69,7 @@ services:
     - DESECSTACK_IPV6_SUBNET
     - DESECSTACK_IPV6_ADDRESS
     - DESECSTACK_NSLORD_DEFAULT_TTL
+    - DESECSTACK_NSMASTER_TSIGKEY
     # faketime setup
     - LD_PRELOAD=/lib/libfaketime.so
     - FAKETIME_TIMESTAMP_FILE=/etc/faketime/faketime.rc

+ 4 - 0
docker-compose.yml

@@ -191,12 +191,16 @@ services:
     init: true
     cap_add:
     - NET_ADMIN
+    ports:
+    - "${DESECSTACK_PORT_XFR}:53"
     environment:
     - DESECSTACK_IPV4_REAR_PREFIX16
     - DESECSTACK_DBMASTER_PASSWORD_pdns
+    - DESECSTACK_NSMASTER_ALSO_NOTIFY
     - DESECSTACK_NSMASTER_APIKEY
     - DESECSTACK_NSMASTER_CARBONSERVER
     - DESECSTACK_NSMASTER_CARBONOURNAME
+    - DESECSTACK_NSMASTER_TSIGKEY
     depends_on:
     - dbmaster
     networks:

+ 1 - 1
nsmaster/conf/pdns.conf.var

@@ -1,7 +1,7 @@
 api=yes
 api-key=${DESECSTACK_NSMASTER_APIKEY}
 allow-axfr-ips=10.8.0.0/24
-also-notify=239.1.2.3
+also-notify=239.1.2.3,${DESECSTACK_NSMASTER_ALSO_NOTIFY}
 only-notify=
 setgid=pdns
 setuid=pdns

+ 3 - 0
nsmaster/entrypoint.sh

@@ -13,4 +13,7 @@ host=dbmaster; port=3306; n=120; i=0; while ! (echo > /dev/tcp/$host/$port) 2> /
 # Manage credentials
 envsubst < /etc/powerdns/pdns.conf.var > /etc/powerdns/pdns.conf
 
+echo "Provisioning default TSIG key ..."
+pdnsutil import-tsig-key default hmac-sha256 "${DESECSTACK_NSMASTER_TSIGKEY}" > /dev/null
+
 exec pdns_server --daemon=no

+ 3 - 0
test/e2e2/conftest.py

@@ -461,6 +461,9 @@ def return_eventually(expression: callable, min_pause: float = .1, max_pause: fl
                 raise e
             time.sleep(wait)
             wait = min(2 * wait, max_pause)
+        except Exception as e:
+            tsprint(f'{expression.__code__} raised unexpected exception {e}')
+            raise
 
 
 def assert_eventually(assertion: callable, min_pause: float = .1, max_pause: float = 2, timeout: float = 5,

+ 32 - 0
test/e2e2/spec/test_replication.py

@@ -1,3 +1,8 @@
+from base64 import b64decode
+import os
+import socket
+
+import dns.query
 import pytest
 
 from conftest import DeSECAPIV1Client, return_eventually, query_replication, random_domainname, assert_eventually, \
@@ -76,3 +81,30 @@ def test_signature_rotation_performance(api_user_domain: DeSECAPIV1Client):
                     lambda: soa_rrsig[name] != query_replication(name, "", 'RRSIG', covers='SOA'),
                     timeout=600,  # depending on number of domains in the database, this value requires increase
                 )
+
+def test_tsig_axfr(api_user_domain: DeSECAPIV1Client):
+    ns_ip = socket.gethostbyname('nsmaster')
+
+    def count_xfr_rrsets(**kwargs):
+        xfr = dns.query.xfr(ns_ip, api_user_domain.domain, **kwargs)
+        zone = dns.zone.from_xfr(xfr)
+
+        ## from dnspython 2.2.0 on
+        #zone = dns.zone.Zone(api_user_domain.domain)
+        #query, _ = dns.xfr.make_query(zone, **kwargs)
+        #dns.query.inbound_xfr(ns_ip, zone, query)
+        return sum(1 for _ in zone.iterate_rdatasets())
+
+    with pytest.raises(dns.xfr.TransferError) as exc_info:
+        count_xfr_rrsets()
+
+    assert exc_info.value.rcode == dns.rcode.NOTAUTH
+
+    keyring = {'default.': b64decode('XXXXXXXXXXXXXXXXXXXXXX==')}
+    with pytest.raises(dns.xfr.TransferError) as exc_info:
+        count_xfr_rrsets(keyring=keyring, keyname=None)
+
+    assert exc_info.value.rcode == dns.rcode.NOTAUTH
+
+    keyring = {'default.': b64decode(os.environ['DESECSTACK_NSMASTER_TSIGKEY'])}
+    assert_eventually(lambda: count_xfr_rrsets(keyring=keyring, keyname=None) > 5, timeout=20, retry_on=(Exception,))