diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ccacd00a6..b95da94c5ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Add support for Kafka 3.1.0; remove Kafka 2.8.0 and 2.8.1 * Update Open Policy Agent authorizer to 1.4.0 and add support for enabling metrics * Added the option `createBootstrapService` in the Kafka Spec to disable the creation of the bootstrap service for the Load Balancer Type Listener. It will save the cost of one load balancer resource, specially in the public cloud. +* Fix renewing your own CA certificates [#5466](https://github.com/strimzi/strimzi-kafka-operator/issues/5466) ## 0.27.0 diff --git a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/AbstractModel.java b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/AbstractModel.java index 82652d1a8c0..a8e99e49d26 100644 --- a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/AbstractModel.java +++ b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/AbstractModel.java @@ -979,6 +979,10 @@ protected Secret createSecret(String name, Map data) { return ModelUtils.createSecret(name, namespace, labels, createOwnerReference(), data, emptyMap(), emptyMap()); } + protected Secret createSecret(String name, Map data, Map customAnnotations) { + return ModelUtils.createSecret(name, namespace, labels, createOwnerReference(), data, customAnnotations, emptyMap()); + } + protected Secret createJmxSecret(String name, Map data) { return ModelUtils.createSecret(name, namespace, labels, createOwnerReference(), data, templateJmxSecretAnnotations, templateJmxSecretLabels); } diff --git a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ClusterCa.java b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ClusterCa.java index cc1e3b812ee..e932ff7da22 100644 --- a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ClusterCa.java +++ b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ClusterCa.java @@ -241,4 +241,19 @@ public Map generateBrokerCerts(Kafka kafka, Set exte isMaintenanceTimeWindowsSatisfied); } + @Override + protected String caCertGenerationAnnotation() { + return ANNO_STRIMZI_IO_CLUSTER_CA_CERT_GENERATION; + } + + @SuppressWarnings("BooleanExpressionComplexity") + @Override + protected boolean hasCaCertGenerationChanged() { + // at least one Secret has a different cluster CA certificate thumbprint. + // it is useful when a renewal cluster CA certificate process needs to be recovered after an operator crash + return hasCaCertGenerationChanged(zkNodesSecret) || hasCaCertGenerationChanged(brokersSecret) || + hasCaCertGenerationChanged(entityTopicOperatorSecret) || hasCaCertGenerationChanged(entityUserOperatorSecret) || + hasCaCertGenerationChanged(kafkaExporterSecret) || hasCaCertGenerationChanged(cruiseControlSecret) || + hasCaCertGenerationChanged(clusterOperatorSecret); + } } diff --git a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/CruiseControl.java b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/CruiseControl.java index ca54f224a53..62a7731837a 100644 --- a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/CruiseControl.java +++ b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/CruiseControl.java @@ -692,7 +692,8 @@ public Secret generateSecret(Kafka kafka, ClusterCa clusterCa, boolean isMainten data.put(keyCertName + ".p12", cert.keyStoreAsBase64String()); data.put(keyCertName + ".password", cert.storePasswordAsBase64String()); - return createSecret(CruiseControl.secretName(cluster), data); + return createSecret(CruiseControl.secretName(cluster), data, + Collections.singletonMap(clusterCa.caCertGenerationAnnotation(), String.valueOf(clusterCa.certGeneration()))); } /** diff --git a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/KafkaCluster.java b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/KafkaCluster.java index f94b5e8ab03..e9c41616cad 100644 --- a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/KafkaCluster.java +++ b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/KafkaCluster.java @@ -1357,9 +1357,11 @@ public StrimziPodSet generatePodSet(int replicas, * internal communication with Zookeeper. * It also contains the related Kafka brokers private keys. * + * @param clusterCa The CA for cluster certificates + * @param clientsCa The CA for clients certificates * @return The generated Secret */ - public Secret generateBrokersSecret() { + public Secret generateBrokersSecret(ClusterCa clusterCa, ClientsCa clientsCa) { Map data = new HashMap<>(replicas * 4); for (int i = 0; i < replicas; i++) { @@ -1369,7 +1371,11 @@ public Secret generateBrokersSecret() { data.put(KafkaCluster.kafkaPodName(cluster, i) + ".p12", cert.keyStoreAsBase64String()); data.put(KafkaCluster.kafkaPodName(cluster, i) + ".password", cert.storePasswordAsBase64String()); } - return createSecret(KafkaCluster.brokersSecretName(cluster), data); + + Map annotations = Map.of( + clusterCa.caCertGenerationAnnotation(), String.valueOf(clusterCa.certGeneration()), + clientsCa.caCertGenerationAnnotation(), String.valueOf(clientsCa.certGeneration())); + return createSecret(KafkaCluster.brokersSecretName(cluster), data, annotations); } /** diff --git a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ModelUtils.java b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ModelUtils.java index 6b6e083c123..45dbbffc1a5 100644 --- a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ModelUtils.java +++ b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ModelUtils.java @@ -144,7 +144,9 @@ public static Secret buildSecret(Reconciliation reconciliation, ClusterCa cluste reasons.add("certificate doesn't exist yet"); shouldBeRegenerated = true; } else { - if (clusterCa.keyCreated() || clusterCa.certRenewed() || (isMaintenanceTimeWindowsSatisfied && clusterCa.isExpiring(secret, keyCertName + ".crt"))) { + if (clusterCa.keyCreated() || clusterCa.certRenewed() || + (isMaintenanceTimeWindowsSatisfied && clusterCa.isExpiring(secret, keyCertName + ".crt")) || + clusterCa.hasCaCertGenerationChanged(secret)) { reasons.add("certificate needs to be renewed"); shouldBeRegenerated = true; } @@ -191,7 +193,8 @@ public static Secret buildSecret(Reconciliation reconciliation, ClusterCa cluste data.put(keyCertName + ".password", certAndKey.storePasswordAsBase64String()); } - return createSecret(secretName, namespace, labels, ownerReference, data, emptyMap(), emptyMap()); + return createSecret(secretName, namespace, labels, ownerReference, data, + Collections.singletonMap(clusterCa.caCertGenerationAnnotation(), String.valueOf(clusterCa.certGeneration())), emptyMap()); } public static Secret createSecret(String name, String namespace, Labels labels, OwnerReference ownerReference, diff --git a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ZookeeperCluster.java b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ZookeeperCluster.java index e2cb4f099db..da65f8f3ab2 100644 --- a/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ZookeeperCluster.java +++ b/cluster-operator/src/main/java/io/strimzi/operator/cluster/model/ZookeeperCluster.java @@ -576,9 +576,10 @@ public void generateCertificates(Kafka kafka, ClusterCa clusterCa, boolean isMai * internal communication with Kafka. * It also contains the related Zookeeper nodes private keys. * + * @param clusterCa The CA for cluster certificates * @return The generated Secret. */ - public Secret generateNodesSecret() { + public Secret generateNodesSecret(ClusterCa clusterCa) { Map data = new HashMap<>(replicas * 4); for (int i = 0; i < replicas; i++) { @@ -588,7 +589,9 @@ public Secret generateNodesSecret() { data.put(ZookeeperCluster.zookeeperPodName(cluster, i) + ".p12", cert.keyStoreAsBase64String()); data.put(ZookeeperCluster.zookeeperPodName(cluster, i) + ".password", cert.storePasswordAsBase64String()); } - return createSecret(ZookeeperCluster.nodesSecretName(cluster), data); + + return createSecret(ZookeeperCluster.nodesSecretName(cluster), data, + Collections.singletonMap(clusterCa.caCertGenerationAnnotation(), String.valueOf(clusterCa.certGeneration()))); } @Override diff --git a/cluster-operator/src/main/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperator.java b/cluster-operator/src/main/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperator.java index 377a1917a3a..a3ddf50015f 100644 --- a/cluster-operator/src/main/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperator.java +++ b/cluster-operator/src/main/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperator.java @@ -611,6 +611,7 @@ Future reconcileCas(Supplier dateSupplier) { Secret clusterCaKeySecret = null; Secret clientsCaCertSecret = null; Secret clientsCaKeySecret = null; + Secret brokersSecret = null; List clusterSecrets = secretOperations.list(reconciliation.namespace(), selectorLabels); for (Secret secret : clusterSecrets) { String secretName = secret.getMetadata().getName(); @@ -622,6 +623,8 @@ Future reconcileCas(Supplier dateSupplier) { clientsCaCertSecret = secret; } else if (secretName.equals(clientsCaKeyName)) { clientsCaKeySecret = secret; + } else if (secretName.equals(KafkaCluster.brokersSecretName(name))) { + brokersSecret = secret; } } OwnerReference ownerRef = new OwnerReferenceBuilder() @@ -654,14 +657,13 @@ Future reconcileCas(Supplier dateSupplier) { ModelUtils.getCertificateValidity(clusterCaConfig), ModelUtils.getRenewalDays(clusterCaConfig), clusterCaConfig == null || clusterCaConfig.isGenerateCertificateAuthority(), clusterCaConfig != null ? clusterCaConfig.getCertificateExpirationPolicy() : null); + this.clusterCa.initCaSecrets(clusterSecrets); clusterCa.createRenewOrReplace( reconciliation.namespace(), reconciliation.name(), caLabels.toMap(), clusterCaCertLabels, clusterCaCertAnnotations, clusterCaConfig != null && !clusterCaConfig.isGenerateSecretOwnerReference() ? null : ownerRef, isMaintenanceTimeWindowsSatisfied(dateSupplier)); - this.clusterCa.initCaSecrets(clusterSecrets); - CertificateAuthority clientsCaConfig = kafkaAssembly.getSpec().getClientsCa(); // When we are not supposed to generate the CA but it does not exist, we should just throw an error @@ -674,6 +676,7 @@ Future reconcileCas(Supplier dateSupplier) { ModelUtils.getCertificateValidity(clientsCaConfig), ModelUtils.getRenewalDays(clientsCaConfig), clientsCaConfig == null || clientsCaConfig.isGenerateCertificateAuthority(), clientsCaConfig != null ? clientsCaConfig.getCertificateExpirationPolicy() : null); + this.clientsCa.initBrokerSecret(brokersSecret); clientsCa.createRenewOrReplace(reconciliation.namespace(), reconciliation.name(), caLabels.toMap(), emptyMap(), emptyMap(), clientsCaConfig != null && !clientsCaConfig.isGenerateSecretOwnerReference() ? null : ownerRef, @@ -1357,7 +1360,7 @@ Future updateCertificateSecretWithDiff(String secretName, Secret secret } Future zkNodesSecret() { - return updateCertificateSecretWithDiff(ZookeeperCluster.nodesSecretName(name), zkCluster.generateNodesSecret()) + return updateCertificateSecretWithDiff(ZookeeperCluster.nodesSecretName(name), zkCluster.generateNodesSecret(clusterCa)) .map(changed -> { existingZookeeperCertsChanged = changed; return this; @@ -2771,7 +2774,7 @@ Future kafkaAncillaryCm() { } Future kafkaBrokersSecret() { - return updateCertificateSecretWithDiff(KafkaCluster.brokersSecretName(name), kafkaCluster.generateBrokersSecret()) + return updateCertificateSecretWithDiff(KafkaCluster.brokersSecretName(name), kafkaCluster.generateBrokersSecret(clusterCa, clientsCa)) .map(changed -> { existingKafkaCertsChanged = changed; return this; diff --git a/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/CaRenewalTest.java b/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/CaRenewalTest.java index dca29370a39..dc1cde23388 100644 --- a/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/CaRenewalTest.java +++ b/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/CaRenewalTest.java @@ -6,8 +6,11 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.strimzi.api.kafka.model.CertificateExpirationPolicy; import io.strimzi.certs.CertAndKey; +import io.strimzi.certs.CertManager; import io.strimzi.certs.Subject; +import io.strimzi.operator.common.PasswordGenerator; import io.strimzi.operator.common.Reconciliation; import io.strimzi.test.annotations.ParallelSuite; import io.strimzi.test.annotations.ParallelTest; @@ -30,33 +33,7 @@ public class CaRenewalTest { @ParallelTest public void renewalOfStatefulSetCertificatesWithNullSecret() throws IOException { - Ca mockedCa = new Ca(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null) { - private AtomicInteger invocationCount = new AtomicInteger(0); - - @Override - public boolean certRenewed() { - return false; - } - - @Override - public boolean isExpiring(Secret secret, String certKey) { - return false; - } - - @Override - protected CertAndKey generateSignedCert(Subject subject, - File csrFile, File keyFile, File certFile, File keyStoreFile) throws IOException { - int index = invocationCount.getAndIncrement(); - - return new CertAndKey( - ("new-key" + index).getBytes(), - ("new-cert" + index).getBytes(), - ("new-truststore" + index).getBytes(), - ("new-keystore" + index).getBytes(), - "new-password" + index - ); - } - }; + Ca mockedCa = new MockedCa(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null); int replicas = 3; Function subjectFn = i -> new Subject.Builder().build(); @@ -87,33 +64,8 @@ protected CertAndKey generateSignedCert(Subject subject, @ParallelTest public void renewalOfStatefulSetCertificatesWithCaRenewal() throws IOException { - Ca mockedCa = new Ca(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null) { - private AtomicInteger invocationCount = new AtomicInteger(0); - - @Override - public boolean certRenewed() { - return true; - } - - @Override - public boolean isExpiring(Secret secret, String certKey) { - return false; - } - - @Override - protected CertAndKey generateSignedCert(Subject subject, - File csrFile, File keyFile, File certFile, File keyStoreFile) throws IOException { - int index = invocationCount.getAndIncrement(); - - return new CertAndKey( - ("new-key" + index).getBytes(), - ("new-cert" + index).getBytes(), - ("new-truststore" + index).getBytes(), - ("new-keystore" + index).getBytes(), - "new-password" + index - ); - } - }; + MockedCa mockedCa = new MockedCa(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null); + mockedCa.setCertRenewed(true); Secret initialSecret = new SecretBuilder() .withNewMetadata() @@ -163,43 +115,8 @@ protected CertAndKey generateSignedCert(Subject subject, @ParallelTest public void renewalOfStatefulSetCertificatesDelayedRenewalInWindow() throws IOException { - Ca mockedCa = new Ca(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null) { - private AtomicInteger invocationCount = new AtomicInteger(0); - - @Override - public boolean certRenewed() { - return false; - } - - @Override - public boolean isExpiring(Secret secret, String certKey) { - return true; - } - - @Override - protected boolean certSubjectChanged(CertAndKey certAndKey, Subject desiredSubject, String podName) { - return false; - } - - @Override - public X509Certificate getAsX509Certificate(Secret secret, String key) { - return null; - } - - @Override - protected CertAndKey generateSignedCert(Subject subject, - File csrFile, File keyFile, File certFile, File keyStoreFile) throws IOException { - int index = invocationCount.getAndIncrement(); - - return new CertAndKey( - ("new-key" + index).getBytes(), - ("new-cert" + index).getBytes(), - ("new-truststore" + index).getBytes(), - ("new-keystore" + index).getBytes(), - "new-password" + index - ); - } - }; + MockedCa mockedCa = new MockedCa(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null); + mockedCa.setCertExpiring(true); Secret initialSecret = new SecretBuilder() .withNewMetadata() @@ -249,43 +166,8 @@ protected CertAndKey generateSignedCert(Subject subject, @ParallelTest public void renewalOfStatefulSetCertificatesDelayedRenewalOutsideWindow() throws IOException { - Ca mockedCa = new Ca(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null) { - private AtomicInteger invocationCount = new AtomicInteger(0); - - @Override - public boolean certRenewed() { - return false; - } - - @Override - public boolean isExpiring(Secret secret, String certKey) { - return true; - } - - @Override - protected boolean certSubjectChanged(CertAndKey certAndKey, Subject desiredSubject, String podName) { - return false; - } - - @Override - public X509Certificate getAsX509Certificate(Secret secret, String key) { - return null; - } - - @Override - protected CertAndKey generateSignedCert(Subject subject, - File csrFile, File keyFile, File certFile, File keyStoreFile) throws IOException { - int index = invocationCount.getAndIncrement(); - - return new CertAndKey( - ("new-key" + index).getBytes(), - ("new-cert" + index).getBytes(), - ("new-truststore" + index).getBytes(), - ("new-keystore" + index).getBytes(), - "new-password" + index - ); - } - }; + MockedCa mockedCa = new MockedCa(Reconciliation.DUMMY_RECONCILIATION, null, null, null, null, null, null, null, 2, 1, true, null); + mockedCa.setCertExpiring(true); Secret initialSecret = new SecretBuilder() .withNewMetadata() @@ -332,4 +214,66 @@ protected CertAndKey generateSignedCert(Subject subject, assertThat(new String(newCerts.get("pod2").keyStore()), is("old-keystore")); assertThat(newCerts.get("pod2").storePassword(), is("old-password")); } + + public class MockedCa extends Ca { + private boolean isCertRenewed; + private boolean isCertExpiring; + private AtomicInteger invocationCount = new AtomicInteger(0); + + public MockedCa(Reconciliation reconciliation, CertManager certManager, PasswordGenerator passwordGenerator, String commonName, String caCertSecretName, Secret caCertSecret, String caKeySecretName, Secret caKeySecret, int validityDays, int renewalDays, boolean generateCa, CertificateExpirationPolicy policy) { + super(reconciliation, certManager, passwordGenerator, commonName, caCertSecretName, caCertSecret, caKeySecretName, caKeySecret, validityDays, renewalDays, generateCa, policy); + } + + @Override + public boolean certRenewed() { + return isCertRenewed; + } + + @Override + public boolean isExpiring(Secret secret, String certKey) { + return isCertExpiring; + } + + @Override + protected boolean certSubjectChanged(CertAndKey certAndKey, Subject desiredSubject, String podName) { + return false; + } + + @Override + public X509Certificate getAsX509Certificate(Secret secret, String key) { + return null; + } + + @Override + protected CertAndKey generateSignedCert(Subject subject, + File csrFile, File keyFile, File certFile, File keyStoreFile) throws IOException { + int index = invocationCount.getAndIncrement(); + + return new CertAndKey( + ("new-key" + index).getBytes(), + ("new-cert" + index).getBytes(), + ("new-truststore" + index).getBytes(), + ("new-keystore" + index).getBytes(), + "new-password" + index + ); + } + + @Override + protected boolean hasCaCertGenerationChanged() { + return false; + } + + @Override + protected String caCertGenerationAnnotation() { + return null; + } + + public void setCertRenewed(boolean certRenewed) { + isCertRenewed = certRenewed; + } + + public void setCertExpiring(boolean certExpiring) { + isCertExpiring = certExpiring; + } + } } \ No newline at end of file diff --git a/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/KafkaClusterTest.java b/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/KafkaClusterTest.java index 9945607b47c..db44220ed3d 100644 --- a/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/KafkaClusterTest.java +++ b/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/KafkaClusterTest.java @@ -44,6 +44,7 @@ import io.fabric8.openshift.api.model.Route; import io.strimzi.api.kafka.model.CertSecretSource; import io.strimzi.api.kafka.model.CertSecretSourceBuilder; +import io.strimzi.api.kafka.model.CertificateExpirationPolicy; import io.strimzi.api.kafka.model.ContainerEnvVar; import io.strimzi.api.kafka.model.GenericSecretSourceBuilder; import io.strimzi.api.kafka.model.InlineLogging; @@ -1461,9 +1462,11 @@ public void testGenerateBrokerSecretExternalWithManyDNS() throws CertificatePars private Secret generateBrokerSecret(Set externalBootstrapAddress, Map> externalAddresses) { ClusterCa clusterCa = new ClusterCa(Reconciliation.DUMMY_RECONCILIATION, new OpenSslCertManager(), new PasswordGenerator(10, "a", "a"), cluster, null, null); clusterCa.createRenewOrReplace(namespace, cluster, emptyMap(), emptyMap(), emptyMap(), null, true); + ClientsCa clientsCa = new ClientsCa(Reconciliation.DUMMY_RECONCILIATION, new OpenSslCertManager(), new PasswordGenerator(10, "a", "a"), null, null, null, null, 365, 30, true, CertificateExpirationPolicy.RENEW_CERTIFICATE); + clientsCa.createRenewOrReplace(namespace, cluster, emptyMap(), emptyMap(), emptyMap(), null, true); kc.generateCertificates(kafkaAssembly, clusterCa, externalBootstrapAddress, externalAddresses, true); - return kc.generateBrokersSecret(); + return kc.generateBrokersSecret(clusterCa, clientsCa); } @ParallelTest diff --git a/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/ZookeeperClusterTest.java b/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/ZookeeperClusterTest.java index 15d424721e1..ad3d885f961 100644 --- a/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/ZookeeperClusterTest.java +++ b/cluster-operator/src/test/java/io/strimzi/operator/cluster/model/ZookeeperClusterTest.java @@ -461,7 +461,7 @@ private Secret generateNodeSecret() { clusterCa.createRenewOrReplace(namespace, cluster, emptyMap(), emptyMap(), emptyMap(), null, true); zc.generateCertificates(ka, clusterCa, true); - return zc.generateNodesSecret(); + return zc.generateNodesSecret(clusterCa); } @ParallelTest diff --git a/cluster-operator/src/test/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperatorTest.java b/cluster-operator/src/test/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperatorTest.java index 3368e0c9d6d..bcefee63bf3 100644 --- a/cluster-operator/src/test/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperatorTest.java +++ b/cluster-operator/src/test/java/io/strimzi/operator/cluster/operator/assembly/KafkaAssemblyOperatorTest.java @@ -1336,7 +1336,7 @@ public void testReconcile(Params params, VertxTestContext context) { invocation -> new ArrayList<>(asList( barClientsCa.caKeySecret(), barClientsCa.caCertSecret(), - barCluster.generateBrokersSecret(), + barCluster.generateBrokersSecret(barClusterCa, barClientsCa), barClusterCa.caCertSecret())) ); when(mockSecretOps.get(eq(kafkaNamespace), eq(AbstractModel.clusterCaCertSecretName(bar.getMetadata().getName())))).thenReturn(barSecrets.get(0)); @@ -1421,7 +1421,7 @@ public void testReconcileAllNamespaces(Params params, VertxTestContext context) invocation -> new ArrayList<>(asList( barClientsCa.caKeySecret(), barClientsCa.caCertSecret(), - barCluster.generateBrokersSecret(), + barCluster.generateBrokersSecret(barClusterCa, barClientsCa), barClusterCa.caCertSecret())) ); diff --git a/operator-common/src/main/java/io/strimzi/operator/cluster/model/Ca.java b/operator-common/src/main/java/io/strimzi/operator/cluster/model/Ca.java index 4b7b7c9efd7..9919ff1d1c3 100644 --- a/operator-common/src/main/java/io/strimzi/operator/cluster/model/Ca.java +++ b/operator-common/src/main/java/io/strimzi/operator/cluster/model/Ca.java @@ -43,6 +43,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -412,7 +413,7 @@ protected Map maybeCopyOrGenerateCerts( for (int i = replicasInSecret; i < replicas; i++) { String podName = podNameFn.apply(i); - LOGGER.debugCr(reconciliation, "Certificate for {} to generate", podName); + LOGGER.debugCr(reconciliation, "Certificate for pod {} to generate", podName); CertAndKey k = generateSignedCert(subjectFn.apply(i), brokerCsrFile, brokerKeyFile, brokerCertFile, brokerKeyStoreFile); certs.put(podName, k); @@ -508,11 +509,12 @@ public void createRenewOrReplace(String namespace, String clusterName, Map certData; Map keyData; - int caCertGeneration = caCertSecret != null ? Annotations.intAnnotation(caCertSecret, ANNO_STRIMZI_IO_CA_CERT_GENERATION, INIT_GENERATION) : INIT_GENERATION; - int caKeyGeneration = caKeySecret != null ? Annotations.intAnnotation(caKeySecret, ANNO_STRIMZI_IO_CA_KEY_GENERATION, INIT_GENERATION) : INIT_GENERATION; + int caCertGeneration = certGeneration(); + int caKeyGeneration = keyGeneration(); if (!generateCa) { certData = caCertSecret != null ? caCertSecret.getData() : emptyMap(); keyData = caKeySecret != null ? singletonMap(CA_KEY, caKeySecret.getData().get(CA_KEY)) : emptyMap(); + renewalType = hasCaCertGenerationChanged() ? RenewalType.REPLACE_KEY : RenewalType.NOOP; caCertsRemoved = false; } else { this.renewalType = shouldCreateOrRenew(currentCert, namespace, clusterName, maintenanceWindowSatisfied); @@ -757,6 +759,20 @@ public boolean keyCreated() { return renewalType.equals(RenewalType.CREATE); } + /** + * @return the generation of the current CA certificate + */ + public int certGeneration() { + return caCertSecret != null ? Annotations.intAnnotation(caCertSecret, ANNO_STRIMZI_IO_CA_CERT_GENERATION, INIT_GENERATION) : INIT_GENERATION; + } + + /** + * @return the generation of the current CA key + */ + public int keyGeneration() { + return caKeySecret != null ? Annotations.intAnnotation(caKeySecret, ANNO_STRIMZI_IO_CA_KEY_GENERATION, INIT_GENERATION) : INIT_GENERATION; + } + private int removeExpiredCerts(Map newData) { Iterator> iter = newData.entrySet().iterator(); List removed = new ArrayList<>(); @@ -1004,4 +1020,37 @@ private void renewCaCert(Subject subject, Map certData) { throw new RuntimeException(e); } } + + /** + * @return the name of the annotation bringing the generation of the specific CA certificate type (cluster or clients) + * on the Secrets containing certificates signed by that CA (i.e ZooKeeper nodes, Kafka brokers, ...) + */ + protected abstract String caCertGenerationAnnotation(); + + /** + * @return if the current (cluster or clients) CA certificate generation is changed compared to the the one + * brought on Secrets containing certificates signed by that CA (i.e ZooKeeper nodes, Kafka brokers, ...) + */ + protected abstract boolean hasCaCertGenerationChanged(); + + /** + * It checks if the current (cluster or clients) CA certificate generation is changed compared to the one + * brought by the corresponding annotation on the provided Secret (i.e ZooKeeper nodes, Kafka brokers, ...) + * + * @param secret Secret containing certificates signed by the current (clients or cluster) CA + * @return if the current (cluster or clients) CA certificate generation is changed compared to the one + * brought by the corresponding annotation on the provided Secret + */ + protected boolean hasCaCertGenerationChanged(Secret secret) { + if (secret != null) { + String caCertGenerationAnno = Optional.ofNullable(secret.getMetadata().getAnnotations()) + .map(annotations -> annotations.get(caCertGenerationAnnotation())) + .orElse(null); + int currentCaCertGeneration = certGeneration(); + LOGGER.debugOp("Secret {}/{} generation anno = {}, current CA generation = {}", + secret.getMetadata().getNamespace(), secret.getMetadata().getName(), caCertGenerationAnno, currentCaCertGeneration); + return caCertGenerationAnno != null && Integer.parseInt(caCertGenerationAnno) != currentCaCertGeneration; + } + return false; + } } diff --git a/operator-common/src/main/java/io/strimzi/operator/cluster/model/ClientsCa.java b/operator-common/src/main/java/io/strimzi/operator/cluster/model/ClientsCa.java index aeeed858026..4a77fa10995 100644 --- a/operator-common/src/main/java/io/strimzi/operator/cluster/model/ClientsCa.java +++ b/operator-common/src/main/java/io/strimzi/operator/cluster/model/ClientsCa.java @@ -11,6 +11,9 @@ import io.strimzi.operator.common.Reconciliation; public class ClientsCa extends Ca { + + private Secret brokersSecret; + public ClientsCa(Reconciliation reconciliation, CertManager certManager, PasswordGenerator passwordGenerator, String caCertSecretName, Secret clientsCaCert, String caSecretKeyName, Secret clientsCaKey, int validityDays, int renewalDays, boolean generateCa, CertificateExpirationPolicy policy) { @@ -37,6 +40,20 @@ public static Secret adapt060ClientsCaSecret(Secret clientsCaKey) { return clientsCaKey; } + public void initBrokerSecret(Secret secret) { + this.brokersSecret = secret; + } + + @Override + protected String caCertGenerationAnnotation() { + return ANNO_STRIMZI_IO_CLIENTS_CA_CERT_GENERATION; + } + + @Override + protected boolean hasCaCertGenerationChanged() { + return hasCaCertGenerationChanged(brokersSecret); + } + @Override public String toString() { return "clients-ca";