blob: 4f41d516153f65fa4a41e318a4c36232fb641297 [file] [log] [blame]
/*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.ozone;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.cert.CertificateExpiredException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.validator.routines.DomainValidator;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.conf.DefaultConfigManager;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.protocol.proto.SCMSecurityProtocolProtos;
import org.apache.hadoop.hdds.protocol.proto.SCMSecurityProtocolProtos.SCMGetCertResponseProto;
import org.apache.hadoop.hdds.protocolPB.SCMSecurityProtocolClientSideTranslatorPB;
import org.apache.hadoop.hdds.scm.ScmConfig;
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
import org.apache.hadoop.hdds.scm.ScmInfo;
import org.apache.hadoop.hdds.scm.HddsTestUtils;
import org.apache.hadoop.hdds.scm.client.ScmTopologyClient;
import org.apache.hadoop.hdds.scm.ha.HASecurityUtils;
import org.apache.hadoop.hdds.scm.ha.SCMHANodeDetails;
import org.apache.hadoop.hdds.scm.ha.SCMHAUtils;
import org.apache.hadoop.hdds.scm.ha.SCMRatisServerImpl;
import org.apache.hadoop.hdds.scm.protocol.ScmBlockLocationProtocol;
import org.apache.hadoop.hdds.scm.protocol.StorageContainerLocationProtocol;
import org.apache.hadoop.hdds.scm.server.SCMHTTPServerConfig;
import org.apache.hadoop.hdds.scm.server.SCMStorageConfig;
import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
import org.apache.hadoop.hdds.security.exception.SCMSecurityException;
import org.apache.hadoop.hdds.security.SecurityConfig;
import org.apache.hadoop.hdds.security.symmetric.SecretKeyManager;
import org.apache.hadoop.hdds.security.x509.certificate.authority.CAType;
import org.apache.hadoop.hdds.security.x509.certificate.authority.DefaultApprover;
import org.apache.hadoop.hdds.security.x509.certificate.authority.profile.DefaultProfile;
import org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClient;
import org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClientTestImpl;
import org.apache.hadoop.hdds.security.x509.certificate.client.DefaultCertificateClient;
import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateCodec;
import org.apache.hadoop.hdds.security.x509.certificate.utils.CertificateSignRequest;
import org.apache.hadoop.hdds.security.x509.certificate.utils.SelfSignedCertificate;
import org.apache.hadoop.hdds.security.x509.exception.CertificateException;
import org.apache.hadoop.hdds.security.x509.keys.HDDSKeyGenerator;
import org.apache.hadoop.hdds.security.x509.keys.KeyCodec;
import org.apache.hadoop.hdds.utils.HAUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.ipc.Client;
import org.apache.hadoop.ipc.Server;
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
import org.apache.hadoop.minikdc.MiniKdc;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneClientFactory;
import org.apache.hadoop.ozone.common.Storage;
import org.apache.hadoop.ozone.om.OMConfigKeys;
import org.apache.hadoop.ozone.om.OMStorage;
import org.apache.hadoop.ozone.om.OzoneManager;
import org.apache.hadoop.ozone.om.ScmBlockLocationTestingClient;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
import org.apache.hadoop.ozone.om.helpers.S3SecretValue;
import org.apache.hadoop.ozone.om.helpers.ServiceInfoEx;
import org.apache.hadoop.ozone.om.protocolPB.GrpcOmTransport;
import org.apache.hadoop.ozone.om.protocolPB.GrpcOmTransportFactory;
import org.apache.hadoop.ozone.om.protocolPB.OmTransportFactory;
import org.apache.hadoop.ozone.om.protocolPB.OzoneManagerProtocolClientSideTranslatorPB;
import org.apache.hadoop.ozone.security.OMCertificateClient;
import org.apache.hadoop.ozone.security.OzoneTokenIdentifier;
import org.apache.hadoop.security.KerberosAuthException;
import org.apache.hadoop.security.SaslRpcServer.AuthMethod;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
import org.apache.hadoop.security.token.Token;
import org.apache.hadoop.util.ExitUtil;
import org.apache.ozone.test.GenericTestUtils;
import org.apache.ozone.test.GenericTestUtils.LogCapturer;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
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_ENABLED;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_GRPC_TLS_ENABLED;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_CA_ROTATION_ACK_TIMEOUT;
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_GRACE_DURATION_TOKEN_CHECKS_ENABLED;
import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_X509_RENEW_GRACE_DURATION;
import static org.apache.hadoop.hdds.HddsConfigKeys.OZONE_METADATA_DIRS;
import static org.apache.hadoop.hdds.scm.ScmConfig.ConfigStrings.HDDS_SCM_KERBEROS_KEYTAB_FILE_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfig.ConfigStrings.HDDS_SCM_KERBEROS_PRINCIPAL_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_BLOCK_CLIENT_PORT_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_ADDRESS_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_CLIENT_PORT_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_DATANODE_PORT_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_GRPC_PORT_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_RATIS_PORT_KEY;
import static org.apache.hadoop.hdds.scm.ScmConfigKeys.OZONE_SCM_SECURITY_SERVICE_PORT_KEY;
import static org.apache.hadoop.hdds.scm.server.SCMHTTPServerConfig.ConfigStrings.HDDS_SCM_HTTP_KERBEROS_KEYTAB_FILE_KEY;
import static org.apache.hadoop.hdds.scm.server.SCMHTTPServerConfig.ConfigStrings.HDDS_SCM_HTTP_KERBEROS_PRINCIPAL_KEY;
import static org.apache.hadoop.hdds.security.x509.exception.CertificateException.ErrorCode.ROLLBACK_ERROR;
import static org.apache.hadoop.hdds.utils.HddsServerUtil.getScmSecurityClient;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_ADMINISTRATORS;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_FAILOVER_MAX_ATTEMPTS_KEY;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SECURITY_ENABLED_KEY;
import static org.apache.hadoop.ozone.OzoneConsts.SCM_SUB_CA;
import static org.apache.hadoop.ozone.om.OMConfigKeys.DELEGATION_TOKEN_MAX_LIFETIME_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_HTTP_KERBEROS_KEYTAB_FILE;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_HTTP_KERBEROS_PRINCIPAL_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_KERBEROS_KEYTAB_FILE_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_KERBEROS_PRINCIPAL_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_S3_GPRC_SERVER_ENABLED;
import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_TRANSPORT_CLASS;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.TOKEN_EXPIRED;
import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.USER_MISMATCH;
import static org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod.KERBEROS;
import org.apache.ozone.test.LambdaTestUtils;
import org.apache.ozone.test.tag.Flaky;
import org.apache.ozone.test.tag.Unhealthy;
import org.apache.ratis.protocol.ClientId;
import org.apache.ratis.util.ExitUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.ozone.test.GenericTestUtils.PortAllocator.getFreePort;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.slf4j.event.Level.INFO;
/**
* Test class to for security enabled Ozone cluster.
*/
@Timeout(80)
final class TestSecureOzoneCluster {
private static final String COMPONENT = "om";
private static final String OM_CERT_SERIAL_ID = "9879877970576";
private static final Logger LOG = LoggerFactory
.getLogger(TestSecureOzoneCluster.class);
@TempDir
private File tempDir;
private MiniKdc miniKdc;
private OzoneConfiguration conf;
private File workDir;
private File scmKeytab;
private File spnegoKeytab;
private File omKeyTab;
private File testUserKeytab;
private String testUserPrincipal;
private StorageContainerManager scm;
private ScmBlockLocationProtocol scmBlockClient;
private OzoneManager om;
private HddsProtos.OzoneManagerDetailsProto omInfo;
private String host;
private String clusterId;
private String scmId;
private String omId;
private OzoneManagerProtocolClientSideTranslatorPB omClient;
private KeyPair keyPair;
private Path omMetaDirPath;
private int certGraceTime = 10 * 1000; // 10s
private int delegationTokenMaxTime = 9 * 1000; // 9s
@BeforeEach
void init() {
try {
conf = new OzoneConfiguration();
conf.set(OZONE_SCM_CLIENT_ADDRESS_KEY, "localhost");
conf.setInt(OZONE_SCM_CLIENT_PORT_KEY, getFreePort());
conf.setInt(OZONE_SCM_DATANODE_PORT_KEY, getFreePort());
conf.setInt(OZONE_SCM_BLOCK_CLIENT_PORT_KEY, getFreePort());
conf.setInt(OZONE_SCM_SECURITY_SERVICE_PORT_KEY, getFreePort());
conf.setInt(OZONE_SCM_RATIS_PORT_KEY, getFreePort());
conf.setInt(OZONE_SCM_GRPC_PORT_KEY, getFreePort());
conf.set(OZONE_OM_ADDRESS_KEY,
InetAddress.getLocalHost().getCanonicalHostName() + ":" + getFreePort());
conf.setBoolean(ScmConfigKeys.OZONE_SCM_HA_ENABLE_KEY, false);
DefaultMetricsSystem.setMiniClusterMode(true);
ExitUtils.disableSystemExit();
final String path = tempDir.getAbsolutePath();
omMetaDirPath = Paths.get(path, "om-meta");
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.setBoolean(HDDS_X509_GRACE_DURATION_TOKEN_CHECKS_ENABLED, false);
conf.set(HDDS_X509_CA_ROTATION_CHECK_INTERNAL,
Duration.ofMillis(certGraceTime - 1000).toString());
conf.set(HDDS_X509_CA_ROTATION_ACK_TIMEOUT,
Duration.ofMillis(certGraceTime - 1000).toString());
conf.setLong(OMConfigKeys.DELEGATION_TOKEN_MAX_LIFETIME_KEY,
delegationTokenMaxTime);
workDir = new File(tempDir, "workdir");
clusterId = UUID.randomUUID().toString();
scmId = UUID.randomUUID().toString();
omId = UUID.randomUUID().toString();
scmBlockClient = new ScmBlockLocationTestingClient(null, null, 0);
startMiniKdc();
setSecureConfig();
createCredentialsInKDC();
generateKeyPair();
omInfo = OzoneManager.getOmDetailsProto(conf, omId);
} catch (Exception e) {
LOG.error("Failed to initialize TestSecureOzoneCluster", e);
}
}
@AfterEach
void stop() throws Exception {
try {
stopMiniKdc();
if (scm != null) {
scm.stop();
scm.join();
}
if (om != null) {
om.stop();
om.join();
}
IOUtils.closeQuietly(omClient);
} catch (Exception e) {
LOG.error("Failed to stop TestSecureOzoneCluster", e);
}
DefaultConfigManager.clearDefaultConfigs();
}
private void createCredentialsInKDC() throws Exception {
ScmConfig scmConfig = conf.getObject(ScmConfig.class);
SCMHTTPServerConfig httpServerConfig =
conf.getObject(SCMHTTPServerConfig.class);
createPrincipal(scmKeytab, scmConfig.getKerberosPrincipal());
createPrincipal(spnegoKeytab, httpServerConfig.getKerberosPrincipal());
createPrincipal(testUserKeytab, testUserPrincipal);
createPrincipal(omKeyTab,
conf.get(OZONE_OM_KERBEROS_PRINCIPAL_KEY));
}
private void createPrincipal(File keytab, String... principal)
throws Exception {
miniKdc.createPrincipal(keytab, principal);
}
private void startMiniKdc() throws Exception {
Properties securityProperties = MiniKdc.createConf();
miniKdc = new MiniKdc(securityProperties, workDir);
miniKdc.start();
}
private void stopMiniKdc() {
miniKdc.stop();
}
private void setSecureConfig() throws IOException {
conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
host = InetAddress.getLocalHost().getCanonicalHostName()
.toLowerCase();
conf.set(HADOOP_SECURITY_AUTHENTICATION, "kerberos");
String curUser = UserGroupInformation.getCurrentUser().getUserName();
conf.set(OZONE_ADMINISTRATORS, curUser);
String realm = miniKdc.getRealm();
String hostAndRealm = host + "@" + realm;
conf.set(HDDS_SCM_KERBEROS_PRINCIPAL_KEY, "scm/" + hostAndRealm);
conf.set(HDDS_SCM_HTTP_KERBEROS_PRINCIPAL_KEY, "HTTP_SCM/" + hostAndRealm);
conf.set(OZONE_OM_KERBEROS_PRINCIPAL_KEY, "om/" + hostAndRealm);
conf.set(OZONE_OM_HTTP_KERBEROS_PRINCIPAL_KEY, "HTTP_OM/" + hostAndRealm);
scmKeytab = new File(workDir, "scm.keytab");
spnegoKeytab = new File(workDir, "http.keytab");
omKeyTab = new File(workDir, "om.keytab");
testUserKeytab = new File(workDir, "testuser.keytab");
testUserPrincipal = "test@" + realm;
conf.set(HDDS_SCM_KERBEROS_KEYTAB_FILE_KEY,
scmKeytab.getAbsolutePath());
conf.set(HDDS_SCM_HTTP_KERBEROS_KEYTAB_FILE_KEY,
spnegoKeytab.getAbsolutePath());
conf.set(OZONE_OM_KERBEROS_KEYTAB_FILE_KEY,
omKeyTab.getAbsolutePath());
conf.set(OZONE_OM_HTTP_KERBEROS_KEYTAB_FILE,
spnegoKeytab.getAbsolutePath());
}
@Test
void testSecureScmStartupSuccess() throws Exception {
initSCM();
scm = HddsTestUtils.getScmSimple(conf);
//Reads the SCM Info from SCM instance
ScmInfo scmInfo = scm.getClientProtocolServer().getScmInfo();
assertEquals(clusterId, scmInfo.getClusterId());
assertEquals(scmId, scmInfo.getScmId());
assertEquals(2, scm.getScmCertificateClient().getTrustChain().size());
}
@Test
void testSCMSecurityProtocol() throws Exception {
initSCM();
scm = HddsTestUtils.getScmSimple(conf);
//Reads the SCM Info from SCM instance
try {
scm.start();
// Case 1: User with Kerberos credentials should succeed.
UserGroupInformation ugi =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
testUserPrincipal, testUserKeytab.getCanonicalPath());
ugi.setAuthenticationMethod(KERBEROS);
try (SCMSecurityProtocolClientSideTranslatorPB securityClient =
getScmSecurityClient(conf, ugi)) {
assertNotNull(securityClient);
String caCert = securityClient.getCACertificate();
assertNotNull(caCert);
// Get some random certificate, used serial id 100 which will be
// unavailable as our serial id is time stamp. Serial id 1 is root CA,
// and it is persisted in DB.
SCMSecurityException securityException = assertThrows(
SCMSecurityException.class,
() -> securityClient.getCertificate("100"));
assertThat(securityException)
.hasMessageContaining("Certificate not found");
}
// Case 2: User without Kerberos credentials should fail.
ugi = UserGroupInformation.createRemoteUser("test");
ugi.setAuthenticationMethod(AuthMethod.TOKEN);
try (SCMSecurityProtocolClientSideTranslatorPB securityClient =
getScmSecurityClient(conf, ugi)) {
String cannotAuthMessage = "Client cannot authenticate via:[KERBEROS]";
IOException ioException = assertThrows(IOException.class,
securityClient::getCACertificate);
assertThat(ioException).hasMessageContaining(cannotAuthMessage);
ioException = assertThrows(IOException.class,
() -> securityClient.getCertificate("1"));
assertThat(ioException).hasMessageContaining(cannotAuthMessage);
}
} finally {
if (scm != null) {
scm.stop();
}
}
}
@Test
void testAdminAccessControlException() throws Exception {
initSCM();
scm = HddsTestUtils.getScmSimple(conf);
//Reads the SCM Info from SCM instance
try {
scm.start();
//case 1: Run admin command with non-admin user.
UserGroupInformation ugi =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
testUserPrincipal, testUserKeytab.getCanonicalPath());
StorageContainerLocationProtocol scmRpcClient =
HAUtils.getScmContainerClient(conf, ugi);
IOException ioException = assertThrows(IOException.class,
scmRpcClient::forceExitSafeMode);
assertThat(ioException).hasMessageContaining("Access denied");
// Case 2: User without Kerberos credentials should fail.
ugi = UserGroupInformation.createRemoteUser("test");
ugi.setAuthenticationMethod(AuthMethod.TOKEN);
scmRpcClient =
HAUtils.getScmContainerClient(conf, ugi);
String cannotAuthMessage = "Client cannot authenticate via:[KERBEROS]";
ioException = assertThrows(IOException.class,
scmRpcClient::forceExitSafeMode);
assertThat(ioException).hasMessageContaining(cannotAuthMessage);
} finally {
if (scm != null) {
scm.stop();
}
}
}
@Test
void testSecretManagerInitializedNonHASCM() throws Exception {
conf.setBoolean(HDDS_BLOCK_TOKEN_ENABLED, true);
initSCM();
scm = HddsTestUtils.getScmSimple(conf);
//Reads the SCM Info from SCM instance
try {
scm.start();
SecretKeyManager secretKeyManager = scm.getSecretKeyManager();
boolean inSafeMode = scm.getScmSafeModeManager().getInSafeMode();
assertFalse(SCMHAUtils.isSCMHAEnabled(conf));
assertTrue(inSafeMode);
assertNotNull(secretKeyManager);
assertTrue(secretKeyManager.isInitialized());
} finally {
if (scm != null) {
scm.stop();
}
}
}
private void initSCM() throws IOException {
Path scmPath = new File(tempDir, "scm-meta").toPath();
Files.createDirectories(scmPath);
conf.set(OZONE_METADATA_DIRS, scmPath.toString());
SCMStorageConfig scmStore = new SCMStorageConfig(conf);
scmStore.setClusterId(clusterId);
scmStore.setScmId(scmId);
HASecurityUtils.initializeSecurity(scmStore, conf,
InetAddress.getLocalHost().getHostName(), true);
scmStore.setPrimaryScmNodeId(scmId);
// writes the version file properties
scmStore.initialize();
if (SCMHAUtils.isSCMHAEnabled(conf)) {
SCMRatisServerImpl.initialize(clusterId, scmId,
SCMHANodeDetails.loadSCMHAConfig(conf, scmStore)
.getLocalNodeDetails(), conf);
}
/*
* As all these processes run inside the same JVM, there are issues around
* the Hadoop UGI if different processes run with different principals.
* In this test, the OM has to contact the SCM to download certs. SCM runs
* as scm/host@REALM, but the OM logs in as om/host@REALM, and then the test
* fails, and the OM is unable to contact the SCM due to kerberos login
* issues. To work around that, have the OM run as the same principal as the
* SCM, and then the test passes.
*
*/
conf.set(HDDS_SCM_KERBEROS_PRINCIPAL_KEY,
"om/" + host + "@" + miniKdc.getRealm());
File keyTab = new File(workDir, "om.keytab");
conf.set(HDDS_SCM_KERBEROS_KEYTAB_FILE_KEY, keyTab.getAbsolutePath());
}
@Test
void testSecureScmStartupFailure() throws Exception {
initSCM();
conf.set(HDDS_SCM_KERBEROS_KEYTAB_FILE_KEY, "");
conf.set(HADOOP_SECURITY_AUTHENTICATION, "kerberos");
IOException ioException = assertThrows(IOException.class,
() -> HddsTestUtils.getScmSimple(conf));
assertThat(ioException)
.hasMessageContaining("Running in secure mode, but config doesn't have a keytab");
conf.set(HDDS_SCM_KERBEROS_PRINCIPAL_KEY,
"scm/_HOST@EXAMPLE.com");
conf.set(HDDS_SCM_KERBEROS_KEYTAB_FILE_KEY,
"/etc/security/keytabs/scm.keytab");
testCommonKerberosFailures(
() -> HddsTestUtils.getScmSimple(conf));
}
private void testCommonKerberosFailures(Callable<?> test) {
KerberosAuthException kerberosAuthException = assertThrows(
KerberosAuthException.class, test::call);
assertThat(kerberosAuthException)
.hasMessageContaining("failure to login: for principal:");
String invalidValue = "OAuth2";
conf.set(HADOOP_SECURITY_AUTHENTICATION, invalidValue);
IllegalArgumentException argumentException =
assertThrows(IllegalArgumentException.class, test::call);
assertThat(argumentException)
.hasMessageContaining("Invalid attribute value for " + HADOOP_SECURITY_AUTHENTICATION + " of " + invalidValue);
conf.set(HADOOP_SECURITY_AUTHENTICATION, "KERBEROS_SSL");
AuthenticationException authException = assertThrows(
AuthenticationException.class,
test::call);
assertThat(authException)
.hasMessageContaining("KERBEROS_SSL authentication method not");
}
/**
* Tests the secure om Initialization Failure.
*/
@Test
void testSecureOMInitializationFailure() throws Exception {
initSCM();
// Create a secure SCM instance as om client will connect to it
scm = HddsTestUtils.getScmSimple(conf);
try {
scm.start();
setupOm(conf);
conf.set(OZONE_OM_KERBEROS_PRINCIPAL_KEY,
"non-existent-user@EXAMPLE.com");
testCommonKerberosFailures(() -> OzoneManager.createOm(conf));
} finally {
if (scm != null) {
scm.stop();
}
}
}
/**
* Tests the secure om Initialization success.
*/
@Test
void testSecureOmInitializationSuccess() throws Exception {
initSCM();
// Create a secure SCM instance as om client will connect to it
scm = HddsTestUtils.getScmSimple(conf);
LogCapturer logs = LogCapturer.captureLogs(OzoneManager.getLogger());
GenericTestUtils.setLogLevel(OzoneManager.getLogger(), INFO);
try {
scm.start();
setupOm(conf);
om.start();
} catch (Exception ex) {
// Expects timeout failure from scmClient in om but om user login via
// kerberos should succeed.
assertThat(logs.getOutput()).contains("Ozone Manager login successful");
} finally {
if (scm != null) {
scm.stop();
}
}
}
@Test
void testAccessControlExceptionOnClient() throws Exception {
initSCM();
LogCapturer logs = LogCapturer.captureLogs(OzoneManager.getLogger());
GenericTestUtils.setLogLevel(OzoneManager.getLogger(), INFO);
try {
// Create a secure SCM instance as om client will connect to it
scm = HddsTestUtils.getScmSimple(conf);
scm.start();
setupOm(conf);
om.setCertClient(new CertificateClientTestImpl(conf));
om.setScmTopologyClient(new ScmTopologyClient(scmBlockClient));
om.start();
} catch (Exception ex) {
// Expects timeout failure from scmClient in om but om user login via
// kerberos should succeed.
assertThat(logs.getOutput()).contains("Ozone Manager login successful");
}
UserGroupInformation ugi =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
testUserPrincipal, testUserKeytab.getCanonicalPath());
ugi.setAuthenticationMethod(KERBEROS);
OzoneManagerProtocolClientSideTranslatorPB secureClient =
new OzoneManagerProtocolClientSideTranslatorPB(
OmTransportFactory.create(conf, ugi, null),
ClientId.randomId().toString());
secureClient.createVolume(
new OmVolumeArgs.Builder().setVolume("vol1")
.setOwnerName("owner1")
.setAdminName("admin")
.build());
ugi = UserGroupInformation.createUserForTesting(
"testuser1", new String[] {"test"});
OzoneManagerProtocolClientSideTranslatorPB unsecureClient =
new OzoneManagerProtocolClientSideTranslatorPB(
OmTransportFactory.create(conf, ugi, null),
ClientId.randomId().toString());
String exMessage = "org.apache.hadoop.security.AccessControlException: " +
"Client cannot authenticate via:[TOKEN, KERBEROS]";
logs = LogCapturer.captureLogs(Client.LOG);
IOException ioException = assertThrows(IOException.class,
() -> unsecureClient.listAllVolumes(null, null, 0));
assertThat(ioException).hasMessageContaining(exMessage);
assertEquals(1, StringUtils.countMatches(logs.getOutput(), exMessage),
"There should be no retry on AccessControlException");
}
private void generateKeyPair() throws Exception {
SecurityConfig securityConfig = new SecurityConfig(conf);
HDDSKeyGenerator keyGenerator = new HDDSKeyGenerator(securityConfig);
keyPair = keyGenerator.generateKey();
KeyCodec pemWriter = new KeyCodec(securityConfig, COMPONENT);
pemWriter.writeKey(keyPair, true);
}
/**
* Tests delegation token renewal.
*/
@Test
void testDelegationTokenRenewal() throws Exception {
GenericTestUtils
.setLogLevel(LoggerFactory.getLogger(Server.class.getName()), INFO);
LogCapturer omLogs = LogCapturer.captureLogs(OzoneManager.getLogger());
// Setup SCM
initSCM();
scm = HddsTestUtils.getScmSimple(conf);
try {
// Start SCM
scm.start();
// Setup secure OM for start.
int tokenMaxLifetime = 1000;
conf.setLong(DELEGATION_TOKEN_MAX_LIFETIME_KEY, tokenMaxLifetime);
setupOm(conf);
OzoneManager.setTestSecureOmFlag(true);
om.setCertClient(new CertificateClientTestImpl(conf));
om.setScmTopologyClient(new ScmTopologyClient(scmBlockClient));
om.start();
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
// Get first OM client which will authenticate via Kerberos
omClient = new OzoneManagerProtocolClientSideTranslatorPB(
OmTransportFactory.create(conf, ugi, null),
RandomStringUtils.randomAscii(5));
// Since client is already connected get a delegation token
Token<OzoneTokenIdentifier> token = omClient.getDelegationToken(
new Text("om"));
// Check if token is of right kind and renewer is running om instance
assertNotNull(token);
assertEquals("OzoneToken", token.getKind().toString());
assertEquals(SecurityUtil.buildTokenService(
om.getNodeDetails().getRpcAddress()).toString(),
token.getService().toString());
// Renew delegation token
long expiryTime = omClient.renewDelegationToken(token);
assertThat(expiryTime).isGreaterThan(0);
omLogs.clearOutput();
// Test failure of delegation renewal
// 1. When token maxExpiryTime exceeds
Thread.sleep(tokenMaxLifetime);
OMException ex = assertThrows(OMException.class,
() -> omClient.renewDelegationToken(token));
assertEquals(TOKEN_EXPIRED, ex.getResult());
omLogs.clearOutput();
// 2. When renewer doesn't match (implicitly covers when renewer is
// null or empty )
Token<OzoneTokenIdentifier> token2 = omClient.getDelegationToken(
new Text("randomService"));
assertNotNull(token2);
ex = assertThrows(OMException.class,
() -> omClient.renewDelegationToken(token2));
assertThat(ex).hasMessageContaining("Delegation token renewal failed");
assertThat(omLogs.getOutput()).contains(" with non-matching renewer randomService");
omLogs.clearOutput();
// 3. Test tampered token
OzoneTokenIdentifier tokenId = OzoneTokenIdentifier.readProtoBuf(
token.getIdentifier());
tokenId.setRenewer(new Text("om"));
tokenId.setMaxDate(System.currentTimeMillis() * 2);
Token<OzoneTokenIdentifier> tamperedToken = new Token<>(
tokenId.getBytes(), token2.getPassword(), token2.getKind(),
token2.getService());
ex = assertThrows(OMException.class,
() -> omClient.renewDelegationToken(tamperedToken));
assertThat(ex).hasMessageContaining("Delegation token renewal failed");
assertThat(omLogs.getOutput()).contains("can't be found in cache");
omLogs.clearOutput();
} finally {
if (scm != null) {
scm.stop();
}
IOUtils.closeQuietly(om);
}
}
private void setupOm(OzoneConfiguration config) throws Exception {
OMStorage omStore = new OMStorage(config);
omStore.setClusterId(clusterId);
omStore.setOmCertSerialId(OM_CERT_SERIAL_ID);
// writes the version file properties
omStore.initialize();
OzoneManager.setTestSecureOmFlag(true);
om = OzoneManager.createOm(config);
}
@Test
@Flaky("HDDS-9349")
void testGetSetRevokeS3Secret() throws Exception {
initSCM();
try {
scm = HddsTestUtils.getScmSimple(conf);
scm.start();
// Setup secure OM for start
setupOm(conf);
// Start OM
om.setCertClient(new CertificateClientTestImpl(conf));
om.setScmTopologyClient(new ScmTopologyClient(scmBlockClient));
om.start();
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
String username = ugi.getUserName();
// Get first OM client which will authenticate via Kerberos
omClient = new OzoneManagerProtocolClientSideTranslatorPB(
OmTransportFactory.create(conf, ugi, null),
RandomStringUtils.randomAscii(5));
// Creates a secret since it does not exist
S3SecretValue attempt1 = omClient.getS3Secret(username);
// A second getS3Secret on the same username should throw exception
try {
omClient.getS3Secret(username);
} catch (OMException omEx) {
assertEquals(OMException.ResultCodes.S3_SECRET_ALREADY_EXISTS,
omEx.getResult());
}
// Revoke the existing secret
omClient.revokeS3Secret(username);
// Set secret should fail since the accessId is revoked
final String secretKeySet = "somesecret1";
try {
omClient.setS3Secret(username, secretKeySet);
} catch (OMException omEx) {
assertEquals(OMException.ResultCodes.ACCESS_ID_NOT_FOUND,
omEx.getResult());
}
// Get a new secret
S3SecretValue attempt3 = omClient.getS3Secret(username);
// secret should differ because it has been revoked previously
assertNotEquals(attempt3.getAwsSecret(), attempt1.getAwsSecret());
// accessKey is still the same because it is derived from username
assertEquals(attempt3.getAwsAccessKey(), attempt1.getAwsAccessKey());
// Admin can set secret for any user
S3SecretValue attempt4 = omClient.setS3Secret(username, secretKeySet);
assertEquals(secretKeySet, attempt4.getAwsSecret());
// A second getS3Secret on the same username should throw exception
try {
omClient.getS3Secret(username);
} catch (OMException omEx) {
assertEquals(OMException.ResultCodes.S3_SECRET_ALREADY_EXISTS,
omEx.getResult());
}
// Clean up
omClient.revokeS3Secret(username);
// Admin can get and revoke other users' secrets
// omClient's ugi is current user, which is added as an OM admin
omClient.getS3Secret("HADOOP/ALICE");
omClient.revokeS3Secret("HADOOP/ALICE");
// testUser is not an admin
final UserGroupInformation ugiNonAdmin =
UserGroupInformation.loginUserFromKeytabAndReturnUGI(
testUserPrincipal, testUserKeytab.getCanonicalPath());
final OzoneManagerProtocolClientSideTranslatorPB omClientNonAdmin =
new OzoneManagerProtocolClientSideTranslatorPB(
OmTransportFactory.create(conf, ugiNonAdmin, null),
RandomStringUtils.randomAscii(5));
OMException omException = assertThrows(OMException.class,
() -> omClientNonAdmin.getS3Secret("HADOOP/JOHN"));
assertSame(USER_MISMATCH, omException.getResult());
omException = assertThrows(OMException.class,
() -> omClientNonAdmin.revokeS3Secret("HADOOP/DOE"));
assertSame(USER_MISMATCH, omException.getResult());
} finally {
if (scm != null) {
scm.stop();
}
IOUtils.closeQuietly(om);
}
}
/**
* Tests functionality to init secure OM when it is already initialized.
*/
@Test
void testSecureOmReInit() throws Exception {
LogCapturer omLogs =
LogCapturer.captureLogs(OMCertificateClient.LOG);
omLogs.clearOutput();
initSCM();
try {
scm = HddsTestUtils.getScmSimple(conf);
scm.start();
conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, false);
OMStorage omStore = new OMStorage(conf);
initializeOmStorage(omStore);
OzoneManager.setTestSecureOmFlag(true);
om = OzoneManager.createOm(conf);
assertNull(om.getCertificateClient());
String logOutput = omLogs.getOutput();
assertThat(logOutput)
.doesNotContain("Init response: GETCERT");
assertThat(logOutput)
.doesNotContain("Successfully stored SCM signed certificate");
if (om.stop()) {
om.join();
}
conf.setBoolean(OZONE_SECURITY_ENABLED_KEY, true);
conf.setBoolean(OZONE_OM_S3_GPRC_SERVER_ENABLED, true);
conf.set(OZONE_OM_ADDRESS_KEY,
InetAddress.getLocalHost().getCanonicalHostName() + ":" + getFreePort());
OzoneManager.omInit(conf);
om = OzoneManager.createOm(conf);
assertNotNull(om.getCertificateClient());
assertNotNull(om.getCertificateClient().getPublicKey());
assertNotNull(om.getCertificateClient().getPrivateKey());
assertNotNull(om.getCertificateClient().getCertificate());
assertThat(omLogs.getOutput())
.contains("Init response: GETCERT")
.contains("Successfully stored OM signed certificate");
X509Certificate certificate = om.getCertificateClient().getCertificate();
validateCertificate(certificate);
} finally {
if (scm != null) {
scm.stop();
}
}
}
/**
* Test functionality to get SCM signed certificate for OM.
*/
@Test
void testSecureOmInitSuccess() throws Exception {
LogCapturer omLogs =
LogCapturer.captureLogs(OMCertificateClient.LOG);
omLogs.clearOutput();
initSCM();
try {
scm = HddsTestUtils.getScmSimple(conf);
scm.start();
OMStorage omStore = new OMStorage(conf);
initializeOmStorage(omStore);
OzoneManager.setTestSecureOmFlag(true);
om = OzoneManager.createOm(conf);
assertNotNull(om.getCertificateClient());
assertNotNull(om.getCertificateClient().getPublicKey());
assertNotNull(om.getCertificateClient().getPrivateKey());
assertNotNull(om.getCertificateClient().getCertificate());
assertEquals(3, om.getCertificateClient().getTrustChain().size());
assertThat(omLogs.getOutput())
.contains("Init response: GETCERT")
.contains("Successfully stored OM signed certificate");
X509Certificate certificate = om.getCertificateClient().getCertificate();
validateCertificate(certificate);
String pemEncodedCACert =
scm.getSecurityProtocolServer().getCACertificate();
X509Certificate caCert =
CertificateCodec.getX509Certificate(pemEncodedCACert);
X509Certificate caCertStored = om.getCertificateClient()
.getCertificate(caCert.getSerialNumber().toString());
assertEquals(caCert, caCertStored);
} finally {
if (scm != null) {
scm.stop();
}
if (om != null) {
om.stop();
}
IOUtils.closeQuietly(om);
}
}
/**
* Test successful certificate rotation.
*/
@Test
void testCertificateRotation() throws Exception {
OMStorage omStorage = new OMStorage(conf);
omStorage.setClusterId(clusterId);
omStorage.setOmId(omId);
OzoneManager.setTestSecureOmFlag(true);
SecurityConfig securityConfig = new SecurityConfig(conf);
// save first cert
final int certificateLifetime = 20; // seconds
KeyCodec keyCodec =
new KeyCodec(securityConfig, securityConfig.getKeyLocation("om"));
X509Certificate cert = generateSelfSignedX509Cert(securityConfig,
new KeyPair(keyCodec.readPublicKey(), keyCodec.readPrivateKey()),
null, Duration.ofSeconds(certificateLifetime));
String certId = cert.getSerialNumber().toString();
omStorage.setOmCertSerialId(certId);
omStorage.forceInitialize();
CertificateCodec certCodec = new CertificateCodec(securityConfig, "om");
certCodec.writeCertificate(cert);
String caCertFileName = CAType.ROOT.getFileNamePrefix() + cert.getSerialNumber().toString() + ".crt";
certCodec.writeCertificate(cert, caCertFileName);
// first renewed cert
X509Certificate newCert =
generateSelfSignedX509Cert(securityConfig, null,
LocalDateTime.now().plus(securityConfig.getRenewalGracePeriod()),
Duration.ofSeconds(certificateLifetime));
String pemCert = CertificateCodec.getPEMEncodedString(newCert);
SCMGetCertResponseProto responseProto =
SCMGetCertResponseProto.newBuilder()
.setResponseCode(SCMSecurityProtocolProtos
.SCMGetCertResponseProto.ResponseCode.success)
.setX509Certificate(pemCert)
.setX509CACertificate(pemCert)
.build();
SCMSecurityProtocolClientSideTranslatorPB scmClient =
mock(SCMSecurityProtocolClientSideTranslatorPB.class);
when(scmClient.getOMCertChain(any(), anyString()))
.thenReturn(responseProto);
try (OMCertificateClient client = new OMCertificateClient(
securityConfig, scmClient, omStorage, omInfo, "", scmId, null, null)) {
client.init();
// create Ozone Manager instance, it will start the monitor task
conf.set(OZONE_SCM_CLIENT_ADDRESS_KEY, "localhost");
om = OzoneManager.createOm(conf);
om.setScmTopologyClient(new ScmTopologyClient(scmBlockClient));
om.setCertClient(client);
// check after renew, client will have the new cert ID
String id1 = newCert.getSerialNumber().toString();
GenericTestUtils.waitFor(() ->
id1.equals(client.getCertificate().getSerialNumber().toString()),
1000, certificateLifetime * 1000);
// test the second time certificate rotation
// second renewed cert
newCert = generateSelfSignedX509Cert(securityConfig, null, null, Duration.ofSeconds(certificateLifetime));
pemCert = CertificateCodec.getPEMEncodedString(newCert);
responseProto = SCMGetCertResponseProto.newBuilder()
.setResponseCode(SCMSecurityProtocolProtos
.SCMGetCertResponseProto.ResponseCode.success)
.setX509Certificate(pemCert)
.setX509CACertificate(pemCert)
.build();
when(scmClient.getOMCertChain(any(), anyString()))
.thenReturn(responseProto);
String id2 = newCert.getSerialNumber().toString();
// check after renew, client will have the new cert ID
GenericTestUtils.waitFor(() ->
id2.equals(client.getCertificate().getSerialNumber().toString()),
1000, certificateLifetime * 1000);
}
}
/**
* Test unexpected SCMGetCertResponseProto returned from SCM.
*/
@Test
void testCertificateRotationRecoverableFailure() throws Exception {
LogCapturer omLogs = LogCapturer.captureLogs(OMCertificateClient.LOG);
OMStorage omStorage = new OMStorage(conf);
omStorage.setClusterId(clusterId);
omStorage.setOmId(omId);
OzoneManager.setTestSecureOmFlag(true);
SecurityConfig securityConfig = new SecurityConfig(conf);
CertificateCodec certCodec = new CertificateCodec(securityConfig, "om");
// save first cert
final int certificateLifetime = 20; // seconds
KeyCodec keyCodec =
new KeyCodec(securityConfig, securityConfig.getKeyLocation("om"));
X509Certificate certHolder = generateSelfSignedX509Cert(securityConfig,
new KeyPair(keyCodec.readPublicKey(), keyCodec.readPrivateKey()),
null, Duration.ofSeconds(certificateLifetime));
String certId = certHolder.getSerialNumber().toString();
certCodec.writeCertificate(certHolder);
omStorage.setOmCertSerialId(certId);
omStorage.forceInitialize();
// prepare a mocked scmClient to certificate signing
SCMSecurityProtocolClientSideTranslatorPB scmClient =
mock(SCMSecurityProtocolClientSideTranslatorPB.class);
Duration gracePeriod = securityConfig.getRenewalGracePeriod();
X509Certificate newCertHolder = generateSelfSignedX509Cert(
securityConfig, null,
LocalDateTime.now().plus(gracePeriod),
Duration.ofSeconds(certificateLifetime));
String pemCert = CertificateCodec.getPEMEncodedString(newCertHolder);
// provide an invalid SCMGetCertResponseProto. Without
// setX509CACertificate(pemCert), signAndStoreCert will throw exception.
SCMSecurityProtocolProtos.SCMGetCertResponseProto responseProto =
SCMSecurityProtocolProtos.SCMGetCertResponseProto
.newBuilder().setResponseCode(SCMSecurityProtocolProtos
.SCMGetCertResponseProto.ResponseCode.success)
.setX509Certificate(pemCert)
.build();
when(scmClient.getOMCertChain(any(), anyString()))
.thenReturn(responseProto);
try (OMCertificateClient client =
new OMCertificateClient(
securityConfig, scmClient, omStorage, omInfo, "", scmId, null, null)
) {
client.init();
// check that new cert ID should not equal to current cert ID
String certId1 = newCertHolder.getSerialNumber().toString();
assertNotEquals(certId1,
client.getCertificate().getSerialNumber().toString());
// certificate failed to renew, client still hold the old expired cert.
Thread.sleep(certificateLifetime * 1000);
assertEquals(certId,
client.getCertificate().getSerialNumber().toString());
assertThrows(CertificateExpiredException.class,
() -> client.getCertificate().checkValidity());
assertThat(omLogs.getOutput())
.contains("Error while signing and storing SCM signed certificate.");
// provide a new valid SCMGetCertResponseProto
newCertHolder = generateSelfSignedX509Cert(securityConfig, null, null,
Duration.ofSeconds(certificateLifetime));
pemCert = CertificateCodec.getPEMEncodedString(newCertHolder);
responseProto = SCMSecurityProtocolProtos.SCMGetCertResponseProto
.newBuilder().setResponseCode(SCMSecurityProtocolProtos
.SCMGetCertResponseProto.ResponseCode.success)
.setX509Certificate(pemCert)
.setX509CACertificate(pemCert)
.build();
when(scmClient.getOMCertChain(any(), anyString()))
.thenReturn(responseProto);
String certId2 = newCertHolder.getSerialNumber().toString();
// check after renew, client will have the new cert ID
GenericTestUtils.waitFor(() -> {
String newCertId = client.getCertificate().getSerialNumber().toString();
return newCertId.equals(certId2);
}, 1000, certificateLifetime * 1000);
}
}
/**
* Test the directory rollback failure case.
*/
@Test
void testCertificateRotationUnRecoverableFailure() throws Exception {
ExitUtils.disableSystemExit();
ExitUtil.disableSystemExit();
LogCapturer certClientLogs = LogCapturer.captureLogs(OMCertificateClient.LOG);
LogCapturer exitUtilLog = LogCapturer.captureLogs(LoggerFactory.getLogger(ExitUtil.class));
OMStorage omStorage = new OMStorage(conf);
omStorage.setClusterId(clusterId);
omStorage.setOmId(omId);
OzoneManager.setTestSecureOmFlag(true);
SecurityConfig securityConfig = new SecurityConfig(conf);
CertificateCodec certCodec = new CertificateCodec(securityConfig, "om");
try (OMCertificateClient client =
new OMCertificateClient(securityConfig, null, omStorage, omInfo, "", scmId, null, null)
) {
client.init();
// save first cert
final int certificateLifetime = 20; // seconds
KeyPair newKeyPair = new KeyPair(client.getPublicKey(), client.getPrivateKey());
X509Certificate newCert =
generateSelfSignedX509Cert(securityConfig, newKeyPair, null, Duration.ofSeconds(certificateLifetime));
String certId = newCert.getSerialNumber().toString();
certCodec.writeCertificate(newCert);
omStorage.setOmCertSerialId(certId);
omStorage.forceInitialize();
// create Ozone Manager instance, it will start the monitor task
conf.set(OZONE_SCM_CLIENT_ADDRESS_KEY, "localhost");
om = OzoneManager.createOm(conf);
om.getCertificateClient().close();
om.setScmTopologyClient(new ScmTopologyClient(scmBlockClient));
OMCertificateClient mockClient = new OMCertificateClient(securityConfig, null, omStorage, omInfo,
om.getOMServiceId(), scmId, null, om::terminateOM) {
@Override
public Duration timeBeforeExpiryGracePeriod(X509Certificate certificate) {
return Duration.ZERO;
}
@Override
public String renewAndStoreKeyAndCertificate(boolean force) throws CertificateException {
throw new CertificateException("renewAndStoreKeyAndCert failed", ROLLBACK_ERROR);
}
};
om.setCertClient(mockClient);
// check error message during renew
GenericTestUtils.waitFor(() ->
certClientLogs.getOutput().contains("Failed to rollback key and cert after an unsuccessful renew try."),
1000, certificateLifetime * 1000);
GenericTestUtils.waitFor(() -> exitUtilLog.getOutput().contains("Exiting with status 1: ExitException"),
100, 5000);
}
}
/**
* Tests delegation token renewal after a certificate renew.
*/
@Test
void testDelegationTokenRenewCrossCertificateRenew() throws Exception {
initSCM();
try {
scm = HddsTestUtils.getScmSimple(conf);
scm.start();
// Setup secure OM for start.
final int certLifetime = 40 * 1000; // 40s
OzoneConfiguration newConf = new OzoneConfiguration(conf);
newConf.set(HDDS_X509_DEFAULT_DURATION,
Duration.ofMillis(certLifetime).toString());
newConf.set(HDDS_X509_RENEW_GRACE_DURATION,
Duration.ofMillis(certLifetime - 15 * 1000).toString());
newConf.setLong(OMConfigKeys.DELEGATION_TOKEN_MAX_LIFETIME_KEY,
certLifetime - 20 * 1000);
setupOm(newConf);
OzoneManager.setTestSecureOmFlag(true);
CertificateClientTestImpl certClient =
new CertificateClientTestImpl(newConf, true);
X509Certificate omCert = certClient.getCertificate();
String omCertId1 = omCert.getSerialNumber().toString();
// Start OM
om.setCertClient(certClient);
om.setScmTopologyClient(new ScmTopologyClient(scmBlockClient));
om.start();
GenericTestUtils.waitFor(() -> om.isLeaderReady(), 100, 10000);
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
// Get first OM client which will authenticate via Kerberos
omClient = new OzoneManagerProtocolClientSideTranslatorPB(
OmTransportFactory.create(newConf, ugi, null),
RandomStringUtils.randomAscii(5));
// Since client is already connected get a delegation token
Token<OzoneTokenIdentifier> token1 = omClient.getDelegationToken(
new Text("om"));
// Check if token is of right kind and renewer is running om instance
assertNotNull(token1);
assertEquals("OzoneToken", token1.getKind().toString());
assertEquals(SecurityUtil.buildTokenService(
om.getNodeDetails().getRpcAddress()).toString(),
token1.getService().toString());
assertEquals(omCertId1, token1.decodeIdentifier().getOmCertSerialId());
// Renew delegation token
long expiryTime = omClient.renewDelegationToken(token1);
assertThat(expiryTime).isGreaterThan(0);
// Wait for OM certificate to renew
LambdaTestUtils.await(certLifetime, 100, () ->
!StringUtils.equals(token1.decodeIdentifier().getOmCertSerialId(),
omClient.getDelegationToken(new Text("om"))
.decodeIdentifier().getOmCertSerialId()));
String omCertId2 =
certClient.getCertificate().getSerialNumber().toString();
assertNotEquals(omCertId1, omCertId2);
// Get a new delegation token
Token<OzoneTokenIdentifier> token2 = omClient.getDelegationToken(
new Text("om"));
assertEquals(omCertId2, token2.decodeIdentifier().getOmCertSerialId());
// Because old certificate is still valid, so renew old token will succeed
expiryTime = omClient.renewDelegationToken(token1);
assertThat(expiryTime)
.isGreaterThan(0)
.isLessThan(omCert.getNotAfter().getTime());
} finally {
if (scm != null) {
scm.stop();
}
IOUtils.closeQuietly(om);
}
}
/**
* Test functionality to get SCM signed certificate for OM.
*/
@Test
@Unhealthy("HDDS-8764")
void testOMGrpcServerCertificateRenew() throws Exception {
initSCM();
try {
OzoneManager.setTestSecureOmFlag(false);
scm = HddsTestUtils.getScmSimple(conf);
scm.start();
conf.set(OZONE_METADATA_DIRS, omMetaDirPath.toString());
int certLifetime = 30; // second
conf.set(HDDS_X509_DEFAULT_DURATION,
Duration.ofSeconds(certLifetime).toString());
conf.setInt(OZONE_CLIENT_FAILOVER_MAX_ATTEMPTS_KEY, 2);
// initialize OmStorage, save om Cert and CA Certs to disk
OMStorage omStore = new OMStorage(conf);
omStore.setClusterId(clusterId);
omStore.setOmId(omId);
// Prepare the certificates for OM before OM start
SecurityConfig securityConfig = new SecurityConfig(conf);
CertificateClient scmCertClient = scm.getScmCertificateClient();
CertificateCodec certCodec = new CertificateCodec(securityConfig, "om");
X509Certificate scmCert = scmCertClient.getCertificate();
X509Certificate rootCert = scmCertClient.getCACertificate();
X509Certificate cert = signX509Cert(securityConfig, keyPair, new KeyPair(scmCertClient.getPublicKey(),
scmCertClient.getPrivateKey()), scmCert, "om_cert", clusterId);
String certId = cert.getSerialNumber().toString();
certCodec.writeCertificate(cert);
certCodec.writeCertificate(scmCert, String.format(DefaultCertificateClient.CERT_FILE_NAME_FORMAT,
CAType.SUBORDINATE.getFileNamePrefix() + scmCert.getSerialNumber().toString()));
certCodec.writeCertificate(scmCertClient.getCACertificate(),
String.format(DefaultCertificateClient.CERT_FILE_NAME_FORMAT,
CAType.ROOT.getFileNamePrefix() + rootCert.getSerialNumber().toString()));
omStore.setOmCertSerialId(certId);
omStore.initialize();
conf.setBoolean(HDDS_GRPC_TLS_ENABLED, true);
conf.setBoolean(OZONE_OM_S3_GPRC_SERVER_ENABLED, true);
conf.setBoolean(HddsConfigKeys.HDDS_GRPC_TLS_TEST_CERT, true);
UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
// In this process, SCM has already logged in using Kerberos. So pass a
// specific UGI to DefaultCertificateClient and OzoneManager to avoid
// conflict with SCM.
OzoneManager.setUgi(ugi);
om = OzoneManager.createOm(conf);
om.start();
CertificateClient omCertClient = om.getCertificateClient();
X509Certificate omCert = omCertClient.getCertificate();
X509Certificate caCert = omCertClient.getRootCACertificate();
X509Certificate rootCaCert = omCertClient.getRootCACertificate();
List<X509Certificate> certList = new ArrayList<>();
certList.add(caCert);
certList.add(rootCaCert);
// set certificates in GrpcOmTransport
GrpcOmTransport.setCaCerts(certList);
GenericTestUtils.waitFor(() -> om.isLeaderReady(), 500, 10000);
String transportCls = GrpcOmTransportFactory.class.getName();
conf.set(OZONE_OM_TRANSPORT_CLASS, transportCls);
try (OzoneClient client = OzoneClientFactory.getRpcClient(conf)) {
ServiceInfoEx serviceInfoEx = client.getObjectStore()
.getClientProxy().getOzoneManagerClient().getServiceInfo();
assertEquals(CertificateCodec.getPEMEncodedString(caCert), serviceInfoEx.getCaCertificate());
// Wait for OM certificate to renewed
GenericTestUtils.waitFor(() ->
!omCert.getSerialNumber().toString().equals(
omCertClient.getCertificate().getSerialNumber().toString()),
500, certLifetime * 1000);
// rerun the command using old client, it should succeed
serviceInfoEx = client.getObjectStore()
.getClientProxy().getOzoneManagerClient().getServiceInfo();
assertEquals(CertificateCodec.getPEMEncodedString(caCert), serviceInfoEx.getCaCertificate());
}
// get new client, it should succeed.
OzoneClient client1 = OzoneClientFactory.getRpcClient(conf);
client1.close();
// Wait for old OM certificate to expire
GenericTestUtils.waitFor(() -> omCert.getNotAfter().before(new Date()),
500, certLifetime * 1000);
// get new client, it should succeed too.
OzoneClient client2 = OzoneClientFactory.getRpcClient(conf);
client2.close();
} finally {
OzoneManager.setUgi(null);
GrpcOmTransport.setCaCerts(null);
}
}
void validateCertificate(X509Certificate cert) throws Exception {
Matcher m = Pattern.compile(".*CN=([^,]*).*").matcher(cert.getIssuerDN().getName());
String cn = "";
if (m.matches()) {
cn = m.group(1);
}
String hostName = InetAddress.getLocalHost().getHostName();
// Subject name should be om login user in real world but in this test
// UGI has scm user context.
assertThat(cn).contains(SCM_SUB_CA);
assertThat(cn).contains(hostName);
LocalDate today = LocalDateTime.now().toLocalDate();
Date invalidDate;
// Make sure the end date is honored.
invalidDate = java.sql.Date.valueOf(today.plus(1, ChronoUnit.DAYS));
assertTrue(cert.getNotAfter().after(invalidDate));
invalidDate = java.sql.Date.valueOf(today.plus(400, ChronoUnit.DAYS));
assertTrue(cert.getNotAfter().before(invalidDate));
assertThat(cert.getSubjectDN().toString()).contains(scmId);
assertThat(cert.getSubjectDN().toString()).contains(clusterId);
assertThat(cert.getIssuerDN().toString()).contains(scmId);
assertThat(cert.getIssuerDN().toString()).contains(clusterId);
// Verify that certificate matches the public key.
assertEquals(cert.getPublicKey(), om.getCertificateClient().getPublicKey());
}
private void initializeOmStorage(OMStorage omStorage) throws IOException {
if (omStorage.getState() == Storage.StorageState.INITIALIZED) {
return;
}
omStorage.setClusterId(clusterId);
omStorage.setOmId(omId);
// Initialize ozone certificate client if security is enabled.
if (OzoneSecurityUtil.isSecurityEnabled(conf)) {
OzoneManager.initializeSecurity(conf, omStorage, scmId);
}
omStorage.initialize();
}
private static X509Certificate generateSelfSignedX509Cert(
SecurityConfig conf, KeyPair keyPair, LocalDateTime startDate,
Duration certLifetime) throws Exception {
if (keyPair == null) {
keyPair = KeyStoreTestUtil.generateKeyPair("RSA");
}
LocalDateTime start = startDate == null ? LocalDateTime.now() : startDate;
LocalDateTime end = start.plus(certLifetime);
return SelfSignedCertificate.newBuilder()
.setBeginDate(start)
.setEndDate(end)
.setClusterID("cluster")
.setKey(keyPair)
.setSubject("localhost")
.setConfiguration(conf)
.setScmID("test")
.build();
}
private static X509Certificate signX509Cert(
SecurityConfig conf, KeyPair keyPair, KeyPair rootKeyPair,
X509Certificate rootCert, String subject, String clusterId
) throws Exception {
// Generate normal certificate, signed by RootCA certificate
DefaultApprover approver = new DefaultApprover(new DefaultProfile(), conf);
CertificateSignRequest.Builder csrBuilder =
new CertificateSignRequest.Builder();
// Get host name.
csrBuilder.setKey(keyPair)
.setConfiguration(conf)
.setScmID("test")
.setClusterID(clusterId)
.setSubject(subject)
.setDigitalSignature(true)
.setDigitalEncryption(true);
addIpAndDnsDataToBuilder(csrBuilder);
LocalDateTime start = LocalDateTime.now();
Duration certDuration = conf.getDefaultCertDuration();
//TODO: generateCSR!
return approver.sign(conf, rootKeyPair.getPrivate(), rootCert,
Date.from(start.atZone(ZoneId.systemDefault()).toInstant()),
Date.from(start.plus(certDuration)
.atZone(ZoneId.systemDefault()).toInstant()),
csrBuilder.build().generateCSR(), "test", clusterId,
String.valueOf(System.nanoTime()));
}
private static void addIpAndDnsDataToBuilder(
CertificateSignRequest.Builder csrBuilder) throws IOException {
DomainValidator validator = DomainValidator.getInstance();
// Add all valid ips.
List<InetAddress> inetAddresses =
OzoneSecurityUtil.getValidInetsForCurrentHost();
csrBuilder.addInetAddresses(inetAddresses, validator);
}
}