feat: provide dns notify on zone updates
This commit is contained in:
parent
625088abcf
commit
f6a7c9628b
7 changed files with 69 additions and 5 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,5 +1,18 @@
|
|||
# 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 -->
|
||||
## Version 0.4.1
|
||||
|
||||
|
|
15
README.md
15
README.md
|
@ -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
|
||||
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
|
||||
domains:
|
||||
|
@ -298,6 +299,8 @@ domains:
|
|||
- ns2.example.org.
|
||||
- ns1.example.com.
|
||||
- ns2.example.com.
|
||||
notify:
|
||||
- "2001:db8::53@10053"
|
||||
soa:
|
||||
mname: ns1.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.
|
||||
|
||||
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
|
||||
|
||||
acl:
|
||||
- id: acl-easywks.example.org
|
||||
address: [::1]
|
||||
action: notify
|
||||
|
||||
remote:
|
||||
- id: remote-easywks.example.org
|
||||
address: [::1]@10053
|
||||
|
@ -322,6 +332,7 @@ remote:
|
|||
zone:
|
||||
- domain: _openpgpkey.example.org
|
||||
master: remote-easywks.example.org
|
||||
acl: acl-easywks.example.org
|
||||
dnssec-signing: on
|
||||
dnssec-policy: ...
|
||||
```
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
|
||||
__version__ = '0.4.1'
|
||||
__version__ = '0.4.2'
|
||||
|
|
|
@ -144,6 +144,15 @@ def _validate_dane(value):
|
|||
return f'ns items must be strings, got {type(k)}'
|
||||
else:
|
||||
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):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from datetime import datetime
|
||||
|
||||
import dns
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.names import dns, server, common, error
|
||||
from twisted.python import util as tputil, failure
|
||||
|
|
|
@ -8,7 +8,7 @@ from pgpy import PGPKey
|
|||
|
||||
from .config import Config
|
||||
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):
|
||||
|
@ -67,6 +67,7 @@ def init_working_directory():
|
|||
_locked_write(os.path.join(wdir, domain, 'hu', uid), bytes(key), binary=True)
|
||||
digest = dane_digest(Config[domain].submission_address)
|
||||
_locked_write(os.path.join(wdir, domain, 'dane', digest), bytes(key), binary=True)
|
||||
dane_notify(domain)
|
||||
|
||||
|
||||
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])
|
||||
_locked_write(keyfile, joined, binary=True)
|
||||
_locked_write(danefile, joined, binary=True)
|
||||
dane_notify(domain)
|
||||
|
||||
|
||||
def read_pending_key(domain, nonce):
|
||||
|
|
|
@ -6,10 +6,15 @@ import hashlib
|
|||
import secrets
|
||||
import string
|
||||
import textwrap
|
||||
import logging
|
||||
|
||||
from twisted.names import dns
|
||||
from twisted.internet import reactor, defer
|
||||
from pgpy import PGPKey
|
||||
from pgpy.constants import SignatureType
|
||||
|
||||
from .config import Config
|
||||
|
||||
|
||||
def _zrtp_base32(sha1: bytes) -> str:
|
||||
# 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
|
||||
key = [k for k in keys if k not in 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}')
|
||||
|
|
Loading…
Reference in a new issue