blob: afc51c1d8e648d912e9e78ee99f160b5ff4369fd [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubernetes
import (
"context"
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"math/big"
"net"
"path"
"time"
"go.etcd.io/etcd/clientv3"
"k8s.io/client-go/tools/clientcmd"
configapi "k8s.io/client-go/tools/clientcmd/api"
)
const (
etcdPath = "/kube-pki/"
)
var (
// From RFC 5280 Section 4.1.2.5
unknownNotAfter = time.Unix(253402300799, 0)
)
// Directly derived from Kubernetes PKI requirements documented at
// https://kubernetes.io/docs/setup/best-practices/certificates/#configure-certificates-manually
func clientCertTemplate(identity string, groups []string) x509.Certificate {
return x509.Certificate{
Subject: pkix.Name{
CommonName: identity,
Organization: groups,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
}
func serverCertTemplate(dnsNames []string, ips []net.IP) x509.Certificate {
return x509.Certificate{
Subject: pkix.Name{},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: dnsNames,
IPAddresses: ips,
}
}
// Workaround for https://github.com/golang/go/issues/26676 in Go's crypto/x509. Specifically Go
// violates Section 4.2.1.2 of RFC 5280 without this. Should eventually be redundant.
//
// Taken from https://github.com/FiloSottile/mkcert/blob/master/cert.go#L295 written by one of Go's
// crypto engineers
func calculateSKID(pubKey crypto.PublicKey) ([]byte, error) {
spkiASN1, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return nil, err
}
var spki struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
_, err = asn1.Unmarshal(spkiASN1, &spki)
if err != nil {
return nil, err
}
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
return skid[:], nil
}
func newCA(name string) ([]byte, ed25519.PrivateKey, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return []byte{}, privKey, fmt.Errorf("Failed to generate serial number: %w", err)
}
skid, err := calculateSKID(pubKey)
if err != nil {
return []byte{}, privKey, err
}
caCert := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: name,
},
IsCA: true,
BasicConstraintsValid: true,
NotBefore: time.Now(),
NotAfter: unknownNotAfter,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageOCSPSigning},
AuthorityKeyId: skid,
SubjectKeyId: skid,
}
caCertRaw, err := x509.CreateCertificate(rand.Reader, caCert, caCert, pubKey, privKey)
return caCertRaw, privKey, err
}
func storeCert(consensusKV clientv3.KV, name string, cert []byte, key []byte) error {
certPath := path.Join(etcdPath, fmt.Sprintf("%v-cert.der", name))
keyPath := path.Join(etcdPath, fmt.Sprintf("%v-key.der", name))
if _, err := consensusKV.Put(context.Background(), certPath, string(cert)); err != nil {
return fmt.Errorf("failed to store certificate: %w", err)
}
if _, err := consensusKV.Put(context.Background(), keyPath, string(key)); err != nil {
return fmt.Errorf("failed to store key: %w", err)
}
return nil
}
func getCert(consensusKV clientv3.KV, name string) (cert []byte, key []byte, err error) {
certPath := path.Join(etcdPath, fmt.Sprintf("%v-cert.der", name))
keyPath := path.Join(etcdPath, fmt.Sprintf("%v-key.der", name))
certRes, err := consensusKV.Get(context.Background(), certPath)
if err != nil {
err = fmt.Errorf("failed to get certificate: %w", err)
return
}
keyRes, err := consensusKV.Get(context.Background(), keyPath)
if err != nil {
err = fmt.Errorf("failed to get certificate: %w", err)
return
}
if len(certRes.Kvs) != 1 || len(keyRes.Kvs) != 1 {
err = fmt.Errorf("failed to find certificate %v", name)
return
}
cert = certRes.Kvs[0].Value
key = keyRes.Kvs[0].Value
return
}
func getSingle(consensusKV clientv3.KV, name string) ([]byte, error) {
res, err := consensusKV.Get(context.Background(), path.Join(etcdPath, name))
if err != nil {
return []byte{}, fmt.Errorf("failed to get PKI item: %w", err)
}
if len(res.Kvs) != 1 {
return []byte{}, fmt.Errorf("failed to find PKI item %v", name)
}
return res.Kvs[0].Value, nil
}
// newCluster initializes the whole PKI for Kubernetes. It issues a single certificate per control
// plane service since it assumes that etcd is already a secure place to store data. This removes
// the need for revocation and makes the logic much simpler. Thus PKI data can NEVER be stored
// outside of etcd or other secure storage locations. All PKI data is stored in DER form and not
// PEM encoded since that would require more logic to deal with it.
func newCluster(consensusKV clientv3.KV) error {
// This whole issuance procedure is pretty repetitive, but abstracts badly because a lot of it
// is subtly different.
idCA, idKey, err := newCA("Smalltown Kubernetes ID CA")
if err != nil {
return fmt.Errorf("failed to create Kubernetes ID CA: %w", err)
}
if err := storeCert(consensusKV, "id-ca", idCA, idKey); err != nil {
return err
}
aggregationCA, aggregationKey, err := newCA("Smalltown OpenAPI Aggregation CA")
if err != nil {
return fmt.Errorf("failed to create OpenAPI Aggregation CA: %w", err)
}
if err := storeCert(consensusKV, "aggregation-ca", aggregationCA, aggregationKey); err != nil {
return err
}
// ServiceAccounts don't support ed25519 yet, so use RSA (better side-channel resistance than ECDSA)
serviceAccountPrivKeyRaw, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
serviceAccountPrivKey, err := x509.MarshalPKCS8PrivateKey(serviceAccountPrivKeyRaw)
if err != nil {
panic(err) // Always a programmer error
}
_, err = consensusKV.Put(context.Background(), path.Join(etcdPath, "service-account-privkey.der"),
string(serviceAccountPrivKey))
if err != nil {
return fmt.Errorf("failed to store service-account-privkey.der: %w", err)
}
apiserverCert, apiserverKey, err := issueCertificate(
serverCertTemplate([]string{
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local",
"localhost",
}, []net.IP{{127, 0, 0, 1}}, // TODO: Add service internal IP
),
idCA, idKey,
)
if err != nil {
return fmt.Errorf("failed to issue certificate for apiserver: %w", err)
}
if err := storeCert(consensusKV, "apiserver", apiserverCert, apiserverKey); err != nil {
return err
}
kubeletClientCert, kubeletClientKey, err := issueCertificate(
clientCertTemplate("kube-apiserver-kubelet-client", []string{"system:masters"}),
idCA, idKey,
)
if err != nil {
return fmt.Errorf("failed to issue certificate for kubelet client: %w", err)
}
if err := storeCert(consensusKV, "kubelet-client", kubeletClientCert, kubeletClientKey); err != nil {
return err
}
frontProxyClientCert, frontProxyClientKey, err := issueCertificate(
clientCertTemplate("front-proxy-client", []string{}),
aggregationCA, aggregationKey,
)
if err != nil {
return fmt.Errorf("failed to issue certificate for OpenAPI frontend: %w", err)
}
if err := storeCert(consensusKV, "front-proxy-client", frontProxyClientCert, frontProxyClientKey); err != nil {
return err
}
controllerManagerClientCert, controllerManagerClientKey, err := issueCertificate(
clientCertTemplate("system:kube-controller-manager", []string{}),
idCA, idKey,
)
if err != nil {
return fmt.Errorf("failed to issue certificate for controller-manager client: %w", err)
}
controllerManagerKubeconfig, err := makeLocalKubeconfig(idCA, controllerManagerClientCert,
controllerManagerClientKey)
if err != nil {
return fmt.Errorf("failed to create kubeconfig for controller-manager: %w", err)
}
_, err = consensusKV.Put(context.Background(), path.Join(etcdPath, "controller-manager.kubeconfig"),
string(controllerManagerKubeconfig))
if err != nil {
return fmt.Errorf("failed to store controller-manager kubeconfig: %w", err)
}
controllerManagerCert, controllerManagerKey, err := issueCertificate(
serverCertTemplate([]string{"kube-controller-manager.local"}, []net.IP{}),
idCA, idKey,
)
if err != nil {
return fmt.Errorf("failed to issue certificate for controller-manager: %w", err)
}
if err := storeCert(consensusKV, "controller-manager", controllerManagerCert, controllerManagerKey); err != nil {
return err
}
schedulerClientCert, schedulerClientKey, err := issueCertificate(
clientCertTemplate("system:kube-scheduler", []string{}),
idCA, idKey,
)
if err != nil {
return fmt.Errorf("failed to issue certificate for scheduler client: %w", err)
}
schedulerKubeconfig, err := makeLocalKubeconfig(idCA, schedulerClientCert, schedulerClientKey)
if err != nil {
return fmt.Errorf("failed to create kubeconfig for scheduler: %w", err)
}
_, err = consensusKV.Put(context.Background(), path.Join(etcdPath, "scheduler.kubeconfig"),
string(schedulerKubeconfig))
if err != nil {
return fmt.Errorf("failed to store controller-manager kubeconfig: %w", err)
}
schedulerCert, schedulerKey, err := issueCertificate(
serverCertTemplate([]string{"kube-scheduler.local"}, []net.IP{}),
idCA, idKey,
)
if err != nil {
return fmt.Errorf("failed to issue certificate for scheduler: %w", err)
}
if err := storeCert(consensusKV, "scheduler", schedulerCert, schedulerKey); err != nil {
return err
}
return nil
}
func issueCertificate(template x509.Certificate, caCert []byte, privateKey interface{}) (cert []byte, privkey []byte, err error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 127)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
err = fmt.Errorf("Failed to generate serial number: %w", err)
return
}
caCertObj, err := x509.ParseCertificate(caCert)
if err != nil {
err = fmt.Errorf("failed to parse CA certificate: %w", err)
}
pubKey, privKeyRaw, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return
}
privkey, err = x509.MarshalPKCS8PrivateKey(privKeyRaw)
if err != nil {
return
}
template.SerialNumber = serialNumber
template.IsCA = false
template.BasicConstraintsValid = true
template.NotBefore = time.Now()
template.NotAfter = unknownNotAfter
cert, err = x509.CreateCertificate(rand.Reader, &template, caCertObj, pubKey, privateKey)
return
}
func makeLocalKubeconfig(ca, cert, key []byte) ([]byte, error) {
kubeconfig := configapi.NewConfig()
cluster := configapi.NewCluster()
cluster.Server = "https://localhost:6443"
cluster.CertificateAuthorityData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca})
kubeconfig.Clusters["default"] = cluster
authInfo := configapi.NewAuthInfo()
authInfo.ClientCertificateData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert})
authInfo.ClientKeyData = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key})
kubeconfig.AuthInfos["default"] = authInfo
ctx := configapi.NewContext()
ctx.Cluster = "default"
ctx.AuthInfo = "default"
kubeconfig.Contexts["default"] = ctx
kubeconfig.CurrentContext = "default"
return clientcmd.Write(*kubeconfig)
}