Use pki.IssueCert() for nodeup client certs

This commit is contained in:
John Gardiner Myers 2020-05-16 23:33:10 -07:00
parent 08cdee1de2
commit d64e760d5b
9 changed files with 131 additions and 232 deletions

View File

@ -17,12 +17,10 @@ limitations under the License.
package model
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"os"
"path/filepath"
"time"
"k8s.io/klog"
"k8s.io/kops/pkg/pki"
@ -78,26 +76,22 @@ func (b *EtcdManagerTLSBuilder) Build(ctx *fi.ModelBuilderContext) error {
}
func (b *EtcdManagerTLSBuilder) buildKubeAPIServerKeypair() error {
etcdClientsCACertificate, err := b.KeyStore.FindCert("etcd-clients-ca")
if err != nil {
return err
req := &pki.IssueCertRequest{
Signer: "etcd-clients-ca",
Type: "client",
Subject: pkix.Name{
CommonName: "kube-apiserver",
},
MinValidDays: 455,
}
etcdClientsCAPrivateKey, err := b.KeyStore.FindPrivateKey("etcd-clients-ca")
if err != nil {
return err
}
if etcdClientsCACertificate == nil {
klog.Errorf("unable to find etcd-clients-ca certificate, won't build key for apiserver")
return nil
}
if etcdClientsCAPrivateKey == nil {
klog.Errorf("unable to find etcd-clients-ca private key, won't build key for apiserver")
return nil
}
dir := "/etc/kubernetes/pki/kube-apiserver"
name := "etcd-client"
humanName := dir + "/" + name
klog.Infof("signing certificate for %q", humanName)
cert, privateKey, etcdClientsCACertificate, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating directories %q: %v", dir, err)
@ -110,39 +104,12 @@ func (b *EtcdManagerTLSBuilder) buildKubeAPIServerKeypair() error {
}
}
name := "etcd-client"
humanName := dir + "/" + name
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return fmt.Errorf("unable to create private key %q: %v", humanName, err)
}
certTmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: "kube-apiserver",
},
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
klog.Infof("signing certificate for %q", humanName)
cert, err := pki.SignNewCertificate(privateKey, certTmpl, etcdClientsCACertificate.Certificate, etcdClientsCAPrivateKey)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
p := filepath.Join(dir, name)
{
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
{
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
return nil

View File

@ -17,7 +17,6 @@ limitations under the License.
package model
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"path/filepath"
@ -86,7 +85,17 @@ func (b *KubeAPIServerBuilder) addHealthcheckSidecarTasks(c *fi.ModelBuilderCont
})
}
clientKey, clientCert, err := b.buildClientKeypair(id)
req := &pki.IssueCertRequest{
Signer: fi.CertificateId_CA,
Type: "client",
Subject: pkix.Name{
CommonName: id,
},
MinValidDays: 455,
}
klog.Infof("signing certificate for %q", id)
clientCert, clientKey, _, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return err
}
@ -136,68 +145,3 @@ func (b *KubeAPIServerBuilder) addHealthcheckSidecarTasks(c *fi.ModelBuilderCont
return nil
}
func (b *KubeAPIServerBuilder) buildClientKeypair(commonName string) (*pki.PrivateKey, *pki.Certificate, error) {
signerID := fi.CertificateId_CA
var signerKey *pki.PrivateKey
{
k, err := b.KeyStore.FindPrivateKey(signerID)
if err != nil {
return nil, nil, err
}
if k == nil {
return nil, nil, fmt.Errorf("private key %q not found", signerID)
}
signerKey = k
}
var signerCertificate *pki.Certificate
{
cert, err := b.KeyStore.FindCert(signerID)
if err != nil {
return nil, nil, err
}
if cert == nil {
return nil, nil, fmt.Errorf("certificate %q not found", signerID)
}
signerCertificate = cert
}
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return nil, nil, err
}
template := &x509.Certificate{
BasicConstraintsValid: true,
IsCA: false,
Subject: pkix.Name{
CommonName: commonName,
},
}
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
//
// Digital signature allows the certificate to be used to verify
// digital signatures used during TLS negotiation.
template.KeyUsage = template.KeyUsage | x509.KeyUsageDigitalSignature
// KeyEncipherment allows the cert/key pair to be used to encrypt
// keys, including the symmetric keys negotiated during TLS setup
// and used for data transfer.
template.KeyUsage = template.KeyUsage | x509.KeyUsageKeyEncipherment
// ClientAuth allows the cert to be used by a TLS client to
// authenticate itself to the TLS server.
template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
klog.Infof("signing certificate for %q", commonName)
cert, err := pki.SignNewCertificate(privateKey, template, signerCertificate.Certificate, signerKey)
if err != nil {
return nil, nil, fmt.Errorf("error signing certificate for %q: %v", commonName, err)
}
return privateKey, cert, nil
}

