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