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