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_IPV4_REAR_PREFIX16=172.16
 DESECSTACK_IPV6_SUBNET=fda8:7213:9e5e:1::/80
 DESECSTACK_IPV6_SUBNET=fda8:7213:9e5e:1::/80
 DESECSTACK_IPV6_ADDRESS=fda8:7213:9e5e:1::0642:ac10:0080
 DESECSTACK_IPV6_ADDRESS=fda8:7213:9e5e:1::0642:ac10:0080
+DESECSTACK_PORT_XFR=53
 
 
 # certificates
 # certificates
 DESECSTACK_WWW_CERTS=./certs
 DESECSTACK_WWW_CERTS=./certs
@@ -33,9 +34,11 @@ DESECSTACK_NSLORD_DEFAULT_TTL=3600
 
 
 # nsmaster-related
 # nsmaster-related
 DESECSTACK_DBMASTER_PASSWORD_pdns=
 DESECSTACK_DBMASTER_PASSWORD_pdns=
+DESECSTACK_NSMASTER_ALSO_NOTIFY=
 DESECSTACK_NSMASTER_APIKEY=
 DESECSTACK_NSMASTER_APIKEY=
 DESECSTACK_NSMASTER_CARBONSERVER=
 DESECSTACK_NSMASTER_CARBONSERVER=
 DESECSTACK_NSMASTER_CARBONOURNAME=
 DESECSTACK_NSMASTER_CARBONOURNAME=
+DESECSTACK_NSMASTER_TSIGKEY=
 
 
 # monitoring
 # monitoring
 DESECSTACK_WATCHDOG_SECONDARIES=ns1.example.org ns2.example.net
 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_DB_PASSWORD_pdnsmaster: 9Fn33T5yGukjwelt
   DESECSTACK_NSLORD_APIKEY: 9Fn33T5yGukjekwjew
   DESECSTACK_NSLORD_APIKEY: 9Fn33T5yGukjekwjew
   DESECSTACK_NSLORD_DEFAULT_TTL: 1234
   DESECSTACK_NSLORD_DEFAULT_TTL: 1234
-  DESECSTACK_NSMASTER_APIKEY: LLq1orOQuXCINUz4TV
   DESECSTACK_DBMASTER_PORT: 13306
   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_IPV4_REAR_PREFIX16: 172.16
   DESECSTACK_IPV6_SUBNET: bade:affe:dead:beef:b011::/80
   DESECSTACK_IPV6_SUBNET: bade:affe:dead:beef:b011::/80
   DESECSTACK_IPV6_ADDRESS: bade:affe:dead:beef:b011:0642:ac10:0080
   DESECSTACK_IPV6_ADDRESS: bade:affe:dead:beef:b011:0642:ac10:0080
+  DESECSTACK_PORT_XFR: 12353
   DESECSTACK_WWW_CERTS: ./certs
   DESECSTACK_WWW_CERTS: ./certs
   DESECSTACK_MINIMUM_TTL_DEFAULT: 3600
   DESECSTACK_MINIMUM_TTL_DEFAULT: 3600
   DESECSTACK_PROMETHEUS_PASSWORD: Je9NNkqbULsg
   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.
         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_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_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
     - 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.
       - `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
     - 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
       - `DESECSTACK_NSLORD_DEFAULT_TTL`: TTL to use by default, including for default NS records
     - nsmaster-related
     - nsmaster-related
       - `DESECSTACK_DBMASTER_PASSWORD_pdns`: mysql password for pdns on nsmaster
       - `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_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_CARBONSERVER`: pdns `carbon-server` setting on nsmaster (optional)
       - `DESECSTACK_NSMASTER_CARBONOURNAME`: pdns `carbon-ourname` 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
     - monitoring-related
       - `DESECSTACK_WATCHDOG_SECONDARIES`: space-separated list of secondary hostnames; used to check correct replication of recent DNS changes
       - `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/`
       - `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,
                     'name': self.domain_name_normalized,
                     'kind': 'SLAVE',
                     '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:
   nslord:
     networks:
     networks:
       front:
       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:
     environment:
     - DESECSTACK_NSLORD_CACHE_TTL=0
     - DESECSTACK_NSLORD_CACHE_TTL=0
     # faketime setup
     # faketime setup
@@ -54,6 +54,11 @@ services:
     volumes:
     volumes:
     - faketime:/etc/faketime/:ro
     - faketime:/etc/faketime/:ro
 
 
+  nsmaster:
+    networks:
+      front:
+        ipv4_address: ${DESECSTACK_IPV4_REAR_PREFIX16}.0.130  # make available for test-e2e
+
   test-e2e2:
   test-e2e2:
     build: test/e2e2
     build: test/e2e2
     restart: "no"
     restart: "no"
@@ -64,6 +69,7 @@ services:
     - DESECSTACK_IPV6_SUBNET
     - DESECSTACK_IPV6_SUBNET
     - DESECSTACK_IPV6_ADDRESS
     - DESECSTACK_IPV6_ADDRESS
     - DESECSTACK_NSLORD_DEFAULT_TTL
     - DESECSTACK_NSLORD_DEFAULT_TTL
+    - DESECSTACK_NSMASTER_TSIGKEY
     # faketime setup
     # faketime setup
     - LD_PRELOAD=/lib/libfaketime.so
     - LD_PRELOAD=/lib/libfaketime.so
     - FAKETIME_TIMESTAMP_FILE=/etc/faketime/faketime.rc
     - FAKETIME_TIMESTAMP_FILE=/etc/faketime/faketime.rc

+ 4 - 0
docker-compose.yml

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

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

@@ -1,7 +1,7 @@
 api=yes
 api=yes
 api-key=${DESECSTACK_NSMASTER_APIKEY}
 api-key=${DESECSTACK_NSMASTER_APIKEY}
 allow-axfr-ips=10.8.0.0/24
 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=
 only-notify=
 setgid=pdns
 setgid=pdns
 setuid=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
 # Manage credentials
 envsubst < /etc/powerdns/pdns.conf.var > /etc/powerdns/pdns.conf
 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
 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
                 raise e
             time.sleep(wait)
             time.sleep(wait)
             wait = min(2 * wait, max_pause)
             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,
 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
 import pytest
 
 
 from conftest import DeSECAPIV1Client, return_eventually, query_replication, random_domainname, assert_eventually, \
 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'),
                     lambda: soa_rrsig[name] != query_replication(name, "", 'RRSIG', covers='SOA'),
                     timeout=600,  # depending on number of domains in the database, this value requires increase
                     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,))