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