blob: dda4161c6ea6300afb69ae022263394e55c0b7bc [file] [log] [blame]
Sergiusz Bazanskide061802019-01-13 21:14:02 +01001#!/usr/bin/env python
2
3from builtins import object
4
5import datetime
6from io import BytesIO
7import logging
8import os
9import tempfile
10import subprocess
11import sys
12
13from cryptography import x509
14from cryptography.hazmat.backends import default_backend
15import fabric
16
17import secretstore
18
19
20cluster = 'k0.hswaw.net'
21remote_root = '/opt/hscloud'
22local_root = os.getenv('hscloud_root')
23
24if local_root is None:
25 raise Exception("Please source env.sh")
26
27logger = logging.getLogger(__name__)
28logger.setLevel(logging.DEBUG)
29logger.addHandler(logging.StreamHandler())
30
31
32def decrypt(base):
33 src = os.path.join(local_root, 'cluster/secrets/cipher', base)
34 dst = os.path.join(local_root, 'cluster/secrets/plain', base)
35 secretstore.decrypt(src, dst)
36
37
38class PKI(object):
39 def __init__(self):
40 self.cacert = os.path.join(local_root, 'cluster/certs/ca.crt')
41 self.cakey = os.path.join(local_root, 'cluster/secrets/plain/ca.key')
42
43 if not os.path.exists(self.cakey):
44 decrypt('ca.key')
45
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +020046 def sign(self, csr, crt, conf, days=365, san=[]):
Sergiusz Bazanskide061802019-01-13 21:14:02 +010047 logger.info('pki: signing {} for {} days'.format(csr, days))
48 subprocess.check_call([
49 'openssl', 'x509', '-req',
50 '-in', csr,
51 '-CA', self.cacert,
52 '-CAkey', self.cakey,
53 '-out', crt,
Sergiusz Bazanskide061802019-01-13 21:14:02 +010054 '-days', str(days),
Sergiusz Bazanski4c186db2019-01-13 21:48:47 +010055 ] + ([
56 '-extensions', 'SAN', '-extfile', conf,
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +020057 ] if san else []))
Sergiusz Bazanskide061802019-01-13 21:14:02 +010058
59
60class Subject(object):
61 hswaw = "Stowarzyszenie Warszawski Hackerspace"
62 def __init__(self, o, ou, cn):
63 self.c = 'PL'
64 self.st = 'Mazowieckie'
65 self.l = 'Warszawa'
66 self.o = o
67 self.ou = ou
68 self.cn = cn
69
70 @property
71 def parts(self):
72 return {
73 'C': self.c,
74 'ST': self.st,
75 'L': self.l,
76 'O': self.o,
77 'OU': self.ou,
78 'CN': self.cn,
79 }
80
81 def __str__(self):
82 parts = self.parts
83 res = []
84 for p in ['C', 'ST', 'L', 'O', 'OU', 'CN']:
85 res.append('/{}={}'.format(p, parts[p]))
86 return ''.join(res)
87
88def _file_exists(c, filename):
89 res = c.run('stat "{}"'.format(filename), warn=True, hide=True)
90 return res.exited == 0
91
92def openssl_config(san):
93 with open(os.path.join(local_root, 'cluster/openssl.cnf'), 'rb') as f:
94 config = BytesIO(f.read())
95
Sergiusz Bazanski4c186db2019-01-13 21:48:47 +010096 if san:
97 config.seek(0, 2)
98 config.write(b'\n[SAN]\n')
Sergiusz Bazanski49b9a132019-01-14 00:02:59 +010099 config.write(b'subjectAltName = @alt_names\n')
100 config.write(b'basicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\n')
101 config.write(b'[alt_names]\n')
102
103 ipcnt = 1
104 dnscnt = 1
Sergiusz Bazanski4c186db2019-01-13 21:48:47 +0100105 for s in san:
Sergiusz Bazanski49b9a132019-01-14 00:02:59 +0100106 parts = s.split(':')
107 if s.startswith('DNS'):
108 config.write('DNS.{} = {}\n'.format(dnscnt, parts[1]).encode())
109 dnscnt += 1
110 elif s.startswith('IP'):
111 config.write('IP.{} = {}\n'.format(ipcnt, parts[1]).encode())
112 ipcnt += 1
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100113
114 f = tempfile.NamedTemporaryFile(delete=False)
115 path = f.name
116 f.write(config.getvalue())
117 f.close()
118
119 return path
120
121def remote_cert(pki, c, fqdn, cert_name, subj, san=[], days=365):
122 logger.info("{}/{}: remote cert".format(fqdn, cert_name))
123
124 remote_key = os.path.join(remote_root, '{}.key'.format(cert_name))
125 remote_cert = os.path.join(remote_root, '{}.crt'.format(cert_name))
126 remote_csr = os.path.join(remote_root, '{}.csr'.format(cert_name))
127 remote_config = os.path.join(remote_root, 'openssl.cnf')
128
129 generate_cert = False
130 if not _file_exists(c, remote_key):
131 logger.info("{}/{}: generating key".format(fqdn, cert_name))
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200132 c.run("""nix-shell -p openssl --command "openssl genrsa -out '{}' 4096" """.format(remote_key), hide=True)
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100133 genereate_cert = True
134
135 b = BytesIO()
136 try:
137 c.get(local=b, remote=remote_cert)
138 cert = x509.load_pem_x509_certificate(b.getvalue(), default_backend())
139 delta = cert.not_valid_after - datetime.datetime.now()
140 logger.info("{}/{}: existing cert expiry: {}".format(fqdn, cert_name, delta))
141 if delta.total_seconds() < 3600 * 24 * 60:
142 logger.info("{}/{}: expires soon, regenerating".format(fqdn, cert_name))
143 generate_cert = True
144 except (FileNotFoundError, ValueError):
145 generate_cert = True
146
147 if not generate_cert:
148 return False
149
150
151 local_config = openssl_config(san)
152 c.put(local=local_config, remote=remote_config)
153
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200154 sanconf = ""
155 if san:
156 sanconf = "-reqexts SAN"
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100157 c.run("""
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200158 nix-shell -p openssl --command "openssl req -new -key {remote_key} -out {remote_csr} -subj '{subj}' -config {remote_config} {sanconf}"
159 """.format(remote_key=remote_key, remote_csr=remote_csr, subj=str(subj), remote_config=remote_config, sanconf=sanconf))
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100160
161 local_csr_f = tempfile.NamedTemporaryFile(delete=False)
162 local_csr = local_csr_f.name
163 local_csr_f.close()
164
165 local_cert = os.path.join(local_root, 'cluster/certs', '{}-{}.crt'.format(fqdn, cert_name))
166
167 c.get(local=local_csr, remote=remote_csr)
168
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200169 pki.sign(local_csr, local_cert, local_config, days, san)
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100170
171 c.put(local=local_cert, remote=remote_cert)
172
173 os.remove(local_csr)
174 os.remove(local_config)
175
176 return True
177
178
179def shared_cert(pki, c, fqdn, cert_name, subj, san=[], days=365):
180 logger.info("{}/{}: shared cert".format(fqdn, cert_name))
181
182 local_key = os.path.join(local_root, 'cluster/secrets/plain', '{}.key'.format(cert_name))
183 local_cert = os.path.join(local_root, 'cluster/certs', '{}.crt'.format(cert_name))
184 remote_key = os.path.join(remote_root, '{}.key'.format(cert_name))
185 remote_cert = os.path.join(remote_root, '{}.crt'.format(cert_name))
186
187 generate_cert = False
188 if not os.path.exists(local_key):
189 try:
190 decrypt('{}.key'.format(cert_name))
191 except subprocess.CalledProcessError:
192 logger.info("{}/{}: generating key".format(fqdn, cert_name))
193 subprocess.check_call([
194 'openssl', 'genrsa', '-out', local_key, '4096',
195 ])
196 generate_cert = True
197
198 if os.path.exists(local_cert):
199 with open(local_cert, 'rb') as f:
200 b = f.read()
201 cert = x509.load_pem_x509_certificate(b, default_backend())
202 delta = cert.not_valid_after - datetime.datetime.now()
203 logger.info("{}/{}: existing cert expiry: {}".format(fqdn, cert_name, delta))
204 if delta.total_seconds() < 3600 * 24 * 60:
205 logger.info("{}/{}: expires soon, regenerating".format(fqdn, cert_name))
206 generate_cert = True
207 else:
208 generate_cert = True
209
Sergiusz Bazanski49b9a132019-01-14 00:02:59 +0100210 if generate_cert:
211 local_csr_f = tempfile.NamedTemporaryFile(delete=False)
212 local_csr = local_csr_f.name
213 local_csr_f.close()
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100214
Sergiusz Bazanski49b9a132019-01-14 00:02:59 +0100215 local_config = openssl_config(san)
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100216
Sergiusz Bazanski49b9a132019-01-14 00:02:59 +0100217 subprocess.check_call([
218 'openssl', 'req', '-new',
219 '-key', local_key,
220 '-out', local_csr,
221 '-subj', str(subj),
222 '-config', local_config,
223 ] + ([
224 '-reqexts', 'SAN',
225 ] if san else []))
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100226
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200227 pki.sign(local_csr, local_cert, local_config, days, san)
Sergiusz Bazanski49b9a132019-01-14 00:02:59 +0100228 os.remove(local_csr)
229 os.remove(local_config)
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100230
231 c.put(local=local_key, remote=remote_key)
232 c.put(local=local_cert, remote=remote_cert)
233
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100234 return True
235
236
237def configure_k8s(username, ca, cert, key):
238 subprocess.check_call([
239 'kubectl', 'config',
240 'set-cluster', cluster,
241 '--certificate-authority=' + ca,
242 '--embed-certs=true',
243 '--server=https://' + cluster + ':4001',
244 ])
245 subprocess.check_call([
246 'kubectl', 'config',
247 'set-credentials', username,
248 '--client-certificate=' + cert,
249 '--client-key=' + key,
250 '--embed-certs=true',
251 ])
252 subprocess.check_call([
253 'kubectl', 'config',
254 'set-context', cluster,
255 '--cluster=' + cluster,
256 '--user=' + username,
257 ])
258 subprocess.check_call([
259 'kubectl', 'config',
260 'use-context', cluster,
261 ])
262
263def admincreds(args):
264 if len(args) != 1:
265 sys.stderr.write("Usage: admincreds q3k\n")
266 return 1
267 username = args[0]
268
269 pki = PKI()
270
271 local_key = os.path.join(local_root, '.kubectl/admin.key')
272 local_cert = os.path.join(local_root, '.kubectl/admin.crt')
273 local_csr = os.path.join(local_root, '.kubectl/admin.csr')
274
Sergiusz Bazanskiae56b6a2019-01-13 21:39:16 +0100275 kubectl = os.path.join(local_root, '.kubectl')
276 if not os.path.exists(kubectl):
277 os.mkdir(kubectl)
278
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100279 generate_cert = False
280 if not os.path.exists(local_key):
281 subprocess.check_call([
282 'openssl', 'genrsa', '-out', local_key, '4096',
283 ])
284 generate_cert = True
285
286 if os.path.exists(local_cert):
287 with open(local_cert, 'rb') as f:
288 b = f.read()
289 cert = x509.load_pem_x509_certificate(b, default_backend())
290 delta = cert.not_valid_after - datetime.datetime.now()
291 logger.info("admin: existing cert expiry: {}".format(delta))
292 if delta.total_seconds() < 3600 * 24:
293 logger.info("admin: expires soon, regenerating")
294 generate_cert = True
295 else:
296 generate_cert = True
297
298 if not generate_cert:
299 return configure_k8s(username, pki.cacert, local_cert, local_key)
300
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100301 subj = Subject('system:masters', "Kubernetes Admin Account for {}".format(username), username)
302
303 subprocess.check_call([
304 'openssl', 'req', '-new',
305 '-key', local_key,
306 '-out', local_csr,
307 '-subj', str(subj),
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100308 ])
309
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200310 pki.sign(local_csr, local_cert, None, 5, [])
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100311
312 configure_k8s(username, pki.cacert, local_cert, local_key)
313
314
315def nodestrap(args):
316 if len(args) != 1:
317 sys.stderr.write("Usage: nodestrap bc01n01.hswaw.net\n")
318 return 1
319 fqdn = args[0]
320
321 logger.info("Nodestrapping {}...".format(fqdn))
322
323 c = fabric.Connection('root@{}'.format(fqdn))
324 p = PKI()
325
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200326 local_cacert = os.path.join(local_root, 'cluster/certs/ca.crt')
327 remote_cacert = os.path.join(remote_root, 'ca.crt')
328 c.put(local=local_cacert, remote=remote_cacert)
329
330 remote_cert(p, c, fqdn, "node", Subject(Subject.hswaw, 'Node Certificate', fqdn))
331 remote_cert(p, c, fqdn, "kube-node", Subject('system:nodes', 'Kubelet Certificate', 'system:node:' + fqdn), san=["DNS:"+fqdn,])
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100332 for component in ['controller-manager', 'proxy', 'scheduler']:
333 o = 'system:kube-{}'.format(component)
334 ou = 'Kuberneter Component {}'.format(component)
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200335 shared_cert(p, c, fqdn, 'kube-{}'.format(component), Subject(o, ou, o))
336 shared_cert(p, c, fqdn, 'kube-apiserver', Subject(Subject.hswaw, 'Kubernetes API', cluster), san=['IP:10.10.12.1', 'DNS:' + cluster])
337 shared_cert(p, c, fqdn, 'kube-serviceaccounts', Subject(Subject.hswaw, 'Kubernetes Service Account Signer', 'service-accounts'))
338 shared_cert(p, c, fqdn, 'kube-calico', Subject(Subject.hswaw, 'Kubernetes Calico Account', 'calico'))
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100339
Sergiusz Bazanskieeed6fb2019-04-01 16:19:28 +0200340 #c.run('nixos-rebuild switch')
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100341
342def usage():
343 sys.stderr.write("Usage: {} <nodestrap|admincreds>\n".format(sys.argv[0]))
344
345def main():
346 if len(sys.argv) < 2:
347 usage()
348 return 1
349
350 mode = sys.argv[1]
351 if mode == "nodestrap":
352 return nodestrap(sys.argv[2:])
353 elif mode == "admincreds":
354 return admincreds(sys.argv[2:])
355 else:
356 usage()
357 return 1
358
359if __name__ == '__main__':
360 sys.exit(main() or 0)