This is an automated email from the ASF dual-hosted git repository.

sammichen pushed a commit to branch HDDS-7391-ca-cert-rot
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/HDDS-7391-ca-cert-rot by this 
push:
     new 3c15fd407d HDDS-8694. Add SCM HA aware Root CA certificate monitor 
task (#4792)
3c15fd407d is described below

commit 3c15fd407d937775b98100c7e5413ab61f0fb8d1
Author: Sammi Chen <[email protected]>
AuthorDate: Wed Jun 7 14:51:29 2023 +0800

    HDDS-8694. Add SCM HA aware Root CA certificate monitor task (#4792)
---
 .../org/apache/hadoop/hdds/HddsConfigKeys.java     |   9 +
 .../hadoop/hdds/security/x509/SecurityConfig.java  |  41 ++++
 .../common/src/main/resources/ozone-default.xml    |  18 ++
 .../hadoop/ozone/TestHddsSecureDatanodeInit.java   |   2 +
 .../{crl => security}/CRLStatusReportHandler.java  |   2 +-
 .../hdds/scm/security/RootCARotationManager.java   | 272 +++++++++++++++++++++
 .../hdds/scm/{crl => security}/package-info.java   |   4 +-
 .../hdds/scm/server/StorageContainerManager.java   |  25 +-
 .../TestCRLStatusReportHandler.java                |   2 +-
 .../scm/security/TestRootCARotationManager.java    | 214 ++++++++++++++++
 .../dist/src/main/compose/ozonesecure-ha/test.sh   |   4 +
 .../hadoop/ozone/TestSecureOzoneCluster.java       |   3 +
 .../ozoneimpl/TestOzoneContainerWithTLS.java       |   2 +
 13 files changed, 593 insertions(+), 5 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
index 2ace9ad49f..4cad18fac6 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
@@ -203,6 +203,15 @@ public final class HddsConfigKeys {
   public static final String HDDS_X509_RENEW_GRACE_DURATION_DEFAULT = "P28D";
   public static final String HDDS_NEW_KEY_CERT_DIR_NAME_SUFFIX = "-next";
   public static final String HDDS_BACKUP_KEY_CERT_DIR_NAME_SUFFIX = 
"-previous";
+  public static final String HDDS_X509_CA_ROTATION_CHECK_INTERNAL =
+      "hdds.x509.ca.rotation.check.interval";
+  public static final String HDDS_X509_CA_ROTATION_CHECK_INTERNAL_DEFAULT =
+      "P1D";
+  public static final String HDDS_X509_CA_ROTATION_TIME_OF_DAY =
+      "hdds.x509.ca.rotation.time-of-day";
+  // format hh:mm:ss, representing hour, minute, and second
+  public static final String HDDS_X509_CA_ROTATION_TIME_OF_DAY_DEFAULT =
+      "02:00:00";
 
   public static final String HDDS_CONTAINER_REPLICATION_COMPRESSION =
       "hdds.container.replication.compression";
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/SecurityConfig.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/SecurityConfig.java
index 6a8a91e7b2..519b3a4d16 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/SecurityConfig.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/security/x509/SecurityConfig.java
@@ -25,6 +25,8 @@ import java.security.Provider;
 import java.security.Security;
 import java.time.Duration;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.hadoop.hdds.conf.ConfigurationSource;
 import org.apache.hadoop.ozone.OzoneConfigKeys;
@@ -38,6 +40,10 @@ import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_TOKEN_ENABLED
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_DEFAULT_KEY_ALGORITHM;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_DEFAULT_KEY_LEN;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_DEFAULT_SECURITY_PROVIDER;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_CHECK_INTERNAL;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_CHECK_INTERNAL_DEFAULT;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_TIME_OF_DAY;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_TIME_OF_DAY_DEFAULT;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_ROOTCA_CERTIFICATE_FILE;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_ROOTCA_CERTIFICATE_FILE_DEFAULT;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_ROOTCA_PRIVATE_KEY_FILE;
@@ -116,6 +122,10 @@ public class SecurityConfig {
   private final String externalRootCaPublicKeyPath;
   private final String externalRootCaPrivateKeyPath;
   private final String externalRootCaCert;
+  private final Duration caCheckInterval;
+  private final String caRotationTimeOfDay;
+  private final Pattern caRotationTimeOfDayPattern =
+      Pattern.compile("\\d{2}:\\d{2}:\\d{2}");
 
   /**
    * Constructs a SecurityConfig.
@@ -180,6 +190,23 @@ public class SecurityConfig {
         HDDS_X509_RENEW_GRACE_DURATION_DEFAULT);
     renewalGracePeriod = Duration.parse(renewalGraceDurationString);
 
+    String caCheckIntervalString = configuration.get(
+        HDDS_X509_CA_ROTATION_CHECK_INTERNAL,
+        HDDS_X509_CA_ROTATION_CHECK_INTERNAL_DEFAULT);
+    caCheckInterval = Duration.parse(caCheckIntervalString);
+
+    String timeOfDayString = configuration.get(
+        HDDS_X509_CA_ROTATION_TIME_OF_DAY,
+        HDDS_X509_CA_ROTATION_TIME_OF_DAY_DEFAULT);
+
+    Matcher matcher = caRotationTimeOfDayPattern.matcher(timeOfDayString);
+    if (!matcher.matches()) {
+      throw new IllegalArgumentException("Property value of " +
+          HDDS_X509_CA_ROTATION_TIME_OF_DAY +
+          " should follow the hh:mm:ss format.");
+    }
+    caRotationTimeOfDay = "1970-01-01T" + timeOfDayString;
+
     validateCertificateValidityConfig();
 
     this.externalRootCaCert = this.configuration.get(
@@ -244,6 +271,12 @@ public class SecurityConfig {
       LOG.error(msg);
       throw new IllegalArgumentException(msg);
     }
+
+    if (caCheckInterval.compareTo(renewalGracePeriod) >= 0) {
+      throw new IllegalArgumentException("Property value of " +
+          HDDS_X509_CA_ROTATION_CHECK_INTERNAL +
+          " should be smaller than " + HDDS_X509_RENEW_GRACE_DURATION);
+    }
   }
 
   /**
@@ -450,6 +483,14 @@ public class SecurityConfig {
     return externalRootCaCert;
   }
 
+  public Duration getCaCheckInterval() {
+    return caCheckInterval;
+  }
+
+  public String getCaRotationTimeOfDay() {
+    return caRotationTimeOfDay;
+  }
+
   /**
    * Return true if using test certificates with authority as localhost. This
    * should be used only for unit test where certificates are generated by
diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml 
b/hadoop-hdds/common/src/main/resources/ozone-default.xml
index 0de5c43034..e6d43a15bf 100644
--- a/hadoop-hdds/common/src/main/resources/ozone-default.xml
+++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml
@@ -2234,6 +2234,24 @@
       they can be configured separately.
     </description>
   </property>
+  <property>
+    <name>hdds.x509.ca.rotation.check.interval</name>
+    <value>P1D</value>
+    <tag>OZONE, HDDS, SECURITY</tag>
+    <description>Check interval of whether internal root certificate is going
+      to expire and needs to start rotation or not. Default is 1 day. The 
property
+      value should be less than the value of property 
hdds.x509.renew.grace.duration.
+    </description>
+  </property>
+  <property>
+    <name>hdds.x509.ca.rotation.time-of-day</name>
+    <value>02:00:00</value>
+    <tag>OZONE, HDDS, SECURITY</tag>
+    <description>Time of day to start the rotation. Default 02:00 AM to avoid 
impacting
+      daily workload. The supported format is 'hh:mm:ss', representing hour, 
minute,
+      and second.
+    </description>
+  </property>
   <property>
     <name>ozone.scm.security.handler.count.key</name>
     <value>2</value>
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/TestHddsSecureDatanodeInit.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/TestHddsSecureDatanodeInit.java
index a95e12c828..0b856fd2c3 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/TestHddsSecureDatanodeInit.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/TestHddsSecureDatanodeInit.java
@@ -47,6 +47,7 @@ import org.apache.hadoop.util.ServicePlugin;
 
 import org.apache.commons.io.FileUtils;
 
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_CHECK_INTERNAL;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_RENEW_GRACE_DURATION;
 import static org.apache.hadoop.ozone.HddsDatanodeService.getLogger;
 import static 
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_KEY;
@@ -99,6 +100,7 @@ public class TestHddsSecureDatanodeInit {
         TestHddsDatanodeService.MockService.class,
         ServicePlugin.class);
     conf.set(HDDS_X509_RENEW_GRACE_DURATION, "PT5S"); // 5s
+    conf.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "PT1S"); // 1s
     securityConfig = new SecurityConfig(conf);
 
     service = HddsDatanodeService.createHddsDatanodeService(args);
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/crl/CRLStatusReportHandler.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/CRLStatusReportHandler.java
similarity index 98%
rename from 
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/crl/CRLStatusReportHandler.java
rename to 
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/CRLStatusReportHandler.java
index 35c5825da3..69cb9a7c34 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/crl/CRLStatusReportHandler.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/CRLStatusReportHandler.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.hadoop.hdds.scm.crl;
+package org.apache.hadoop.hdds.scm.security;
 
 import com.google.common.base.Preconditions;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/RootCARotationManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/RootCARotationManager.java
new file mode 100644
index 0000000000..33f44146a1
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/RootCARotationManager.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.security;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.scm.ha.SCMContext;
+import org.apache.hadoop.hdds.scm.ha.SCMService;
+import org.apache.hadoop.hdds.scm.ha.SCMServiceException;
+import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
+import org.apache.hadoop.hdds.security.x509.SecurityConfig;
+import 
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Root CA Rotation Manager is a service in SCM to control the CA rotation.
+ */
+public class RootCARotationManager implements SCMService {
+
+  public static final Logger LOG =
+      LoggerFactory.getLogger(RootCARotationManager.class);
+
+  private StorageContainerManager scm;
+  private final SCMContext scmContext;
+  private OzoneConfiguration ozoneConf;
+  private SecurityConfig secConf;
+  private ScheduledExecutorService executorService;
+  private Duration checkInterval;
+  private Duration renewalGracePeriod;
+  private Date timeOfDay;
+  private CertificateClient scmCertClient;
+  private AtomicBoolean isRunning = new AtomicBoolean(false);
+  private AtomicBoolean isScheduled = new AtomicBoolean(false);
+  private String threadName = this.getClass().getSimpleName();
+
+  /**
+   * Constructs RootCARotationManager with the specified arguments.
+   *
+   * @param scm the storage container manager
+   */
+  public RootCARotationManager(StorageContainerManager scm) {
+    this.scm = scm;
+    this.ozoneConf = scm.getConfiguration();
+    this.secConf = new SecurityConfig(ozoneConf);
+    this.scmContext = scm.getScmContext();
+
+    checkInterval = secConf.getCaCheckInterval();
+    timeOfDay = Date.from(LocalDateTime.parse(secConf.getCaRotationTimeOfDay())
+        .atZone(ZoneId.systemDefault()).toInstant());
+    renewalGracePeriod = secConf.getRenewalGracePeriod();
+
+    executorService = Executors.newScheduledThreadPool(1,
+        new ThreadFactoryBuilder().setNameFormat(threadName)
+            .setDaemon(true).build());
+
+    scmCertClient = scm.getScmCertificateClient();
+    scm.getSCMServiceManager().register(this);
+  }
+
+  /**
+   * Receives a notification for raft or safe mode related status changes.
+   * Stops monitor task if it's running and the current SCM becomes a
+   * follower or enter safe mode. Starts monitor if the current SCM becomes
+   * leader and not in safe mode.
+   */
+  @Override
+  public void notifyStatusChanged() {
+    // stop the monitor task
+    if (!scmContext.isLeader() || scmContext.isInSafeMode()) {
+      if (isRunning.compareAndSet(true, false)) {
+        LOG.info("notifyStatusChanged: disable monitor task.");
+      }
+      return;
+    }
+
+    if (isRunning.compareAndSet(false, true)) {
+      LOG.info("notifyStatusChanged: enable monitor task");
+    }
+    return;
+  }
+
+  /**
+   * Checks if monitor task should start (after a leader change, restart
+   * etc.) by reading persisted state.
+   * @return true if the persisted state is true, otherwise false
+   */
+  @Override
+  public boolean shouldRun() {
+    return true;
+  }
+
+  /**
+   * @return Name of this service.
+   */
+  @Override
+  public String getServiceName() {
+    return RootCARotationManager.class.getSimpleName();
+  }
+
+  /**
+   * Schedule monitor task.
+   */
+  @Override
+  public void start() throws SCMServiceException {
+    executorService.scheduleAtFixedRate(
+        new MonitorTask(scmCertClient), 0, checkInterval.toMillis(),
+        TimeUnit.MILLISECONDS);
+    LOG.info("Monitor task for root certificate {} is started with " +
+        "interval {}.", scmCertClient.getCACertificate().getSerialNumber(),
+        checkInterval);
+  }
+
+  public boolean isRunning() {
+    return isRunning.get();
+  }
+
+  /**
+   *  Task to monitor certificate lifetime and start rotation if needed.
+   */
+  public class MonitorTask implements Runnable {
+    private CertificateClient certClient;
+
+    public MonitorTask(CertificateClient client) {
+      this.certClient = client;
+    }
+
+    @Override
+    public void run() {
+      Thread.currentThread().setName(threadName +
+          (isRunning() ? "-Active" : "-Inactive"));
+      if (!isRunning.get() || isScheduled.get()) {
+        return;
+      }
+      // Lock to protect the root CA certificate rotation process,
+      // to make sure there is only one task is ongoing at one time.
+      synchronized (RootCARotationManager.class) {
+        X509Certificate rootCACert = certClient.getCACertificate();
+        Duration timeLeft = timeBefore2ExpiryGracePeriod(rootCACert);
+        if (timeLeft.isZero()) {
+          LOG.info("Root certificate {} has entered the 2 * expiry" +
+                  " grace period({}).", 
rootCACert.getSerialNumber().toString(),
+              renewalGracePeriod);
+          // schedule root CA rotation task
+          LocalDateTime now = LocalDateTime.now();
+          LocalDateTime timeToSchedule = LocalDateTime.of(
+              now.getYear(), now.getMonthValue(), now.getDayOfMonth(),
+              timeOfDay.getHours(), timeOfDay.getMinutes(),
+              timeOfDay.getSeconds());
+          if (timeToSchedule.isBefore(now)) {
+            timeToSchedule = timeToSchedule.plusDays(1);
+          }
+          long delay = Duration.between(now, timeToSchedule).toMillis();
+          if (timeToSchedule.isAfter(rootCACert.getNotAfter().toInstant()
+              .atZone(ZoneId.systemDefault()).toLocalDateTime())) {
+            LOG.info("Configured rotation time {} is after root" +
+                " certificate {} end time {}. Start the rotation immediately.",
+                timeToSchedule, rootCACert.getSerialNumber().toString(),
+                rootCACert.getNotAfter());
+            delay = 0;
+          }
+
+          executorService.schedule(new RotationTask(certClient), delay,
+              TimeUnit.MILLISECONDS);
+          isScheduled.set(true);
+          LOG.info("Root certificate {} rotation task is scheduled with {} ms "
+              + "delay", rootCACert.getSerialNumber().toString(), delay);
+        }
+      }
+    }
+  }
+
+  /**
+   *  Task to rotate root certificate.
+   */
+  public class RotationTask implements Runnable {
+    private CertificateClient certClient;
+
+    public RotationTask(CertificateClient client) {
+      this.certClient = client;
+    }
+
+    @Override
+    public void run() {
+      isScheduled.set(false);
+      if (!isRunning.get()) {
+        return;
+      }
+      // Lock to protect the root CA certificate rotation process,
+      // to make sure there is only one task is ongoing at one time.
+      // Root CA rotation steps:
+      //  1. generate new root CA keys and certificate, persist to disk
+      //  2. start new Root CA server
+      //  3. send scm Sub-CA rotation preparation request through RATIS
+      //  4. send scm Sub-CA rotation commit request through RATIS
+      //  5. send scm Sub-CA rotation finish request through RATIS
+      synchronized (RootCARotationManager.class) {
+        X509Certificate rootCACert = certClient.getCACertificate();
+        Duration timeLeft = timeBefore2ExpiryGracePeriod(rootCACert);
+        if (timeLeft.isZero()) {
+          LOG.info("Root certificate {} rotation is started.",
+              rootCACert.getSerialNumber().toString());
+          // TODO: start the root CA rotation process
+        } else {
+          LOG.warn("Root certificate {} hasn't entered the 2 * expiry" +
+                  " grace period {}. Skip root certificate rotation this 
time.",
+              rootCACert.getSerialNumber().toString(), renewalGracePeriod);
+        }
+      }
+    }
+  }
+
+  /**
+   * Calculate time before root certificate will enter 2 * expiry grace period.
+   * @return Duration, time before certificate enters the 2 * grace
+   *                   period defined by "hdds.x509.renew.grace.duration"
+   */
+  public Duration timeBefore2ExpiryGracePeriod(X509Certificate certificate) {
+    LocalDateTime gracePeriodStart = certificate.getNotAfter().toInstant()
+        .minus(renewalGracePeriod).minus(renewalGracePeriod)
+        .atZone(ZoneId.systemDefault()).toLocalDateTime();
+    LocalDateTime currentTime = LocalDateTime.now();
+    if (gracePeriodStart.isBefore(currentTime)) {
+      // Cert is already in grace period time.
+      return Duration.ZERO;
+    } else {
+      return Duration.between(currentTime, gracePeriodStart);
+    }
+  }
+
+  /**
+   * Stops scheduled monitor task.
+   */
+  @Override
+  public void stop() {
+    try {
+      executorService.shutdown();
+      if (!executorService.awaitTermination(3, TimeUnit.SECONDS)) {
+        executorService.shutdownNow();
+      }
+    } catch (InterruptedException ie) {
+      // Ignore, we don't really care about the failure.
+      Thread.currentThread().interrupt();
+    }
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/crl/package-info.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/package-info.java
similarity index 89%
rename from 
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/crl/package-info.java
rename to 
hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/package-info.java
index 997aac2b3c..118c9dc6d6 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/crl/package-info.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/security/package-info.java
@@ -17,6 +17,6 @@
  */
 
 /**
- * This package contains CRL related classes.
+ * This package contains security related classes.
  */
-package org.apache.hadoop.hdds.scm.crl;
+package org.apache.hadoop.hdds.scm.security;
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/StorageContainerManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/StorageContainerManager.java
index e6880add77..681625463c 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/StorageContainerManager.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/StorageContainerManager.java
@@ -50,7 +50,8 @@ import 
org.apache.hadoop.hdds.scm.container.balancer.MoveManager;
 import 
org.apache.hadoop.hdds.scm.container.replication.ContainerReplicaPendingOps;
 import 
org.apache.hadoop.hdds.scm.container.replication.DatanodeCommandCountUpdatedHandler;
 import 
org.apache.hadoop.hdds.scm.container.replication.LegacyReplicationManager;
-import org.apache.hadoop.hdds.scm.crl.CRLStatusReportHandler;
+import org.apache.hadoop.hdds.scm.ha.SCMServiceException;
+import org.apache.hadoop.hdds.scm.security.CRLStatusReportHandler;
 import org.apache.hadoop.hdds.scm.ha.BackgroundSCMService;
 import org.apache.hadoop.hdds.scm.ha.HASecurityUtils;
 import org.apache.hadoop.hdds.scm.ha.SCMContext;
@@ -67,6 +68,7 @@ import org.apache.hadoop.hdds.scm.ha.SCMHAUtils;
 import org.apache.hadoop.hdds.scm.ha.SequenceIdGenerator;
 import org.apache.hadoop.hdds.scm.ScmInfo;
 import org.apache.hadoop.hdds.scm.node.NodeAddressUpdateHandler;
+import org.apache.hadoop.hdds.scm.security.RootCARotationManager;
 import org.apache.hadoop.hdds.scm.server.upgrade.FinalizationManager;
 import org.apache.hadoop.hdds.scm.server.upgrade.FinalizationManagerImpl;
 import org.apache.hadoop.hdds.scm.ha.StatefulServiceStateManager;
@@ -269,6 +271,7 @@ public final class StorageContainerManager extends 
ServiceRuntimeInfoImpl
 
   private SCMSafeModeManager scmSafeModeManager;
   private CertificateClient scmCertificateClient;
+  private RootCARotationManager rootCARotationManager;
   private ContainerTokenSecretManager containerTokenMgr;
 
   private JvmPauseMonitor jvmPauseMonitor;
@@ -882,6 +885,7 @@ public final class StorageContainerManager extends 
ServiceRuntimeInfoImpl
     if (securityConfig.isContainerTokenEnabled()) {
       containerTokenMgr = createContainerTokenSecretManager(configuration);
     }
+    rootCARotationManager = new RootCARotationManager(this);
   }
 
   /** Persist primary SCM root ca cert and sub-ca certs to DB.
@@ -1502,6 +1506,14 @@ public final class StorageContainerManager extends 
ServiceRuntimeInfoImpl
       persistSCMCertificates();
     }
 
+    if (rootCARotationManager != null) {
+      try {
+        rootCARotationManager.start();
+      } catch (SCMServiceException e) {
+        throw new IOException("Failed to start root CA rotation manager", e);
+      }
+    }
+
     scmBlockManager.start();
     leaseManager.start();
 
@@ -1623,6 +1635,10 @@ public final class StorageContainerManager extends 
ServiceRuntimeInfoImpl
       getSecurityProtocolServer().stop();
     }
 
+    if (rootCARotationManager != null) {
+      rootCARotationManager.stop();
+    }
+
     try {
       LOG.info("Stopping Block Manager Service.");
       scmBlockManager.stop();
@@ -1834,6 +1850,13 @@ public final class StorageContainerManager extends 
ServiceRuntimeInfoImpl
     return moveManager;
   }
 
+  /**
+   * Returns SCM root CA rotation manager.
+   */
+  public RootCARotationManager getRootCARotationManager() {
+    return rootCARotationManager;
+  }
+
   /**
    * Check if the current scm is the leader and ready for accepting requests.
    * @return - if the current scm is the leader and is ready.
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/crl/TestCRLStatusReportHandler.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/security/TestCRLStatusReportHandler.java
similarity index 99%
rename from 
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/crl/TestCRLStatusReportHandler.java
rename to 
hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/security/TestCRLStatusReportHandler.java
index 91c9dbc09a..e5b206728f 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/crl/TestCRLStatusReportHandler.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/security/TestCRLStatusReportHandler.java
@@ -14,7 +14,7 @@
  * License for the specific language governing permissions and limitations 
under
  * the License.
  */
-package org.apache.hadoop.hdds.scm.crl;
+package org.apache.hadoop.hdds.scm.security;
 
 import org.apache.hadoop.hdds.HddsConfigKeys;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/security/TestRootCARotationManager.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/security/TestRootCARotationManager.java
new file mode 100644
index 0000000000..16c2b45606
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/security/TestRootCARotationManager.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations 
under
+ * the License.
+ */
+package org.apache.hadoop.hdds.scm.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.fs.FileUtil;
+import org.apache.hadoop.hdds.HddsConfigKeys;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.scm.container.TestContainerManagerImpl;
+import org.apache.hadoop.hdds.scm.ha.SCMContext;
+import org.apache.hadoop.hdds.scm.ha.SCMServiceManager;
+import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
+import 
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
+import 
org.apache.hadoop.hdds.security.x509.certificate.utils.SelfSignedCertificate;
+import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
+import org.apache.ozone.test.GenericTestUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_CHECK_INTERNAL;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_TIME_OF_DAY;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_RENEW_GRACE_DURATION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.when;
+import static org.slf4j.event.Level.INFO;
+
+/**
+ * Test for root CA rotation manager.
+ */
+public class TestRootCARotationManager {
+
+  private OzoneConfiguration ozoneConfig;
+  private RootCARotationManager rootCARotationManager;
+  private StorageContainerManager scm;
+  private CertificateClient scmCertClient;
+  private SCMServiceManager scmServiceManager;
+  private SCMContext scmContext;
+  private File testDir;
+
+  @BeforeEach
+  public void init() throws IOException, TimeoutException {
+    ozoneConfig = new OzoneConfiguration();
+    testDir = GenericTestUtils.getTestDir(
+        TestContainerManagerImpl.class.getSimpleName() + UUID.randomUUID());
+    ozoneConfig.set(HddsConfigKeys.OZONE_METADATA_DIRS,
+        testDir.getAbsolutePath());
+    scm = Mockito.mock(StorageContainerManager.class);
+    scmCertClient = Mockito.mock(CertificateClient.class);
+    scmServiceManager = new SCMServiceManager();
+    scmContext = Mockito.mock(SCMContext.class);
+    when(scmContext.isLeader()).thenReturn(true);
+    when(scm.getConfiguration()).thenReturn(ozoneConfig);
+    when(scm.getScmCertificateClient()).thenReturn(scmCertClient);
+    when(scm.getScmContext()).thenReturn(scmContext);
+    when(scm.getSCMServiceManager()).thenReturn(scmServiceManager);
+  }
+
+  @AfterEach
+  public void tearDown() throws Exception {
+    if (rootCARotationManager != null) {
+      rootCARotationManager.stop();
+    }
+
+    FileUtil.fullyDelete(testDir);
+  }
+
+  @Test
+  public void testProperties() {
+    // invalid check interval
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "P28");
+    try {
+      rootCARotationManager = new RootCARotationManager(scm);
+      fail("Should fail");
+    } catch (Exception e) {
+      Assertions.assertTrue(e instanceof DateTimeParseException);
+    }
+
+    // check interval should be less than grace period
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "P28D");
+    try {
+      rootCARotationManager = new RootCARotationManager(scm);
+      fail("Should fail");
+    } catch (Exception e) {
+      Assertions.assertTrue(e instanceof IllegalArgumentException);
+      Assertions.assertTrue(e.getMessage().contains("should be smaller than"));
+    }
+
+    // invalid time of day format
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "P1D");
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_TIME_OF_DAY, "01:00");
+    try {
+      rootCARotationManager = new RootCARotationManager(scm);
+      fail("Should fail");
+    } catch (Exception e) {
+      Assertions.assertTrue(e instanceof IllegalArgumentException);
+      Assertions.assertTrue(
+          e.getMessage().contains("should follow the hh:mm:ss format"));
+    }
+
+    // valid properties
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "P1D");
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_TIME_OF_DAY, "01:00:00");
+
+    try {
+      rootCARotationManager = new RootCARotationManager(scm);
+    } catch (Exception e) {
+      fail("Should succeed");
+    }
+  }
+
+  @Test
+  public void testRotationOnSchedule() throws Exception {
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "PT1S");
+    ozoneConfig.set(HDDS_X509_RENEW_GRACE_DURATION, "PT15S");
+    Date date = Calendar.getInstance().getTime();
+    date.setSeconds(date.getSeconds() + 10);
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_TIME_OF_DAY,
+        String.format("%02d", date.getHours()) + ":" +
+            String.format("%02d", date.getMinutes()) + ":" +
+            String.format("%02d", date.getSeconds()));
+
+    X509Certificate cert = generateX509Cert(ozoneConfig,
+        LocalDateTime.now(), Duration.ofSeconds(35));
+    when(scmCertClient.getCACertificate()).thenReturn(cert);
+
+    rootCARotationManager = new RootCARotationManager(scm);
+    GenericTestUtils.LogCapturer logs =
+        GenericTestUtils.LogCapturer.captureLogs(RootCARotationManager.LOG);
+    GenericTestUtils.setLogLevel(RootCARotationManager.LOG, INFO);
+    rootCARotationManager.start();
+    rootCARotationManager.notifyStatusChanged();
+
+    String msg = "Root certificate " +
+        cert.getSerialNumber().toString() + " rotation is started.";
+    GenericTestUtils.waitFor(
+        () -> !logs.getOutput().contains("Start the rotation immediately") &&
+            logs.getOutput().contains(msg),
+        100, 10000);
+    assertEquals(1, StringUtils.countMatches(logs.getOutput(), msg));
+  }
+
+  @Test
+  public void testRotationImmediately() throws Exception {
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "PT1S");
+    ozoneConfig.set(HDDS_X509_RENEW_GRACE_DURATION, "PT15S");
+    Date date = Calendar.getInstance().getTime();
+    date.setMinutes(date.getMinutes() + 5);
+    ozoneConfig.set(HDDS_X509_CA_ROTATION_TIME_OF_DAY,
+        String.format("%02d", date.getHours()) + ":" +
+            String.format("%02d", date.getMinutes()) + ":" +
+            String.format("%02d", date.getSeconds()));
+
+    X509Certificate cert = generateX509Cert(ozoneConfig,
+        LocalDateTime.now(), Duration.ofSeconds(35));
+    when(scmCertClient.getCACertificate()).thenReturn(cert);
+
+    rootCARotationManager = new RootCARotationManager(scm);
+    GenericTestUtils.LogCapturer logs =
+        GenericTestUtils.LogCapturer.captureLogs(RootCARotationManager.LOG);
+    GenericTestUtils.setLogLevel(RootCARotationManager.LOG, INFO);
+    rootCARotationManager.start();
+    rootCARotationManager.notifyStatusChanged();
+
+    GenericTestUtils.waitFor(
+        () -> logs.getOutput().contains("Start the rotation immediately") &&
+        logs.getOutput().contains("Root certificate " +
+            cert.getSerialNumber().toString() + " rotation is started."),
+        100, 10000);
+  }
+
+  private X509Certificate generateX509Cert(
+      OzoneConfiguration conf, LocalDateTime startDate,
+      Duration certLifetime) throws Exception {
+    KeyPair keyPair = KeyStoreTestUtil.generateKeyPair("RSA");
+    LocalDateTime start = startDate == null ? LocalDateTime.now() : startDate;
+    LocalDateTime end = start.plus(certLifetime);
+    return new JcaX509CertificateConverter().getCertificate(
+        SelfSignedCertificate.newBuilder().setBeginDate(start)
+            .setEndDate(end).setClusterID("cluster").setKey(keyPair)
+            .setSubject("localhost").setConfiguration(conf).setScmID("test")
+            .build());
+  }
+}
diff --git a/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test.sh 
b/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test.sh
index 10d71c08cf..9fe32168e3 100755
--- a/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test.sh
+++ b/hadoop-ozone/dist/src/main/compose/ozonesecure-ha/test.sh
@@ -51,7 +51,11 @@ done
 execute_robot_test s3g admincli
 
 execute_robot_test s3g omha/om-leader-transfer.robot
