Add DANE OPENPGPKEY support
This commit is contained in:
parent
360303d72f
commit
6c27d799e3
13 changed files with 264 additions and 6 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,5 +1,18 @@
|
||||||
# EasyWKS Changelog
|
# EasyWKS Changelog
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.4.0 -->
|
||||||
|
## Version 0.4.0
|
||||||
|
|
||||||
|
Feature release
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.4.0 -->
|
||||||
|
- Add authoritative DNS server providing DANE OPENPGPKEY (TYPE61) DNS records
|
||||||
|
<!-- END CHANGES 0.4.0-->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.4.0 -->
|
||||||
|
|
||||||
<!-- BEGIN RELEASE v0.3.1 -->
|
<!-- BEGIN RELEASE v0.3.1 -->
|
||||||
## Version 0.3.1
|
## Version 0.3.1
|
||||||
|
|
||||||
|
|
76
README.md
76
README.md
|
@ -7,7 +7,7 @@
|
||||||
## What is WKD/WKS?
|
## What is WKD/WKS?
|
||||||
|
|
||||||
Due to all the issues involved with the PGP key servers we're using today, GnuPG introduced a feature named [**Web Key
|
Due to all the issues involved with the PGP key servers we're using today, GnuPG introduced a feature named [**Web Key
|
||||||
Discovery**][wkd] (WKD): Instead of searching for keys on the usual key servers, WKD is taking a **fully**
|
Directory**][wkd] (WKD): Instead of searching for keys on the usual key servers, WKD is taking a **fully**
|
||||||
decentralized and federated approach, where each mail domain is responsible for hosting its users public keys on an
|
decentralized and federated approach, where each mail domain is responsible for hosting its users public keys on an
|
||||||
HTTPS web directory. For example, in order to retrieve the key for `john.doe@example.org`, they key can be located at
|
HTTPS web directory. For example, in order to retrieve the key for `john.doe@example.org`, they key can be located at
|
||||||
|
|
||||||
|
@ -50,6 +50,8 @@ gpg-wks-server to EasyWKS.
|
||||||
- PyYAML
|
- PyYAML
|
||||||
- bottle.py
|
- bottle.py
|
||||||
- PGPy
|
- PGPy
|
||||||
|
- dnspython (for DANE support)
|
||||||
|
- Twisted (for DANE support)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -116,6 +118,11 @@ lmtpd:
|
||||||
host: "::1"
|
host: "::1"
|
||||||
port: 8024
|
port: 8024
|
||||||
|
|
||||||
|
# Configure the authoritative DNS server for DANE zones
|
||||||
|
dnsd:
|
||||||
|
host: "::1"
|
||||||
|
port: 8053
|
||||||
|
|
||||||
# You can override the mail response templates with your own text.
|
# You can override the mail response templates with your own text.
|
||||||
# The following templates can be overridden:
|
# The following templates can be overridden:
|
||||||
# - "header": Placed in front of every message.
|
# - "header": Placed in front of every message.
|
||||||
|
@ -252,6 +259,73 @@ gpgwks@example.org lmtp:localhost:10024
|
||||||
webkey@example.com lmtp:localhost:10024
|
webkey@example.com lmtp:localhost:10024
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### DANE DNS Setup
|
||||||
|
|
||||||
|
Apart from WKD, EasyWKS can also serve PGP keys using RFC7929 DNS records ("OPENPGPKEY" or "TYPE61" records). However,
|
||||||
|
since EasyWKS does not implement DNSSEC signing, it cannot do this alone. The authoritative DNS server in EasyWKS only
|
||||||
|
responds to AXFR zone transfer requests. In order for DANE lookups to work, the zones must be replicated (AXFR'd) by an
|
||||||
|
authoritative secondary nameserver that signs the zones itself.
|
||||||
|
|
||||||
|
#### EasyWKS DNS Server
|
||||||
|
|
||||||
|
Configure EasyWKS to run the DNS server, e.g. using the following systemd unit:
|
||||||
|
|
||||||
|
```unit file (systemd)
|
||||||
|
[Unit]
|
||||||
|
Description=OpenPGP WKS for Human Beings - DANE DNS Server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/path/to/easywks dnsd
|
||||||
|
Restart=on-failure
|
||||||
|
User=webkey
|
||||||
|
Group=webkey
|
||||||
|
WorkingDirectory=/var/lib/easywks
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
domains:
|
||||||
|
example.org:
|
||||||
|
ns:
|
||||||
|
- ns1.example.org.
|
||||||
|
- ns2.example.org.
|
||||||
|
- ns1.example.com.
|
||||||
|
- ns2.example.com.
|
||||||
|
soa:
|
||||||
|
mname: ns1.example.org.
|
||||||
|
rname: dnsadmin.example.org.
|
||||||
|
refresh: 300
|
||||||
|
retry: 60
|
||||||
|
expire: 1209600
|
||||||
|
minimal: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Knot
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
remote:
|
||||||
|
- id: remote-easywks.example.org
|
||||||
|
address: [::1]@10053
|
||||||
|
|
||||||
|
zone:
|
||||||
|
- domain: _openpgpkey.example.org
|
||||||
|
master: remote-easywks.example.org
|
||||||
|
dnssec-signing: on
|
||||||
|
dnssec-policy: ...
|
||||||
|
```
|
||||||
|
|
||||||
## EasyWKS Client
|
## EasyWKS Client
|
||||||
|
|
||||||
The file `client.py` contains a self-contained WKS client, which
|
The file `client.py` contains a self-contained WKS client, which
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
|
|
||||||
__version__ = '0.3.1'
|
__version__ = '0.4.0'
|
||||||
|
|
|
@ -92,6 +92,15 @@ def _validate_lmtpd_config(value):
|
||||||
return f'port must be a int, got {type(value["port"])}'
|
return f'port must be a int, got {type(value["port"])}'
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dnsd_config(value):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return f'must be a map, got {type(value)}'
|
||||||
|
if not isinstance(value['host'], str):
|
||||||
|
return f'host must be a str, got {type(value["host"])}'
|
||||||
|
if not isinstance(value['port'], int):
|
||||||
|
return f'port must be a int, got {type(value["port"])}'
|
||||||
|
|
||||||
|
|
||||||
def _validate_policy_flags(value):
|
def _validate_policy_flags(value):
|
||||||
alphabet = string.ascii_lowercase + string.digits + '-._'
|
alphabet = string.ascii_lowercase + string.digits + '-._'
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
|
@ -119,6 +128,24 @@ def _validate_policy_flags(value):
|
||||||
return f'invalid type {v.__class__.__name__} for flag {flag}'
|
return f'invalid type {v.__class__.__name__} for flag {flag}'
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dane(value):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return f'must be a map, got {type(value)}'
|
||||||
|
if 'soa' in value:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
value['soa'] = {}
|
||||||
|
if 'ns' in value:
|
||||||
|
ns = value['ns']
|
||||||
|
if not isinstance(ns, list):
|
||||||
|
return f'ns must map to a list, got {type(ns)}'
|
||||||
|
for k in ns:
|
||||||
|
if not isinstance(k, str):
|
||||||
|
return f'ns items must be strings, got {type(k)}'
|
||||||
|
else:
|
||||||
|
value['ns'] = ['localhost.']
|
||||||
|
|
||||||
|
|
||||||
def _validate_responses(value):
|
def _validate_responses(value):
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
return f'must be a map, got {type(value)}'
|
return f'must be a map, got {type(value)}'
|
||||||
|
@ -215,7 +242,8 @@ class _GlobalConfig(_Config):
|
||||||
self.__domains[domain] = _Config(
|
self.__domains[domain] = _Config(
|
||||||
submission_address=_ConfigOption('submission_address', str, f'gpgwks@{domain}'),
|
submission_address=_ConfigOption('submission_address', str, f'gpgwks@{domain}'),
|
||||||
passphrase=_ConfigOption('passphrase', str, ''),
|
passphrase=_ConfigOption('passphrase', str, ''),
|
||||||
policy_flags=_ConfigOption('policy_flags', dict, {}, validator=_validate_policy_flags)
|
policy_flags=_ConfigOption('policy_flags', dict, {}, validator=_validate_policy_flags),
|
||||||
|
dane=_ConfigOption('dane', dict, {}, validator=_validate_dane)
|
||||||
)
|
)
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
|
@ -258,6 +286,10 @@ Config = _GlobalConfig(
|
||||||
'host': 'localhost',
|
'host': 'localhost',
|
||||||
'port': 25,
|
'port': 25,
|
||||||
}, validator=_validate_lmtpd_config),
|
}, validator=_validate_lmtpd_config),
|
||||||
|
dnsd=_ConfigOption('dnsd', dict, {
|
||||||
|
'host': '::1',
|
||||||
|
'port': 10053,
|
||||||
|
}, validator=_validate_dnsd_config),
|
||||||
responses=_ConfigOption('responses', dict, {}, validator=_validate_responses),
|
responses=_ConfigOption('responses', dict, {}, validator=_validate_responses),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
89
easywks/dnsd.py
Normal file
89
easywks/dnsd.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .files import read_dane_public_keys
|
||||||
|
|
||||||
|
|
||||||
|
class Record_OPENPGPKEY(tputil.FancyEqMixin, tputil.FancyStrMixin):
|
||||||
|
TYPE = 61
|
||||||
|
fancybasename = 'OPENPGPKEY'
|
||||||
|
compareAttributes = ('data',)
|
||||||
|
showAttributes = ('data',)
|
||||||
|
|
||||||
|
def __init__(self, data=None, ttl=0):
|
||||||
|
self.data = data
|
||||||
|
self.ttl = ttl
|
||||||
|
|
||||||
|
def encode(self, strio, compDict=None):
|
||||||
|
strio.write(self.data)
|
||||||
|
|
||||||
|
def decode(self, strio, length=None):
|
||||||
|
self.data = strio.read(length)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.data)
|
||||||
|
|
||||||
|
|
||||||
|
class DnsServer(common.ResolverBase):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.zones = {}
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
for domain in Config.domains:
|
||||||
|
origin = dns.domainString(f'_openpgpkey.{domain}')
|
||||||
|
self.zones[origin] = domain
|
||||||
|
|
||||||
|
def _make_soa(self, name):
|
||||||
|
domain = self.zones[name]
|
||||||
|
now = int(datetime.utcnow().timestamp()) // 60
|
||||||
|
soa = dns.Record_SOA(mname=Config[domain].dane['soa'].get('mname', 'localhost.'),
|
||||||
|
rname=Config[domain].dane['soa'].get('rname',
|
||||||
|
Config[domain].submission_address.replace('@', '.')),
|
||||||
|
serial=now,
|
||||||
|
refresh=Config[domain].dane['soa'].get('refresh', 300),
|
||||||
|
retry=Config[domain].dane['soa'].get('retry', 60),
|
||||||
|
expire=Config[domain].dane['soa'].get('expire', 2419200),
|
||||||
|
minimum=Config[domain].dane['soa'].get('minimum', 300))
|
||||||
|
return dns.RRHeader(name, dns.SOA, payload=soa, auth=True)
|
||||||
|
|
||||||
|
def _make_ns(self, name):
|
||||||
|
domain = self.zones[name]
|
||||||
|
return [
|
||||||
|
dns.RRHeader(name, dns.NS, payload=dns.Record_NS(host), auth=True)
|
||||||
|
for host in Config[domain].dane['ns']
|
||||||
|
]
|
||||||
|
|
||||||
|
def _lookup(self, name, cls, type, timeout):
|
||||||
|
if type != dns.AXFR:
|
||||||
|
return defer.fail(failure.Failure(error.DNSQueryRefusedError(name)))
|
||||||
|
if name not in self.zones:
|
||||||
|
return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
|
||||||
|
domain = self.zones[name]
|
||||||
|
results = []
|
||||||
|
soa = self._make_soa(name)
|
||||||
|
ns = self._make_ns(name)
|
||||||
|
results.append(soa)
|
||||||
|
results.extend(ns)
|
||||||
|
for digest, key in read_dane_public_keys(domain).items():
|
||||||
|
fqdn = f'{digest}._openpgpkey.{domain}'
|
||||||
|
record = Record_OPENPGPKEY(key)
|
||||||
|
results.append(dns.RRHeader(dns.domainString(fqdn), record.TYPE, payload=record, auth=True))
|
||||||
|
results.append(soa)
|
||||||
|
return defer.succeed((results, [], []))
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
# The "reactor" interface is created dynamically, so the listenTCP and run methods only become available during runtime.
|
||||||
|
def run_dnsd(args):
|
||||||
|
auth = DnsServer()
|
||||||
|
factory = server.DNSServerFactory(authorities=[auth])
|
||||||
|
reactor.listenTCP(Config.dnsd['port'], factory, interface=Config.dnsd['host'])
|
||||||
|
reactor.run()
|
|
@ -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
|
from .util import hash_user_id, armor_keys, split_revoked, dane_digest
|
||||||
|
|
||||||
|
|
||||||
def _locked_read(file: str, binary: bool = False):
|
def _locked_read(file: str, binary: bool = False):
|
||||||
|
@ -58,12 +58,15 @@ def init_working_directory():
|
||||||
os.makedirs(os.path.join(wdir, domain, 'pending'), exist_ok=True)
|
os.makedirs(os.path.join(wdir, domain, 'pending'), exist_ok=True)
|
||||||
_locked_write(os.path.join(wdir, domain, 'submission-address'), make_submission_address_file(domain))
|
_locked_write(os.path.join(wdir, domain, 'submission-address'), make_submission_address_file(domain))
|
||||||
_locked_write(os.path.join(wdir, domain, 'policy'), make_policy_file(domain))
|
_locked_write(os.path.join(wdir, domain, 'policy'), make_policy_file(domain))
|
||||||
|
os.makedirs(os.path.join(wdir, domain, 'dane'), exist_ok=True)
|
||||||
# Create PGP key if it doesn't exist yet
|
# Create PGP key if it doesn't exist yet
|
||||||
create_pgp_key(domain)
|
create_pgp_key(domain)
|
||||||
# Export submission key to hu dir
|
# Export submission key to hu dir
|
||||||
key = privkey_to_pubkey(domain)
|
key = privkey_to_pubkey(domain)
|
||||||
uid = hash_user_id(Config[domain].submission_address)
|
uid = hash_user_id(Config[domain].submission_address)
|
||||||
_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)
|
||||||
|
_locked_write(os.path.join(wdir, domain, 'dane', digest), bytes(key), binary=True)
|
||||||
|
|
||||||
|
|
||||||
def read_public_key(domain, user):
|
def read_public_key(domain, user):
|
||||||
|
@ -77,11 +80,25 @@ def read_hashed_public_key(domain, hu):
|
||||||
return key, revoked
|
return key, revoked
|
||||||
|
|
||||||
|
|
||||||
|
def read_dane_public_keys(domain):
|
||||||
|
path: str = os.path.join(Config.working_directory, domain, 'dane')
|
||||||
|
dane_keys = {}
|
||||||
|
for fname in os.listdir(path):
|
||||||
|
if len(fname) != 56:
|
||||||
|
continue
|
||||||
|
keyfile = os.path.join(path, fname)
|
||||||
|
dane_keys[fname] = _locked_read(keyfile, binary=True)
|
||||||
|
return dane_keys
|
||||||
|
|
||||||
|
|
||||||
def write_public_key(domain, user, key, revoked):
|
def write_public_key(domain, user, key, revoked):
|
||||||
hu = hash_user_id(user)
|
hu = hash_user_id(user)
|
||||||
|
dane = dane_digest(user)
|
||||||
keyfile = os.path.join(Config.working_directory, domain, 'hu', hu)
|
keyfile = os.path.join(Config.working_directory, domain, 'hu', hu)
|
||||||
|
danefile = os.path.join(Config.working_directory, domain, 'dane', dane)
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
def read_pending_key(domain, nonce):
|
def read_pending_key(domain, nonce):
|
||||||
|
|
|
@ -4,6 +4,7 @@ from .files import init_working_directory, clean_stale_requests
|
||||||
from .process import process_mail_from_stdin, process_key_from_stdin
|
from .process import process_mail_from_stdin, process_key_from_stdin
|
||||||
from .httpd import run_server
|
from .httpd import run_server
|
||||||
from .lmtpd import run_lmtpd
|
from .lmtpd import run_lmtpd
|
||||||
|
from .dnsd import run_dnsd
|
||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
@ -32,6 +33,9 @@ def parse_arguments():
|
||||||
server = sp.add_parser('lmtpd', help='Run a LMTP server to receive mails from your MTA. Also see process.')
|
server = sp.add_parser('lmtpd', help='Run a LMTP server to receive mails from your MTA. Also see process.')
|
||||||
server.set_defaults(fn=run_lmtpd)
|
server.set_defaults(fn=run_lmtpd)
|
||||||
|
|
||||||
|
server = sp.add_parser('dnsd', help='Run an authoritative DNS server to provide DANE TYPE61 zones.')
|
||||||
|
server.set_defaults(fn=run_dnsd)
|
||||||
|
|
||||||
imp = sp.add_parser('import', help='Import a public key from stdin directly into the WKD without WKS verification.')
|
imp = sp.add_parser('import', help='Import a public key from stdin directly into the WKD without WKS verification.')
|
||||||
imp.add_argument('--uid', '-u', type=str, action='append',
|
imp.add_argument('--uid', '-u', type=str, action='append',
|
||||||
help='Limit import to a subset of the key\'s UIDs. Can be provided multiple times.')
|
help='Limit import to a subset of the key\'s UIDs. Can be provided multiple times.')
|
||||||
|
|
|
@ -33,6 +33,12 @@ def hash_user_id(uid: str) -> str:
|
||||||
return _zrtp_base32(digest)
|
return _zrtp_base32(digest)
|
||||||
|
|
||||||
|
|
||||||
|
def dane_digest(uid: str) -> str:
|
||||||
|
if '@' in uid:
|
||||||
|
uid, _ = uid.split('@', 1)
|
||||||
|
return hashlib.sha256(uid.encode('utf-8')).hexdigest()[:56]
|
||||||
|
|
||||||
|
|
||||||
def create_nonce(n: int = 32) -> str:
|
def create_nonce(n: int = 32) -> str:
|
||||||
alphabet = string.ascii_letters + string.digits
|
alphabet = string.ascii_letters + string.digits
|
||||||
nonce = ''.join(secrets.choice(alphabet) for _ in range(n))
|
nonce = ''.join(secrets.choice(alphabet) for _ in range(n))
|
||||||
|
|
|
@ -4,7 +4,7 @@ Maintainer: s3lph <1375407-s3lph@users.noreply.gitlab.com>
|
||||||
Section: web
|
Section: web
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends: python3 (>= 3.6), python3-pgpy, python3-bottle, python3-yaml, python3-aiosmtpd
|
Depends: python3 (>= 3.6), python3-pgpy, python3-bottle, python3-yaml, python3-aiosmtpd, python3-dnspython, python3-twisted
|
||||||
Description: OpenPGP WKS for Human Beings
|
Description: OpenPGP WKS for Human Beings
|
||||||
EasyWKS is a drop-in replacement for gpg-wks-server that aims to be
|
EasyWKS is a drop-in replacement for gpg-wks-server that aims to be
|
||||||
much easyier to use manually, while maintaing compatibility with the
|
much easyier to use manually, while maintaing compatibility with the
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
# considered stale and should be removed by easywks clean.
|
# considered stale and should be removed by easywks clean.
|
||||||
#pending_lifetime: 604800
|
#pending_lifetime: 604800
|
||||||
|
|
||||||
# Some clients (including recent versions of gpg-wks-client follow an
|
# Some clients including recent versions of gpg-wks-client follow an
|
||||||
# older version of the WKS standard where signing the confirmation
|
# older version of the WKS standard where signing the confirmation
|
||||||
# response is only recommended, but not required. Set this option to
|
# response is only recommended, but not required. Set this option to
|
||||||
# true if you want to accept such unsigned responses.
|
# true if you want to accept such unsigned responses.
|
||||||
|
@ -46,6 +46,11 @@ lmtpd:
|
||||||
host: "::1"
|
host: "::1"
|
||||||
port: 8024
|
port: 8024
|
||||||
|
|
||||||
|
# Configure the authoritative DNS server for DANE zones
|
||||||
|
dnsd:
|
||||||
|
host: "::1"
|
||||||
|
port: 8053
|
||||||
|
|
||||||
# You can override the mail response templates with your own text.
|
# You can override the mail response templates with your own text.
|
||||||
# The following templates can be overridden:
|
# The following templates can be overridden:
|
||||||
# - "header": Placed in front of every message.
|
# - "header": Placed in front of every message.
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[Unit]
|
||||||
|
Description=OpenPGP WKS for Human Beings - DANE DNS Server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/easywks dnsd
|
||||||
|
Restart=on-failure
|
||||||
|
User=easywks
|
||||||
|
Group=easywks
|
||||||
|
WorkingDirectory=/var/lib/easywks
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -10,4 +10,7 @@ lmtpd:
|
||||||
httpd:
|
httpd:
|
||||||
host: "::"
|
host: "::"
|
||||||
port: 80
|
port: 80
|
||||||
|
dnsd:
|
||||||
|
host: "::"
|
||||||
|
port: 53
|
||||||
domains: {}
|
domains: {}
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -16,8 +16,10 @@ setup(
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'aiosmtpd',
|
'aiosmtpd',
|
||||||
'bottle',
|
'bottle',
|
||||||
|
'dnspython',
|
||||||
'PyYAML',
|
'PyYAML',
|
||||||
'PGPy',
|
'PGPy',
|
||||||
|
'Twisted',
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
|
|
Loading…
Reference in a new issue