blob: 21d998c3bc56660bc0d286d5eaf5b7224e10d9e3 [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
Serge Bazanski5a091422020-06-22 14:01:45 +020017package pki
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010018
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"
Serge Bazanski5a091422020-06-22 14:01:45 +020031 "io/ioutil"
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010032 "math/big"
33 "net"
Lorenz Brun878f5f92020-05-12 16:15:39 +020034 "os"
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010035 "path"
36 "time"
37
Lorenz Brunfc5dbc62020-05-28 12:18:07 +020038 "git.monogon.dev/source/nexantic.git/core/internal/common"
39
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010040 "go.etcd.io/etcd/clientv3"
41 "k8s.io/client-go/tools/clientcmd"
42 configapi "k8s.io/client-go/tools/clientcmd/api"
43)
44
45const (
Serge Bazanski5a091422020-06-22 14:01:45 +020046 EtcdPath = "/kube-pki/"
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010047)
48
49var (
50 // From RFC 5280 Section 4.1.2.5
51 unknownNotAfter = time.Unix(253402300799, 0)
52)
53
54// Directly derived from Kubernetes PKI requirements documented at
55// https://kubernetes.io/docs/setup/best-practices/certificates/#configure-certificates-manually
Serge Bazanski5a091422020-06-22 14:01:45 +020056func ClientCertTemplate(identity string, groups []string) x509.Certificate {
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010057 return x509.Certificate{
58 Subject: pkix.Name{
59 CommonName: identity,
60 Organization: groups,
61 },
62 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
63 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
64 }
65}
Serge Bazanski5a091422020-06-22 14:01:45 +020066func ServerCertTemplate(dnsNames []string, ips []net.IP) x509.Certificate {
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010067 return x509.Certificate{
68 Subject: pkix.Name{},
69 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
70 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
71 DNSNames: dnsNames,
72 IPAddresses: ips,
73 }
74}
75
76// Workaround for https://github.com/golang/go/issues/26676 in Go's crypto/x509. Specifically Go
Lorenz Brund3c59d22020-05-11 16:00:22 +020077// violates Section 4.2.1.2 of RFC 5280 without this.
78// Fixed for 1.15 in https://go-review.googlesource.com/c/go/+/227098/.
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010079//
80// Taken from https://github.com/FiloSottile/mkcert/blob/master/cert.go#L295 written by one of Go's
81// crypto engineers
82func calculateSKID(pubKey crypto.PublicKey) ([]byte, error) {
83 spkiASN1, err := x509.MarshalPKIXPublicKey(pubKey)
84 if err != nil {
85 return nil, err
86 }
87
88 var spki struct {
89 Algorithm pkix.AlgorithmIdentifier
90 SubjectPublicKey asn1.BitString
91 }
92 _, err = asn1.Unmarshal(spkiASN1, &spki)
93 if err != nil {
94 return nil, err
95 }
96 skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
97 return skid[:], nil
98}
99
100func newCA(name string) ([]byte, ed25519.PrivateKey, error) {
101 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
102 if err != nil {
103 panic(err)
104 }
105
106 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
107 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
108 if err != nil {
109 return []byte{}, privKey, fmt.Errorf("Failed to generate serial number: %w", err)
110 }
111
112 skid, err := calculateSKID(pubKey)
113 if err != nil {
114 return []byte{}, privKey, err
115 }
116
117 caCert := &x509.Certificate{
118 SerialNumber: serialNumber,
119 Subject: pkix.Name{
120 CommonName: name,
121 },
122 IsCA: true,
123 BasicConstraintsValid: true,
124 NotBefore: time.Now(),
125 NotAfter: unknownNotAfter,
126 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
127 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning},
128 AuthorityKeyId: skid,
129 SubjectKeyId: skid,
130 }
131
132 caCertRaw, err := x509.CreateCertificate(rand.Reader, caCert, caCert, pubKey, privKey)
133 return caCertRaw, privKey, err
134}
135
136func storeCert(consensusKV clientv3.KV, name string, cert []byte, key []byte) error {
Serge Bazanski5a091422020-06-22 14:01:45 +0200137 certPath := path.Join(EtcdPath, fmt.Sprintf("%v-cert.der", name))
138 keyPath := path.Join(EtcdPath, fmt.Sprintf("%v-key.der", name))
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100139 if _, err := consensusKV.Put(context.Background(), certPath, string(cert)); err != nil {
140 return fmt.Errorf("failed to store certificate: %w", err)
141 }
142 if _, err := consensusKV.Put(context.Background(), keyPath, string(key)); err != nil {
143 return fmt.Errorf("failed to store key: %w", err)
144 }
145 return nil
146}
147
Serge Bazanski5a091422020-06-22 14:01:45 +0200148func GetCert(consensusKV clientv3.KV, name string) (cert []byte, key []byte, err error) {
149 certPath := path.Join(EtcdPath, fmt.Sprintf("%v-cert.der", name))
150 keyPath := path.Join(EtcdPath, fmt.Sprintf("%v-key.der", name))
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100151 certRes, err := consensusKV.Get(context.Background(), certPath)
152 if err != nil {
153 err = fmt.Errorf("failed to get certificate: %w", err)
154 return
155 }
156 keyRes, err := consensusKV.Get(context.Background(), keyPath)
157 if err != nil {
158 err = fmt.Errorf("failed to get certificate: %w", err)
159 return
160 }
161 if len(certRes.Kvs) != 1 || len(keyRes.Kvs) != 1 {
162 err = fmt.Errorf("failed to find certificate %v", name)
163 return
164 }
165 cert = certRes.Kvs[0].Value
166 key = keyRes.Kvs[0].Value
167 return
168}
169
Serge Bazanski5a091422020-06-22 14:01:45 +0200170func GetSingle(consensusKV clientv3.KV, name string) ([]byte, error) {
171 res, err := consensusKV.Get(context.Background(), path.Join(EtcdPath, name))
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100172 if err != nil {
173 return []byte{}, fmt.Errorf("failed to get PKI item: %w", err)
174 }
175 if len(res.Kvs) != 1 {
176 return []byte{}, fmt.Errorf("failed to find PKI item %v", name)
177 }
178 return res.Kvs[0].Value, nil
179}
180
181// newCluster initializes the whole PKI for Kubernetes. It issues a single certificate per control
182// plane service since it assumes that etcd is already a secure place to store data. This removes
183// the need for revocation and makes the logic much simpler. Thus PKI data can NEVER be stored
184// outside of etcd or other secure storage locations. All PKI data is stored in DER form and not
185// PEM encoded since that would require more logic to deal with it.
Serge Bazanski5a091422020-06-22 14:01:45 +0200186func NewCluster(consensusKV clientv3.KV) error {
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100187 // This whole issuance procedure is pretty repetitive, but abstracts badly because a lot of it
188 // is subtly different.
189 idCA, idKey, err := newCA("Smalltown Kubernetes ID CA")
190 if err != nil {
191 return fmt.Errorf("failed to create Kubernetes ID CA: %w", err)
192 }
193 if err := storeCert(consensusKV, "id-ca", idCA, idKey); err != nil {
194 return err
195 }
196 aggregationCA, aggregationKey, err := newCA("Smalltown OpenAPI Aggregation CA")
197 if err != nil {
198 return fmt.Errorf("failed to create OpenAPI Aggregation CA: %w", err)
199 }
200 if err := storeCert(consensusKV, "aggregation-ca", aggregationCA, aggregationKey); err != nil {
201 return err
202 }
203
204 // ServiceAccounts don't support ed25519 yet, so use RSA (better side-channel resistance than ECDSA)
205 serviceAccountPrivKeyRaw, err := rsa.GenerateKey(rand.Reader, 2048)
206 if err != nil {
207 panic(err)
208 }
209 serviceAccountPrivKey, err := x509.MarshalPKCS8PrivateKey(serviceAccountPrivKeyRaw)
210 if err != nil {
211 panic(err) // Always a programmer error
212 }
Serge Bazanski5a091422020-06-22 14:01:45 +0200213 _, err = consensusKV.Put(context.Background(), path.Join(EtcdPath, "service-account-privkey.der"),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100214 string(serviceAccountPrivKey))
215 if err != nil {
216 return fmt.Errorf("failed to store service-account-privkey.der: %w", err)
217 }
218
Serge Bazanski5a091422020-06-22 14:01:45 +0200219 apiserverCert, apiserverKey, err := IssueCertificate(
220 ServerCertTemplate([]string{
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100221 "kubernetes",
222 "kubernetes.default",
223 "kubernetes.default.svc",
224 "kubernetes.default.svc.cluster",
225 "kubernetes.default.svc.cluster.local",
226 "localhost",
227 }, []net.IP{{127, 0, 0, 1}}, // TODO: Add service internal IP
228 ),
229 idCA, idKey,
230 )
231 if err != nil {
232 return fmt.Errorf("failed to issue certificate for apiserver: %w", err)
233 }
234 if err := storeCert(consensusKV, "apiserver", apiserverCert, apiserverKey); err != nil {
235 return err
236 }
237
Serge Bazanski5a091422020-06-22 14:01:45 +0200238 kubeletClientCert, kubeletClientKey, err := IssueCertificate(
239 ClientCertTemplate("smalltown:apiserver-kubelet-client", []string{}),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100240 idCA, idKey,
241 )
242 if err != nil {
243 return fmt.Errorf("failed to issue certificate for kubelet client: %w", err)
244 }
245 if err := storeCert(consensusKV, "kubelet-client", kubeletClientCert, kubeletClientKey); err != nil {
246 return err
247 }
248
Serge Bazanski5a091422020-06-22 14:01:45 +0200249 frontProxyClientCert, frontProxyClientKey, err := IssueCertificate(
250 ClientCertTemplate("front-proxy-client", []string{}),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100251 aggregationCA, aggregationKey,
252 )
253 if err != nil {
254 return fmt.Errorf("failed to issue certificate for OpenAPI frontend: %w", err)
255 }
256 if err := storeCert(consensusKV, "front-proxy-client", frontProxyClientCert, frontProxyClientKey); err != nil {
257 return err
258 }
259
Serge Bazanski5a091422020-06-22 14:01:45 +0200260 controllerManagerClientCert, controllerManagerClientKey, err := IssueCertificate(
261 ClientCertTemplate("system:kube-controller-manager", []string{}),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100262 idCA, idKey,
263 )
264 if err != nil {
265 return fmt.Errorf("failed to issue certificate for controller-manager client: %w", err)
266 }
267
Serge Bazanski5a091422020-06-22 14:01:45 +0200268 controllerManagerKubeconfig, err := MakeLocalKubeconfig(idCA, controllerManagerClientCert,
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100269 controllerManagerClientKey)
270 if err != nil {
271 return fmt.Errorf("failed to create kubeconfig for controller-manager: %w", err)
272 }
273
Serge Bazanski5a091422020-06-22 14:01:45 +0200274 _, err = consensusKV.Put(context.Background(), path.Join(EtcdPath, "controller-manager.kubeconfig"),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100275 string(controllerManagerKubeconfig))
276 if err != nil {
277 return fmt.Errorf("failed to store controller-manager kubeconfig: %w", err)
278 }
279
Serge Bazanski5a091422020-06-22 14:01:45 +0200280 controllerManagerCert, controllerManagerKey, err := IssueCertificate(
281 ServerCertTemplate([]string{"kube-controller-manager.local"}, []net.IP{}),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100282 idCA, idKey,
283 )
284 if err != nil {
285 return fmt.Errorf("failed to issue certificate for controller-manager: %w", err)
286 }
287 if err := storeCert(consensusKV, "controller-manager", controllerManagerCert, controllerManagerKey); err != nil {
288 return err
289 }
290
Serge Bazanski5a091422020-06-22 14:01:45 +0200291 schedulerClientCert, schedulerClientKey, err := IssueCertificate(
292 ClientCertTemplate("system:kube-scheduler", []string{}),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100293 idCA, idKey,
294 )
295 if err != nil {
296 return fmt.Errorf("failed to issue certificate for scheduler client: %w", err)
297 }
298
Serge Bazanski5a091422020-06-22 14:01:45 +0200299 schedulerKubeconfig, err := MakeLocalKubeconfig(idCA, schedulerClientCert, schedulerClientKey)
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100300 if err != nil {
301 return fmt.Errorf("failed to create kubeconfig for scheduler: %w", err)
302 }
303
Serge Bazanski5a091422020-06-22 14:01:45 +0200304 _, err = consensusKV.Put(context.Background(), path.Join(EtcdPath, "scheduler.kubeconfig"),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100305 string(schedulerKubeconfig))
306 if err != nil {
307 return fmt.Errorf("failed to store controller-manager kubeconfig: %w", err)
308 }
309
Serge Bazanski5a091422020-06-22 14:01:45 +0200310 schedulerCert, schedulerKey, err := IssueCertificate(
311 ServerCertTemplate([]string{"kube-scheduler.local"}, []net.IP{}),
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100312 idCA, idKey,
313 )
314 if err != nil {
315 return fmt.Errorf("failed to issue certificate for scheduler: %w", err)
316 }
317 if err := storeCert(consensusKV, "scheduler", schedulerCert, schedulerKey); err != nil {
318 return err
319 }
320
Serge Bazanski5a091422020-06-22 14:01:45 +0200321 masterClientCert, masterClientKey, err := IssueCertificate(
322 ClientCertTemplate("smalltown:master", []string{"system:masters"}),
Lorenz Brun878f5f92020-05-12 16:15:39 +0200323 idCA, idKey,
324 )
325 if err != nil {
326 return fmt.Errorf("failed to issue certificate for master client: %w", err)
327 }
328
Serge Bazanski5a091422020-06-22 14:01:45 +0200329 masterClientKubeconfig, err := MakeLocalKubeconfig(idCA, masterClientCert,
Lorenz Brun878f5f92020-05-12 16:15:39 +0200330 masterClientKey)
331 if err != nil {
332 return fmt.Errorf("failed to create kubeconfig for master client: %w", err)
333 }
334
Serge Bazanski5a091422020-06-22 14:01:45 +0200335 _, err = consensusKV.Put(context.Background(), path.Join(EtcdPath, "master.kubeconfig"),
Lorenz Brun878f5f92020-05-12 16:15:39 +0200336 string(masterClientKubeconfig))
337 if err != nil {
338 return fmt.Errorf("failed to store master kubeconfig: %w", err)
339 }
340
341 hostname, err := os.Hostname()
342 if err != nil {
343 return err
344 }
345 if err := bootstrapLocalKubelet(consensusKV, hostname); err != nil {
346 return err
347 }
348
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100349 return nil
350}
351
Serge Bazanski5a091422020-06-22 14:01:45 +0200352func IssueCertificate(template x509.Certificate, caCert []byte, privateKey interface{}) (cert []byte, privkey []byte, err error) {
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100353 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
354 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
355 if err != nil {
356 err = fmt.Errorf("Failed to generate serial number: %w", err)
357 return
358 }
359
360 caCertObj, err := x509.ParseCertificate(caCert)
361 if err != nil {
362 err = fmt.Errorf("failed to parse CA certificate: %w", err)
363 }
364
365 pubKey, privKeyRaw, err := ed25519.GenerateKey(rand.Reader)
366 if err != nil {
367 return
368 }
369 privkey, err = x509.MarshalPKCS8PrivateKey(privKeyRaw)
370 if err != nil {
371 return
372 }
373
374 template.SerialNumber = serialNumber
375 template.IsCA = false
376 template.BasicConstraintsValid = true
377 template.NotBefore = time.Now()
378 template.NotAfter = unknownNotAfter
379
380 cert, err = x509.CreateCertificate(rand.Reader, &template, caCertObj, pubKey, privateKey)
381 return
382}
383
Serge Bazanski5a091422020-06-22 14:01:45 +0200384func MakeLocalKubeconfig(ca, cert, key []byte) ([]byte, error) {
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100385 kubeconfig := configapi.NewConfig()
386 cluster := configapi.NewCluster()
Lorenz Brunfc5dbc62020-05-28 12:18:07 +0200387 cluster.Server = fmt.Sprintf("https://127.0.0.1:%v", common.KubernetesAPIPort)
Lorenz Brun6e8f69c2019-11-18 10:44:24 +0100388 cluster.CertificateAuthorityData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca})
389 kubeconfig.Clusters["default"] = cluster
390 authInfo := configapi.NewAuthInfo()
391 authInfo.ClientCertificateData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert})
392 authInfo.ClientKeyData = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key})
393 kubeconfig.AuthInfos["default"] = authInfo
394 ctx := configapi.NewContext()
395 ctx.Cluster = "default"
396 ctx.AuthInfo = "default"
397 kubeconfig.Contexts["default"] = ctx
398 kubeconfig.CurrentContext = "default"
399 return clientcmd.Write(*kubeconfig)
400}
Serge Bazanski5a091422020-06-22 14:01:45 +0200401
402func bootstrapLocalKubelet(consensusKV clientv3.KV, nodeName string) error {
403 idCA, idKeyRaw, err := GetCert(consensusKV, "id-ca")
404 if err != nil {
405 return err
406 }
407 idKey := ed25519.PrivateKey(idKeyRaw)
408 cert, key, err := IssueCertificate(ClientCertTemplate("system:node:"+nodeName, []string{"system:nodes"}), idCA, idKey)
409 if err != nil {
410 return err
411 }
412 kubeconfig, err := MakeLocalKubeconfig(idCA, cert, key)
413 if err != nil {
414 return err
415 }
416
417 serverCert, serverKey, err := IssueCertificate(ServerCertTemplate([]string{nodeName}, []net.IP{}), idCA, idKey)
418 if err != nil {
419 return err
420 }
421 if err := os.MkdirAll("/data/kubernetes", 0755); err != nil {
422 return err
423 }
424 if err := ioutil.WriteFile("/data/kubernetes/kubelet.kubeconfig", kubeconfig, 0400); err != nil {
425 return err
426 }
427 if err := ioutil.WriteFile("/data/kubernetes/ca.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: idCA}), 0400); err != nil {
428 return err
429 }
430 if err := ioutil.WriteFile("/data/kubernetes/kubelet.crt", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert}), 0400); err != nil {
431 return err
432 }
433 if err := ioutil.WriteFile("/data/kubernetes/kubelet.key", pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: serverKey}), 0400); err != nil {
434 return err
435 }
436
437 return nil
438}