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 }