View File

@ -17,13 +17,11 @@ limitations under the License.
package model
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"path"
"path/filepath"
"strings"
"time"
"k8s.io/kops/pkg/model/components"
@ -555,54 +553,17 @@ func (b *KubeletBuilder) buildMasterKubeletKubeconfig() (*nodetasks.File, error)
return nil, fmt.Errorf("error getting NodeName: %v", err)
}
caCert, err := b.KeyStore.FindCert(fi.CertificateId_CA)
if err != nil {
return nil, fmt.Errorf("error fetching CA certificate from keystore: %v", err)
}
if caCert == nil {
return nil, fmt.Errorf("unable to find CA certificate %q in keystore", fi.CertificateId_CA)
req := &pki.IssueCertRequest{
Signer: fi.CertificateId_CA,
Type: "client",
Subject: pkix.Name{
CommonName: fmt.Sprintf("system:node:%s", nodeName),
Organization: []string{rbac.NodesGroup},
},
MinValidDays: 455,
}
caKey, err := b.KeyStore.FindPrivateKey(fi.CertificateId_CA)
if err != nil {
return nil, fmt.Errorf("error fetching CA certificate from keystore: %v", err)
}
if caKey == nil {
return nil, fmt.Errorf("unable to find CA key %q in keystore", fi.CertificateId_CA)
}
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return nil, err
}
template := &x509.Certificate{
BasicConstraintsValid: true,
IsCA: false,
}
template.Subject = pkix.Name{
CommonName: fmt.Sprintf("system:node:%s", nodeName),
Organization: []string{rbac.NodesGroup},
}
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
//
// Digital signature allows the certificate to be used to verify
// digital signatures used during TLS negotiation.
template.KeyUsage = template.KeyUsage | x509.KeyUsageDigitalSignature
// KeyEncipherment allows the cert/key pair to be used to encrypt
// keys, including the symmetric keys negotiated during TLS setup
// and used for data transfer.
template.KeyUsage = template.KeyUsage | x509.KeyUsageKeyEncipherment
// ClientAuth allows the cert to be used by a TLS client to
// authenticate itself to the TLS server.
template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
t := time.Now().UnixNano()
template.SerialNumber = pki.BuildPKISerial(t)
certificate, err := pki.SignNewCertificate(privateKey, template, caCert.Certificate, caKey)
certificate, privateKey, caCert, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return nil, fmt.Errorf("error signing certificate for master kubelet: %v", err)
}

View File

