diff --git a/pkg/dclient/certificates.go b/pkg/dclient/certificates.go index 1047e8a7c3..6da2df4bbb 100644 --- a/pkg/dclient/certificates.go +++ b/pkg/dclient/certificates.go @@ -1,16 +1,15 @@ package client import ( - "errors" + "encoding/base64" "fmt" "net/url" - "time" "github.com/kyverno/kyverno/pkg/config" tls "github.com/kyverno/kyverno/pkg/tls" - certificates "k8s.io/api/certificates/v1beta1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/rest" ) @@ -26,7 +25,8 @@ func (c *Client) InitTLSPemPair(configuration *rest.Config, fqdncn bool) (*tls.T tlsPair := c.ReadTlsPair(certProps) if tls.IsTLSPairShouldBeUpdated(tlsPair) { logger.Info("Generating new key/certificate pair for TLS") - tlsPair, err = c.generateTLSPemPair(certProps, fqdncn) + // tlsPair, err = c.generateTLSPemPair(certProps, fqdncn) + tlsPair, err = c.buildTLSPemPair(certProps, fqdncn) if err != nil { return nil, err } @@ -39,107 +39,18 @@ func (c *Client) InitTLSPemPair(configuration *rest.Config, fqdncn bool) (*tls.T return tlsPair, nil } -//generateTlsPemPair Issues TLS certificate for webhook server using given PEM private key +//buildTLSPemPair Issues TLS certificate for webhook server using self-signed CA cert // Returns signed and approved TLS certificate in PEM format -func (c *Client) generateTLSPemPair(props tls.TlsCertificateProps, fqdncn bool) (*tls.TlsPemPair, error) { - privateKey, err := tls.TLSGeneratePrivateKey() +func (c *Client) buildTLSPemPair(props tls.TlsCertificateProps, fqdncn bool) (*tls.TlsPemPair, error) { + caCert, caPEM, err := tls.GenerateCACert() if err != nil { return nil, err } - certRequest, err := tls.CertificateGenerateRequest(privateKey, props, fqdncn) - if err != nil { - return nil, fmt.Errorf("Unable to create certificate request: %v", err) - } - - certRequest, err = c.submitAndApproveCertificateRequest(certRequest) - if err != nil { - return nil, fmt.Errorf("Unable to submit and approve certificate request: %v", err) - } - - tlsCert, err := c.fetchCertificateFromRequest(certRequest, 10) - if err != nil { - return nil, fmt.Errorf("Failed to configure a certificate for the Kyverno controller. A CA certificate is required to allow the Kubernetes API Server to communicate with Kyverno. You can either provide a certificate or configure your cluster to allow certificate signing. Please refer to https://github.com/kyverno/kyverno/installation.md.: %v", err) - } - - return &tls.TlsPemPair{ - Certificate: tlsCert, - PrivateKey: tls.TLSPrivateKeyToPem(privateKey), - }, nil -} - -// Submits and approves certificate request, returns request which need to be fetched -func (c *Client) submitAndApproveCertificateRequest(req *certificates.CertificateSigningRequest) (*certificates.CertificateSigningRequest, error) { - logger := c.log.WithName("submitAndApproveCertificateRequest") - certClient, err := c.GetCSRInterface() - if err != nil { + if err := c.WriteCACert(caPEM, props); err != nil { return nil, err } - csrList, err := c.ListResource("", CSRs, "", nil) - if err != nil { - return nil, fmt.Errorf("Unable to list existing certificate requests: %v", err) - } - - for _, csr := range csrList.Items { - if csr.GetName() == req.ObjectMeta.Name { - err := c.DeleteResource("", CSRs, "", csr.GetName(), false) - if err != nil { - return nil, fmt.Errorf("Unable to delete existing certificate request: %v", err) - } - logger.Info("Old certificate request is deleted") - break - } - } - - unstrRes, err := c.CreateResource("", CSRs, "", req, false) - if err != nil { - return nil, err - } - logger.Info("Certificate request created", "name", unstrRes.GetName()) - - res, err := convertToCSR(unstrRes) - if err != nil { - return nil, err - } - res.Status.Conditions = append(res.Status.Conditions, certificates.CertificateSigningRequestCondition{ - Type: certificates.CertificateApproved, - Reason: "NKP-Approve", - Message: "This CSR was approved by Nirmata kyverno controller", - }) - res, err = certClient.UpdateApproval(res) - if err != nil { - return nil, fmt.Errorf("Unable to approve certificate request: %v", err) - } - logger.Info("Certificate request is approved", "name", res.ObjectMeta.Name) - - return res, nil -} - -// Fetches certificate from given request. Tries to obtain certificate for maxWaitSeconds -func (c *Client) fetchCertificateFromRequest(req *certificates.CertificateSigningRequest, maxWaitSeconds uint8) ([]byte, error) { - // TODO: react of SIGINT and SIGTERM - timeStart := time.Now() - for time.Since(timeStart) < time.Duration(maxWaitSeconds)*time.Second { - unstrR, err := c.GetResource("", CSRs, "", req.ObjectMeta.Name) - if err != nil { - return nil, err - } - r, err := convertToCSR(unstrR) - if err != nil { - return nil, err - } - - if r.Status.Certificate != nil { - return r.Status.Certificate, nil - } - - for _, condition := range r.Status.Conditions { - if condition.Type == certificates.CertificateDenied { - return nil, errors.New(condition.String()) - } - } - } - return nil, fmt.Errorf("Cerificate fetch timeout is reached: %d seconds", maxWaitSeconds) + return tls.GenerateCertPEM(caCert, props, fqdncn) } //ReadRootCASecret returns the RootCA from the pre-defined secret @@ -213,6 +124,56 @@ func (c *Client) ReadTlsPair(props tls.TlsCertificateProps) *tls.TlsPemPair { return &pemPair } +func (c *Client) WriteCACert(caPEM *tls.TlsPemPair, props tls.TlsCertificateProps) error { + logger := c.log.WithName("CAcert") + name := generateRootCASecretName(props) + + secretUnstr, err := c.GetResource("", Secrets, props.Namespace, name) + if err != nil { + secret := &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: props.Namespace, + Annotations: map[string]string{ + selfSignedAnnotation: "true", + }, + }, + Data: map[string][]byte{ + rootCAKey: caPEM.Certificate, + }, + Type: v1.SecretTypeOpaque, + } + + _, err := c.CreateResource("", Secrets, props.Namespace, secret, false) + if err == nil { + logger.Info("secret created", "name", name, "namespace", props.Namespace) + } + return err + } + // secret := v1.Secret{} + if _, ok := secretUnstr.GetAnnotations()[selfSignedAnnotation]; !ok { + secretUnstr.SetAnnotations(map[string]string{selfSignedAnnotation: "true"}) + } + + dataMap := map[string]interface{}{ + rootCAKey: base64.StdEncoding.EncodeToString(caPEM.Certificate)} + + if err := unstructured.SetNestedMap(secretUnstr.Object, dataMap, "data"); err != nil { + return err + } + + _, err = c.UpdateResource("", Secrets, props.Namespace, secretUnstr, false) + if err != nil { + return err + } + logger.Info("secret updated", "name", name, "namespace", props.Namespace) + return nil +} + //WriteTlsPair Writes the pair of TLS certificate and key to the specified secret. // Updates existing secret or creates new one. func (c *Client) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPemPair) error { diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index e3cd9a94df..5dce7d6188 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -1,19 +1,21 @@ package tls import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" + "fmt" + "math/big" "net" "time" - - certificates "k8s.io/api/certificates/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const certValidityDuration = 10 * 365 * 24 * time.Hour + //TlsCertificateProps Properties of TLS certificate which should be issued for webhook server type TlsCertificateProps struct { Service string @@ -27,6 +29,11 @@ type TlsPemPair struct { PrivateKey []byte } +type KeyPair struct { + Cert *x509.Certificate + Key *rsa.PrivateKey +} + //TLSGeneratePrivateKey Generates RSA private key func TLSGeneratePrivateKey() (*rsa.PrivateKey, error) { return rsa.GenerateKey(rand.Reader, 2048) @@ -42,29 +49,92 @@ func TLSPrivateKeyToPem(rsaKey *rsa.PrivateKey) []byte { return pem.EncodeToMemory(privateKey) } -//TlsCertificateRequestToPem Creates PEM block from raw certificate request -func certificateRequestToPem(csrRaw []byte) []byte { - csrBlock := &pem.Block{ - Type: "CERTIFICATE REQUEST", - Bytes: csrRaw, +func pemEncode(certificateDER []byte, key *rsa.PrivateKey) ([]byte, []byte, error) { + certBuf := &bytes.Buffer{} + if err := pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: certificateDER}); err != nil { + return nil, nil, fmt.Errorf("encoding cert: %v", err) } - - return pem.EncodeToMemory(csrBlock) + keyBuf := &bytes.Buffer{} + if err := pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}); err != nil { + return nil, nil, fmt.Errorf("encoding key: %v", err) + } + return certBuf.Bytes(), keyBuf.Bytes(), nil } -//CertificateGenerateRequest Generates raw certificate signing request -func CertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificateProps, fqdncn bool) (*certificates.CertificateSigningRequest, error) { +func TLSCertificateToPem(certificateDER []byte) []byte { + certificate := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certificateDER, + } + + return pem.EncodeToMemory(certificate) +} + +// GenerateCACert creates the self-signed CA cert and private key +// it will be used to sign the webhook server certificate +func GenerateCACert() (*KeyPair, *TlsPemPair, error) { + now := time.Now() + begin := now.Add(-1 * time.Hour) + end := now.Add(certValidityDuration) + templ := &x509.Certificate{ + SerialNumber: big.NewInt(0), + Subject: pkix.Name{ + CommonName: "*.kyverno.svc", + }, + NotBefore: begin, + NotAfter: end, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, fmt.Errorf("error generating key: %v", err) + } + der, err := x509.CreateCertificate(rand.Reader, templ, templ, key.Public(), key) + if err != nil { + return nil, nil, fmt.Errorf("error creating certificate: %v", err) + } + + pemPair := &TlsPemPair{ + Certificate: TLSCertificateToPem(der), + PrivateKey: TLSPrivateKeyToPem(key), + } + + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, nil, fmt.Errorf("error parsing certificate %v", err) + } + + caCert := &KeyPair{ + Cert: cert, + Key: key, + } + + return caCert, pemPair, nil +} + +// GenerateCertPEM takes the results of GenerateCACert and uses it to create the +// PEM-encoded public certificate and private key, respectively +func GenerateCertPEM(caCert *KeyPair, props TlsCertificateProps, fqdncn bool) (*TlsPemPair, error) { + now := time.Now() + begin := now.Add(-1 * time.Hour) + end := now.Add(certValidityDuration) + dnsNames := make([]string, 3) - dnsNames[0] = props.Service - dnsNames[1] = props.Service + "." + props.Namespace + dnsNames[0] = fmt.Sprintf("%s", props.Service) + csCommonName := dnsNames[0] + + dnsNames[1] = fmt.Sprintf("%s.%s", props.Service, props.Namespace) // The full service name is the CommonName for the certificate commonName := GenerateInClusterServiceName(props) - dnsNames[2] = commonName - csCommonName := props.Service + dnsNames[2] = fmt.Sprintf("%s", commonName) + if fqdncn { // use FQDN as CommonName as a workaournd for https://github.com/kyverno/kyverno/issues/542 csCommonName = commonName } + var ips []net.IP apiServerIP := net.ParseIP(props.ApiServerHost) if apiServerIP != nil { @@ -73,39 +143,47 @@ func CertificateGenerateRequest(privateKey *rsa.PrivateKey, props TlsCertificate dnsNames = append(dnsNames, props.ApiServerHost) } - csrTemplate := x509.CertificateRequest{ + templ := &x509.Certificate{ + SerialNumber: big.NewInt(1), Subject: pkix.Name{ CommonName: csCommonName, }, - SignatureAlgorithm: x509.SHA256WithRSA, - DNSNames: dnsNames, - IPAddresses: ips, + DNSNames: dnsNames, + // IPAddresses: ips, + NotBefore: begin, + NotAfter: end, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, } - csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey) + key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - return nil, err + return nil, fmt.Errorf("error generating key for webhook %v", err) + } + der, err := x509.CreateCertificate(rand.Reader, templ, caCert.Cert, key.Public(), caCert.Key) + if err != nil { + return nil, fmt.Errorf("error creating certificate for webhook %v", err) } - return &certificates.CertificateSigningRequest{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "certificates.k8s.io/v1beta1", - Kind: "CertificateSigningRequest", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: props.Service + "." + props.Namespace + ".cert-request", - }, - Spec: certificates.CertificateSigningRequestSpec{ - Request: certificateRequestToPem(csrBytes), - Groups: []string{"system:masters", "system:authenticated"}, - Usages: []certificates.KeyUsage{ - certificates.UsageDigitalSignature, - certificates.UsageKeyEncipherment, - certificates.UsageServerAuth, - certificates.UsageClientAuth, - }, - }, - }, nil + pemPair := &TlsPemPair{ + Certificate: TLSCertificateToPem(der), + PrivateKey: TLSPrivateKeyToPem(key), + } + + // certPEM := TLSCertificateToPem(der) + // keyPEM := TLSPrivateKeyToPem(key) + return pemPair, nil +} + +//TlsCertificateRequestToPem Creates PEM block from raw certificate request +func certificateRequestToPem(csrRaw []byte) []byte { + csrBlock := &pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrRaw, + } + + return pem.EncodeToMemory(csrBlock) } //GenerateInClusterServiceName The generated service name should be the common name for TLS certificate