Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ public final class HddsConfigKeys {

public static final String HDDS_X509_RENEW_GRACE_DURATION_DEFAULT = "P28D";

public static final String HDDS_X509_ROOTCA_CERTIFICATE_FILE =
"hdds.x509.rootca.certificate.file";

public static final String HDDS_X509_ROOTCA_CERTIFICATE_FILE_DEFAULT =
"";

public static final String HDDS_X509_ROOTCA_PUBLIC_KEY_FILE =
"hdds.x509.rootca.public.key.file";

public static final String HDDS_X509_ROOTCA_PUBLIC_KEY_FILE_DEFAULT =
"";

public static final String HDDS_X509_ROOTCA_PRIVATE_KEY_FILE =
"hdds.x509.rootca.private.key.file";

public static final String HDDS_X509_ROOTCA_PRIVATE_KEY_FILE_DEFAULT =
"";

/**
* Do not instantiate.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@
import org.apache.hadoop.ozone.OzoneConfigKeys;

import com.google.common.base.Preconditions;

import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_BLOCK_TOKEN_ENABLED;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_BLOCK_TOKEN_ENABLED_DEFAULT;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_TOKEN_ENABLED;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_CONTAINER_TOKEN_ENABLED_DEFAULT;
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_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;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_ROOTCA_PRIVATE_KEY_FILE_DEFAULT;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_ROOTCA_PUBLIC_KEY_FILE;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_ROOTCA_PUBLIC_KEY_FILE_DEFAULT;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_GRPC_TLS_ENABLED;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_GRPC_TLS_ENABLED_DEFAULT;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_GRPC_TLS_PROVIDER;
Expand Down Expand Up @@ -74,6 +81,7 @@
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SECURITY_SSL_TRUSTSTORE_RELOAD_INTERVAL_DEFAULT;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_DEFAULT;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_KEY;

import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
Expand Down Expand Up @@ -111,6 +119,9 @@ public class SecurityConfig {
private boolean grpcTlsUseTestCert;
private final long keystoreReloadInterval;
private final long truststoreReloadInterval;
private final String externalRootCaPublicKeyPath;
private final String externalRootCaPrivateKeyPath;
private final String externalRootCaCert;

/**
* Constructs a SecurityConfig.
Expand Down Expand Up @@ -182,8 +193,18 @@ public SecurityConfig(ConfigurationSource configuration) {
"greater than maximum Certificate duration");
}

this.externalRootCaCert = this.configuration.get(
HDDS_X509_ROOTCA_CERTIFICATE_FILE,
HDDS_X509_ROOTCA_CERTIFICATE_FILE_DEFAULT);
this.externalRootCaPublicKeyPath = this.configuration.get(
HDDS_X509_ROOTCA_PUBLIC_KEY_FILE,
HDDS_X509_ROOTCA_PUBLIC_KEY_FILE_DEFAULT);
this.externalRootCaPrivateKeyPath = this.configuration.get(
HDDS_X509_ROOTCA_PRIVATE_KEY_FILE,
HDDS_X509_ROOTCA_PRIVATE_KEY_FILE_DEFAULT);

this.crlName = this.configuration.get(HDDS_X509_CRL_NAME,
HDDS_X509_CRL_NAME_DEFAULT);
HDDS_X509_CRL_NAME_DEFAULT);

// First Startup -- if the provider is null, check for the provider.
if (SecurityConfig.provider == null) {
Expand Down Expand Up @@ -399,6 +420,18 @@ public SslProvider getGrpcSslProvider() {
HDDS_GRPC_TLS_PROVIDER_DEFAULT));
}

public String getExternalRootCaPrivateKeyPath() {
return externalRootCaPrivateKeyPath;
}

public String getExternalRootCaPublicKeyPath() {
return externalRootCaPublicKeyPath;
}

public String getExternalRootCaCert() {
return externalRootCaCert;
}

/**
* Return true if using test certificates with authority as localhost. This
* should be used only for unit test where certificates are generated by
Expand Down
38 changes: 37 additions & 1 deletion hadoop-hdds/common/src/main/resources/ozone-default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2087,14 +2087,50 @@
<description>Max time for which certificate issued by SCM CA are valid.
This duration is used for self-signed root cert and scm sub-ca certs
issued by root ca. The formats accepted are based on the ISO-8601
duration format PnDTnHnMn.nS</description>
duration format PnDTnHnMn.nS
</description>
</property>
<property>
<name>hdds.x509.signature.algorithm</name>
<value>SHA256withRSA</value>
<tag>OZONE, HDDS, SECURITY</tag>
<description>X509 signature certificate.</description>
</property>
<property>
<name>hdds.x509.rootca.certificate.file</name>
<value></value>
<description>Path to an external CA certificate. The file format is expected
to be pem. This certificate is used when initializing SCM to create
a root certificate authority. By default, a self-signed certificate is
generated instead. Note that this certificate is only used for Ozone's
internal communication, and it does not affect the certificates used for
HTTPS protocol at WebUIs as they can be configured separately.
</description>
</property>
<property>
<name>hdds.x509.rootca.private.key.file</name>
<value></value>
<description>Path to an external private key. The file format is expected
to be pem. This private key is later used when initializing SCM to sign
certificates as the root certificate authority. When not specified a
private and public key is generated instead.
These keys are only used for Ozone's internal communication, and it does
not affect the HTTPS protocol at WebUIs as they can be configured
separately.
</description>
</property>
<property>
<name>hdds.x509.rootca.public.key.file</name>
<value></value>
<description>Path to an external public key. The file format is expected
to be pem. This public key is later used when initializing SCM to sign
certificates as the root certificate authority.
When only the private key is specified the public key is read from the
external certificate. Note that this is only used for Ozone's internal
communication, and it does not affect the HTTPS protocol at WebUIs as
they can be configured separately.
</description>
</property>
<property>
<name>ozone.scm.security.handler.count.key</name>
<value>2</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
Expand Down Expand Up @@ -252,7 +254,6 @@ public Future<X509CertificateHolder> requestCertificate(
LOG.error("Certificate storage failed, retrying one more time.", e);
xcert = signAndStoreCertificate(beginDate, endDate, csr, role);
}

xcertHolder.complete(xcert);
break;
default:
Expand Down Expand Up @@ -474,19 +475,7 @@ Consumer<SecurityConfig> processVerificationStatus(
break;
case INITIALIZE:
if (type == CAType.SELF_SIGNED_CA) {
consumer = (arg) -> {
try {
generateSelfSignedCA(arg);
} catch (NoSuchProviderException | NoSuchAlgorithmException
| IOException e) {
LOG.error("Unable to initialize CertificateServer.", e);
}
VerificationStatus newStatus = verifySelfSignedCA(arg);
if (newStatus != VerificationStatus.SUCCESS) {
LOG.error("Unable to initialize CertificateServer, failed in " +
"verification.");
}
};
consumer = this::initRootCa;
} else if (type == CAType.INTERMEDIARY_CA) {
// For sub CA certificates are generated during bootstrap/init. If
// both keys/certs are missing, init/bootstrap is missed to be
Expand All @@ -506,6 +495,29 @@ Consumer<SecurityConfig> processVerificationStatus(
return consumer;
}

private void initRootCa(SecurityConfig securityConfig) {
if (isExternalCaSpecified(securityConfig)) {
initWithExternalRootCa(securityConfig);
} else {
try {
generateSelfSignedCA(securityConfig);
} catch (NoSuchProviderException | NoSuchAlgorithmException
| IOException e) {
LOG.error("Unable to initialize CertificateServer.", e);
}
}
VerificationStatus newStatus = verifySelfSignedCA(securityConfig);
if (newStatus != VerificationStatus.SUCCESS) {
LOG.error("Unable to initialize CertificateServer, failed in " +
"verification.");
}
}

private boolean isExternalCaSpecified(SecurityConfig conf) {
return !conf.getExternalRootCaCert().isEmpty() &&
!conf.getExternalRootCaPrivateKeyPath().isEmpty();
}

/**
* Generates a KeyPair for the Certificate.
*
Expand All @@ -529,12 +541,13 @@ private KeyPair generateKeys(SecurityConfig securityConfig)
* Generates a self-signed Root Certificate for CA.
*
* @param securityConfig - SecurityConfig
* @param key - KeyPair.
* @param key - KeyPair.
* @throws IOException - on Error.
* @throws SCMSecurityException - on Error.
*/
private void generateRootCertificate(SecurityConfig securityConfig,
KeyPair key) throws IOException, SCMSecurityException {
private void generateRootCertificate(
SecurityConfig securityConfig, KeyPair key)
throws IOException, SCMSecurityException {
Preconditions.checkNotNull(this.config);
LocalDateTime beginDate =
LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT);
Expand Down Expand Up @@ -563,7 +576,7 @@ private void generateRootCertificate(SecurityConfig securityConfig,
} catch (IOException e) {
throw new org.apache.hadoop.hdds.security.x509
.exceptions.CertificateException(
"Error while adding ip to CA self signed certificate", e,
"Error while adding ip to CA self signed certificate", e,
CSR_ERROR);
}
X509CertificateHolder selfSignedCertificate = builder.build();
Expand All @@ -573,6 +586,65 @@ private void generateRootCertificate(SecurityConfig securityConfig,
certCodec.writeCertificate(selfSignedCertificate);
}

private void initWithExternalRootCa(SecurityConfig conf) {
String externalRootCaLocation = conf.getExternalRootCaCert();
Path extCertPath = Paths.get(externalRootCaLocation);
Path extPrivateKeyPath = Paths.get(conf.getExternalRootCaPrivateKeyPath());
String externalPublicKeyLocation = conf.getExternalRootCaPublicKeyPath();

KeyCodec keyCodec = new KeyCodec(config, componentName);
CertificateCodec certificateCodec =
new CertificateCodec(config, componentName);
try {
Path extCertParent = extCertPath.getParent();
Path extCertName = extCertPath.getFileName();
if (extCertParent == null || extCertName == null) {
throw new IOException("External cert path is not correct: " +
extCertPath);
}
X509CertificateHolder certHolder = certificateCodec.readCertificate(
extCertParent, extCertName.toString());
Path extPrivateKeyParent = extPrivateKeyPath.getParent();
Path extPrivateKeyFileName = extPrivateKeyPath.getFileName();
if (extPrivateKeyParent == null || extPrivateKeyFileName == null) {
throw new IOException("External private key path is not correct: " +
extPrivateKeyPath);
}
PrivateKey privateKey = keyCodec.readPrivateKey(extPrivateKeyParent,
extPrivateKeyFileName.toString());
PublicKey publicKey;
publicKey = readPublicKeyWithExternalData(
externalPublicKeyLocation, keyCodec, certHolder);
keyCodec.writeKey(new KeyPair(publicKey, privateKey));
certificateCodec.writeCertificate(certHolder);
} catch (IOException | CertificateException | NoSuchAlgorithmException |
InvalidKeySpecException e) {
LOG.error("External root CA certificate initialization failed", e);
}
}

private PublicKey readPublicKeyWithExternalData(
String externalPublicKeyLocation, KeyCodec keyCodec,
X509CertificateHolder certHolder)
throws CertificateException, NoSuchAlgorithmException,
InvalidKeySpecException, IOException {
PublicKey publicKey;
if (externalPublicKeyLocation.isEmpty()) {
publicKey = CertificateCodec.getX509Certificate(certHolder)
.getPublicKey();
} else {
Path publicKeyPath = Paths.get(externalPublicKeyLocation);
Path publicKeyPathFileName = publicKeyPath.getFileName();
Path publicKeyParent = publicKeyPath.getParent();
if (publicKeyPathFileName == null || publicKeyParent == null) {
throw new IOException("Public key path incorrect: " + publicKeyParent);
}
publicKey = keyCodec.readPublicKey(
publicKeyParent, publicKeyPathFileName.toString());
}
return publicKey;
}

/**
* This represents the verification status of the CA. Based on this enum
* appropriate action is taken in the Init.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,9 @@ private synchronized void writeKey(Path basePath, KeyPair keyPair,
checkPreconditions(basePath);

File privateKeyFile =
Paths.get(location.toString(), privateKeyFileName).toFile();
Paths.get(basePath.toString(), privateKeyFileName).toFile();
File publicKeyFile =
Paths.get(location.toString(), publicKeyFileName).toFile();
Paths.get(basePath.toString(), publicKeyFileName).toFile();
checkKeyFile(privateKeyFile, force, publicKeyFile);

try (PemWriter privateKeyWriter = new PemWriter(new
Expand Down
Loading