Skip to content

Commit 0e321d1

Browse files
committed
Add kubernetes TLS cert generation script
1 parent 8faec06 commit 0e321d1

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed

sbin/gen-kube-cert

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../staff/kubernetes/gen-kube-cert

staff/kubernetes/gen-kube-cert

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

0 commit comments

Comments
 (0)