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
|
# 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
|
||||||
|
|
||||||
|
|
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
|
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: ...
|
||||||
```
|
```
|
||||||
|
|
|
@ -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)}'
|
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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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}')
|
||||||
|
|
Loading…
Reference in a new issue