blob: 25e2e4b42006e7c71dfda63f2bce77686fc137e3 [file] [log] [blame]
Lorenz Brun6e8f69c2019-11-18 10:44:24 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package kubernetes
18
19import (
20 "context"
21 "crypto"
22 "crypto/ed25519"
23 "crypto/rand"
24 "crypto/rsa"
25 "crypto/sha1"
26 "crypto/x509"
27 "crypto/x509/pkix"
28 "encoding/asn1"
29 "encoding/pem"
30 "fmt"
31 "math/big"
32 "net"
Lorenz Brun878f5f92020-05-12 16:15:39 +020033 "os"
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010034 "path"
35 "time"
36
37 "go.etcd.io/etcd/clientv3"
38 "k8s.io/client-go/tools/clientcmd"
39 configapi "k8s.io/client-go/tools/clientcmd/api"
40)
41
42const (
43 etcdPath = "/kube-pki/"
44)
45
46var (
47 // From RFC 5280 Section 4.1.2.5
48 unknownNotAfter = time.Unix(253402300799, 0)
49)
50
51// Directly derived from Kubernetes PKI requirements documented at
52// https://kubernetes.io/docs/setup/best-practices/certificates/#configure-certificates-manually
53func clientCertTemplate(identity string, groups []string) x509.Certificate {
54 return x509.Certificate{
55 Subject: pkix.Name{
56 CommonName: identity,
57 Organization: groups,
58 },
59 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
60 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
61 }
62}
63func serverCertTemplate(dnsNames []string, ips []net.IP) x509.Certificate {
64 return x509.Certificate{
65 Subject: pkix.Name{},
66 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
67 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
68 DNSNames: dnsNames,
69 IPAddresses: ips,
70 }
71}
72
73// Workaround for https://github.com/golang/go/issues/26676 in Go's crypto/x509. Specifically Go
Lorenz Brund3c59d22020-05-11 16:00:22 +020074// violates Section 4.2.1.2 of RFC 5280 without this.
75// Fixed for 1.15 in https://go-review.googlesource.com/c/go/+/227098/.
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010076//
77// Taken from https://github.com/FiloSottile/mkcert/blob/master/cert.go#L295 written by one of Go's
78// crypto engineers
79func calculateSKID(pubKey crypto.PublicKey) ([]byte, error) {
80 spkiASN1, err := x509.MarshalPKIXPublicKey(pubKey)
81 if err != nil {
82 return nil, err
83 }
84
85 var spki struct {
86 Algorithm pkix.AlgorithmIdentifier
87 SubjectPublicKey asn1.BitString
88 }
89 _, err = asn1.Unmarshal(spkiASN1, &spki)
90 if err != nil {
91 return nil, err
92 }
93 skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
94 return skid[:], nil
95}
96
97func newCA(name string) ([]byte, ed25519.PrivateKey, error) {
98 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
99 if err != nil {
100 panic(err)
101 }
102
103 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
104 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
105 if err != nil {
106 return []byte{}, privKey, fmt.Errorf("Failed to generate serial number: %w", err)
107 }
108
109 skid, err := calculateSKID(pubKey)
110 if err != nil {
111 return []byte{}, privKey, err
112 }
113
114 caCert := &x509.Certificate{
115 SerialNumber: serialNumber,
116 Subject: pkix.Name{
117 CommonName: name,
118 },
119 IsCA: true,
120 BasicConstraintsValid: true,
121 NotBefore: time.Now(),
122 NotAfter: unknownNotAfter,
123 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
124 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning},
125 AuthorityKeyId: skid,
126 SubjectKeyId: skid,
127 }
128
129 caCertRaw, err := x509.CreateCertificate(rand.Reader, caCert, caCert, pubKey, privKey)
130 return caCertRaw, privKey, err
131}
132
133func storeCert(consensusKV clientv3.KV, name string, cert []byte, key []byte) error {
134 certPath := path.Join(etcdPath, fmt.Sprintf("%v-cert.der", name))
135 keyPath := path.Join(etcdPath, fmt.Sprintf("%v-key.der", name))
136 if _, err := consensusKV.Put(context.Background(), certPath, string(cert)); err != nil {
137 return fmt.Errorf("failed to store certificate: %w", err)
138 }
139 if _, err := consensusKV.Put(context.Background(), keyPath, string(key)); err != nil {
140 return fmt.Errorf("failed to store key: %w", err)
141 }
142 return nil
143}
144
145func getCert(consensusKV clientv3.KV, name string) (cert []byte, key []byte, err error) {
146 certPath := path.Join(etcdPath, fmt.Sprintf("%v-cert.der", name))
147 keyPath := path.Join(etcdPath, fmt.Sprintf("%v-key.der", name))
148 certRes, err := consensusKV.Get(context.Background(), certPath)
149 if err != nil {
150 err = fmt.Errorf("failed to get certificate: %w", err)
151 return
152 }
153 keyRes, err := consensusKV.Get(context.Background(), keyPath)
154 if err != nil {
155 err = fmt.Errorf("failed to get certificate: %w", err)
156 return
157 }
158 if len(certRes.Kvs) != 1 || len(keyRes.Kvs) != 1 {
159 err = fmt.Errorf("failed to find certificate %v", name)
160 return
161 }
162 cert = certRes.Kvs[0].Value
163 key = keyRes.Kvs[0].Value
164 return
165}
166
167func getSingle(consensusKV clientv3.KV, name string) ([]byte, error) {
168 res, err := consensusKV.Get(context.Background(), path.Join(etcdPath, name))
169 if err != nil {
170 return []byte{}, fmt.Errorf("failed to get PKI item: %w", err)
171 }
172 if len(res.Kvs) != 1 {
173 return []byte{}, fmt.Errorf("failed to find PKI item %v", name)
174 }
175 return res.Kvs[0].Value, nil
176}
177
178// newCluster initializes the whole PKI for Kubernetes. It issues a single certificate per control
179// plane service since it assumes that etcd is already a secure place to store data. This removes
180// the need for revocation and makes the logic much simpler. Thus PKI data can NEVER be stored
181// outside of etcd or other secure storage locations. All PKI data is stored in DER form and not
182// PEM encoded since that would require more logic to deal with it.
183func newCluster(consensusKV clientv3.KV) error {
184 // This whole issuance procedure is pretty repetitive, but abstracts badly because a lot of it
185 // is subtly different.
186 idCA, idKey, err := newCA("Smalltown Kubernetes ID CA")
187 if err != nil {
188 return fmt.Errorf("failed to create Kubernetes ID CA: %w", err)
189 }
190 if err := storeCert(consensusKV, "id-ca", idCA, idKey); err != nil {
191 return err
192 }
193 aggregationCA, aggregationKey, err := newCA("Smalltown OpenAPI Aggregation CA")
194 if err != nil {
195 return fmt.Errorf("failed to create OpenAPI Aggregation CA: %w", err)
196 }
197 if err := storeCert(consensusKV, "aggregation-ca", aggregationCA, aggregationKey); err != nil {
198 return err
199 }
200
201 // ServiceAccounts don't support ed25519 yet, so use RSA (better side-channel resistance than ECDSA)
202 serviceAccountPrivKeyRaw, err := rsa.GenerateKey(rand.Reader, 2048)
203 if err != nil {
204 panic(err)
205 }
206 serviceAccountPrivKey, err := x509.MarshalPKCS8PrivateKey(serviceAccountPrivKeyRaw)
207 if err != nil {
208 panic(err) // Always a programmer error
209 }
210 _, err = consensusKV.Put(context.Background(), path.Join(etcdPath, "service-account-privkey.der"),
211 string(serviceAccountPrivKey))
212 if err != nil {
213 return fmt.Errorf("failed to store service-account-privkey.der: %w", err)
214 }
215
216 apiserverCert, apiserverKey, err := issueCertificate(
217 serverCertTemplate([]string{
218 "kubernetes",
219 "kubernetes.default",
220 "kubernetes.default.svc",
221 "kubernetes.default.svc.cluster",
222 "kubernetes.default.svc.cluster.local",
223 "localhost",
224 }, []net.IP{{127, 0, 0, 1}}, // TODO: Add service internal IP
225 ),
226 idCA, idKey,
227 )
228 if err != nil {
229 return fmt.Errorf("failed to issue certificate for apiserver: %w", err)
230 }
231 if err := storeCert(consensusKV, "apiserver", apiserverCert, apiserverKey); err != nil {
232 return err
233 }
234
235 kubeletClientCert, kubeletClientKey, err := issueCertificate(
Lorenz Brun878f5f92020-05-12 16:15:39 +0200236 clientCertTemplate("smalltown:apiserver-kubelet-client", []string{}),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100237 idCA, idKey,
238 )
239 if err != nil {
240 return fmt.Errorf("failed to issue certificate for kubelet client: %w", err)
241 }
242 if err := storeCert(consensusKV, "kubelet-client", kubeletClientCert, kubeletClientKey); err != nil {
243 return err
244 }
245
246 frontProxyClientCert, frontProxyClientKey, err := issueCertificate(
247 clientCertTemplate("front-proxy-client", []string{}),
248 aggregationCA, aggregationKey,
249 )
250 if err != nil {
251 return fmt.Errorf("failed to issue certificate for OpenAPI frontend: %w", err)
252 }
253 if err := storeCert(consensusKV, "front-proxy-client", frontProxyClientCert, frontProxyClientKey); err != nil {
254 return err
255 }
256
257 controllerManagerClientCert, controllerManagerClientKey, err := issueCertificate(
258 clientCertTemplate("system:kube-controller-manager", []string{}),
259 idCA, idKey,
260 )
261 if err != nil {
262 return fmt.Errorf("failed to issue certificate for controller-manager client: %w", err)
263 }
264
265 controllerManagerKubeconfig, err := makeLocalKubeconfig(idCA, controllerManagerClientCert,
266 controllerManagerClientKey)
267 if err != nil {
268 return fmt.Errorf("failed to create kubeconfig for controller-manager: %w", err)
269 }
270
271 _, err = consensusKV.Put(context.Background(), path.Join(etcdPath, "controller-manager.kubeconfig"),
272 string(controllerManagerKubeconfig))
273 if err != nil {
274 return fmt.Errorf("failed to store controller-manager kubeconfig: %w", err)
275 }
276
277 controllerManagerCert, controllerManagerKey, err := issueCertificate(
278 serverCertTemplate([]string{"kube-controller-manager.local"}, []net.IP{}),
279 idCA, idKey,
280 )
281 if err != nil {
282 return fmt.Errorf("failed to issue certificate for controller-manager: %w", err)
283 }
284 if err := storeCert(consensusKV, "controller-manager", controllerManagerCert, controllerManagerKey); err != nil {
285 return err
286 }
287
288 schedulerClientCert, schedulerClientKey, err := issueCertificate(
289 clientCertTemplate("system:kube-scheduler", []string{}),
290 idCA, idKey,
291 )
292 if err != nil {
293 return fmt.Errorf("failed to issue certificate for scheduler client: %w", err)
294 }
295
296 schedulerKubeconfig, err := makeLocalKubeconfig(idCA, schedulerClientCert, schedulerClientKey)
297 if err != nil {
298 return fmt.Errorf("failed to create kubeconfig for scheduler: %w", err)
299 }
300
301 _, err = consensusKV.Put(context.Background(), path.Join(etcdPath, "scheduler.kubeconfig"),
302 string(schedulerKubeconfig))
303 if err != nil {
304 return fmt.Errorf("failed to store controller-manager kubeconfig: %w", err)
305 }
306
307 schedulerCert, schedulerKey, err := issueCertificate(
308 serverCertTemplate([]string{"kube-scheduler.local"}, []net.IP{}),
309 idCA, idKey,
310 )
311 if err != nil {
312 return fmt.Errorf("failed to issue certificate for scheduler: %w", err)
313 }
314 if err := storeCert(consensusKV, "scheduler", schedulerCert, schedulerKey); err != nil {
315 return err
316 }
317
Lorenz Brun878f5f92020-05-12 16:15:39 +0200318 masterClientCert, masterClientKey, err := issueCertificate(
319 clientCertTemplate("smalltown:master", []string{"system:masters"}),
320 idCA, idKey,
321 )
322 if err != nil {
323 return fmt.Errorf("failed to issue certificate for master client: %w", err)
324 }
325
326 masterClientKubeconfig, err := makeLocalKubeconfig(idCA, masterClientCert,
327 masterClientKey)
328 if err != nil {
329 return fmt.Errorf("failed to create kubeconfig for master client: %w", err)
330 }
331
332 _, err = consensusKV.Put(context.Background(), path.Join(etcdPath, "master.kubeconfig"),
333 string(masterClientKubeconfig))
334 if err != nil {
335 return fmt.Errorf("failed to store master kubeconfig: %w", err)
336 }
337
338 hostname, err := os.Hostname()
339 if err != nil {
340 return err
341 }
342 if err := bootstrapLocalKubelet(consensusKV, hostname); err != nil {
343 return err
344 }
345
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100346 return nil
347}
348
349func issueCertificate(template x509.Certificate, caCert []byte, privateKey interface{}) (cert []byte, privkey []byte, err error) {
350 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
351 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
352 if err != nil {
353 err = fmt.Errorf("Failed to generate serial number: %w", err)
354 return
355 }
356
357 caCertObj, err := x509.ParseCertificate(caCert)
358 if err != nil {
359 err = fmt.Errorf("failed to parse CA certificate: %w", err)
360 }
361
362 pubKey, privKeyRaw, err := ed25519.GenerateKey(rand.Reader)
363 if err != nil {
364 return
365 }
366 privkey, err = x509.MarshalPKCS8PrivateKey(privKeyRaw)
367 if err != nil {
368 return
369 }
370
371 template.SerialNumber = serialNumber
372 template.IsCA = false
373 template.BasicConstraintsValid = true
374 template.NotBefore = time.Now()
375 template.NotAfter = unknownNotAfter
376
377 cert, err = x509.CreateCertificate(rand.Reader, &template, caCertObj, pubKey, privateKey)
378 return
379}
380
381func makeLocalKubeconfig(ca, cert, key []byte) ([]byte, error) {
382 kubeconfig := configapi.NewConfig()
383 cluster := configapi.NewCluster()
Leopold Schabela4516f92019-12-04 20:27:05 +0000384 cluster.Server = "https://127.0.0.1:6443"
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100385 cluster.CertificateAuthorityData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca})
386 kubeconfig.Clusters["default"] = cluster
387 authInfo := configapi.NewAuthInfo()
388 authInfo.ClientCertificateData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert})
389 authInfo.ClientKeyData = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key})
390 kubeconfig.AuthInfos["default"] = authInfo
391 ctx := configapi.NewContext()
392 ctx.Cluster = "default"
393 ctx.AuthInfo = "default"
394 kubeconfig.Contexts["default"] = ctx
395 kubeconfig.CurrentContext = "default"
396 return clientcmd.Write(*kubeconfig)
397}