letsencrypt dns challenge

This commit is contained in:
mwiegand 2021-11-08 10:49:37 +01:00
parent 34e9366c61
commit fdcfa8a82b
21 changed files with 457 additions and 249 deletions

View file

@ -0,0 +1,62 @@
from ipaddress import ip_interface
@metadata_reactor.provides(
'dns',
)
def acme_records(metadata):
domains = set()
for other_node in repo.nodes:
for domain, conf in other_node.metadata.get('letsencrypt/domains', {}).items():
domains.add(domain)
domains.update(conf.get('aliases', []))
return {
'dns': {
f'_acme-challenge.{domain}': {
'CNAME': {f"{domain}.{metadata.get('bind/acme_zone')}."},
}
for domain in domains
}
}
@metadata_reactor.provides(
'bind/acls/acme',
'bind/keys/acme',
'bind/views/external/zones',
)
def acme_zone(metadata):
allowed_ips = {
str(ip_interface(other_node.metadata.get('network/internal/ipv4')).ip)
for other_node in repo.nodes
if other_node.metadata.get('letsencrypt/domains', {})
}
return {
'bind': {
'acls': {
'acme': {
'key acme',
'!{ !{' + ' '.join(f'{ip};' for ip in sorted(allowed_ips)) + '}; any;}',
},
},
'views': {
'external': {
'keys': {
'acme': {},
},
'zones': {
metadata.get('bind/acme_zone'): {
'allow_update': {
'acme',
},
},
},
},
},
},
}
#https://lists.isc.org/pipermail/bind-users/2006-January/061051.html

View file

@ -11,8 +11,8 @@ $TTL 600
900 ;Negative response caching TTL
)
% for record in sorted(records, key=lambda r: (r['name'], r['type'], r['value'])):
${(record['name'] or '@').ljust(column_width('name', records))} \
% for record in sorted(records, key=lambda r: (tuple(reversed(r['name'].split('.'))), r['type'], r['value'])):
(${(record['name'] or '@').rjust(column_width('name', records))}) \
IN \
${record['type'].ljust(column_width('type', records))} \
% if record['type'] == 'TXT':

View file

