blob: 8847753a20e58d4219c3afbe7d60d525dfe780bd [file] [log] [blame]
// Copyright Istio Authors
//
// 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 chiron
import (
"bytes"
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"os"
"time"
)
import (
"istio.io/pkg/log"
certv1 "k8s.io/api/certificates/v1"
certv1beta1 "k8s.io/api/certificates/v1beta1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
rand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/apimachinery/pkg/watch"
clientset "k8s.io/client-go/kubernetes"
)
import (
"github.com/apache/dubbo-go-pixiu/security/pkg/pki/util"
)
const (
randomLength = 18
csrRetriesMax = 3
// cert-manager use below annotation on kubernetes CSR to control TTL for the generated cert.
RequestLifeTimeAnnotationForCertManager = "experimental.cert-manager.io/request-duration"
)
type CsrNameGenerator func(string, string) string
// GenCsrName : Generate CSR Name for Resource. Guarantees returning a resource name that doesn't already exist
func GenCsrName() string {
name := fmt.Sprintf("csr-workload-%s", rand.String(randomLength))
return name
}
// GenKeyCertK8sCA : Generates a key pair and gets public certificate signed by K8s_CA
// Options are meant to sign DNS certs
// 1. Generate a CSR
// 2. Call SignCSRK8sCA to finish rest of the flow
func GenKeyCertK8sCA(client clientset.Interface, dnsName,
secretName, secretNamespace, caFilePath string,
signerName string, approveCsr bool, requestedLifetime time.Duration,
) ([]byte, []byte, []byte, error) {
// 1. Generate a CSR
options := util.CertOptions{
Host: dnsName,
RSAKeySize: keySize,
IsDualUse: false,
PKCS8Key: false,
}
csrPEM, keyPEM, err := util.GenCSR(options)
if err != nil {
log.Errorf("CSR generation error (%v)", err)
return nil, nil, nil, err
}
usages := []certv1.KeyUsage{
certv1.UsageDigitalSignature,
certv1.UsageKeyEncipherment,
certv1.UsageServerAuth,
certv1.UsageClientAuth,
}
if signerName == "" {
signerName = "kubernetes.io/legacy-unknown"
}
certChain, caCert, err := SignCSRK8s(client, csrPEM, signerName, usages, dnsName, caFilePath, approveCsr, true, requestedLifetime)
return certChain, keyPEM, caCert, err
}
// SignCSRK8s generates a certificate from CSR using the K8s CA
// 1. Submit a CSR
// 2. Approve a CSR
// 3. Read the signed certificate
// 4. Clean up the artifacts (e.g., delete CSR)
func SignCSRK8s(client clientset.Interface, csrData []byte, signerName string, usages []certv1.KeyUsage,
dnsName, caFilePath string, approveCsr, appendCaCert bool, requestedLifetime time.Duration,
) ([]byte, []byte, error) {
var err error
v1Req := false
// 1. Submit the CSR
csrName, v1CsrReq, v1Beta1CsrReq, err := submitCSR(client, csrData, signerName, usages, csrRetriesMax, requestedLifetime)
if err != nil {
return nil, nil, fmt.Errorf("unable to submit CSR request (%v). Error: %v", csrName, err)
}
log.Debugf("CSR (%v) has been created", csrName)
if v1CsrReq != nil {
v1Req = true
}
// clean up certificate request after deletion
defer func() {
_ = cleanUpCertGen(client, v1Req, csrName)
}()
// 2. Approve the CSR
if approveCsr {
csrMsg := fmt.Sprintf("CSR (%s) for the certificate (%s) is approved", csrName, dnsName)
err = approveCSR(csrName, csrMsg, client, v1CsrReq, v1Beta1CsrReq)
if err != nil {
return nil, nil, fmt.Errorf("unable to approve CSR request. Error: %v", err)
}
log.Debugf("CSR (%v) is approved", csrName)
}
// 3. Read the signed certificate
certChain, caCert, err := readSignedCertificate(client,
csrName, certWatchTimeout, certReadInterval, maxNumCertRead, caFilePath, appendCaCert, v1Req)
if err != nil {
return nil, nil, err
}
// If there is a failure of cleaning up CSR, the error is returned.
return certChain, caCert, err
}
// Read CA certificate and check whether it is a valid certificate.
func readCACert(caCertPath string) ([]byte, error) {
caCert, err := os.ReadFile(caCertPath)
if err != nil {
log.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err)
return nil, fmt.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err)
}
b, _ := pem.Decode(caCert)
if b == nil {
return nil, fmt.Errorf("could not decode pem")
}
if b.Type != "CERTIFICATE" {
return nil, fmt.Errorf("ca certificate contains wrong type: %v", b.Type)
}
if _, err := x509.ParseCertificate(b.Bytes); err != nil {
return nil, fmt.Errorf("ca certificate parsing returns an error: %v", err)
}
return caCert, nil
}
func isTCPReachable(host string, port int) bool {
addr := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", addr, 1*time.Second)
if err != nil {
log.Debugf("DialTimeout() returns err: %v", err)
// No connection yet, so no need to conn.Close()
return false
}
conn.Close()
return true
}
// Reload CA cert from file and return whether CA cert is changed
func reloadCACert(wc *WebhookController) (bool, error) {
certChanged := false
wc.certMutex.Lock()
defer wc.certMutex.Unlock()
caCert, err := readCACert(wc.k8sCaCertFile)
if err != nil {
return certChanged, err
}
if !bytes.Equal(caCert, wc.CACert) {
wc.CACert = append([]byte(nil), caCert...)
certChanged = true
}
return certChanged, nil
}
func submitCSR(clientset clientset.Interface,
csrData []byte, signerName string,
usages []certv1.KeyUsage, numRetries int, requestedLifetime time.Duration) (string, *certv1.CertificateSigningRequest,
*certv1beta1.CertificateSigningRequest, error,
) {
var lastErr error
useV1 := true
csrName := ""
for i := 0; i < numRetries; i++ {
if csrName == "" {
csrName = GenCsrName()
}
if useV1 && len(usages) > 0 && len(signerName) > 0 && signerName != "kubernetes.io/legacy-unknown" {
log.Debugf("trial %v using v1 api to create CSR (%v)", i+1, csrName)
csr := &certv1.CertificateSigningRequest{
// Username, UID, Groups will be injected by API server.
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
ObjectMeta: metav1.ObjectMeta{
Name: csrName,
},
Spec: certv1.CertificateSigningRequestSpec{
Request: csrData,
Usages: usages,
SignerName: signerName,
},
}
if requestedLifetime != time.Duration(0) {
csr.ObjectMeta.Annotations = map[string]string{RequestLifeTimeAnnotationForCertManager: requestedLifetime.String()}
}
v1req, err := clientset.CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
if err == nil {
return csrName, v1req, nil, nil
}
lastErr = err
if apierrors.IsAlreadyExists(err) {
csrName = ""
continue
} else if apierrors.IsNotFound(err) {
// don't attempt to use older api unless we get an API error
useV1 = false
} else {
continue
}
}
// Only exercise v1beta1 logic if v1 api was not found
log.Debugf("trial %v using v1beta1 api for csr %v", i+1, csrName)
// convert relevant bits to v1beta1
v1beta1csr := &certv1beta1.CertificateSigningRequest{
ObjectMeta: metav1.ObjectMeta{
Name: csrName,
},
Spec: certv1beta1.CertificateSigningRequestSpec{
SignerName: &signerName,
Request: csrData,
},
}
for _, usage := range usages {
v1beta1csr.Spec.Usages = append(v1beta1csr.Spec.Usages, certv1beta1.KeyUsage(usage))
}
if requestedLifetime != time.Duration(0) {
v1beta1csr.ObjectMeta.Annotations = map[string]string{RequestLifeTimeAnnotationForCertManager: requestedLifetime.String()}
}
// create v1beta1 certificate request
v1beta1req, err := clientset.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), v1beta1csr, metav1.CreateOptions{})
if err == nil {
return csrName, nil, v1beta1req, nil
}
lastErr = err
if apierrors.IsAlreadyExists(err) {
csrName = ""
}
}
log.Errorf("retry attempts exceeded when creating csr request %v", csrName)
return "", nil, nil, lastErr
}
func approveCSR(csrName string, csrMsg string, client clientset.Interface,
v1CsrReq *certv1.CertificateSigningRequest, v1Beta1CsrReq *certv1beta1.CertificateSigningRequest,
) error {
err := errors.New("invalid CSR")
if v1Beta1CsrReq != nil {
v1Beta1CsrReq.Status.Conditions = append(v1Beta1CsrReq.Status.Conditions, certv1beta1.CertificateSigningRequestCondition{
Type: certv1beta1.CertificateApproved,
Reason: csrMsg,
Message: csrMsg,
})
_, err = client.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(context.TODO(), v1Beta1CsrReq, metav1.UpdateOptions{})
if err != nil {
log.Errorf("failed to approve CSR (%v): %v", csrName, err)
return err
}
} else if v1CsrReq != nil {
v1CsrReq.Status.Conditions = append(v1CsrReq.Status.Conditions, certv1.CertificateSigningRequestCondition{
Type: certv1.CertificateApproved,
Reason: csrMsg,
Message: csrMsg,
Status: corev1.ConditionTrue,
})
_, err = client.CertificatesV1().CertificateSigningRequests().UpdateApproval(context.TODO(), csrName, v1CsrReq, metav1.UpdateOptions{})
if err != nil {
log.Errorf("failed to approve CSR (%v): %v", csrName, err)
return err
}
}
return err
}
// Read the signed certificate
// verify and append CA certificate to certChain if appendCaCert is true
func readSignedCertificate(client clientset.Interface, csrName string,
watchTimeout, readInterval time.Duration,
maxNumRead int, caCertPath string, appendCaCert bool, usev1 bool,
) ([]byte, []byte, error) {
// First try to read the signed CSR through a watching mechanism
certPEM := readSignedCsr(client, csrName, watchTimeout, readInterval, maxNumRead, usev1)
if len(certPEM) == 0 {
return []byte{}, []byte{}, fmt.Errorf("no certificate returned for the CSR: %q", csrName)
}
certsParsed, err := util.ParsePemEncodedCertificateChain(certPEM)
if err != nil {
return nil, nil, fmt.Errorf("decoding certificate failed")
}
caCert := []byte{}
certChain := []byte{}
certChain = append(certChain, certPEM...)
if appendCaCert && caCertPath != "" {
caCert, err = readCACert(caCertPath)
if err != nil {
return nil, nil, fmt.Errorf("error when retrieving CA cert: (%v)", err)
}
// Verify the certificate chain before returning the certificate
roots := x509.NewCertPool()
if roots == nil {
return nil, nil, fmt.Errorf("failed to create cert pool")
}
if ok := roots.AppendCertsFromPEM(caCert); !ok {
return nil, nil, fmt.Errorf("failed to append CA certificate")
}
intermediates := x509.NewCertPool()
if len(certsParsed) > 1 {
for _, cert := range certsParsed[1:] {
intermediates.AddCert(cert)
}
}
_, err = certsParsed[0].Verify(x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to verify the certificate chain: %v", err)
}
certChain = append(certChain, caCert...)
}
return certChain, caCert, nil
}
func checkDuplicateCsr(client clientset.Interface, csrName string) (*certv1.CertificateSigningRequest, *certv1beta1.CertificateSigningRequest) {
v1CsrReq, err := client.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), csrName, metav1.GetOptions{})
if err == nil {
return v1CsrReq, nil
}
v1Beta1CsrReq, err := client.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), csrName, metav1.GetOptions{})
if err == nil {
return nil, v1Beta1CsrReq
}
return nil, nil
}
func getSignedCsr(client clientset.Interface, csrName string, readInterval time.Duration, maxNumRead int, usev1 bool) []byte {
var err error
if usev1 {
var r *certv1.CertificateSigningRequest
for i := 0; i < maxNumRead; i++ {
r, err = client.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), csrName, metav1.GetOptions{})
if err == nil && r.Status.Certificate != nil {
// Certificate is ready
return r.Status.Certificate
}
time.Sleep(readInterval)
}
if err != nil || r.Status.Certificate == nil {
if err != nil {
log.Errorf("failed to read the CSR (%v): %v", csrName, err)
} else if r.Status.Certificate == nil {
for _, c := range r.Status.Conditions {
if c.Type == certv1.CertificateDenied {
log.Errorf("CertificateDenied, name: %v, uid: %v, cond-type: %v, cond: %s",
r.Name, r.UID, c.Type, c.String())
break
}
}
}
return []byte{}
}
} else {
var r *certv1beta1.CertificateSigningRequest
for i := 0; i < maxNumRead; i++ {
r, err = client.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), csrName, metav1.GetOptions{})
if err == nil && r.Status.Certificate != nil {
// Certificate is ready
return r.Status.Certificate
}
time.Sleep(readInterval)
}
if err != nil || r.Status.Certificate == nil {
if err != nil {
log.Errorf("failed to read the CSR (%v): %v", csrName, err)
} else if r.Status.Certificate == nil {
for _, c := range r.Status.Conditions {
if c.Type == certv1beta1.CertificateDenied {
log.Errorf("CertificateDenied, name: %v, uid: %v, cond-type: %v, cond: %s",
r.Name, r.UID, c.Type, c.String())
break
}
}
}
return []byte{}
}
}
return []byte{}
}
// Return signed CSR through a watcher. If no CSR is read, return nil.
func readSignedCsr(client clientset.Interface, csrName string, watchTimeout time.Duration, readInterval time.Duration,
maxNumRead int, usev1 bool,
) []byte {
var watcher watch.Interface
var err error
selector := fields.OneTermEqualSelector("metadata.name", csrName).String()
if usev1 {
// Setup a List+Watch, like informers do
// A simple Watch will fail if the cert is signed too quickly
l, _ := client.CertificatesV1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{
FieldSelector: selector,
})
if l != nil && len(l.Items) > 0 {
reqSigned := l.Items[0]
if reqSigned.Status.Certificate != nil {
return reqSigned.Status.Certificate
}
}
var rv string
if l != nil {
rv = l.ResourceVersion
}
watcher, err = client.CertificatesV1().CertificateSigningRequests().Watch(context.TODO(), metav1.ListOptions{
ResourceVersion: rv,
FieldSelector: selector,
})
} else {
l, _ := client.CertificatesV1beta1().CertificateSigningRequests().List(context.TODO(), metav1.ListOptions{
FieldSelector: selector,
})
if l != nil && len(l.Items) > 0 {
reqSigned := l.Items[0]
if reqSigned.Status.Certificate != nil {
return reqSigned.Status.Certificate
}
}
var rv string
if l != nil {
rv = l.ResourceVersion
}
watcher, err = client.CertificatesV1beta1().CertificateSigningRequests().Watch(context.TODO(), metav1.ListOptions{
ResourceVersion: rv,
FieldSelector: selector,
})
}
if err == nil {
timeout := false
// Set a timeout
timer := time.After(watchTimeout)
for {
select {
case r := <-watcher.ResultChan():
if usev1 {
reqSigned := r.Object.(*certv1.CertificateSigningRequest)
if reqSigned.Status.Certificate != nil {
return reqSigned.Status.Certificate
}
} else {
reqSigned := r.Object.(*certv1beta1.CertificateSigningRequest)
if reqSigned.Status.Certificate != nil {
return reqSigned.Status.Certificate
}
}
case <-timer:
log.Debugf("timeout when watching CSR %v", csrName)
timeout = true
}
if timeout {
break
}
}
}
return getSignedCsr(client, csrName, readInterval, maxNumRead, usev1)
}
// Clean up the CSR
func cleanUpCertGen(client clientset.Interface, usev1 bool, csrName string) error {
var err error
if usev1 {
err = client.CertificatesV1().CertificateSigningRequests().Delete(context.TODO(), csrName, metav1.DeleteOptions{})
} else {
err = client.CertificatesV1beta1().CertificateSigningRequests().Delete(context.TODO(), csrName, metav1.DeleteOptions{})
}
if err != nil {
log.Errorf("failed to delete CSR (%v): %v", csrName, err)
} else {
log.Debugf("deleted CSR: %v", csrName)
}
return err
}