+
+# verify root CA rotation monitor task
+wait_for_execute_command scm1.org 30 "jps | grep 
StorageContainerManagerStarter | awk -F' ' '{print $1}' | xargs -I {} jstack {} 
| grep 'RootCARotationManager-MonitorTask-Active'"
 execute_robot_test s3g scmha/scm-leader-transfer.robot
+wait_for_execute_command scm1.org 30 "jps | grep 
StorageContainerManagerStarter | awk -F' ' '{print $1}' | xargs -I {} jstack {} 
| grep 'RootCARotationManager-MonitorTask-Inactive'"
 
 execute_robot_test s3g httpfs
 
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
index 16c1cb62d8..0e8d2c882c 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
@@ -118,6 +118,7 @@ import org.apache.commons.lang3.StringUtils;
 import static 
org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_BLOCK_TOKEN_EXPIRY_TIME;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_TOKEN_ENABLED;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_CHECK_INTERNAL;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_DEFAULT_DURATION;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_MAX_DURATION;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_DEFAULT_DURATION_DEFAULT;
@@ -251,6 +252,8 @@ public final class TestSecureOzoneCluster {
       conf.set(OZONE_METADATA_DIRS, omMetaDirPath.toString());
       conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
       conf.set(HADOOP_SECURITY_AUTHENTICATION, KERBEROS.name());
+      conf.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL,
+          Duration.ofMillis(certGraceTime - 1000).toString());
       conf.set(HDDS_X509_RENEW_GRACE_DURATION,
           Duration.ofMillis(certGraceTime).toString());
       conf.setLong(OMConfigKeys.DELEGATION_TOKEN_MAX_LIFETIME_KEY,
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainerWithTLS.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainerWithTLS.java
index 22d55992cf..7d26663ea8 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainerWithTLS.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOzoneContainerWithTLS.java
@@ -73,6 +73,7 @@ import java.util.concurrent.TimeUnit;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_KEY_DIR_NAME;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_KEY_DIR_NAME_DEFAULT;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_KEY_LEN;
+import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_CHECK_INTERNAL;
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_DEFAULT_DURATION;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_RENEW_GRACE_DURATION;
 import static org.apache.hadoop.hdds.HddsConfigKeys.OZONE_METADATA_DIRS;
@@ -138,6 +139,7 @@ public class TestOzoneContainerWithTLS {
     conf.set(HDDS_X509_DEFAULT_DURATION,
         Duration.ofMillis(certLifetime).toString());
     conf.set(HDDS_X509_RENEW_GRACE_DURATION, "PT2S");
+    conf.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL, "PT1S"); // 1s
 
     long expiryTime = conf.getTimeDuration(
         HddsConfigKeys.HDDS_BLOCK_TOKEN_EXPIRY_TIME, "1s",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to