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