]> git.siccegge.de Git - tools.git/blob - tls/addrecord
rebuild actual zonefiles
[tools.git] / tls / addrecord
1 #!/usr/bin/python3
2 #
3 # ssh-based server to allow hosts to update their own TLS-related RRs
4 # intended to be used like
5 # command="/srv/tls/bin/addrecord hepworth.siccegge.de" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOIlLx3R+Q5LgBZbJ6USuzam/uAEQITl6vzOn/ylk4fq christoph@mitoraj
6
7 import os
8 import sys
9 import re
10 import datetime
11 import fcntl
12
13 import yaml
14
15 class Addrecord:
16 def __init__(self, host):
17 self._host = host
18 self._ports = {'www': [443],
19 'smtp': [25, 587, 465],
20 'imap': [993, 143],
21 'pop': [110, 995],
22 'xmpp': [5222, 5269],
23 }
24 self._tlsa_re = re.compile('TLSA 3 1 1 [0-9a-f]{64}')
25 self._acme_re = re.compile(r'[a-zA-Z0-9_-]{43}')
26 with open('inventory.yaml') as inv:
27 self._inventory = yaml.load(inv)
28
29
30 def tlsa(self):
31 host = input("Hostname: ")
32 service = input("Service: ")
33 value = input("Value: ")
34 if host not in self._inventory[self._host][service]:
35 sys.stderr.write("Not authorized to update entries for service '%s' on '%s'\n"
36 % (service, host))
37 return 1
38
39 if re.fullmatch(self._tlsa_re, value) is None:
40 sys.stderr.write("Not a valid TLSA record: '%s'\n" % value)
41 return 2
42
43 records = []
44 for port in self._ports[service]:
45 records.append("{0:<35s}\t{1}\n".format("_%d._tcp.%s" % (port, host),
46 value))
47 self._update_records('tlsa', records)
48 print("OK")
49 return 0
50
51
52 def acme(self):
53 host = input("Hostname: ")
54 value = input("Value: ")
55 allowed_hosts = set()
56 for value in self._inventory[self._host].values():
57 allowed_hosts = allowed_hosts.union(value)
58
59 if host not in allowed_hosts:
60 sys.stderr.write("Not authorized to update entries for host '%s'\n" % host)
61 return 1
62
63 if re.fullmatch(self._acme_re, value) is None:
64 sys.stderr.write("Not a valid ACME challenge record: '%s'\n" % value)
65 return 2
66
67 records = [ "{0:<35s}\t{1}\n".format("_acme-challenge.%s" % host, value) ]
68 self._update_records('acme', records)
69 print("OK")
70 return 0
71
72
73 def _update_records(self, sort, records):
74 to_remove = [ i.split()[0] for i in records ]
75
76 with open('%s/%s.m4.lock' % (sort, self._host), 'w') as lockfd:
77 fcntl.flock(lockfd, fcntl.LOCK_EX)
78 with open('%s/%s.m4' % (sort, self._host), 'r') as oldzone:
79 lines = oldzone.readlines()
80 lines = [ line for line in lines
81 if line == '\n' or line.split()[0] not in to_remove ]
82
83 lines.append('\n')
84 lines.append("; Last updated %s by %s\n"
85 % (datetime.datetime.utcnow().isoformat(),
86 self._host))
87 lines = lines + records
88
89 with open('%s/%s.m4.new' % (sort, self._host), 'w') as newzone:
90 newtext = ''.join(lines)
91 newtext = re.sub(r'\n[\n]+', '\n\n', newtext)
92 newtext = re.sub(r'\n;.*\n\n;', '\n;', newtext)
93 newzone.write(newtext)
94
95 os.rename('%s/%s.m4.new' % (sort, self._host),
96 '%s/%s.m4' % (sort, self._host))
97
98 # forced-command is make -C ...
99 # rebuilds the actual zone file
100 subprocess.call(["ssh", "opendnssec@localhost"])
101 fcntl.flock(lockfd, fcntl.LOCK_UN)
102
103
104 def main():
105 command = os.environ['SSH_ORIGINAL_COMMAND']
106 host = sys.argv[1]
107 addrecord = Addrecord(host)
108
109 if command == 'acme':
110 return addrecord.acme()
111 elif command == 'tlsa':
112 return addrecord.tlsa()
113
114
115 if __name__ == '__main__':
116 sys.exit(main())