blob: be3fea7ace58b8956ddff92d2c2d97d910673367 [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
46 def sign(self, csr, crt, conf, days=365):
47 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,
54 '-extensions', 'SAN', '-extfile', conf,
55 '-days', str(days),
56 ])
57
58
59class Subject(object):
60 hswaw = "Stowarzyszenie Warszawski Hackerspace"
61 def __init__(self, o, ou, cn):
62 self.c = 'PL'
63 self.st = 'Mazowieckie'
64 self.l = 'Warszawa'
65 self.o = o
66 self.ou = ou
67 self.cn = cn
68
69 @property
70 def parts(self):
71 return {
72 'C': self.c,
73 'ST': self.st,
74 'L': self.l,
75 'O': self.o,
76 'OU': self.ou,
77 'CN': self.cn,
78 }
79
80 def __str__(self):
81 parts = self.parts
82 res = []
83 for p in ['C', 'ST', 'L', 'O', 'OU', 'CN']:
84 res.append('/{}={}'.format(p, parts[p]))
85 return ''.join(res)
86
87def _file_exists(c, filename):
88 res = c.run('stat "{}"'.format(filename), warn=True, hide=True)
89 return res.exited == 0
90
91def openssl_config(san):
92 with open(os.path.join(local_root, 'cluster/openssl.cnf'), 'rb') as f:
93 config = BytesIO(f.read())
94
95 config.seek(0, 2)
96 config.write(b'\n[SAN]\n')
97 for s in san:
98 config.write('subjectAltName=DNS:{}\n'.format(s).encode())
99
100 f = tempfile.NamedTemporaryFile(delete=False)
101 path = f.name
102 f.write(config.getvalue())
103 f.close()
104
105 return path
106
107def remote_cert(pki, c, fqdn, cert_name, subj, san=[], days=365):
108 logger.info("{}/{}: remote cert".format(fqdn, cert_name))
109
110 remote_key = os.path.join(remote_root, '{}.key'.format(cert_name))
111 remote_cert = os.path.join(remote_root, '{}.crt'.format(cert_name))
112 remote_csr = os.path.join(remote_root, '{}.csr'.format(cert_name))
113 remote_config = os.path.join(remote_root, 'openssl.cnf')
114
115 generate_cert = False
116 if not _file_exists(c, remote_key):
117 logger.info("{}/{}: generating key".format(fqdn, cert_name))
118 c.run('openssl genrsa -out "{}" 4096'.format(remote_key), hide=True)
119 genereate_cert = True
120
121 b = BytesIO()
122 try:
123 c.get(local=b, remote=remote_cert)
124 cert = x509.load_pem_x509_certificate(b.getvalue(), default_backend())
125 delta = cert.not_valid_after - datetime.datetime.now()
126 logger.info("{}/{}: existing cert expiry: {}".format(fqdn, cert_name, delta))
127 if delta.total_seconds() < 3600 * 24 * 60:
128 logger.info("{}/{}: expires soon, regenerating".format(fqdn, cert_name))
129 generate_cert = True
130 except (FileNotFoundError, ValueError):
131 generate_cert = True
132
133 if not generate_cert:
134 return False
135
136
137 local_config = openssl_config(san)
138 c.put(local=local_config, remote=remote_config)
139
140 c.run("""
141 nix-shell -p openssl --command "openssl req -new -key {remote_key} -out {remote_csr} -subj '{subj}' -config {remote_config} -reqexts SAN"
142 """.format(remote_key=remote_key, remote_csr=remote_csr, subj=str(subj), remote_config=remote_config))
143
144 local_csr_f = tempfile.NamedTemporaryFile(delete=False)
145 local_csr = local_csr_f.name
146 local_csr_f.close()
147
148 local_cert = os.path.join(local_root, 'cluster/certs', '{}-{}.crt'.format(fqdn, cert_name))
149
150 c.get(local=local_csr, remote=remote_csr)
151
152 pki.sign(local_csr, local_cert, local_config, days)
153
154 c.put(local=local_cert, remote=remote_cert)
155
156 os.remove(local_csr)
157 os.remove(local_config)
158
159 return True
160
161
162def shared_cert(pki, c, fqdn, cert_name, subj, san=[], days=365):
163 logger.info("{}/{}: shared cert".format(fqdn, cert_name))
164
165 local_key = os.path.join(local_root, 'cluster/secrets/plain', '{}.key'.format(cert_name))
166 local_cert = os.path.join(local_root, 'cluster/certs', '{}.crt'.format(cert_name))
167 remote_key = os.path.join(remote_root, '{}.key'.format(cert_name))
168 remote_cert = os.path.join(remote_root, '{}.crt'.format(cert_name))
169
170 generate_cert = False
171 if not os.path.exists(local_key):
172 try:
173 decrypt('{}.key'.format(cert_name))
174 except subprocess.CalledProcessError:
175 logger.info("{}/{}: generating key".format(fqdn, cert_name))
176 subprocess.check_call([
177 'openssl', 'genrsa', '-out', local_key, '4096',
178 ])
179 generate_cert = True
180
181 if os.path.exists(local_cert):
182 with open(local_cert, 'rb') as f:
183 b = f.read()
184 cert = x509.load_pem_x509_certificate(b, default_backend())
185 delta = cert.not_valid_after - datetime.datetime.now()
186 logger.info("{}/{}: existing cert expiry: {}".format(fqdn, cert_name, delta))
187 if delta.total_seconds() < 3600 * 24 * 60:
188 logger.info("{}/{}: expires soon, regenerating".format(fqdn, cert_name))
189 generate_cert = True
190 else:
191 generate_cert = True
192
193 if not generate_cert:
194 return False
195
196 local_csr_f = tempfile.NamedTemporaryFile(delete=False)
197 local_csr = local_csr_f.name
198 local_csr_f.close()
199
200 local_config = openssl_config(san)
201
202 subprocess.check_call([
203 'openssl', 'req', '-new',
204 '-key', local_key,
205 '-out', local_csr,
206 '-subj', str(subj),
207 '-config', local_config,
208 '-reqexts', 'SAN',
209 ])
210
211 pki.sign(local_csr, local_cert, local_config, days)
212
213 c.put(local=local_key, remote=remote_key)
214 c.put(local=local_cert, remote=remote_cert)
215
216 os.remove(local_csr)
217 os.remove(local_config)
218 return True
219
220
221def configure_k8s(username, ca, cert, key):
222 subprocess.check_call([
223 'kubectl', 'config',
224 'set-cluster', cluster,
225 '--certificate-authority=' + ca,
226 '--embed-certs=true',
227 '--server=https://' + cluster + ':4001',
228 ])
229 subprocess.check_call([
230 'kubectl', 'config',
231 'set-credentials', username,
232 '--client-certificate=' + cert,
233 '--client-key=' + key,
234 '--embed-certs=true',
235 ])
236 subprocess.check_call([
237 'kubectl', 'config',
238 'set-context', cluster,
239 '--cluster=' + cluster,
240 '--user=' + username,
241 ])
242 subprocess.check_call([
243 'kubectl', 'config',
244 'use-context', cluster,
245 ])
246
247def admincreds(args):
248 if len(args) != 1:
249 sys.stderr.write("Usage: admincreds q3k\n")
250 return 1
251 username = args[0]
252
253 pki = PKI()
254
255 local_key = os.path.join(local_root, '.kubectl/admin.key')
256 local_cert = os.path.join(local_root, '.kubectl/admin.crt')
257 local_csr = os.path.join(local_root, '.kubectl/admin.csr')
258
Sergiusz Bazanskiae56b6a2019-01-13 21:39:16 +0100259 kubectl = os.path.join(local_root, '.kubectl')
260 if not os.path.exists(kubectl):
261 os.mkdir(kubectl)
262
Sergiusz Bazanskide061802019-01-13 21:14:02 +0100263 generate_cert = False
264 if not os.path.exists(local_key):
265 subprocess.check_call([
266 'openssl', 'genrsa', '-out', local_key, '4096',
267 ])
268 generate_cert = True
269
270 if os.path.exists(local_cert):
271 with open(local_cert, 'rb') as f:
272 b = f.read()
273 cert = x509.load_pem_x509_certificate(b, default_backend())
274 delta = cert.not_valid_after - datetime.datetime.now()
275 logger.info("admin: existing cert expiry: {}".format(delta))
276 if delta.total_seconds() < 3600 * 24:
277 logger.info("admin: expires soon, regenerating")
278 generate_cert = True
279 else:
280 generate_cert = True
281
282 if not generate_cert:
283 return configure_k8s(username, pki.cacert, local_cert, local_key)
284
285 local_config = openssl_config([])
286 subj = Subject('system:masters', "Kubernetes Admin Account for {}".format(username), username)
287
288 subprocess.check_call([
289 'openssl', 'req', '-new',
290 '-key', local_key,
291 '-out', local_csr,
292 '-subj', str(subj),
293 '-config', local_config,
294 '-reqexts', 'SAN',
295 ])
296
297 pki.sign(local_csr, local_cert, local_config, 5)
298 os.remove(local_config)
299
300 configure_k8s(username, pki.cacert, local_cert, local_key)
301
302
303def nodestrap(args):
304 if len(args) != 1:
305 sys.stderr.write("Usage: nodestrap bc01n01.hswaw.net\n")
306 return 1
307 fqdn = args[0]
308
309 logger.info("Nodestrapping {}...".format(fqdn))
310
311 c = fabric.Connection('root@{}'.format(fqdn))
312 p = PKI()
313
314 modified = False
315 modified |= remote_cert(p, c, fqdn, "node", Subject(Subject.hswaw, 'Node Certificate', fqdn))
316 modified |= remote_cert(p, c, fqdn, "kube-node", Subject('system:nodes', 'Kubelet Certificate', 'system:node:' + fqdn), san=[fqdn,])
317 for component in ['controller-manager', 'proxy', 'scheduler']:
318 o = 'system:kube-{}'.format(component)
319 ou = 'Kuberneter Component {}'.format(component)
320 modified |= shared_cert(p, c, fqdn, 'kube-{}'.format(component), Subject(o, ou, o))
321 modified |= shared_cert(p, c, fqdn, 'kube-apiserver', Subject(Subject.hswaw, 'Kubernetes API', cluster))
322 modified |= shared_cert(p, c, fqdn, 'kube-serviceaccounts', Subject(Subject.hswaw, 'Kubernetes Service Account Signer', 'service-accounts'))
323
324 if modified:
325 logger.info('{}: cert(s) modified, restarting services...'.format(fqdn))
326
327 services = [
328 'kubelet', 'kube-proxy',
329 'kube-apiserver', 'kube-controller-manager', 'kube-scheduler',
330 'etcd'
331 ]
332
333 for s in services:
334 c.run('systemctl stop {}'.format(s))
335 for s in services[::-1]:
336 c.run('systemctl start {}'.format(s))
337
338def usage():
339 sys.stderr.write("Usage: {} <nodestrap|admincreds>\n".format(sys.argv[0]))
340
341def main():
342 if len(sys.argv) < 2:
343 usage()
344 return 1
345
346 mode = sys.argv[1]
347 if mode == "nodestrap":
348 return nodestrap(sys.argv[2:])
349 elif mode == "admincreds":
350 return admincreds(sys.argv[2:])
351 else:
352 usage()
353 return 1
354
355if __name__ == '__main__':
356 sys.exit(main() or 0)