letsencrypt dns challenge
This commit is contained in:
parent
34e9366c61
commit
fdcfa8a82b
21 changed files with 457 additions and 249 deletions
62
bundles/bind-acme/metadata.py
Normal file
62
bundles/bind-acme/metadata.py
Normal 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
|
|
@ -11,8 +11,8 @@ $TTL 600
|
||||||
900 ;Negative response caching TTL
|
900 ;Negative response caching TTL
|
||||||
)
|
)
|
||||||
|
|
||||||
% for record in sorted(records, key=lambda r: (r['name'], r['type'], r['value'])):
|
% for record in sorted(records, key=lambda r: (tuple(reversed(r['name'].split('.'))), r['type'], r['value'])):
|
||||||
${(record['name'] or '@').ljust(column_width('name', records))} \
|
(${(record['name'] or '@').rjust(column_width('name', records))}) \
|
||||||
IN \
|
IN \
|
||||||
${record['type'].ljust(column_width('type', records))} \
|
${record['type'].ljust(column_width('type', records))} \
|
||||||
% if record['type'] == 'TXT':
|
% if record['type'] == 'TXT':
|
||||||
|
|
|
@ -1,14 +1,33 @@
|
||||||
% for view in views:
|
# KEYS
|
||||||
acl "${view['name']}" {
|
|
||||||
${' '.join(f'{e};' for e in view['acl'])}
|
% 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
|
% endfor
|
||||||
|
|
||||||
% for view in views:
|
# VIEWS
|
||||||
view "${view['name']}" {
|
|
||||||
match-clients { ${view['name']}; };
|
|
||||||
|
|
||||||
% 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;
|
recursion yes;
|
||||||
% else:
|
% else:
|
||||||
recursion no;
|
recursion no;
|
||||||
|
@ -25,13 +44,20 @@ view "${view['name']}" {
|
||||||
8.8.8.8;
|
8.8.8.8;
|
||||||
};
|
};
|
||||||
|
|
||||||
% for zone in zones:
|
% for zone_name, zone_conf in sorted(view_conf['zones'].items()):
|
||||||
zone "${zone}" {
|
zone "${zone_name}" {
|
||||||
type ${type};
|
type ${type};
|
||||||
% if type == 'slave':
|
% if type == 'slave':
|
||||||
masters { ${master_ip}; };
|
masters { ${master_ip}; };
|
||||||
% endif
|
% endif
|
||||||
file "/var/lib/bind/${view['name']}/db.${zone}";
|
% 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
|
% endfor
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ options {
|
||||||
dnssec-validation auto;
|
dnssec-validation auto;
|
||||||
|
|
||||||
listen-on-v6 { any; };
|
listen-on-v6 { any; };
|
||||||
allow-query { any; };
|
allow-query { any; };
|
||||||
|
|
||||||
max-cache-size 30%;
|
max-cache-size 30%;
|
||||||
querylog yes;
|
querylog yes;
|
||||||
|
|
|
@ -2,18 +2,13 @@ from ipaddress import ip_address, ip_interface
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
if node.metadata.get('bind/type') == 'master':
|
if node.metadata.get('bind/type') == 'master':
|
||||||
zones = node.metadata.get('bind/zones')
|
master_node = node
|
||||||
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')
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
zones = repo.get_node(node.metadata.get('bind/master_node')).metadata.get('bind/zones')
|
master_node = repo.get_node(node.metadata.get('bind/master_node'))
|
||||||
master_ip = ip_interface(repo.get_node(node.metadata.get('bind/master_node')).metadata.get('network/external/ipv4')).ip
|
|
||||||
slave_ips = []
|
|
||||||
|
|
||||||
directories[f'/var/lib/bind'] = {
|
directories[f'/var/lib/bind'] = {
|
||||||
|
'owner': 'bind',
|
||||||
|
'group': 'bind',
|
||||||
'purge': True,
|
'purge': True,
|
||||||
'needed_by': [
|
'needed_by': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
|
@ -46,11 +41,13 @@ files['/etc/bind/named.conf'] = {
|
||||||
'svc_systemd:bind9:restart',
|
'svc_systemd:bind9:restart',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
files['/etc/bind/named.conf.options'] = {
|
files['/etc/bind/named.conf.options'] = {
|
||||||
'content_type': 'mako',
|
'content_type': 'mako',
|
||||||
'context': {
|
'context': {
|
||||||
'type': node.metadata.get('bind/type'),
|
'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',
|
'owner': 'root',
|
||||||
'group': 'bind',
|
'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'] = {
|
files['/etc/bind/named.conf.local'] = {
|
||||||
'content_type': 'mako',
|
'content_type': 'mako',
|
||||||
'context': {
|
'context': {
|
||||||
'type': node.metadata.get('bind/type'),
|
'type': node.metadata.get('bind/type'),
|
||||||
'master_ip': master_ip,
|
'master_ip': node.metadata.get('bind/master_ip', None),
|
||||||
'views': views,
|
'acls': {
|
||||||
'zones': sorted(zones),
|
**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',
|
'owner': 'root',
|
||||||
'group': 'bind',
|
'group': 'bind',
|
||||||
|
@ -107,26 +92,10 @@ files['/etc/bind/named.conf.local'] = {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def record_matches_view(record, records, view):
|
for view_name, view_conf in master_node.metadata.get('bind/views').items():
|
||||||
if record['type'] in ['A', 'AAAA']:
|
directories[f"/var/lib/bind/{view_name}"] = {
|
||||||
if view == 'external':
|
'owner': 'bind',
|
||||||
# no internal addresses in external view
|
'group': 'bind',
|
||||||
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']}"] = {
|
|
||||||
'purge': True,
|
'purge': True,
|
||||||
'needed_by': [
|
'needed_by': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
|
@ -136,29 +105,12 @@ for view in views:
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
for zone, records in zones.items():
|
for zone_name, zone_conf in view_conf['zones'].items():
|
||||||
unique_records = [
|
files[f"/var/lib/bind/{view_name}/db.{zone_name}"] = {
|
||||||
dict(record_tuple)
|
'owner': 'bind',
|
||||||
for record_tuple in set(
|
|
||||||
tuple(record.items()) for record in records
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
files[f"/var/lib/bind/{view['name']}/db.{zone}"] = {
|
|
||||||
'group': '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': [
|
'needs': [
|
||||||
f"directory:/var/lib/bind/{view['name']}",
|
f"directory:/var/lib/bind/{view_name}",
|
||||||
],
|
],
|
||||||
'needed_by': [
|
'needed_by': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
|
@ -167,6 +119,18 @@ for view in views:
|
||||||
'svc_systemd:bind9:restart',
|
'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'] = {}
|
svc_systemd['bind9'] = {}
|
||||||
|
|
||||||
|
@ -175,5 +139,6 @@ actions['named-checkconf'] = {
|
||||||
'unless': 'named-checkconf -z',
|
'unless': 'named-checkconf -z',
|
||||||
'needs': [
|
'needs': [
|
||||||
'svc_systemd:bind9',
|
'svc_systemd:bind9',
|
||||||
|
'svc_systemd:bind9:restart',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from ipaddress import ip_interface
|
from ipaddress import ip_interface
|
||||||
from json import dumps
|
from json import dumps
|
||||||
h = repo.libs.hashable.hashable
|
h = repo.libs.hashable.hashable
|
||||||
|
repo.libs.bind.repo = repo
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
'apt': {
|
'apt': {
|
||||||
|
@ -9,8 +10,36 @@ defaults = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'bind': {
|
'bind': {
|
||||||
'zones': {},
|
|
||||||
'slaves': {},
|
'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': {
|
'telegraf': {
|
||||||
'config': {
|
'config': {
|
||||||
|
@ -28,13 +57,27 @@ defaults = {
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'bind/type',
|
'bind/type',
|
||||||
|
'bind/master_ip',
|
||||||
|
'bind/slave_ips',
|
||||||
)
|
)
|
||||||
def type(metadata):
|
def master_slave(metadata):
|
||||||
return {
|
if metadata.get('bind/master_node', None):
|
||||||
'bind': {
|
return {
|
||||||
'type': 'slave' if metadata.get('bind/master_node', None) else 'master',
|
'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(
|
@metadata_reactor.provides(
|
||||||
|
@ -49,47 +92,52 @@ def dns(metadata):
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'bind/zones',
|
'bind/views',
|
||||||
)
|
)
|
||||||
def collect_records(metadata):
|
def collect_records(metadata):
|
||||||
if metadata.get('bind/type') == 'slave':
|
if metadata.get('bind/type') == 'slave':
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
zones = {}
|
views = {}
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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():
|
name = fqdn[0:-len(zone) - 1]
|
||||||
for value in values:
|
|
||||||
zones\
|
for type, values in records.items():
|
||||||
.setdefault(zone, set())\
|
for value in values:
|
||||||
.add(
|
if repo.libs.bind.record_matches_view(value, type, name, zone, view_name, metadata):
|
||||||
h({'name': name, 'type': type, 'value': value})
|
views\
|
||||||
)
|
.setdefault(view_name, {})\
|
||||||
|
.setdefault('zones', {})\
|
||||||
|
.setdefault(zone, {})\
|
||||||
|
.setdefault('records', set())\
|
||||||
|
.add(
|
||||||
|
h({'name': name, 'type': type, 'value': value})
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'bind': {
|
'bind': {
|
||||||
'zones': zones,
|
'views': views,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@metadata_reactor.provides(
|
@metadata_reactor.provides(
|
||||||
'bind/zones',
|
'bind/views',
|
||||||
)
|
)
|
||||||
def ns_records(metadata):
|
def ns_records(metadata):
|
||||||
if metadata.get('bind/type') == 'slave':
|
if metadata.get('bind/type') == 'slave':
|
||||||
|
@ -104,12 +152,20 @@ def ns_records(metadata):
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
'bind': {
|
'bind': {
|
||||||
'zones': {
|
'views': {
|
||||||
zone: {
|
view_name: {
|
||||||
# FIXME: bw currently cant handle lists of dicts :(
|
'zones': {
|
||||||
h({'name': '@', 'type': 'NS', 'value': f"{nameserver}."})
|
zone_name: {
|
||||||
for nameserver in nameservers
|
'records': {
|
||||||
} for zone in metadata.get('bind/zones').keys()
|
# 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()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
9
bundles/letsencrypt/README.md
Normal file
9
bundles/letsencrypt/README.md
Normal 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==
|
||||||
|
```
|
|
@ -3,3 +3,4 @@ BASEDIR=/var/lib/dehydrated
|
||||||
WELLKNOWN="${BASEDIR}/acme-challenges"
|
WELLKNOWN="${BASEDIR}/acme-challenges"
|
||||||
DOMAINS_TXT="/etc/dehydrated/domains.txt"
|
DOMAINS_TXT="/etc/dehydrated/domains.txt"
|
||||||
HOOK="/etc/dehydrated/hook.sh"
|
HOOK="/etc/dehydrated/hook.sh"
|
||||||
|
CA="https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
% for domain, aliases in sorted(node.metadata.get('letsencrypt/domains', {}).items()):
|
% for domain, conf in sorted(domains.items()):
|
||||||
${domain} ${' '.join(sorted(aliases))}
|
${domain} ${' '.join(sorted(conf.get('aliases', [])))}
|
||||||
% endfor
|
% endfor
|
||||||
|
|
|
@ -1,37 +1,57 @@
|
||||||
deploy_cert() {<%text>
|
set -e
|
||||||
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"</%text>
|
set -u
|
||||||
% for service, config in node.metadata.get('letsencrypt/concat_and_deploy', {}).items():
|
set -o pipefail
|
||||||
|
|
||||||
# concat_and_deploy ${service}
|
deploy_challenge() {
|
||||||
if [ "$DOMAIN" = "${config['match_domain']}" ]; then
|
echo "
|
||||||
cat $KEYFILE > ${config['target']}
|
server 10.0.10.2
|
||||||
cat $FULLCHAINFILE >> ${config['target']}
|
zone ${zone}.
|
||||||
% if 'chown' in config:
|
update add $1.${zone}. 60 IN TXT \"$3\"
|
||||||
chown ${config['chown']} ${config['target']}
|
send
|
||||||
% endif
|
" | tee | nsupdate -y hmac-sha512:${acme_key_name}:${acme_key}
|
||||||
% if 'chmod' in config:
|
|
||||||
chmod ${config['chmod']} ${config['target']}
|
sleep 10
|
||||||
% endif
|
|
||||||
% if 'commands' in config:
|
|
||||||
% for command in config['commands']:
|
|
||||||
${command}
|
|
||||||
% endfor
|
|
||||||
% endif
|
|
||||||
fi
|
|
||||||
% endfor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clean_challenge() {
|
||||||
exit_hook() {<%text>
|
echo "
|
||||||
local ERROR="${1:-}"</%text>
|
server 10.0.10.2
|
||||||
|
zone ${zone}.
|
||||||
% for service in sorted(node.metadata.get('letsencrypt/reload_after', set())):
|
update delete $1.${zone}. TXT
|
||||||
systemctl reload-or-restart ${service}
|
send
|
||||||
% endfor
|
" | 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
|
HANDLER="$1"; shift
|
||||||
if [[ "${HANDLER}" =~ ^(deploy_cert|exit_hook)$ ]]; then
|
if [[ $HANDLER =~ ^(deploy_cert|deploy_challenge|clean_challenge)$ ]]
|
||||||
|
then
|
||||||
"$HANDLER" "$@"
|
"$HANDLER" "$@"
|
||||||
fi</%text>
|
fi
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
assert node.has_bundle('nginx')
|
assert node.has_bundle('nginx')
|
||||||
|
|
||||||
|
from ipaddress import ip_interface
|
||||||
|
|
||||||
delegated = 'delegate_to_node' in node.metadata.get('letsencrypt')
|
delegated = 'delegate_to_node' in node.metadata.get('letsencrypt')
|
||||||
|
acme_node = repo.get_node(node.metadata.get('letsencrypt/acme_node'))
|
||||||
|
|
||||||
directories = {
|
directories = {
|
||||||
'/etc/dehydrated/conf.d': {},
|
'/etc/dehydrated/conf.d': {},
|
||||||
|
@ -10,6 +13,9 @@ directories = {
|
||||||
files = {
|
files = {
|
||||||
'/etc/dehydrated/domains.txt': {
|
'/etc/dehydrated/domains.txt': {
|
||||||
'content_type': 'mako',
|
'content_type': 'mako',
|
||||||
|
'context': {
|
||||||
|
'domains': node.metadata.get('letsencrypt/domains'),
|
||||||
|
},
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'action:letsencrypt_update_certificates',
|
'action:letsencrypt_update_certificates',
|
||||||
},
|
},
|
||||||
|
@ -21,6 +27,16 @@ files = {
|
||||||
},
|
},
|
||||||
'/etc/dehydrated/hook.sh': {
|
'/etc/dehydrated/hook.sh': {
|
||||||
'content_type': 'mako',
|
'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',
|
'mode': '0755',
|
||||||
},
|
},
|
||||||
'/etc/dehydrated/letsencrypt-ensure-some-certificate': {
|
'/etc/dehydrated/letsencrypt-ensure-some-certificate': {
|
||||||
|
@ -29,7 +45,7 @@ files = {
|
||||||
}
|
}
|
||||||
|
|
||||||
actions['letsencrypt_update_certificates'] = {
|
actions['letsencrypt_update_certificates'] = {
|
||||||
'command': 'dehydrated --cron --accept-terms --challenge http-01',
|
'command': 'dehydrated --cron --accept-terms --challenge dns-01',
|
||||||
'triggered': True,
|
'triggered': True,
|
||||||
'skip': delegated,
|
'skip': delegated,
|
||||||
'needs': {
|
'needs': {
|
||||||
|
@ -48,6 +64,6 @@ for domain in node.metadata.get('letsencrypt/domains').keys():
|
||||||
'svc_systemd:nginx',
|
'svc_systemd:nginx',
|
||||||
},
|
},
|
||||||
'triggers': {
|
'triggers': {
|
||||||
'action:letsencrypt_update_certificates',
|
'action:letsencrypt_update_certificates',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,71 +4,17 @@ defaults = {
|
||||||
'apt': {
|
'apt': {
|
||||||
'packages': {
|
'packages': {
|
||||||
'dehydrated': {},
|
'dehydrated': {},
|
||||||
|
'dnsutils': {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'letsencrypt': {
|
'letsencrypt': {
|
||||||
'domains': {
|
'domains': {
|
||||||
# 'example.com': {'alias1.example.com', 'alias2.example.com'},
|
# 'example.com': {
|
||||||
},
|
# 'aliases': {'www.example.com'},
|
||||||
},
|
# 'reload': {'nginx'},
|
||||||
'pacman': {
|
# 'owner': 'www-data',
|
||||||
'packages': {
|
# 'location': '/opt/app/certs',
|
||||||
'dehydrated': {},
|
# },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
@ -60,7 +60,9 @@ def letsencrypt(metadata):
|
||||||
return {
|
return {
|
||||||
'letsencrypt': {
|
'letsencrypt': {
|
||||||
'domains': {
|
'domains': {
|
||||||
metadata.get('mailserver/hostname'): set(),
|
metadata.get('mailserver/hostname'): {
|
||||||
|
'reload': {'dovecot', 'postfix'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ def letsencrypt(metadata):
|
||||||
return {
|
return {
|
||||||
'letsencrypt': {
|
'letsencrypt': {
|
||||||
'domains': {
|
'domains': {
|
||||||
metadata.get('mosquitto/hostname'): set(),
|
metadata.get('mosquitto/hostname'): {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,10 +104,10 @@ def letsencrypt(metadata):
|
||||||
return {
|
return {
|
||||||
'letsencrypt': {
|
'letsencrypt': {
|
||||||
'domains': {
|
'domains': {
|
||||||
domain: set() for domain in metadata.get('nginx/vhosts').keys()
|
domain: {
|
||||||
},
|
'reload': {'nginx'},
|
||||||
'reload_after': {
|
}
|
||||||
'nginx',
|
for domain in metadata.get('nginx/vhosts').keys()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,8 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'letsencrypt': {
|
||||||
|
'acme_node': 'htz.mails',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
libs/bind.py
Normal file
29
libs/bind.py
Normal 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
10
libs/hmac.py
Normal 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()
|
|
@ -30,9 +30,6 @@
|
||||||
'gateway4': '10.0.0.1',
|
'gateway4': '10.0.0.1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'letsencrypt': {
|
|
||||||
'delegate_to_node': 'htz.mails',
|
|
||||||
},
|
|
||||||
'nginx': {
|
'nginx': {
|
||||||
'vhosts': {
|
'vhosts': {
|
||||||
'openhab.ckn.li': {
|
'openhab.ckn.li': {
|
||||||
|
@ -43,6 +40,11 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'letsencrypt': {
|
||||||
|
'domains': {
|
||||||
|
'test12.ckn.li': {},
|
||||||
|
}
|
||||||
|
},
|
||||||
'java': {
|
'java': {
|
||||||
'version': 11,
|
'version': 11,
|
||||||
},
|
},
|
||||||
|
|
|
@ -50,9 +50,6 @@
|
||||||
'readonly_token': '!decrypt:encrypt$gAAAAABg3z1-0hnUdzsfivocxhJm58YnPLn96OUvnHiPaehdRhKd6TZBgEPc5YyR07t2-GEUfOvEwoie-O6QsVhWYxrwxNTBXux_iUSx7W6e-fLQA_3MgWf5G97q_3kx_wCgQ6V0iKRyxH988TpNSMACfS4WhCXdSes1CaMpic4VV3S3ox_gCrSHxO7yVXQkJDnOW0MixY5T',
|
'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',
|
'writeonly_token': '!decrypt:encrypt$gAAAAABg3z6fGrOy2tNdo03RoYAXmpJoJYkfhBfpblPh_wxYfqmdjtABaD7XyV9mSh9xl8oWQlTAtCk9KndVCDQy7BJ-ju7S3HCKJ0k244Y5YKxUnQtqt9fc9nnm8XD-NOJqLKyfy0QhL_I8dFT02pygoJeCUR5NkZcTKf6julb-iGXI6vWcQgolJTYrW643pHObd-Z-vIEl',
|
||||||
},
|
},
|
||||||
'letsencrypt': {
|
|
||||||
'delegate_to_node': 'htz.mails',
|
|
||||||
},
|
|
||||||
'mosquitto': {
|
'mosquitto': {
|
||||||
'hostname': 'mqtt.sublimity.de',
|
'hostname': 'mqtt.sublimity.de',
|
||||||
'users': {
|
'users': {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
'dnsserver',
|
'dnsserver',
|
||||||
],
|
],
|
||||||
'bundles': [
|
'bundles': [
|
||||||
|
'bind-acme',
|
||||||
'islamicstate.eu',
|
'islamicstate.eu',
|
||||||
'wireguard',
|
'wireguard',
|
||||||
'zfs',
|
'zfs',
|
||||||
|
@ -31,18 +32,19 @@
|
||||||
},
|
},
|
||||||
'bind': {
|
'bind': {
|
||||||
'hostname': 'resolver.name',
|
'hostname': 'resolver.name',
|
||||||
|
'acme_zone': 'acme.sublimity.de',
|
||||||
'zones': {
|
'zones': {
|
||||||
'sublimity.de': {},
|
'sublimity.de',
|
||||||
'freibrief.net': {},
|
'freibrief.net',
|
||||||
'nadenau.net': {},
|
'nadenau.net',
|
||||||
'naeder.net': {},
|
'naeder.net',
|
||||||
'rolfwerner.eu': {},
|
'rolfwerner.eu',
|
||||||
'wettengl.net': {},
|
'wettengl.net',
|
||||||
'wingl.de': {},
|
'wingl.de',
|
||||||
'woodpipe.de': {},
|
'woodpipe.de',
|
||||||
'ckn.li': {},
|
'ckn.li',
|
||||||
'islamicstate.eu': {},
|
'islamicstate.eu',
|
||||||
'hausamsilberberg.de': {},
|
'hausamsilberberg.de',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'dns': {
|
'dns': {
|
||||||
|
@ -61,9 +63,9 @@
|
||||||
},
|
},
|
||||||
'letsencrypt': {
|
'letsencrypt': {
|
||||||
'domains': {
|
'domains': {
|
||||||
'ckn.li': set(),
|
'ckn.li': {},
|
||||||
'sublimity.de': set(),
|
'sublimity.de': {},
|
||||||
'freibrief.net': set(),
|
'freibrief.net': {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'mailserver': {
|
'mailserver': {
|
||||||
|
|
Loading…
Reference in a new issue