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
|
||||
)
|
||||
|
||||
% 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':
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ options {
|
|||
dnssec-validation auto;
|
||||
|
||||
listen-on-v6 { any; };
|
||||
allow-query { any; };
|
||||
allow-query { any; };
|
||||
|
||||
max-cache-size 30%;
|
||||
querylog yes;
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
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"
|
||||
DOMAINS_TXT="/etc/dehydrated/domains.txt"
|
||||
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()):
|
||||
${domain} ${' '.join(sorted(aliases))}
|
||||
% for domain, conf in sorted(domains.items()):
|
||||
${domain} ${' '.join(sorted(conf.get('aliases', [])))}
|
||||
% endfor
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
@ -60,7 +60,9 @@ def letsencrypt(metadata):
|
|||
return {
|
||||
'letsencrypt': {
|
||||
'domains': {
|
||||
metadata.get('mailserver/hostname'): set(),
|
||||
metadata.get('mailserver/hostname'): {
|
||||
'reload': {'dovecot', 'postfix'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ def letsencrypt(metadata):
|
|||
return {
|
||||
'letsencrypt': {
|
||||
'domains': {
|
||||
metadata.get('mosquitto/hostname'): set(),
|
||||
metadata.get('mosquitto/hostname'): {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
'letsencrypt': {
|
||||
'delegate_to_node': 'htz.mails',
|
||||
},
|
||||
'nginx': {
|
||||
'vhosts': {
|
||||
'openhab.ckn.li': {
|
||||
|
@ -43,6 +40,11 @@
|
|||
},
|
||||
},
|
||||
},
|
||||
'letsencrypt': {
|
||||
'domains': {
|
||||
'test12.ckn.li': {},
|
||||
}
|
||||
},
|
||||
'java': {
|
||||
'version': 11,
|
||||
},
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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': {
|
||||
|
|
Loading…
Reference in a new issue