github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/certificate/reenroller/hsmreenroller.go (about)

     1  /*
     2   * Copyright contributors to the Hyperledger Fabric Operator project
     3   *
     4   * SPDX-License-Identifier: Apache-2.0
     5   *
     6   * Licensed under the Apache License, Version 2.0 (the "License");
     7   * you may not use this file except in compliance with the License.
     8   * You may obtain a copy of the License at:
     9   *
    10   * 	  http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  
    19  package reenroller
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"path/filepath"
    25  	"time"
    26  
    27  	"github.com/hyperledger/fabric-ca/lib"
    28  	"github.com/hyperledger/fabric-ca/lib/tls"
    29  	"github.com/pkg/errors"
    30  
    31  	current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1"
    32  	commonapi "github.com/IBM-Blockchain/fabric-operator/pkg/apis/common"
    33  	"github.com/IBM-Blockchain/fabric-operator/pkg/initializer/common/config"
    34  	k8sclient "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient"
    35  	jobv1 "github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources/job"
    36  	"github.com/IBM-Blockchain/fabric-operator/pkg/util"
    37  
    38  	batchv1 "k8s.io/api/batch/v1"
    39  	corev1 "k8s.io/api/core/v1"
    40  	"k8s.io/apimachinery/pkg/api/resource"
    41  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    42  	"k8s.io/apimachinery/pkg/runtime"
    43  	"k8s.io/apimachinery/pkg/types"
    44  	"k8s.io/apimachinery/pkg/util/wait"
    45  
    46  	"sigs.k8s.io/controller-runtime/pkg/client"
    47  	"sigs.k8s.io/yaml"
    48  )
    49  
    50  type Instance interface {
    51  	metav1.Object
    52  	EnrollerImage() string
    53  	GetPullSecrets() []corev1.LocalObjectReference
    54  	GetResource(current.Component) corev1.ResourceRequirements
    55  	PVCName() string
    56  }
    57  
    58  type HSMReenroller struct {
    59  	CAClient *lib.Client
    60  	Identity Identity
    61  
    62  	HomeDir   string
    63  	Config    *current.Enrollment
    64  	BCCSP     bool
    65  	Timeout   time.Duration
    66  	HSMConfig *config.HSMConfig
    67  	Instance  Instance
    68  	Client    k8sclient.Client
    69  	Scheme    *runtime.Scheme
    70  	NewKey    bool
    71  }
    72  
    73  func NewHSMReenroller(cfg *current.Enrollment, homeDir string, bccsp *commonapi.BCCSP, timeoutstring string, hsmConfig *config.HSMConfig, instance Instance, client k8sclient.Client, scheme *runtime.Scheme, newKey bool) (*HSMReenroller, error) {
    74  	if cfg == nil {
    75  		return nil, errors.New("unable to reenroll, Enrollment config must be passed")
    76  	}
    77  
    78  	err := EnrollmentConfigValidation(cfg)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	caclient := &lib.Client{
    84  		HomeDir: homeDir,
    85  		Config: &lib.ClientConfig{
    86  			TLS: tls.ClientTLSConfig{
    87  				Enabled:   true,
    88  				CertFiles: []string{"tlsCert.pem"},
    89  			},
    90  			URL: fmt.Sprintf("https://%s:%s", cfg.CAHost, cfg.CAPort),
    91  		},
    92  	}
    93  
    94  	bccsp.PKCS11.Library = filepath.Join("/hsm/lib", filepath.Base(hsmConfig.Library.FilePath))
    95  
    96  	caclient = GetClient(caclient, bccsp)
    97  
    98  	timeout, err := time.ParseDuration(timeoutstring)
    99  	if err != nil || timeoutstring == "" {
   100  		timeout = time.Duration(60 * time.Second)
   101  	}
   102  
   103  	r := &HSMReenroller{
   104  		CAClient:  caclient,
   105  		HomeDir:   homeDir,
   106  		Config:    cfg,
   107  		Timeout:   timeout,
   108  		HSMConfig: hsmConfig,
   109  		Instance:  instance,
   110  		Client:    client,
   111  		Scheme:    scheme,
   112  		NewKey:    newKey,
   113  	}
   114  
   115  	if bccsp != nil {
   116  		r.BCCSP = true
   117  	}
   118  
   119  	return r, nil
   120  }
   121  
   122  func (r *HSMReenroller) IsCAReachable() bool {
   123  	log.Info("Check if CA is reachable before triggering enroll job")
   124  
   125  	timeout := r.Timeout
   126  	url := fmt.Sprintf("https://%s:%s/cainfo", r.Config.CAHost, r.Config.CAPort)
   127  
   128  	// Convert TLS certificate from base64 to file
   129  	tlsCertBytes, err := util.Base64ToBytes(r.Config.CATLS.CACert)
   130  	if err != nil {
   131  		log.Error(err, "Cannot convert TLS Certificate from base64")
   132  		return false
   133  	}
   134  
   135  	err = wait.Poll(500*time.Millisecond, timeout, func() (bool, error) {
   136  		err = util.HealthCheck(url, tlsCertBytes, timeout)
   137  		if err == nil {
   138  			return true, nil
   139  		}
   140  		return false, nil
   141  	})
   142  	if err != nil {
   143  		log.Error(err, "Health check failed")
   144  		return false
   145  	}
   146  
   147  	return true
   148  }
   149  
   150  func (r *HSMReenroller) Reenroll() (*config.Response, error) {
   151  	if !r.IsCAReachable() {
   152  		return nil, errors.New("unable to enroll, CA is not reachable")
   153  	}
   154  
   155  	// Deleting CA client config is an unfortunate requirement since the ca client
   156  	// config map was not properly deleted after a successfull reenrollment request.
   157  	// This is problematic when recreating a resource with same name, as it will
   158  	// try to use old settings in the config map, which might no longer apply, thus
   159  	// it must be removed if found before proceeding.
   160  	if err := deleteCAClientConfig(r.Client, r.Instance); err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	if err := createRootTLSSecret(r.Client, r.Instance, r.Scheme, r.Config.CATLS.CACert); err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	if err := createCAClientConfig(r.Client, r.Instance, r.Scheme, r.CAClient.Config); err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	job := r.initHSMJob(r.Instance, r.HSMConfig, r.Timeout)
   173  	if err := r.Client.Create(context.TODO(), job.Job, k8sclient.CreateOption{
   174  		Owner:  r.Instance,
   175  		Scheme: r.Scheme,
   176  	}); err != nil {
   177  		return nil, errors.Wrap(err, "failed to create HSM ca initialization job")
   178  	}
   179  	log.Info(fmt.Sprintf("Job '%s' created", job.GetName()))
   180  
   181  	if err := job.WaitUntilActive(r.Client); err != nil {
   182  		return nil, err
   183  	}
   184  	log.Info(fmt.Sprintf("Job '%s' active", job.GetName()))
   185  
   186  	if err := job.WaitUntilFinished(r.Client); err != nil {
   187  		return nil, err
   188  	}
   189  	log.Info(fmt.Sprintf("Job '%s' finished", job.GetName()))
   190  
   191  	status, err := job.Status(r.Client)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	switch status {
   197  	case jobv1.FAILED:
   198  		return nil, fmt.Errorf("Job '%s' finished unsuccessfully, not cleaning up pods to allow for error evaluation", job.GetName())
   199  	case jobv1.COMPLETED:
   200  		if err := job.Delete(r.Client); err != nil {
   201  			return nil, err
   202  		}
   203  
   204  		if err := deleteRootTLSSecret(r.Client, r.Instance); err != nil {
   205  			return nil, err
   206  		}
   207  
   208  		if err := deleteCAClientConfig(r.Client, r.Instance); err != nil {
   209  			return nil, err
   210  		}
   211  	}
   212  
   213  	if err := r.setControllerReferences(); err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	return &config.Response{}, nil
   218  }
   219  
   220  func (r *HSMReenroller) setControllerReferences() error {
   221  	if err := setControllerReferenceFor(r.Client, r.Instance, r.Scheme, fmt.Sprintf("ecert-%s-signcert", r.Instance.GetName()), false); err != nil {
   222  		return err
   223  	}
   224  
   225  	if err := setControllerReferenceFor(r.Client, r.Instance, r.Scheme, fmt.Sprintf("ecert-%s-cacerts", r.Instance.GetName()), false); err != nil {
   226  		return err
   227  	}
   228  
   229  	if err := setControllerReferenceFor(r.Client, r.Instance, r.Scheme, fmt.Sprintf("ecert-%s-intercerts", r.Instance.GetName()), true); err != nil {
   230  		return err
   231  	}
   232  
   233  	return nil
   234  }
   235  
   236  func setControllerReferenceFor(client k8sclient.Client, instance Instance, scheme *runtime.Scheme, name string, skipIfNotFound bool) error {
   237  	nn := types.NamespacedName{
   238  		Name:      name,
   239  		Namespace: instance.GetNamespace(),
   240  	}
   241  
   242  	sec := &corev1.Secret{}
   243  	if err := client.Get(context.TODO(), nn, sec); err != nil {
   244  		if skipIfNotFound {
   245  			return nil
   246  		}
   247  
   248  		return err
   249  	}
   250  
   251  	if err := client.Update(context.TODO(), sec, k8sclient.UpdateOption{
   252  		Owner:  instance,
   253  		Scheme: scheme,
   254  	}); err != nil {
   255  		return errors.Wrapf(err, "failed to update secret '%s' with controller reference", instance.GetName())
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func createRootTLSSecret(client k8sclient.Client, instance Instance, scheme *runtime.Scheme, cert string) error {
   262  	tlsCertBytes, err := util.Base64ToBytes(cert)
   263  	if err != nil {
   264  		return err
   265  	}
   266  
   267  	secret := &corev1.Secret{
   268  		ObjectMeta: metav1.ObjectMeta{
   269  			Name:      fmt.Sprintf("%s-init-roottls", instance.GetName()),
   270  			Namespace: instance.GetNamespace(),
   271  		},
   272  		Data: map[string][]byte{
   273  			"tlsCert.pem": tlsCertBytes,
   274  		},
   275  	}
   276  
   277  	if err := client.Create(context.TODO(), secret, k8sclient.CreateOption{
   278  		Owner:  instance,
   279  		Scheme: scheme,
   280  	}); err != nil {
   281  		return errors.Wrap(err, "failed to create secret")
   282  	}
   283  
   284  	return nil
   285  }
   286  
   287  func deleteRootTLSSecret(client k8sclient.Client, instance Instance) error {
   288  	secret := &corev1.Secret{
   289  		ObjectMeta: metav1.ObjectMeta{
   290  			Name:      fmt.Sprintf("%s-init-roottls", instance.GetName()),
   291  			Namespace: instance.GetNamespace(),
   292  		},
   293  	}
   294  
   295  	if err := client.Delete(context.TODO(), secret); err != nil {
   296  		return errors.Wrap(err, "failed to delete secret")
   297  	}
   298  
   299  	return nil
   300  }
   301  
   302  func createCAClientConfig(client k8sclient.Client, instance Instance, scheme *runtime.Scheme, config *lib.ClientConfig) error {
   303  	configBytes, err := yaml.Marshal(config)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	cm := &corev1.ConfigMap{
   309  		ObjectMeta: metav1.ObjectMeta{
   310  			Name:      fmt.Sprintf("%s-init-config", instance.GetName()),
   311  			Namespace: instance.GetNamespace(),
   312  		},
   313  		BinaryData: map[string][]byte{
   314  			"fabric-ca-client-config.yaml": configBytes,
   315  		},
   316  	}
   317  
   318  	if err := client.Create(context.TODO(), cm, k8sclient.CreateOption{
   319  		Owner:  instance,
   320  		Scheme: scheme,
   321  	}); err != nil {
   322  		return errors.Wrap(err, "failed to create config map")
   323  	}
   324  
   325  	return nil
   326  }
   327  
   328  func deleteCAClientConfig(k8sClient k8sclient.Client, instance Instance) error {
   329  	cm := &corev1.ConfigMap{
   330  		ObjectMeta: metav1.ObjectMeta{
   331  			Name:      fmt.Sprintf("%s-init-config", instance.GetName()),
   332  			Namespace: instance.GetNamespace(),
   333  		},
   334  	}
   335  
   336  	if err := k8sClient.Delete(context.TODO(), cm); client.IgnoreNotFound(err) != nil {
   337  		return errors.Wrap(err, "failed to delete confk8smap")
   338  	}
   339  
   340  	return nil
   341  }
   342  
   343  func (r *HSMReenroller) initHSMJob(instance Instance, hsmConfig *config.HSMConfig, timeout time.Duration) *jobv1.Job {
   344  	hsmLibraryPath := hsmConfig.Library.FilePath
   345  	hsmLibraryName := filepath.Base(hsmLibraryPath)
   346  
   347  	jobName := fmt.Sprintf("%s-reenroll", instance.GetName())
   348  
   349  	f := false
   350  	user := int64(0)
   351  	backoffLimit := int32(0)
   352  	mountPath := "/shared"
   353  
   354  	k8sJob := &batchv1.Job{
   355  		ObjectMeta: metav1.ObjectMeta{
   356  			Name:      jobName,
   357  			Namespace: instance.GetNamespace(),
   358  			Labels: map[string]string{
   359  				"name":  jobName,
   360  				"owner": instance.GetName(),
   361  			},
   362  		},
   363  		Spec: batchv1.JobSpec{
   364  			BackoffLimit: &backoffLimit,
   365  			Template: corev1.PodTemplateSpec{
   366  				Spec: corev1.PodSpec{
   367  					ServiceAccountName: instance.GetName(),
   368  					ImagePullSecrets:   util.AppendImagePullSecretIfMissing(instance.GetPullSecrets(), hsmConfig.BuildPullSecret()),
   369  					RestartPolicy:      corev1.RestartPolicyNever,
   370  					InitContainers: []corev1.Container{
   371  						corev1.Container{
   372  							Name:            "hsm-client",
   373  							Image:           hsmConfig.Library.Image,
   374  							ImagePullPolicy: corev1.PullAlways,
   375  							Command: []string{
   376  								"sh",
   377  								"-c",
   378  								fmt.Sprintf("mkdir -p %s/hsm && dst=\"%s/hsm/%s\" && echo \"Copying %s to ${dst}\" && mkdir -p $(dirname $dst) && cp -r %s $dst", mountPath, mountPath, hsmLibraryName, hsmLibraryPath, hsmLibraryPath),
   379  							},
   380  							SecurityContext: &corev1.SecurityContext{
   381  								RunAsUser:    &user,
   382  								RunAsNonRoot: &f,
   383  							},
   384  							VolumeMounts: []corev1.VolumeMount{
   385  								corev1.VolumeMount{
   386  									Name:      "shared",
   387  									MountPath: mountPath,
   388  								},
   389  							},
   390  							Resources: corev1.ResourceRequirements{
   391  								Requests: corev1.ResourceList{
   392  									corev1.ResourceCPU:              resource.MustParse("0.1"),
   393  									corev1.ResourceMemory:           resource.MustParse("100Mi"),
   394  									corev1.ResourceEphemeralStorage: resource.MustParse("100Mi"),
   395  								},
   396  								Limits: corev1.ResourceList{
   397  									corev1.ResourceCPU:              resource.MustParse("1"),
   398  									corev1.ResourceMemory:           resource.MustParse("500Mi"),
   399  									corev1.ResourceEphemeralStorage: resource.MustParse("1Gi"),
   400  								},
   401  							},
   402  						},
   403  					},
   404  					Containers: []corev1.Container{
   405  						corev1.Container{
   406  							Name:            "init",
   407  							Image:           instance.EnrollerImage(),
   408  							ImagePullPolicy: corev1.PullAlways,
   409  							SecurityContext: &corev1.SecurityContext{
   410  								RunAsUser:    &user,
   411  								RunAsNonRoot: &f,
   412  							},
   413  							Command: []string{
   414  								"sh",
   415  								"-c",
   416  								fmt.Sprintf("/usr/local/bin/enroller node reenroll %s %s %s %s %s %s %s %s %s %t", r.HomeDir, "/tmp/fabric-ca-client-config.yaml", r.Config.CAHost, r.Config.CAPort, r.Config.CAName, instance.GetName(), instance.GetNamespace(), r.Config.EnrollID, fmt.Sprintf("%s/cert.pem", r.HomeDir), r.NewKey),
   417  							},
   418  							VolumeMounts: []corev1.VolumeMount{
   419  								corev1.VolumeMount{
   420  									Name:      "tlscertfile",
   421  									MountPath: fmt.Sprintf("%s/tlsCert.pem", r.HomeDir),
   422  									SubPath:   "tlsCert.pem",
   423  								},
   424  								corev1.VolumeMount{
   425  									Name:      "certfile",
   426  									MountPath: fmt.Sprintf("%s/cert.pem", r.HomeDir),
   427  									SubPath:   "cert.pem",
   428  								},
   429  								corev1.VolumeMount{
   430  									Name:      "clientconfig",
   431  									MountPath: fmt.Sprintf("/tmp/%s", "fabric-ca-client-config.yaml"),
   432  									SubPath:   "fabric-ca-client-config.yaml",
   433  								},
   434  								corev1.VolumeMount{
   435  									Name:      "shared",
   436  									MountPath: "/hsm/lib",
   437  									SubPath:   "hsm",
   438  								},
   439  							},
   440  						},
   441  					},
   442  					Volumes: []corev1.Volume{
   443  						corev1.Volume{
   444  							Name: "shared",
   445  							VolumeSource: corev1.VolumeSource{
   446  								EmptyDir: &corev1.EmptyDirVolumeSource{
   447  									Medium: corev1.StorageMediumMemory,
   448  								},
   449  							},
   450  						},
   451  						corev1.Volume{
   452  							Name: "tlscertfile",
   453  							VolumeSource: corev1.VolumeSource{
   454  								Secret: &corev1.SecretVolumeSource{
   455  									SecretName: fmt.Sprintf("%s-init-roottls", instance.GetName()),
   456  								},
   457  							},
   458  						},
   459  						corev1.Volume{
   460  							Name: "certfile",
   461  							VolumeSource: corev1.VolumeSource{
   462  								Secret: &corev1.SecretVolumeSource{
   463  									SecretName: fmt.Sprintf("ecert-%s-signcert", instance.GetName()),
   464  								},
   465  							},
   466  						},
   467  						corev1.Volume{
   468  							Name: "clientconfig",
   469  							VolumeSource: corev1.VolumeSource{
   470  								ConfigMap: &corev1.ConfigMapVolumeSource{
   471  									LocalObjectReference: corev1.LocalObjectReference{
   472  										Name: fmt.Sprintf("%s-init-config", instance.GetName()),
   473  									},
   474  								},
   475  							},
   476  						},
   477  					},
   478  				},
   479  			},
   480  		},
   481  	}
   482  
   483  	job := jobv1.New(k8sJob, &jobv1.Timeouts{
   484  		WaitUntilActive:   timeout,
   485  		WaitUntilFinished: timeout,
   486  	})
   487  
   488  	job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, hsmConfig.GetVolumes()...)
   489  	job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, hsmConfig.GetVolumeMounts()...)
   490  
   491  	return job
   492  }