You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by si...@apache.org on 2026/10/17 18:35:04 UTC

(ozone) 06/13: HDDS-16936. Require SCM admin access for SCM role certificates and limit the basic constraints extension to the SCM role (#11486)

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

smengcl pushed a commit to branch ozone-2.2
in repository https://gitbox.apache.org/repos/asf/ozone.git

commit 3127087d73b33d018475f240520157807b72501a
Author: Siyao Meng <50...@users.noreply.github.com>
AuthorDate: Wed Oct 14 18:36:42 2026 -0700

    HDDS-16936. Require SCM admin access for SCM role certificates and limit the basic constraints extension to the SCM role (#11486)
    
    Co-authored-by: Claude Opus 5.5 <no...@anthropic.com>
    (cherry picked from commit 6ba39847deb6140fac0d892b5fa92027e8a880ae)
    
    Conflicts:
            hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestSecureOzoneCluster.java
    
    Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
 .../certificate/authority/DefaultCAServer.java     |  9 +++++
 .../x509/certificate/authority/MockCAStore.java    |  6 +++
 .../certificate/authority/TestDefaultCAServer.java | 43 ++++++++++++++++++++++
 .../hdds/scm/server/SCMSecurityProtocolServer.java |  4 ++
 .../hadoop/ozone/TestSecureOzoneCluster.java       | 20 ++++++++++
 5 files changed, 82 insertions(+)

diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/DefaultCAServer.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/DefaultCAServer.java
index 5fe79918b9e..a2956e7f666 100644
--- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/DefaultCAServer.java
+++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/x509/certificate/authority/DefaultCAServer.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.hdds.security.x509.certificate.authority;
 
 import static org.apache.hadoop.hdds.security.exception.SCMSecurityException.ErrorCode.UNABLE_TO_ISSUE_CERTIFICATE;
+import static org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateSignRequest.getPkcs9Extensions;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -53,6 +54,7 @@
 import org.apache.hadoop.hdds.security.x509.certificate.utils.SelfSignedCertificate;
 import org.apache.hadoop.hdds.security.x509.keys.HDDSKeyGenerator;
 import org.apache.hadoop.hdds.security.x509.keys.KeyStorage;
+import org.bouncycastle.asn1.x509.Extension;
 import org.bouncycastle.operator.OperatorCreationException;
 import org.bouncycastle.pkcs.PKCS10CertificationRequest;
 import org.slf4j.Logger;
@@ -216,8 +218,15 @@ public Future<CertPath> requestCertificate(
       } catch (Exception e) {
         certPathPromise.completeExceptionally(new SCMSecurityException("Failed to verify the CSR.", e));
       }
+      return certPathPromise;
     }
     try {
+      // The basic constraints extension belongs to SCM sub CA certificates only, whatever the profile allows.
+      if (role != NodeType.SCM && getPkcs9Extensions(csr).getExtension(Extension.basicConstraints) != null) {
+        certPathPromise.completeExceptionally(
+            new SCMSecurityException("Basic constraints extension is allowed for the SCM role only, not for " + role));
+        return certPathPromise;
+      }
       switch (approverType) {
       case MANUAL:
         certPathPromise.completeExceptionally(new SCMSecurityException("Manual approval is not yet implemented."));
diff --git a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/MockCAStore.java b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/MockCAStore.java
index c8cb5f73155..ef5664b4fc9 100644
--- a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/MockCAStore.java
+++ b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/MockCAStore.java
@@ -30,11 +30,17 @@
  *
  */
 public class MockCAStore implements CertificateStore {
+  private final List<X509Certificate> storedCertificates = new ArrayList<>();
+
   @Override
   public void storeValidCertificate(BigInteger serialID,
       X509Certificate certificate, NodeType role)
       throws IOException {
+    storedCertificates.add(certificate);
+  }
 
+  public List<X509Certificate> getStoredCertificates() {
+    return storedCertificates;
   }
 
   @Override
diff --git a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java
index 5f8b72dc3a1..abd6a731dfb 100644
--- a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java
+++ b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/x509/certificate/authority/TestDefaultCAServer.java
@@ -237,6 +237,49 @@ public void testRequestCertificateWithInvalidSubject() throws Exception {
     assertNotNull(CertificateCodec.firstCertificateFrom(holder.get()));
   }
 
+  @Test
+  public void testBasicConstraintsAllowedForScmRoleOnly() throws Exception {
+    String scmId = RandomStringUtils.secure().nextAlphabetic(4);
+    String clusterId = RandomStringUtils.secure().nextAlphabetic(4);
+    KeyPair keyPair = new HDDSKeyGenerator(securityConfig).generateKey();
+    PKCS10CertificationRequest caCsr = new CertificateSignRequest.Builder()
+        .setCA(true)
+        .setClusterID(clusterId)
+        .setScmID(scmId)
+        .setSubject("Ozone Cluster")
+        .setConfiguration(securityConfig)
+        .setKey(keyPair)
+        .build()
+        .generateCSR();
+
+    CertificateServer testCA = new DefaultCAServer("testCA",
+        clusterId, scmId, caStore,
+        new DefaultCAProfile(),
+        Paths.get(SCM_CA_CERT_STORAGE_DIR, SCM_CA_PATH).toString());
+    testCA.init(securityConfig, CAType.ROOT);
+
+    ExecutionException execution = assertThrows(ExecutionException.class,
+        () -> testCA.requestCertificate(caCsr, CertificateApprover.ApprovalType.TESTING_AUTOMATIC, OM,
+            String.valueOf(System.nanoTime())).get());
+    assertThat(execution.getCause().getMessage()).contains("allowed for the SCM role only, not for OM");
+    assertThat(caStore.getStoredCertificates()).isEmpty();
+
+    X509Certificate scmCert = CertificateCodec.firstCertificateFrom(testCA.requestCertificate(caCsr,
+        CertificateApprover.ApprovalType.TESTING_AUTOMATIC, SCM, String.valueOf(System.nanoTime())).get());
+    assertThat(scmCert.getBasicConstraints()).isNotEqualTo(-1);
+    assertThat(caStore.getStoredCertificates()).containsExactly(scmCert);
+
+    // A CSR the profile rejects (CA constraint under the non CA profile) is neither signed nor stored.
+    CertificateServer plainCA = new DefaultCAServer("plainCA", clusterId, scmId, caStore, new DefaultProfile(),
+        Paths.get(SCM_CA_CERT_STORAGE_DIR, SCM_CA_PATH).toString());
+    plainCA.init(securityConfig, CAType.ROOT);
+    execution = assertThrows(ExecutionException.class,
+        () -> plainCA.requestCertificate(caCsr, CertificateApprover.ApprovalType.TESTING_AUTOMATIC, SCM,
+            String.valueOf(System.nanoTime())).get());
+    assertThat(execution.getCause().getMessage()).contains("Failed to verify the CSR");
+    assertThat(caStore.getStoredCertificates()).containsExactly(scmCert);
+  }
+
   @Test
   public void testRequestCertificateWithInvalidSubjectFailure() throws Exception {
     KeyPair keyPair =
diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMSecurityProtocolServer.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMSecurityProtocolServer.java
index 87df4930df6..f1cc6fb9c27 100644
--- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMSecurityProtocolServer.java
+++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMSecurityProtocolServer.java
@@ -338,6 +338,10 @@ public String getSCMCertificate(ScmNodeDetailsProto scmNodeDetails,
    */
   private synchronized String getEncodedCertToString(String certSignReq,
       NodeType nodeType) throws IOException {
+    if (nodeType == NodeType.SCM) {
+      // SCM role certificates are CA certificates and are issued to SCM admins only, the check addSCM applies.
+      storageContainerManager.checkAdminAccess(getRpcRemoteUser(), false);
+    }
     Future<CertPath> future;
     PKCS10CertificationRequest csr = getCertificationRequest(certSignReq);
     if (nodeType == NodeType.SCM && rootCertificateServer != null) {
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 42856d846fe..f02fe8e6469 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
@@ -419,6 +419,7 @@ void testAdminAccessControlException() throws Exception {
       IOException ioException = assertThrows(IOException.class,
           scmRpcClient::forceExitSafeMode);
       assertThat(ioException).hasMessageContaining("Access denied");
+      assertScmSecurityProtocolRequiresAdminForScmRoleCertificate();
 
       // Case 2: User without Kerberos credentials should fail.
       ugi = UserGroupInformation.createRemoteUser("test");
@@ -437,6 +438,25 @@ void testAdminAccessControlException() throws Exception {
     }
   }
 
+  /** SCM security protocol - SCM role certificates require SCM admin access, through either signing RPC. */
+  private void assertScmSecurityProtocolRequiresAdminForScmRoleCertificate() throws Exception {
+    UserGroupInformation ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(
+        testUserPrincipal, testUserKeytab.getCanonicalPath());
+    String csr = new CertificateSignRequest.Builder().setKey(keyPair).setConfiguration(new SecurityConfig(conf))
+        .setScmID(scmId).setClusterID(clusterId).setSubject("scm-host").setDigitalSignature(true)
+        .build().toEncodedFormat();
+    HddsProtos.ScmNodeDetailsProto scmDetails = HddsProtos.ScmNodeDetailsProto.newBuilder().setClusterId(clusterId)
+        .setHostName("scm-host").setScmNodeId("scm-node").build();
+    HddsProtos.NodeDetailsProto scmNode = HddsProtos.NodeDetailsProto.newBuilder().setClusterId(clusterId)
+        .setHostName("scm-host").setUuid(UUID.randomUUID().toString()).setNodeType(HddsProtos.NodeType.SCM).build();
+    try (SCMSecurityProtocolClientSideTranslatorPB securityClient = getScmSecurityClient(conf, ugi)) {
+      IOException denied = assertThrows(IOException.class, () -> securityClient.getSCMCertificate(scmDetails, csr));
+      assertThat(denied).hasMessageContaining("Access denied");
+      denied = assertThrows(IOException.class, () -> securityClient.getCertificate(scmNode, csr));
+      assertThat(denied).hasMessageContaining("Access denied");
+    }
+  }
+
   private void initSCM() throws IOException {
     Path scmPath = new File(tempDir, "scm-meta").toPath();
     Files.createDirectories(scmPath);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ozone.apache.org
For additional commands, e-mail: commits-help@ozone.apache.org