feat: provide dns notify on zone updates

This commit is contained in:
s3lph 2023-04-04 22:59:39 +02:00
parent 625088abcf
commit f6a7c9628b
7 changed files with 69 additions and 5 deletions

View file

@ -1,5 +1,18 @@
# EasyWKS Changelog # EasyWKS Changelog
<!-- BEGIN RELEASE v0.4.2 -->
## Version 0.4.2
Minor feature release
### Changes
<!-- BEGIN CHANGES 0.4.2 -->
- Add option to provide DNS NOTIFY to DANE zone replicas
<!-- END CHANGES 0.4.2-->
<!-- END RELEASE v0.4.2 -->
<!-- BEGIN RELEASE v0.4.1 --> <!-- BEGIN RELEASE v0.4.1 -->
## Version 0.4.1 ## Version 0.4.1

View file

@ -288,7 +288,8 @@ WantedBy=multi-user.target
If you're using EasyWKS' DANE feature, it is highly recommended to configure the SOA and NS records for each domain If you're using EasyWKS' DANE feature, it is highly recommended to configure the SOA and NS records for each domain
you're serving. Generally you want to add NS records for all nameservers that will be serving your zone, and at least you're serving. Generally you want to add NS records for all nameservers that will be serving your zone, and at least
set the MNAME and RNAME components of the SOA record: set the MNAME and RNAME components of the SOA record. You can also configure EasyWKS to provide zone update
notifications whenever a key is modified:
```yaml ```yaml
domains: domains:
@ -298,6 +299,8 @@ domains:
- ns2.example.org. - ns2.example.org.
- ns1.example.com. - ns1.example.com.
- ns2.example.com. - ns2.example.com.
notify:
- "2001:db8::53@10053"
soa: soa:
mname: ns1.example.org. mname: ns1.example.org.
rname: dnsadmin.example.org. rname: dnsadmin.example.org.
@ -312,9 +315,16 @@ domains:
Knot is an authoritative nameserver that supports signing a replicated zone by the secondary (replicating) nameserver. Knot is an authoritative nameserver that supports signing a replicated zone by the secondary (replicating) nameserver.
To configure Knot to transfer the zone from EasyWKS, set up an EasyWKS remote and use it as the replication master for To configure Knot to transfer the zone from EasyWKS, set up an EasyWKS remote and use it as the replication master for
DANE zones. DNSSEC signing must be enabled as well: DANE zones. DNSSEC signing must be enabled as well. If you want Knot to be notified of zone changes, set up a notify
ACL too:
```yaml ```yaml
acl:
- id: acl-easywks.example.org
address: [::1]
action: notify
remote: remote:
- id: remote-easywks.example.org - id: remote-easywks.example.org
address: [::1]@10053 address: [::1]@10053
@ -322,6 +332,7 @@ remote:
zone: zone:
- domain: _openpgpkey.example.org - domain: _openpgpkey.example.org
master: remote-easywks.example.org master: remote-easywks.example.org
acl: acl-easywks.example.org
dnssec-signing: on dnssec-signing: on
dnssec-policy: ... dnssec-policy: ...
``` ```

View file

@ -1,2 +1,2 @@
__version__ = '0.4.1' __version__ = '0.4.2'

View file

@ -144,6 +144,15 @@ def _validate_dane(value):
return f'ns items must be strings, got {type(k)}' return f'ns items must be strings, got {type(k)}'
else: else:
value['ns'] = ['localhost.'] value['ns'] = ['localhost.']
if 'notify' in value:
notify = value['notify']
if not isinstance(notify, list):
return f'notify must map to a list, got {type(notify)}'
for k in notify:
if not isinstance(k, str):
return f'notify items must be strings, got {type(k)}'
else:
value['notify'] = []
def _validate_responses(value): def _validate_responses(value):

View file

@ -1,6 +1,5 @@
from datetime import datetime from datetime import datetime
import dns
from twisted.internet import reactor, defer from twisted.internet import reactor, defer
from twisted.names import dns, server, common, error from twisted.names import dns, server, common, error
from twisted.python import util as tputil, failure from twisted.python import util as tputil, failure

View file

@ -8,7 +8,7 @@ from pgpy import PGPKey
from .config import Config from .config import Config
from .crypto import create_pgp_key, privkey_to_pubkey from .crypto import create_pgp_key, privkey_to_pubkey
from .util import hash_user_id, armor_keys, split_revoked, dane_digest from .util import hash_user_id, armor_keys, split_revoked, dane_digest, dane_notify
def _locked_read(file: str, binary: bool = False): def _locked_read(file: str, binary: bool = False):
@ -67,6 +67,7 @@ def init_working_directory():
_locked_write(os.path.join(wdir, domain, 'hu', uid), bytes(key), binary=True) _locked_write(os.path.join(wdir, domain, 'hu', uid), bytes(key), binary=True)
digest = dane_digest(Config[domain].submission_address) digest = dane_digest(Config[domain].submission_address)
_locked_write(os.path.join(wdir, domain, 'dane', digest), bytes(key), binary=True) _locked_write(os.path.join(wdir, domain, 'dane', digest), bytes(key), binary=True)
dane_notify(domain)
def read_public_key(domain, user): def read_public_key(domain, user):
@ -99,6 +100,7 @@ def write_public_key(domain, user, key, revoked):
joined = bytes(key) + b''.join([bytes(k) for k in revoked]) joined = bytes(key) + b''.join([bytes(k) for k in revoked])
_locked_write(keyfile, joined, binary=True) _locked_write(keyfile, joined, binary=True)
_locked_write(danefile, joined, binary=True) _locked_write(danefile, joined, binary=True)
dane_notify(domain)
def read_pending_key(domain, nonce): def read_pending_key(domain, nonce):

View file

@ -6,10 +6,15 @@ import hashlib
import secrets import secrets
import string import string
import textwrap import textwrap
import logging
from twisted.names import dns
from twisted.internet import reactor, defer
from pgpy import PGPKey from pgpy import PGPKey
from pgpy.constants import SignatureType from pgpy.constants import SignatureType
from .config import Config
def _zrtp_base32(sha1: bytes) -> str: def _zrtp_base32(sha1: bytes) -> str:
# https://datatracker.ietf.org/doc/html/rfc6189#section-5.1.6 # https://datatracker.ietf.org/doc/html/rfc6189#section-5.1.6
@ -83,3 +88,28 @@ def split_revoked(keys: Iterable[PGPKey]) -> Tuple[List[PGPKey], List[PGPKey]]:
break break
key = [k for k in keys if k not in revoked_keys] key = [k for k in keys if k not in revoked_keys]
return key, list(revoked_keys) return key, list(revoked_keys)
def dane_notify(domain: str):
secondaries = Config[domain].dane.get('notify', [])
if len(secondaries) == 0:
return
origin = dns.domainString(f'_openpgpkey.{domain}')
# this is ugly, but has to do for now
for host in secondaries:
try:
if '@' in host:
addr, port = host.split('@', 1)
port = int(port)
else:
addr = host
port = 53
# Bind a v4 or v6 UDP client socket
proto = dns.DNSDatagramProtocol(controller=None)
reactor.listenUDP(0, proto, interface='::' if ':' in addr else '0.0.0.0')
# Assemble and send NOTIFY message
m = dns.Message(proto.pickID(), opCode=dns.OP_NOTIFY, auth=1)
m.queries = [dns.Query(origin, dns.SOA, dns.IN)]
proto.writeMessage(m, (addr, port))
except Exception:
logging.exception(f'An error occurred while attempting to notify {host}')