@ -17,12 +17,10 @@ limitations under the License.
package networking
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"os"
"path/filepath"
"time"
"k8s.io/kops/nodeup/pkg/model"
@ -129,17 +127,22 @@ func (b *CiliumBuilder) buildCiliumEtcdSecrets(c *fi.ModelBuilderContext) error
}
}
etcdClientsCACertificate, err := b.KeyStore.FindCert("etcd-clients-ca-cilium")
if err != nil {
return err
req := &pki.IssueCertRequest{
Signer: "etcd-clients-ca-cilium",
Type: "client",
Subject: pkix.Name{
CommonName: "cilium",
},
MinValidDays: 455,
}
etcdClientsCAPrivateKey, err := b.KeyStore.FindPrivateKey("etcd-clients-ca-cilium")
if err != nil {
return err
}
dir := "/etc/kubernetes/pki/cilium"
name := "etcd-client-cilium"
humanName := dir + "/" + name
klog.Infof("signing certificate for %q", humanName)
cert, privateKey, etcdClientsCACertificate, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating directories %q: %v", dir, err)
@ -152,39 +155,12 @@ func (b *CiliumBuilder) buildCiliumEtcdSecrets(c *fi.ModelBuilderContext) error
}
}
name := "etcd-client"
humanName := dir + "/" + name
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return fmt.Errorf("unable to create private key %q: %v", humanName, err)
}
certTmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: "cilium",
},
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
klog.Infof("signing certificate for %q", humanName)
cert, err := pki.SignNewCertificate(privateKey, certTmpl, etcdClientsCACertificate.Certificate, etcdClientsCAPrivateKey)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
p := filepath.Join(dir, name)
{
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
{
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
return nil

View File

@ -97,8 +97,8 @@ func TestGenerateCertificate(t *testing.T) {
CommonName: tc.name,
}
cert, err := SignNewCertificate(key, &tc.template, tc.signer, tc.signerKey)
require.NoError(t, err, "SignNewCertificate")
cert, err := signNewCertificate(key, &tc.template, tc.signer, tc.signerKey)
require.NoError(t, err, "signNewCertificate")
{
subject := cert.Certificate.Subject

View File

@ -45,7 +45,7 @@ func BuildPKISerial(timestamp int64) *big.Int {
return serial
}
func SignNewCertificate(privateKey *PrivateKey, template *x509.Certificate, signer *x509.Certificate, signerPrivateKey *PrivateKey) (*Certificate, error) {
func signNewCertificate(privateKey *PrivateKey, template *x509.Certificate, signer *x509.Certificate, signerPrivateKey *PrivateKey) (*Certificate, error) {
if template.PublicKey == nil {
rsaPrivateKey, ok := privateKey.Key.(*rsa.PrivateKey)
if ok {

View File

@ -20,9 +20,12 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"hash/fnv"
"math/big"
"net"
"sort"
"strings"
"time"
)
var wellKnownCertificateTypes = map[string]string{
@ -44,6 +47,10 @@ type IssueCertRequest struct {
// PrivateKey is the private key for this certificate. If nil, a new private key will be generated.
PrivateKey *PrivateKey
// MinValidDays is the lower bound on the certificate validity, in days. If specified, up to 30 days
// will be added so that certificate generated at the same time on different hosts will be unlikely to
// expire at the same time. The default is 10 years (without the up to 30 day skew).
MinValidDays int
// Serial is the certificate serial number. If nil, a random number will be generated.
Serial *big.Int
@ -58,7 +65,7 @@ type Keystore interface {
FindKeypair(name string) (*Certificate, *PrivateKey, bool, error)
}
func IssueCert(request *IssueCertRequest, keystore Keystore) (*Certificate, *PrivateKey, error) {
func IssueCert(request *IssueCertRequest, keystore Keystore) (*Certificate, *PrivateKey, *Certificate, error) {
certificateType := request.Type
if expanded, found := wellKnownCertificateTypes[certificateType]; found {
certificateType = expanded
@ -75,19 +82,19 @@ func IssueCert(request *IssueCertRequest, keystore Keystore) (*Certificate, *Pri
if strings.HasPrefix(t, "KeyUsage") {
ku, found := parseKeyUsage(t)
if !found {
return nil, nil, fmt.Errorf("unrecognized certificate option: %v", t)
return nil, nil, nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.KeyUsage |= ku
} else if strings.HasPrefix(t, "ExtKeyUsage") {
ku, found := parseExtKeyUsage(t)
if !found {
return nil, nil, fmt.Errorf("unrecognized certificate option: %v", t)
return nil, nil, nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.ExtKeyUsage = append(template.ExtKeyUsage, ku)
} else if t == "CA" {
template.IsCA = true
} else {
return nil, nil, fmt.Errorf("unrecognized certificate option: %q", t)
return nil, nil, nil, fmt.Errorf("unrecognized certificate option: %q", t)
}
}
@ -108,22 +115,22 @@ func IssueCert(request *IssueCertRequest, keystore Keystore) (*Certificate, *Pri
}
}
var caCertificate *x509.Certificate
var caCertificate *Certificate
var caPrivateKey *PrivateKey
var signer *x509.Certificate
if !template.IsCA {
var err error
var caCert *Certificate
caCert, caPrivateKey, _, err = keystore.FindKeypair(request.Signer)
caCertificate, caPrivateKey, _, err = keystore.FindKeypair(request.Signer)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
if caPrivateKey == nil {
return nil, nil, fmt.Errorf("ca key for %q was not found; cannot issue certificates", request.Signer)
return nil, nil, nil, fmt.Errorf("ca key for %q was not found; cannot issue certificates", request.Signer)
}
if caCert == nil {
return nil, nil, fmt.Errorf("ca certificate for %q was not found; cannot issue certificates", request.Signer)
if caCertificate == nil {
return nil, nil, nil, fmt.Errorf("ca certificate for %q was not found; cannot issue certificates", request.Signer)
}
caCertificate = caCert.Certificate
signer = caCertificate.Certificate
}
privateKey := request.PrivateKey
@ -131,14 +138,32 @@ func IssueCert(request *IssueCertRequest, keystore Keystore) (*Certificate, *Pri
var err error
privateKey, err = GeneratePrivateKey()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
}
certificate, err := SignNewCertificate(privateKey, template, caCertificate, caPrivateKey)
if err != nil {
return nil, nil, err
if request.MinValidDays != 0 {
hash := fnv.New32()
addrs, err := net.InterfaceAddrs()
sort.Slice(addrs, func(i, j int) bool {
return addrs[i].String() < addrs[j].String()
})
if err == nil {
for _, addr := range addrs {
_, _ = hash.Write([]byte(addr.String()))
}
}
template.NotAfter = time.Now().Add(time.Hour * 24 * time.Duration(request.MinValidDays)).Add(time.Hour * time.Duration(hash.Sum32()%(30*24))).UTC()
}
return certificate, privateKey, err
certificate, err := signNewCertificate(privateKey, template, signer, caPrivateKey)
if err != nil {
return nil, nil, nil, err
}
if signer == nil {
caCertificate = certificate
}
return certificate, privateKey, caCertificate, err
}

View File

@ -87,7 +87,19 @@ func TestIssueCert(t *testing.T) {
expectedSubject: pkix.Name{CommonName: "Test client", Organization: []string{"system:masters"}},
},
{
name: "clientServer",
name: "clientOneYear",
req: IssueCertRequest{
Type: "client",
Subject: pkix.Name{
CommonName: "Test client",
},
MinValidDays: 365,
},
expectedKeyUsage: x509.KeyUsageDigitalSignature,
expectedExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
expectedSubject: pkix.Name{CommonName: "Test client"},
},
{
req: IssueCertRequest{
Type: "clientServer",
Subject: pkix.Name{
@ -120,7 +132,12 @@ func TestIssueCert(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
minExpectedValidity := time.Now().Add(time.Hour * 10 * 365 * 24).Unix()
var minExpectedValidity int64
if tc.req.MinValidDays == 0 {
minExpectedValidity = time.Now().Add(time.Hour * 10 * 365 * 24).Unix()
} else {
minExpectedValidity = time.Now().Add(time.Hour * 24 * time.Duration(tc.req.MinValidDays)).Unix()
}
var keystore Keystore
if tc.req.Type != "ca" {
@ -132,16 +149,18 @@ func TestIssueCert(t *testing.T) {
key: caPrivateKey,
}
}
certificate, key, err := IssueCert(&tc.req, keystore)
certificate, key, caCert, err := IssueCert(&tc.req, keystore)
require.NoError(t, err)
cert := certificate.Certificate
if tc.req.Signer == "" {
assert.Equal(t, cert.Issuer, cert.Subject, "self-signed")
assert.NoError(t, cert.CheckSignatureFrom(cert), "check signature")
assert.Equal(t, certificate, caCert, "returned CA cert")
} else {
assert.Equal(t, cert.Issuer, caCertificate.Certificate.Subject, "cert signer")
assert.NoError(t, cert.CheckSignatureFrom(caCertificate.Certificate), "check signature")
assert.Equal(t, caCertificate, caCert, "returned CA cert")
}
// type
@ -182,8 +201,15 @@ func TestIssueCert(t *testing.T) {
}
// validity
var maxExpectedValidity int64
if tc.req.MinValidDays == 0 {
maxExpectedValidity = time.Now().Add(time.Hour * 10 * 365 * 24).Unix()
} else {
maxExpectedValidity = time.Now().Add(time.Hour * 24 * time.Duration(tc.req.MinValidDays+30)).Unix()
}
assert.Less(t, cert.NotBefore.Unix(), time.Now().Add(time.Hour*-47).Unix(), "NotBefore")
assert.GreaterOrEqual(t, cert.NotAfter.Unix(), minExpectedValidity, "NotAfter")
assert.LessOrEqual(t, cert.NotAfter.Unix(), maxExpectedValidity, "NotAfter")
})
}

View File

@ -225,7 +225,7 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
PrivateKey: privateKey,
Serial: serial,
}
cert, privateKey, err := pki.IssueCert(&req, c.Keystore)
cert, privateKey, _, err := pki.IssueCert(&req, c.Keystore)
if err != nil {
return err
}