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 }