Skip to content

Commit 34f5db5

Browse files
committed
Add kubernetes TLS cert generation script
1 parent 8faec06 commit 34f5db5

2 files changed

Lines changed: 351 additions & 0 deletions

File tree

sbin/gen-kube-cert

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../staff/kubernetes/gen-kube-cert

staff/kubernetes/gen-kube-cert

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
#!/usr/bin/env python3
2+
# This script generates TLS certificates for use by Kubernetes masters.
3+
# It should be run on the puppet master when new Kubernetes masters are added.
4+
#
5+
#
6+
# Kubernetes SSL is a huge pain
7+
# We have 3 CAs (two for Kubernetes (main/proxy), and one for etcd)
8+
#
9+
# The main Kubernetes CA is used authenticating the following:
10+
# 1. kubelet on each node -> kube-apiserver
11+
# 2. kube-controller-manager -> kube-apiserver
12+
# 3. kube-scheduler -> kube-apiserver
13+
# 4. admin -> kube-apiserver
14+
# 5. kube-apiserver -> kubelet on each node
15+
#
16+
# The etcd CA is required because etcd relies on certificates for
17+
# authorization, but we only want the kubernetes masters to be able
18+
# authorized to read/write etcd. Any worker node should not have a
19+
# certificate signed by the etcd CA.
20+
#
21+
# The etcd CA is used for authenticating the following:
22+
# 1. etcd node -> etcd node
23+
# 2. kube-apiserver -> etcd node
24+
# 3. prometheus (inside kubernetes) -> etcd node
25+
#
26+
# The front-proxy CA is needed to authenticate kubernetes apiserver extensions.
27+
# We need one signed keypair for it.
28+
#
29+
# We also need a keypair to sign/verify service accounts.
30+
#
31+
#
32+
# Usage:
33+
# $0 <cluster_name> <node1> <node2> <node3> <...>
34+
import argparse
35+
import datetime
36+
import ipaddress
37+
import pathlib
38+
import socket
39+
import sys
40+
41+
from cryptography import x509
42+
from cryptography.hazmat.backends import default_backend
43+
from cryptography.hazmat.primitives import hashes
44+
from cryptography.hazmat.primitives import serialization
45+
from cryptography.hazmat.primitives.asymmetric import rsa
46+
from cryptography.x509.oid import NameOID
47+
48+
CERTS_BASE_DIR = pathlib.Path('/opt/puppetlabs/shares/private/kubernetes')
49+
50+
51+
def main():
52+
parser = argparse.ArgumentParser(
53+
description='Generates Kubernetes Certificates.',
54+
epilog='Usage example: {} prod monsoon pileup whirlwind\n'.format(sys.argv[0])
55+
+ ' {} dev hozer-72 hozer-73 hozer-74\n'.format(sys.argv[0])
56+
+ "If you're editing this script, you probably want to wipe the generated directory"
57+
+ 'to ensure that your changes are applied, rather than reusing old certificates.',
58+
)
59+
parser.add_argument('cluster_name', help='Name of the cluster')
60+
parser.add_argument('nodes', nargs='+', help='Hostnames of the nodes')
61+
62+
args = parser.parse_args()
63+
64+
cluster_name = args.cluster_name
65+
kube_ca = get_ca(cluster_name, 'kube-ca')
66+
etcd_ca = get_ca(cluster_name, 'etcd-ca')
67+
front_proxy_ca = get_ca(cluster_name, 'front-proxy-ca')
68+
69+
# get_signed_key is as follows:
70+
# get_signed_key(cluster_name, ca_private_key, file_name, common_name, hostnames=None, subject=None):
71+
72+
# admin client certificate
73+
get_signed_key(cluster_name, kube_ca, 'admin', 'admin', subject='system:masters')
74+
75+
# controller-manager client certificate
76+
get_signed_key(
77+
cluster_name,
78+
kube_ca,
79+
'controller-manager',
80+
'system:kube-controller-manager',
81+
subject='system:kube-controller-manager',
82+
)
83+
84+
# scheduler client certificate
85+
get_signed_key(
86+
cluster_name,
87+
kube_ca,
88+
'scheduler',
89+
'system:kube-scheduler',
90+
subject='system:kube-scheduler',
91+
)
92+
93+
# apiserver server certificate
94+
get_signed_key(
95+
cluster_name,
96+
kube_ca,
97+
'apiserver',
98+
'system:kube-apiserver',
99+
dns_names=['kube-master.ocf.berkeley.edu', 'localhost'],
100+
ip_names=['127.0.0.1'],
101+
)
102+
103+
# kubelet server certificates
104+
for node in args.nodes:
105+
get_signed_key(
106+
cluster_name,
107+
kube_ca,
108+
'{}-kubelet-server'.format(node),
109+
'system:node:{}'.format(node),
110+
subject='system:nodes',
111+
)
112+
113+
# kubelet -> apiserver client certificate
114+
get_signed_key(
115+
cluster_name,
116+
kube_ca,
117+
'apiserver-kubelet-client',
118+
'system:kube-apiserver-kubelet-client',
119+
subject='system:masters',
120+
)
121+
122+
# etcd client certificate
123+
for node in args.nodes:
124+
ip_address = socket.gethostbyname(node)
125+
get_signed_key(
126+
cluster_name,
127+
etcd_ca,
128+
'{}-etcd-client'.format(node),
129+
'{}-etcd-client'.format(node),
130+
ip_names=[ip_address],
131+
)
132+
133+
# etcd server certificate
134+
for node in args.nodes:
135+
ip_address = socket.gethostbyname(node)
136+
get_signed_key(
137+
cluster_name,
138+
etcd_ca,
139+
'{}-etcd-server'.format(node),
140+
'{}-etcd-server'.format(node),
141+
ip_names=['127.0.0.1', ip_address],
142+
)
143+
144+
# front proxy certificate
145+
get_signed_key(
146+
cluster_name, front_proxy_ca, 'front-proxy-client', 'front-proxy-client'
147+
)
148+
149+
# service account keypair
150+
get_keypair(cluster_name, 'service')
151+
152+
153+
def get_ca(cluster_name, ca_name):
154+
"""Gets the CA for the given cluster with the given name.
155+
Generates it if it does not exist."""
156+
157+
cluster_dir = CERTS_BASE_DIR / cluster_name
158+
159+
private_key_path = pathlib.Path(cluster_dir / '{}.key'.format(ca_name))
160+
public_key_path = pathlib.Path(cluster_dir / '{}.crt'.format(ca_name))
161+
162+
if not cluster_dir.exists():
163+
cluster_dir.mkdir()
164+
165+
if cluster_dir.exists() and not cluster_dir.is_dir():
166+
raise RuntimeError('{} is file but expected directory'.format(cluster_dir))
167+
168+
if private_key_path.exists() and public_key_path.exists():
169+
crt_data = private_key_path.read_bytes()
170+
private_key = serialization.load_pem_private_key(
171+
crt_data, password=None, backend=default_backend()
172+
)
173+
return private_key
174+
175+
private_key = rsa.generate_private_key(
176+
public_exponent=65537, key_size=2048, backend=default_backend()
177+
)
178+
179+
certificate = (
180+
x509.CertificateBuilder()
181+
.subject_name(
182+
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'OCF Kubernetes CA')])
183+
)
184+
.issuer_name(
185+
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'OCF Kubernetes CA')])
186+
)
187+
.public_key(private_key.public_key())
188+
.serial_number(x509.random_serial_number())
189+
.not_valid_before(datetime.datetime.utcnow())
190+
.not_valid_after(datetime.datetime(2100, 1, 1))
191+
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
192+
.sign(
193+
private_key=private_key,
194+
algorithm=hashes.SHA256(),
195+
backend=default_backend(),
196+
)
197+
)
198+
199+
assert isinstance(certificate, x509.Certificate)
200+
201+
with private_key_path.open('wb') as f:
202+
f.write(
203+
private_key.private_bytes(
204+
encoding=serialization.Encoding.PEM,
205+
format=serialization.PrivateFormat.TraditionalOpenSSL,
206+
encryption_algorithm=serialization.NoEncryption(),
207+
)
208+
)
209+
210+
with public_key_path.open('wb') as f:
211+
f.write(
212+
certificate.public_bytes(
213+
encoding=serialization.Encoding.PEM,
214+
)
215+
)
216+
217+
return private_key
218+
219+
220+
def get_signed_key(
221+
cluster_name,
222+
ca_private_key,
223+
file_name,
224+
common_name,
225+
ip_names=None,
226+
dns_names=None,
227+
subject=None,
228+
):
229+
"""Generates and signs a certificate with the given CA and CN, with the given SANs"""
230+
cluster_dir = CERTS_BASE_DIR / cluster_name
231+
232+
private_key_path = pathlib.Path(cluster_dir / '{}.key'.format(file_name))
233+
public_key_path = pathlib.Path(cluster_dir / '{}.crt'.format(file_name))
234+
235+
if private_key_path.exists() and public_key_path.exists():
236+
return
237+
238+
private_key = rsa.generate_private_key(
239+
public_exponent=65537, key_size=2048, backend=default_backend()
240+
)
241+
242+
subject_name_attributes = [x509.NameAttribute(NameOID.COMMON_NAME, common_name)]
243+
244+
if subject:
245+
subject_name_attributes.append(
246+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject)
247+
)
248+
249+
builder = (
250+
x509.CertificateBuilder()
251+
.subject_name(x509.Name(subject_name_attributes))
252+
.issuer_name(
253+
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'OCF Kubernetes CA')])
254+
)
255+
.public_key(private_key.public_key())
256+
.serial_number(x509.random_serial_number())
257+
.not_valid_before(datetime.datetime.utcnow())
258+
.not_valid_after(datetime.datetime(2100, 1, 1))
259+
.add_extension(
260+
x509.KeyUsage(
261+
digital_signature=True,
262+
key_encipherment=True,
263+
data_encipherment=False,
264+
key_agreement=False,
265+
content_commitment=False,
266+
key_cert_sign=False,
267+
crl_sign=False,
268+
encipher_only=False,
269+
decipher_only=False,
270+
),
271+
critical=True,
272+
)
273+
.add_extension(
274+
x509.ExtendedKeyUsage(
275+
[
276+
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
277+
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
278+
]
279+
),
280+
critical=False,
281+
)
282+
)
283+
284+
x509_names = []
285+
if dns_names:
286+
x509_names += [x509.DNSName(host) for host in dns_names]
287+
if ip_names:
288+
x509_names += [x509.IPAddress(ipaddress.ip_address(ip)) for ip in ip_names]
289+
if x509_names:
290+
builder = builder.add_extension(
291+
x509.SubjectAlternativeName(x509_names), critical=False
292+
)
293+
294+
certificate = builder.sign(
295+
private_key=ca_private_key, algorithm=hashes.SHA256(), backend=default_backend()
296+
)
297+
298+
assert isinstance(certificate, x509.Certificate)
299+
300+
with private_key_path.open('wb') as f:
301+
f.write(
302+
private_key.private_bytes(
303+
encoding=serialization.Encoding.PEM,
304+
format=serialization.PrivateFormat.TraditionalOpenSSL,
305+
encryption_algorithm=serialization.NoEncryption(),
306+
)
307+
)
308+
309+
with public_key_path.open('wb') as f:
310+
f.write(
311+
certificate.public_bytes(
312+
encoding=serialization.Encoding.PEM,
313+
)
314+
)
315+
316+
317+
def get_keypair(cluster_name, file_name):
318+
"""Generates a keypair and writes it to disk"""
319+
cluster_dir = CERTS_BASE_DIR / cluster_name
320+
321+
private_key_path = pathlib.Path(cluster_dir / '{}.key'.format(file_name))
322+
public_key_path = pathlib.Path(cluster_dir / '{}.pub'.format(file_name))
323+
324+
if private_key_path.exists() and public_key_path.exists():
325+
return
326+
327+
private_key = rsa.generate_private_key(
328+
public_exponent=65537, key_size=2048, backend=default_backend()
329+
)
330+
331+
with private_key_path.open('wb') as f:
332+
f.write(
333+
private_key.private_bytes(
334+
encoding=serialization.Encoding.PEM,
335+
format=serialization.PrivateFormat.TraditionalOpenSSL,
336+
encryption_algorithm=serialization.NoEncryption(),
337+
)
338+
)
339+
340+
with public_key_path.open('wb') as f:
341+
f.write(
342+
private_key.public_key().public_bytes(
343+
encoding=serialization.Encoding.PEM,
344+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
345+
)
346+
)
347+
348+
349+
if __name__ == '__main__':
350+
main()

0 commit comments

Comments
 (0)