github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/certificate/reenroller/hsmdaemonreenroller.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/util/wait"
    44  )
    45  
    46  type HSMDaemonReenroller struct {
    47  	CAClient *lib.Client
    48  	Identity Identity
    49  
    50  	HomeDir   string
    51  	Config    *current.Enrollment
    52  	BCCSP     bool
    53  	Timeout   time.Duration
    54  	HSMConfig *config.HSMConfig
    55  	Instance  Instance
    56  	Client    k8sclient.Client
    57  	Scheme    *runtime.Scheme
    58  	NewKey    bool
    59  }
    60  
    61  func NewHSMDaemonReenroller(cfg *current.Enrollment, homeDir string, bccsp *commonapi.BCCSP, timeoutstring string, hsmConfig *config.HSMConfig, instance Instance, client k8sclient.Client, scheme *runtime.Scheme, newKey bool) (*HSMDaemonReenroller, error) {
    62  	if cfg == nil {
    63  		return nil, errors.New("unable to reenroll, Enrollment config must be passed")
    64  	}
    65  
    66  	err := EnrollmentConfigValidation(cfg)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	caclient := &lib.Client{
    72  		HomeDir: homeDir,
    73  		Config: &lib.ClientConfig{
    74  			TLS: tls.ClientTLSConfig{
    75  				Enabled:   true,
    76  				CertFiles: []string{"tlsCert.pem"},
    77  			},
    78  			URL: fmt.Sprintf("https://%s:%s", cfg.CAHost, cfg.CAPort),
    79  		},
    80  	}
    81  
    82  	bccsp.PKCS11.Library = filepath.Join("/hsm/lib", filepath.Base(hsmConfig.Library.FilePath))
    83  
    84  	caclient = GetClient(caclient, bccsp)
    85  
    86  	timeout, err := time.ParseDuration(timeoutstring)
    87  	if err != nil || timeoutstring == "" {
    88  		timeout = time.Duration(60 * time.Second)
    89  	}
    90  
    91  	r := &HSMDaemonReenroller{
    92  		CAClient:  caclient,
    93  		HomeDir:   homeDir,
    94  		Config:    cfg,
    95  		Timeout:   timeout,
    96  		HSMConfig: hsmConfig,
    97  		Instance:  instance,
    98  		Client:    client,
    99  		Scheme:    scheme,
   100  		NewKey:    newKey,
   101  	}
   102  
   103  	if bccsp != nil {
   104  		r.BCCSP = true
   105  	}
   106  
   107  	return r, nil
   108  }
   109  
   110  func (r *HSMDaemonReenroller) IsCAReachable() bool {
   111  	log.Info("Check if CA is reachable before triggering enroll job")
   112  
   113  	timeout := r.Timeout
   114  	url := fmt.Sprintf("https://%s:%s/cainfo", r.Config.CAHost, r.Config.CAPort)
   115  
   116  	// Convert TLS certificate from base64 to file
   117  	tlsCertBytes, err := util.Base64ToBytes(r.Config.CATLS.CACert)
   118  	if err != nil {
   119  		log.Error(err, "Cannot convert TLS Certificate from base64")
   120  		return false
   121  	}
   122  
   123  	err = wait.Poll(500*time.Millisecond, timeout, func() (bool, error) {
   124  		err = util.HealthCheck(url, tlsCertBytes, timeout)
   125  		if err == nil {
   126  			return true, nil
   127  		}
   128  		return false, nil
   129  	})
   130  	if err != nil {
   131  		log.Error(err, "Health check failed")
   132  		return false
   133  	}
   134  
   135  	return true
   136  }
   137  
   138  func (r *HSMDaemonReenroller) Reenroll() (*config.Response, error) {
   139  	if !r.IsCAReachable() {
   140  		return nil, errors.New("unable to enroll, CA is not reachable")
   141  	}
   142  
   143  	// Deleting CA client config is an unfortunate requirement since the ca client
   144  	// config map was not properly deleted after a successfull reenrollment request.
   145  	// This is problematic when recreating a resource with same name, as it will
   146  	// try to use old settings in the config map, which might no longer apply, thus
   147  	// it must be removed if found before proceeding.
   148  	if err := deleteCAClientConfig(r.Client, r.Instance); err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	if err := createRootTLSSecret(r.Client, r.Instance, r.Scheme, r.Config.CATLS.CACert); err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	if err := createCAClientConfig(r.Client, r.Instance, r.Scheme, r.CAClient.Config); err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	job := r.initHSMJob(r.Instance, r.HSMConfig, r.Timeout)
   161  	if err := r.Client.Create(context.TODO(), job.Job, k8sclient.CreateOption{
   162  		Owner:  r.Instance,
   163  		Scheme: r.Scheme,
   164  	}); err != nil {
   165  		return nil, errors.Wrap(err, "failed to create HSM ca initialization job")
   166  	}
   167  	log.Info(fmt.Sprintf("Job '%s' created", job.GetName()))
   168  
   169  	if err := job.WaitUntilActive(r.Client); err != nil {
   170  		return nil, err
   171  	}
   172  	log.Info(fmt.Sprintf("Job '%s' active", job.GetName()))
   173  
   174  	if err := job.WaitUntilContainerFinished(r.Client, CertGen); err != nil {
   175  		return nil, err
   176  	}
   177  	log.Info(fmt.Sprintf("Job '%s' finished", job.GetName()))
   178  
   179  	status, err := job.ContainerStatus(r.Client, CertGen)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	switch status {
   185  	case jobv1.FAILED:
   186  		return nil, fmt.Errorf("Job '%s' finished unsuccessfully, not cleaning up pods to allow for error evaluation", job.GetName())
   187  	case jobv1.COMPLETED:
   188  		if err := job.Delete(r.Client); err != nil {
   189  			return nil, err
   190  		}
   191  
   192  		if err := deleteRootTLSSecret(r.Client, r.Instance); err != nil {
   193  			return nil, err
   194  		}
   195  
   196  		if err := deleteCAClientConfig(r.Client, r.Instance); err != nil {
   197  			return nil, err
   198  		}
   199  	}
   200  
   201  	if err := r.setControllerReferences(); err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	return &config.Response{}, nil
   206  }
   207  
   208  func (r *HSMDaemonReenroller) setControllerReferences() error {
   209  	if err := setControllerReferenceFor(r.Client, r.Instance, r.Scheme, fmt.Sprintf("ecert-%s-signcert", r.Instance.GetName()), false); err != nil {
   210  		return err
   211  	}
   212  
   213  	if err := setControllerReferenceFor(r.Client, r.Instance, r.Scheme, fmt.Sprintf("ecert-%s-cacerts", r.Instance.GetName()), false); err != nil {
   214  		return err
   215  	}
   216  
   217  	if err := setControllerReferenceFor(r.Client, r.Instance, r.Scheme, fmt.Sprintf("ecert-%s-intercerts", r.Instance.GetName()), true); err != nil {
   218  		return err
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  const (
   225  	// HSMClient is the name of container that contain the HSM client library
   226  	HSMClient = "hsm-client"
   227  	// CertGen is the name of container that runs the command to generate the certificate for the CA
   228  	CertGen = "certgen"
   229  )
   230  
   231  func (r *HSMDaemonReenroller) initHSMJob(instance Instance, hsmConfig *config.HSMConfig, timeout time.Duration) *jobv1.Job {
   232  	hsmLibraryPath := hsmConfig.Library.FilePath
   233  	hsmLibraryName := filepath.Base(hsmLibraryPath)
   234  
   235  	jobName := fmt.Sprintf("%s-reenroll", instance.GetName())
   236  
   237  	f := false
   238  	t := true
   239  	user := int64(0)
   240  	backoffLimit := int32(0)
   241  	mountPath := "/shared"
   242  	pvcVolumeName := fmt.Sprintf("%s-pvc-volume", instance.GetName())
   243  
   244  	k8sJob := &batchv1.Job{
   245  		ObjectMeta: metav1.ObjectMeta{
   246  			Name:      jobName,
   247  			Namespace: instance.GetNamespace(),
   248  			Labels: map[string]string{
   249  				"name":  jobName,
   250  				"owner": instance.GetName(),
   251  			},
   252  		},
   253  		Spec: batchv1.JobSpec{
   254  			BackoffLimit: &backoffLimit,
   255  			Template: corev1.PodTemplateSpec{
   256  				Spec: corev1.PodSpec{
   257  					ServiceAccountName: instance.GetName(),
   258  					ImagePullSecrets:   util.AppendImagePullSecretIfMissing(instance.GetPullSecrets(), hsmConfig.BuildPullSecret()),
   259  					RestartPolicy:      corev1.RestartPolicyNever,
   260  					InitContainers: []corev1.Container{
   261  						corev1.Container{
   262  							Name:            HSMClient,
   263  							Image:           hsmConfig.Library.Image,
   264  							ImagePullPolicy: corev1.PullAlways,
   265  							Command: []string{
   266  								"sh",
   267  								"-c",
   268  								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),
   269  							},
   270  							SecurityContext: &corev1.SecurityContext{
   271  								RunAsUser:    &user,
   272  								RunAsNonRoot: &f,
   273  							},
   274  							VolumeMounts: []corev1.VolumeMount{
   275  								corev1.VolumeMount{
   276  									Name:      "shared",
   277  									MountPath: mountPath,
   278  								},
   279  							},
   280  							Resources: corev1.ResourceRequirements{
   281  								Requests: corev1.ResourceList{
   282  									corev1.ResourceCPU:              resource.MustParse("0.1"),
   283  									corev1.ResourceMemory:           resource.MustParse("100Mi"),
   284  									corev1.ResourceEphemeralStorage: resource.MustParse("100Mi"),
   285  								},
   286  								Limits: corev1.ResourceList{
   287  									corev1.ResourceCPU:              resource.MustParse("1"),
   288  									corev1.ResourceMemory:           resource.MustParse("500Mi"),
   289  									corev1.ResourceEphemeralStorage: resource.MustParse("1Gi"),
   290  								},
   291  							},
   292  						},
   293  					},
   294  					Containers: []corev1.Container{
   295  						corev1.Container{
   296  							Name:            CertGen,
   297  							Image:           instance.EnrollerImage(),
   298  							ImagePullPolicy: corev1.PullAlways,
   299  							SecurityContext: &corev1.SecurityContext{
   300  								RunAsUser:                &user,
   301  								Privileged:               &t,
   302  								AllowPrivilegeEscalation: &t,
   303  							},
   304  							Env: hsmConfig.GetEnvs(),
   305  							Command: []string{
   306  								"sh",
   307  								"-c",
   308  							},
   309  							Args: []string{
   310  								fmt.Sprintf(config.DAEMON_CHECK_CMD+" && /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),
   311  							},
   312  							VolumeMounts: []corev1.VolumeMount{
   313  								corev1.VolumeMount{
   314  									Name:      "tlscertfile",
   315  									MountPath: fmt.Sprintf("%s/tlsCert.pem", r.HomeDir),
   316  									SubPath:   "tlsCert.pem",
   317  								},
   318  								corev1.VolumeMount{
   319  									Name:      "certfile",
   320  									MountPath: fmt.Sprintf("%s/cert.pem", r.HomeDir),
   321  									SubPath:   "cert.pem",
   322  								},
   323  								corev1.VolumeMount{
   324  									Name:      "clientconfig",
   325  									MountPath: fmt.Sprintf("/tmp/%s", "fabric-ca-client-config.yaml"),
   326  									SubPath:   "fabric-ca-client-config.yaml",
   327  								},
   328  								corev1.VolumeMount{
   329  									Name:      "shared",
   330  									MountPath: "/hsm/lib",
   331  									SubPath:   "hsm",
   332  								},
   333  								{
   334  									Name:      "shared",
   335  									MountPath: "/shared",
   336  								},
   337  							},
   338  						},
   339  					},
   340  					Volumes: []corev1.Volume{
   341  						corev1.Volume{
   342  							Name: "shared",
   343  							VolumeSource: corev1.VolumeSource{
   344  								EmptyDir: &corev1.EmptyDirVolumeSource{
   345  									Medium: corev1.StorageMediumMemory,
   346  								},
   347  							},
   348  						},
   349  						corev1.Volume{
   350  							Name: "tlscertfile",
   351  							VolumeSource: corev1.VolumeSource{
   352  								Secret: &corev1.SecretVolumeSource{
   353  									SecretName: fmt.Sprintf("%s-init-roottls", instance.GetName()),
   354  								},
   355  							},
   356  						},
   357  						corev1.Volume{
   358  							Name: "certfile",
   359  							VolumeSource: corev1.VolumeSource{
   360  								Secret: &corev1.SecretVolumeSource{
   361  									SecretName: fmt.Sprintf("ecert-%s-signcert", instance.GetName()),
   362  								},
   363  							},
   364  						},
   365  						corev1.Volume{
   366  							Name: "clientconfig",
   367  							VolumeSource: corev1.VolumeSource{
   368  								ConfigMap: &corev1.ConfigMapVolumeSource{
   369  									LocalObjectReference: corev1.LocalObjectReference{
   370  										Name: fmt.Sprintf("%s-init-config", instance.GetName()),
   371  									},
   372  								},
   373  							},
   374  						},
   375  						{
   376  							Name: pvcVolumeName,
   377  							VolumeSource: corev1.VolumeSource{
   378  								PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   379  									ClaimName: instance.PVCName(),
   380  								},
   381  							},
   382  						},
   383  					},
   384  				},
   385  			},
   386  		},
   387  	}
   388  
   389  	job := jobv1.New(k8sJob, &jobv1.Timeouts{
   390  		WaitUntilActive:   timeout,
   391  		WaitUntilFinished: timeout,
   392  	})
   393  
   394  	job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, hsmConfig.GetVolumes()...)
   395  	job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, hsmConfig.GetVolumeMounts()...)
   396  
   397  	// If daemon settings are configured in HSM config, create a sidecar that is running the daemon image
   398  	if r.HSMConfig.Daemon != nil {
   399  		// Certain token information requires to be stored in persistent store, the administrator
   400  		// responsible for configuring HSM sets the HSM config to point to the path where the PVC
   401  		// needs to be mounted.
   402  		var pvcMount *corev1.VolumeMount
   403  		for _, vm := range r.HSMConfig.MountPaths {
   404  			if vm.UsePVC {
   405  				pvcMount = &corev1.VolumeMount{
   406  					Name:      pvcVolumeName,
   407  					MountPath: vm.MountPath,
   408  				}
   409  			}
   410  		}
   411  
   412  		// Add daemon container to the deployment
   413  		config.AddDaemonContainer(r.HSMConfig, job, instance.GetResource(current.HSMDAEMON), pvcMount)
   414  
   415  		// If a pvc mount has been configured in HSM config, set the volume mount on the CertGen container
   416  		if pvcMount != nil {
   417  			job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, *pvcMount)
   418  		}
   419  	}
   420  
   421  	return job
   422  }