@ -1,14 +1,33 @@
% for view in views:
acl "${view['name']}" {
${' '.join(f'{e};' for e in view['acl'])}
# KEYS
% for view_name, view_conf in views.items():
% for key_name, key_conf in sorted(view_conf['keys'].items()):
key "${key_name}" {
algorithm hmac-sha512;
secret "${key_conf['token']}";
};
% endfor
% endfor
# ACLS
% for acl_name, acl_content in acls.items():
acl "${acl_name}" {
% for ac in sorted(acl_content, key=lambda e: (not e.startswith('!'), not e.startswith('key'), e)):
${ac};
% endfor
};
% endfor
% for view in views:
view "${view['name']}" {
match-clients { ${view['name']}; };
# VIEWS
% if view['is_internal']:
% for view_name, view_conf in views.items():
view "${view_name}" {
match-clients {
${view_name};
};
% if view_conf['is_internal']:
recursion yes;
% else:
recursion no;
@ -25,13 +44,20 @@ view "${view['name']}" {
8.8.8.8;
};
% for zone in zones:
zone "${zone}" {
% for zone_name, zone_conf in sorted(view_conf['zones'].items()):
zone "${zone_name}" {
type ${type};
% if type == 'slave':
% if type == 'slave':
masters { ${master_ip}; };
% endif
file "/var/lib/bind/${view['name']}/db.${zone}";
% endif
% if type == 'master' and zone_conf.get('allow_update', False):
allow-update {
% for allow_update in zone_conf['allow_update']:
${allow_update};
% endfor
};
% endif
file "/var/lib/bind/${view_name}/db.${zone_name}";
};
% endfor

View file

@ -3,7 +3,7 @@ options {
dnssec-validation auto;
listen-on-v6 { any; };
allow-query { any; };
allow-query { any; };
max-cache-size 30%;
querylog yes;

View file

@ -2,18 +2,13 @@ from ipaddress import ip_address, ip_interface
from datetime import datetime
if node.metadata.get('bind/type') == 'master':
zones = node.metadata.get('bind/zones')
master_ip = None
slave_ips = [
ip_interface(repo.get_node(slave).metadata.get('network/external/ipv4')).ip
for slave in node.metadata.get('bind/slaves')
]
master_node = node
else:
zones = repo.get_node(node.metadata.get('bind/master_node')).metadata.get('bind/zones')
master_ip = ip_interface(repo.get_node(node.metadata.get('bind/master_node')).metadata.get('network/external/ipv4')).ip
slave_ips = []
master_node = repo.get_node(node.metadata.get('bind/master_node'))
directories[f'/var/lib/bind'] = {
'owner': 'bind',
'group': 'bind',
'purge': True,
'needed_by': [
'svc_systemd:bind9',
@ -46,11 +41,13 @@ files['/etc/bind/named.conf'] = {
'svc_systemd:bind9:restart',
],
}
files['/etc/bind/named.conf.options'] = {
'content_type': 'mako',
'context': {
'type': node.metadata.get('bind/type'),
'slave_ips': sorted(slave_ips),
'slave_ips': node.metadata.get('bind/slave_ips', []),
'master_ip': node.metadata.get('bind/master_ip', None),
},
'owner': 'root',
'group': 'bind',
@ -65,34 +62,22 @@ files['/etc/bind/named.conf.options'] = {
],
}
views = [
{
'name': 'internal',
'is_internal': True,
'acl': [
'127.0.0.1',
'10.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.168.0.0/16',
]
},
{
'name': 'external',
'is_internal': False,
'acl': [
'any',
]
},
]
files['/etc/bind/named.conf.local'] = {
'content_type': 'mako',
'context': {
'type': node.metadata.get('bind/type'),
'master_ip': master_ip,
'views': views,
'zones': sorted(zones),
'master_ip': node.metadata.get('bind/master_ip', None),
'acls': {
**master_node.metadata.get('bind/acls'),
**{
view_name: view_conf['match_clients']
for view_name, view_conf in master_node.metadata.get('bind/views').items()
},
},
'views': dict(sorted(
master_node.metadata.get('bind/views').items(),
key=lambda e: (e[1].get('default', False), e[0]),
)),
},
'owner': 'root',
'group': 'bind',
@ -107,26 +92,10 @@ files['/etc/bind/named.conf.local'] = {
],
}
def record_matches_view(record, records, view):
if record['type'] in ['A', 'AAAA']:
if view == 'external':
# no internal addresses in external view
if ip_address(record['value']).is_private:
return False
elif view == 'internal':
# external addresses in internal view only, if no internal exists
if ip_address(record['value']).is_global:
for other_record in records:
if (
record['name'] == other_record['name'] and
record['type'] == other_record['type'] and
ip_address(other_record['value']).is_private
):
return False
return True
for view in views:
directories[f"/var/lib/bind/{view['name']}"] = {
for view_name, view_conf in master_node.metadata.get('bind/views').items():
directories[f"/var/lib/bind/{view_name}"] = {
'owner': 'bind',
'group': 'bind',
'purge': True,
'needed_by': [
'svc_systemd:bind9',
@ -136,29 +105,12 @@ for view in views:
],
}
for zone, records in zones.items():
unique_records = [
dict(record_tuple)
for record_tuple in set(
tuple(record.items()) for record in records
)
]
files[f"/var/lib/bind/{view['name']}/db.{zone}"] = {
for zone_name, zone_conf in view_conf['zones'].items():
files[f"/var/lib/bind/{view_name}/db.{zone_name}"] = {
'owner': 'bind',
'group': 'bind',
'source': 'db',
'content_type': 'mako',
'context': {
'view': view['name'],
'serial': datetime.now().strftime('%Y%m%d%H'),
'records': list(filter(
lambda record: record_matches_view(record, records, view['name']),
unique_records
)),
'hostname': node.metadata.get('bind/hostname'),
},
'needs': [
f"directory:/var/lib/bind/{view['name']}",
f"directory:/var/lib/bind/{view_name}",
],
'needed_by': [
'svc_systemd:bind9',
@ -167,6 +119,18 @@ for view in views:
'svc_systemd:bind9:restart',
],
}
#FIXME: slave doesnt get updated if db doesnt get rewritten on each apply
files[f"/var/lib/bind/{view_name}/db.{zone_name}"].update({
'source': 'db',
'content_type': 'mako',
'unless': f"test -f /var/lib/bind/{view_name}/db.{zone_name}" if zone_conf.get('allow_update', False) else 'false',
'context': {
'serial': datetime.now().strftime('%Y%m%d%H'),
'records': zone_conf['records'],
'hostname': node.metadata.get('bind/hostname'),
'type': node.metadata.get('bind/type'),
},
})
svc_systemd['bind9'] = {}
@ -175,5 +139,6 @@ actions['named-checkconf'] = {
'unless': 'named-checkconf -z',
'needs': [
'svc_systemd:bind9',
'svc_systemd:bind9:restart',
]
}

View file

@ -1,6 +1,7 @@
from ipaddress import ip_interface
from json import dumps
h = repo.libs.hashable.hashable
repo.libs.bind.repo = repo
defaults = {
'apt': {
@ -9,8 +10,36 @@ defaults = {
},
},
'bind': {
'zones': {},
'slaves': {},
'acls': {
'our-nets': {
'127.0.0.1',
'10.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.168.0.0/16',
}
},
'views': {
'internal': {
'is_internal': True,
'keys': {},
'match_clients': {
'our-nets',
},
'zones': {},
},
'external': {
'default': True,
'is_internal': False,
'keys': {},
'match_clients': {
'any',
},
'zones': {},
},
},
'zones': set(),
},
'telegraf': {
'config': {
@ -28,13 +57,27 @@ defaults = {
@metadata_reactor.provides(
'bind/type',
'bind/master_ip',
'bind/slave_ips',
)
def type(metadata):
return {
'bind': {
'type': 'slave' if metadata.get('bind/master_node', None) else 'master',
def master_slave(metadata):
if metadata.get('bind/master_node', None):
return {
'bind': {
'type': 'slave',
'master_ip': str(ip_interface(repo.get_node(metadata.get('bind/master_node')).metadata.get('network/external/ipv4')).ip),
}
}
else:
return {
'bind': {
'type': 'master',
'slave_ips': {
str(ip_interface(repo.get_node(slave).metadata.get('network/external/ipv4')).ip)
for slave in metadata.get('bind/slaves')
}
}
}
}
@metadata_reactor.provides(
@ -49,47 +92,52 @@ def dns(metadata):
@metadata_reactor.provides(
'bind/zones',
'bind/views',
)
def collect_records(metadata):
if metadata.get('bind/type') == 'slave':
return {}
zones = {}
for other_node in repo.nodes:
for fqdn, records in other_node.metadata.get('dns', {}).items():
matching_zones = sorted(
filter(
lambda potential_zone: fqdn.endswith(potential_zone),
metadata.get('bind/zones').keys()
),
key=len,
)
if matching_zones:
zone = matching_zones[-1]
else:
continue
views = {}
name = fqdn[0:-len(zone) - 1]
for view_name, view_conf in metadata.get('bind/views').items():
for other_node in repo.nodes:
for fqdn, records in other_node.metadata.get('dns', {}).items():
matching_zones = sorted(
filter(
lambda potential_zone: fqdn.endswith(potential_zone),
metadata.get('bind/zones')
),
key=len,
)
if matching_zones:
zone = matching_zones[-1]
else:
continue
for type, values in records.items():
for value in values:
zones\
.setdefault(zone, set())\
.add(
h({'name': name, 'type': type, 'value': value})
)
name = fqdn[0:-len(zone) - 1]
for type, values in records.items():
for value in values:
if repo.libs.bind.record_matches_view(value, type, name, zone, view_name, metadata):
views\
.setdefault(view_name, {})\
.setdefault('zones', {})\
.setdefault(zone, {})\
.setdefault('records', set())\
.add(
h({'name': name, 'type': type, 'value': value})
)
return {
'bind': {
'zones': zones,
'views': views,
},
}
@metadata_reactor.provides(
'bind/zones',
'bind/views',
)
def ns_records(metadata):
if metadata.get('bind/type') == 'slave':
@ -104,12 +152,20 @@ def ns_records(metadata):
]
return {
'bind': {
'zones': {
zone: {
# FIXME: bw currently cant handle lists of dicts :(
h({'name': '@', 'type': 'NS', 'value': f"{nameserver}."})
for nameserver in nameservers
} for zone in metadata.get('bind/zones').keys()
'views': {
view_name: {
'zones': {
zone_name: {
'records': {
# FIXME: bw currently cant handle lists of dicts :(
h({'name': '@', 'type': 'NS', 'value': f"{nameserver}."})
for nameserver in nameservers
}
}
for zone_name, zone_conf in view_conf['zones'].items()
}
}
for view_name, view_conf in metadata.get('bind/views').items()
},
},
}
@ -131,3 +187,65 @@ def slaves(metadata):
],
},
}
@metadata_reactor.provides(
'bind/views',
)
def generate_keys(metadata):
if metadata.get('bind/type') == 'slave':
return {}
return {
'bind': {
'views': {
view_name: {
'keys': {
key: {
'token':repo.libs.hmac.hmac_sha512(
key,
str(repo.vault.random_bytes_as_base64_for(
f"{metadata.get('id')} bind key {key}",
length=32,
)),
)
}
for key in view_conf['keys']
}
}
for view_name, view_conf in metadata.get('bind/views').items()
}
}
}
@metadata_reactor.provides(
'bind/views',
)
def generate_acl_entries_for_keys(metadata):
if metadata.get('bind/type') == 'slave':
return {}
return {
'bind': {
'views': {
view_name: {
'match_clients': {
# allow keys from this view
*{
f'key {key}'
for key in view_conf['keys']
},
# reject keys from other views
*{
f'! key {key}'
for other_view_name, other_view_conf in metadata.get('bind/views').items()
if other_view_name != view_name
for key in other_view_conf.get('keys', [])
}
}
}
for view_name, view_conf in metadata.get('bind/views').items()
},
},
}

View file

@ -0,0 +1,9 @@
https://github.com/dehydrated-io/dehydrated/wiki/example-dns-01-nsupdate-script
```
printf "server 127.0.0.1
zone acme.resolver.name.
update add _acme-challenge.ckn.li.acme.resolver.name. 600 IN TXT "hello"
send
" | nsupdate -y hmac-sha512:acme:Y9BHl85l352BGZDXa/vg90hh2+5PYe4oJxpkq/oQvIODDkW8bAyQSFr0gKQQxjyIOyYlTjf0MGcdWFv46G/3Rg==
```

View file

@ -3,3 +3,4 @@ BASEDIR=/var/lib/dehydrated
WELLKNOWN="${BASEDIR}/acme-challenges"
DOMAINS_TXT="/etc/dehydrated/domains.txt"
HOOK="/etc/dehydrated/hook.sh"
CA="https://acme-v02.api.letsencrypt.org/directory"

View file

@ -1,3 +1,3 @@
% for domain, aliases in sorted(node.metadata.get('letsencrypt/domains', {}).items()):
${domain} ${' '.join(sorted(aliases))}
% for domain, conf in sorted(domains.items()):
${domain} ${' '.join(sorted(conf.get('aliases', [])))}
% endfor

View file

@ -1,37 +1,57 @@
deploy_cert() {<%text>
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"</%text>
% for service, config in node.metadata.get('letsencrypt/concat_and_deploy', {}).items():
set -e
set -u
set -o pipefail
# concat_and_deploy ${service}
if [ "$DOMAIN" = "${config['match_domain']}" ]; then
cat $KEYFILE > ${config['target']}
cat $FULLCHAINFILE >> ${config['target']}
% if 'chown' in config:
chown ${config['chown']} ${config['target']}
% endif
% if 'chmod' in config:
chmod ${config['chmod']} ${config['target']}
% endif
% if 'commands' in config:
% for command in config['commands']:
${command}
% endfor
% endif
fi
% endfor
deploy_challenge() {
echo "
server 10.0.10.2
zone ${zone}.
update add $1.${zone}. 60 IN TXT \"$3\"
send
" | tee | nsupdate -y hmac-sha512:${acme_key_name}:${acme_key}
sleep 10
}
exit_hook() {<%text>
local ERROR="${1:-}"</%text>
% for service in sorted(node.metadata.get('letsencrypt/reload_after', set())):
systemctl reload-or-restart ${service}
% endfor
clean_challenge() {
echo "
server 10.0.10.2
zone ${zone}.
update delete $1.${zone}. TXT
send
" | tee | nsupdate -y hmac-sha512:${acme_key_name}:${acme_key}
}
deploy_cert() {
DOMAIN="$1"
KEYFILE="$2"
CERTFILE="$3"
FULLCHAINFILE="$4"
CHAINFILE="$5"
case $DOMAIN in
% for domain, conf in sorted(domains.items()):
<% if not conf: continue %>\
${domain})
% if conf.get('location', None):
cat "$KEYFILE" > "${conf['location']}/privkey.pem"
cat "$CERTFILE" > "${conf['location']}/cert.pem"
cat "$FULLCHAINFILE" > "${conf['location']}/fullchain.pem"
cat "$CHAINFILE" > "${conf['location']}/chain.pem"
% endif
% if conf.get('owner', None):
chown ${conf['owner']} "${conf['location']}/privkey.pem" "${conf['location']}/cert.pem" "${conf['location']}/fullchain.pem" "${conf['location']}/chain.pem"
% endif
% for service in sorted(conf.get('reload', [])):
systemctl reload-or-restart ${service}
% endfor
;;
% endfor
esac
}
<%text>
HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_cert|exit_hook)$ ]]; then
if [[ $HANDLER =~ ^(deploy_cert|deploy_challenge|clean_challenge)$ ]]
then
"$HANDLER" "$@"
fi</%text>
fi

View file

@ -1,6 +1,9 @@
assert node.has_bundle('nginx')
from ipaddress import ip_interface
delegated = 'delegate_to_node' in node.metadata.get('letsencrypt')
acme_node = repo.get_node(node.metadata.get('letsencrypt/acme_node'))
directories = {
'/etc/dehydrated/conf.d': {},
@ -10,6 +13,9 @@ directories = {
files = {
'/etc/dehydrated/domains.txt': {
'content_type': 'mako',
'context': {
'domains': node.metadata.get('letsencrypt/domains'),
},
'triggers': {
'action:letsencrypt_update_certificates',
},
@ -21,6 +27,16 @@ files = {
},
'/etc/dehydrated/hook.sh': {
'content_type': 'mako',
'context': {
'server': ip_interface(acme_node.metadata.get('network/internal/ipv4')).ip,
'zone': acme_node.metadata.get('bind/acme_zone'),
'acme_key_name': 'acme',
'acme_key': acme_node.metadata.get('bind/views/external/keys/acme/token'),
'domains': node.metadata.get('letsencrypt/domains'),
},
'mode': '0755',
},
'/etc/dehydrated/letsencrypt-ensure-some-certificate': {
'mode': '0755',
},
'/etc/dehydrated/letsencrypt-ensure-some-certificate': {
@ -29,7 +45,7 @@ files = {
}
actions['letsencrypt_update_certificates'] = {
'command': 'dehydrated --cron --accept-terms --challenge http-01',
'command': 'dehydrated --cron --accept-terms --challenge dns-01',
'triggered': True,
'skip': delegated,
'needs': {
@ -48,6 +64,6 @@ for domain in node.metadata.get('letsencrypt/domains').keys():
'svc_systemd:nginx',
},
'triggers': {
'action:letsencrypt_update_certificates',
'action:letsencrypt_update_certificates',
},
}

View file

@ -4,71 +4,17 @@ defaults = {
'apt': {
'packages': {
'dehydrated': {},
'dnsutils': {},
},
},
'letsencrypt': {
'domains': {
# 'example.com': {'alias1.example.com', 'alias2.example.com'},
},
},
'pacman': {
'packages': {
'dehydrated': {},
# 'example.com': {
# 'aliases': {'www.example.com'},
# 'reload': {'nginx'},
# 'owner': 'www-data',
# 'location': '/opt/app/certs',
# },
},
},
}
@metadata_reactor.provides(
'systemd-timers/letsencrypt',
'mirror/certs',
)
def renew(metadata):
delegated_node = metadata.get('letsencrypt/delegate_to_node', False)
if delegated_node:
delegated_ip = ip_interface(repo.get_node(delegated_node).metadata.get('network/internal/ipv4')).ip
return {
'mirror': {
'certs': {
'from': f"{delegated_ip}:/var/lib/dehydrated/certs",
'to': '/var/lib/dehydrated',
},
},
}
else:
return {
'systemd-timers': {
'letsencrypt': {
'command': '/bin/bash -c "/usr/bin/dehydrated --cron --accept-terms --challenge http-01 && /usr/bin/dehydrated --cleanup"',
'when': 'daily',
},
},
}
@metadata_reactor.provides(
'letsencrypt/domains',
'dns',
)
def delegated_domains(metadata):
delegated_domains = {
domain
for other_node in repo.nodes
if other_node.has_bundle('letsencrypt')
and other_node.metadata.get('letsencrypt/delegate_to_node', None) == node.name
for domain in other_node.metadata.get('letsencrypt/domains').keys()
}
return {
'letsencrypt': {
'domains': {
domain: set()
for domain in delegated_domains
},
},
'dns': {
domain: repo.libs.dns.get_a_records(metadata, internal=False)
for domain in delegated_domains
},
}

View file

@ -60,7 +60,9 @@ def letsencrypt(metadata):
return {
'letsencrypt': {
'domains': {
metadata.get('mailserver/hostname'): set(),
metadata.get('mailserver/hostname'): {
'reload': {'dovecot', 'postfix'},
},
},
},
}

View file

@ -84,7 +84,7 @@ def letsencrypt(metadata):
return {
'letsencrypt': {
'domains': {
metadata.get('mosquitto/hostname'): set(),
metadata.get('mosquitto/hostname'): {},
},
},
}

View file

@ -104,10 +104,10 @@ def letsencrypt(metadata):
return {
'letsencrypt': {
'domains': {
domain: set() for domain in metadata.get('nginx/vhosts').keys()
},
'reload_after': {
'nginx',
domain: {
'reload': {'nginx'},
}
for domain in metadata.get('nginx/vhosts').keys()
},
},
}

View file

@ -17,5 +17,8 @@
},
},
},
'letsencrypt': {
'acme_node': 'htz.mails',
},
}
}

29
libs/bind.py Normal file
View file

@ -0,0 +1,29 @@
from ipaddress import ip_address
def _values_from_all_nodes(type, name, zone):
return {
value
for node in repo.nodes
for value in node.metadata.get(f'dns/{name}{"." if name else ""}{zone}/{type}', [])
}
def record_matches_view(value, type, name, zone, view, metadata):
if type not in ['A', 'AAAA']:
return True
if metadata.get(f'bind/views/{view}/is_internal'):
if ip_address(value).is_private:
return True
elif not list(filter(
lambda other_value: ip_address(other_value).is_private,
_values_from_all_nodes(type, name, zone),
)):
return True
else:
if ip_address(value).is_global:
return True
elif not list(filter(
lambda other_value: ip_address(other_value).is_global,
_values_from_all_nodes(type, name, zone),
)):
return True

10
libs/hmac.py Normal file
View file

@ -0,0 +1,10 @@
import hmac, hashlib, base64
def hmac_sha512(secret, iv):
return base64.b64encode(
hmac.new(
bytes(iv , 'latin-1'),
msg=bytes(secret , 'latin-1'),
digestmod=hashlib.sha512
).digest()
).decode()

View file

@ -30,9 +30,6 @@
'gateway4': '10.0.0.1',
},
},
'letsencrypt': {
'delegate_to_node': 'htz.mails',
},
'nginx': {
'vhosts': {
'openhab.ckn.li': {
@ -43,6 +40,11 @@
},
},
},
'letsencrypt': {
'domains': {
'test12.ckn.li': {},
}
},
'java': {
'version': 11,
},

View file

@ -50,9 +50,6 @@
'readonly_token': '!decrypt:encrypt$gAAAAABg3z1-0hnUdzsfivocxhJm58YnPLn96OUvnHiPaehdRhKd6TZBgEPc5YyR07t2-GEUfOvEwoie-O6QsVhWYxrwxNTBXux_iUSx7W6e-fLQA_3MgWf5G97q_3kx_wCgQ6V0iKRyxH988TpNSMACfS4WhCXdSes1CaMpic4VV3S3ox_gCrSHxO7yVXQkJDnOW0MixY5T',
'writeonly_token': '!decrypt:encrypt$gAAAAABg3z6fGrOy2tNdo03RoYAXmpJoJYkfhBfpblPh_wxYfqmdjtABaD7XyV9mSh9xl8oWQlTAtCk9KndVCDQy7BJ-ju7S3HCKJ0k244Y5YKxUnQtqt9fc9nnm8XD-NOJqLKyfy0QhL_I8dFT02pygoJeCUR5NkZcTKf6julb-iGXI6vWcQgolJTYrW643pHObd-Z-vIEl',
},
'letsencrypt': {
'delegate_to_node': 'htz.mails',
},
'mosquitto': {
'hostname': 'mqtt.sublimity.de',
'users': {

View file

@ -10,6 +10,7 @@
'dnsserver',
],
'bundles': [
'bind-acme',
'islamicstate.eu',
'wireguard',
'zfs',
@ -31,18 +32,19 @@
},
'bind': {
'hostname': 'resolver.name',
'acme_zone': 'acme.sublimity.de',
'zones': {
'sublimity.de': {},
'freibrief.net': {},
'nadenau.net': {},
'naeder.net': {},
'rolfwerner.eu': {},
'wettengl.net': {},
'wingl.de': {},
'woodpipe.de': {},
'ckn.li': {},
'islamicstate.eu': {},
'hausamsilberberg.de': {},
'sublimity.de',
'freibrief.net',
'nadenau.net',
'naeder.net',
'rolfwerner.eu',
'wettengl.net',
'wingl.de',
'woodpipe.de',
'ckn.li',
'islamicstate.eu',
'hausamsilberberg.de',
},
},
'dns': {
@ -61,9 +63,9 @@
},
'letsencrypt': {
'domains': {
'ckn.li': set(),
'sublimity.de': set(),
'freibrief.net': set(),
'ckn.li': {},
'sublimity.de': {},
'freibrief.net': {},
},
},
'mailserver': {