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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6ba39847deb HDDS-16936. Require SCM admin access for SCM role 
certificates and limit the basic constraints extension to the SCM role (#11486)
6ba39847deb is described below

commit 6ba39847deb6140fac0d892b5fa92027e8a880ae
Author: Siyao Meng <[email protected]>
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 <[email protected]>
---
 .../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 7e435bf9f60..5f19b6d3e02 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
@@ -282,6 +282,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 5561e13a062..15bbb9ee4fe 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
@@ -289,6 +289,7 @@ void testSecureScmAndOmStartupAndAccessControl() throws 
Exception {
     assertScmSecurityProtocolRejectsNonKerberosUser();
     assertScmAdminProtocolDeniesNonAdminUser();
     assertScmAdminProtocolRejectsNonKerberosUser();
+    assertScmSecurityProtocolRequiresAdminForScmRoleCertificate();
 
     // Secure OM initialization against the shared SCM.
     assertSecureOmInitFailsWithInvalidTokenAndSecretKeyConfig();
@@ -353,6 +354,25 @@ private void assertScmAdminProtocolDeniesNonAdminUser() 
throws IOException {
     assertThat(adminException).hasMessageContaining("Access denied");
   }
 
+  /** SCM security protocol - SCM role certificates require SCM admin access, 
through either signing RPC. */
+  private void assertScmSecurityProtocolRequiresAdminForScmRoleCertificate() 
throws Exception {
+    UserGroupInformation ugi = 
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
+        getTestUserPrincipal(), getTestUserKeytab().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");
+    }
+  }
+
   /** SCM admin protocol - user without Kerberos credentials fails. */
   private void assertScmAdminProtocolRejectsNonKerberosUser() throws 
IOException {
     UserGroupInformation ugi = UserGroupInformation.createRemoteUser("test");


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

Reply via email to