AMBARI-17908. Ensure the supplied hostname is a valid hostname when signing agent-side host certs (rlevas)
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
index 9655bf9..6e3eef7 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
@@ -116,6 +116,7 @@
public static final String API_GZIP_MIN_COMPRESSION_SIZE_KEY = "api.gzip.compression.min.size";
public static final String AGENT_API_GZIP_COMPRESSION_ENABLED_KEY = "agent.api.gzip.compression.enabled";
public static final String AGENT_USE_SSL = "agent.ssl";
+ public static final String SRVR_AGENT_HOSTNAME_VALIDATE_KEY = "security.agent.hostname.validate";
public static final String SRVR_TWO_WAY_SSL_KEY = "security.server.two_way_ssl";
public static final String SRVR_TWO_WAY_SSL_PORT_KEY = "security.server.two_way_ssl.port";
public static final String SRVR_ONE_WAY_SSL_PORT_KEY = "security.server.one_way_ssl.port";
@@ -477,6 +478,7 @@
private static final String SERVER_JDBC_USER_PASSWD_DEFAULT = "bigdata";
private static final String SERVER_JDBC_RCA_USER_NAME_DEFAULT = "mapred";
private static final String SERVER_JDBC_RCA_USER_PASSWD_DEFAULT = "mapred";
+ private static final String SRVR_AGENT_HOSTNAME_VALIDATE_DEFAULT = "true";
private static final String SRVR_TWO_WAY_SSL_DEFAULT = "false";
private static final String SRVR_KSTR_DIR_DEFAULT = ".";
private static final String API_CSRF_PREVENTION_DEFAULT = "true";
@@ -928,6 +930,8 @@
configsMap.putAll(agentConfigsMap);
configsMap.put(AMBARI_PYTHON_WRAP_KEY, properties.getProperty(
AMBARI_PYTHON_WRAP_KEY, AMBARI_PYTHON_WRAP_DEFAULT));
+ configsMap.put(SRVR_AGENT_HOSTNAME_VALIDATE_KEY, properties.getProperty(
+ SRVR_AGENT_HOSTNAME_VALIDATE_KEY, SRVR_AGENT_HOSTNAME_VALIDATE_DEFAULT));
configsMap.put(SRVR_TWO_WAY_SSL_KEY, properties.getProperty(
SRVR_TWO_WAY_SSL_KEY, SRVR_TWO_WAY_SSL_DEFAULT));
configsMap.put(SRVR_TWO_WAY_SSL_PORT_KEY, properties.getProperty(
@@ -1674,6 +1678,16 @@
}
/**
+ * Check to see if the hostname of the agent is to be validated as a proper hostname or not
+ *
+ * @return true if agent hostnames should be checked as a valid hostnames; otherwise false
+ */
+ public boolean validateAgentHostnames() {
+ return ("true".equals(properties.getProperty(SRVR_AGENT_HOSTNAME_VALIDATE_KEY,
+ SRVR_AGENT_HOSTNAME_VALIDATE_DEFAULT)));
+ }
+
+ /**
* Check to see if two-way SSL auth should be used between server and agents
* or not
*
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/CertificateManager.java b/ambari-server/src/main/java/org/apache/ambari/server/security/CertificateManager.java
index b698ef3..9f70dc0 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/security/CertificateManager.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/security/CertificateManager.java
@@ -20,10 +20,12 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.utils.HostUtils;
import org.apache.ambari.server.utils.ShellCommandUtil;
import org.apache.commons.io.FileUtils;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
@@ -40,11 +42,10 @@
@Singleton
public class CertificateManager {
+ private static final Logger LOG = LoggerFactory.getLogger(CertificateManager.class);
+
@Inject Configuration configs;
- private static final Log LOG = LogFactory.getLog(CertificateManager.class);
-
-
private static final String GEN_SRVR_KEY = "openssl genrsa -des3 " +
"-passout pass:{0} -out {1}" + File.separator + "{2} 4096 ";
private static final String GEN_SRVR_REQ = "openssl req -passin pass:{0} " +
@@ -97,7 +98,7 @@
*
* @return command execution exit code
*/
- private int runCommand(String command) {
+ protected int runCommand(String command) {
String line = null;
Process process = null;
BufferedReader br= null;
@@ -185,7 +186,38 @@
*/
public synchronized SignCertResponse signAgentCrt(String agentHostname, String agentCrtReqContent, String passphraseAgent) {
SignCertResponse response = new SignCertResponse();
- LOG.info("Signing of agent certificate");
+ LOG.info("Signing agent certificate");
+
+ // Ensure the hostname is not empty or null...
+ agentHostname = StringUtils.trim(agentHostname);
+
+ if(StringUtils.isEmpty(agentHostname)) {
+ LOG.warn("The agent hostname is missing");
+ response.setResult(SignCertResponse.ERROR_STATUS);
+ response.setMessage("The agent hostname is missing");
+ return response;
+ }
+
+ // Optionally check the supplied hostname to make sure it is a valid hostname.
+ // By default, this feature is turned on. If this check is not desired (maybe the validation
+ // rules are too strict), the feature may be turned off by setting the following
+ // property in the ambari.properties file:
+ //
+ // security.agent.hostname.validate = "false"
+ //
+ if(configs.validateAgentHostnames()) {
+ LOG.info("Validating agent hostname: {}", agentHostname);
+ if(!HostUtils.isValidHostname(agentHostname)) {
+ LOG.warn("The agent hostname is not a valid hostname");
+ response.setResult(SignCertResponse.ERROR_STATUS);
+ response.setMessage("The agent hostname is not a valid hostname");
+ return response;
+ }
+ }
+ else {
+ LOG.info("Skipping validation of agent hostname: {}", agentHostname);
+ }
+
LOG.info("Verifying passphrase");
String passphraseSrvr = configs.getConfigsMap().get(Configuration.
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/utils/HostUtils.java b/ambari-server/src/main/java/org/apache/ambari/server/utils/HostUtils.java
new file mode 100644
index 0000000..36c16f5
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/utils/HostUtils.java
@@ -0,0 +1,47 @@
+/*
+ * 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.ambari.server.utils;
+
+import java.util.regex.Pattern;
+
+/**
+ * HostUtils provides utilities related to processing and validating hosts and host names.
+ */
+public class HostUtils {
+ /**
+ * A regular expression to validate that a hostname meets the specifications described in RFC 1123.
+ * <p>
+ * Various sources on the Internet specific this regular expression for hostname validation.
+ */
+ private static final Pattern REGEX_VALID_HOSTNAME = Pattern.compile("^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$");
+
+ /**
+ * Validates a hostname to ensure it meets the specifications described in RFC 1123.
+ * <p>
+ * Note: this does not validate whether the host exists or if the name is a valid DNS name.
+ * For example, host.example.anything is a valid host name however may be an invalid DNS name
+ * since "anything" may not be a valid top-level domain.
+ *
+ * @param hostname a hostname
+ * @return true if the hostname is valid; false otherwise
+ */
+ public static boolean isValidHostname(String hostname) {
+ return (hostname != null) && REGEX_VALID_HOSTNAME.matcher(hostname).matches();
+ }
+}
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java b/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java
index b5d0e02..e9aebc0 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/configuration/ConfigurationTest.java
@@ -72,6 +72,38 @@
}
/**
+ * ambari.properties doesn't contain "security.agent.hostname.validate" option
+ */
+ @Test
+ public void testValidateAgentHostnames() {
+ Assert.assertTrue(new Configuration().validateAgentHostnames());
+ }
+
+ /**
+ * ambari.properties contains "security.agent.hostname.validate=true" option
+ */
+ @Test
+ public void testValidateAgentHostnamesOn() {
+ Properties ambariProperties = new Properties();
+ ambariProperties.setProperty(Configuration.SRVR_AGENT_HOSTNAME_VALIDATE_KEY, "true");
+ Configuration conf = new Configuration(ambariProperties);
+ Assert.assertTrue(conf.validateAgentHostnames());
+ Assert.assertEquals("true", conf.getConfigsMap().get(Configuration.SRVR_AGENT_HOSTNAME_VALIDATE_KEY));
+ }
+
+ /**
+ * ambari.properties contains "security.agent.hostname.validate=false" option
+ */
+ @Test
+ public void testValidateAgentHostnamesOff() {
+ Properties ambariProperties = new Properties();
+ ambariProperties.setProperty(Configuration.SRVR_AGENT_HOSTNAME_VALIDATE_KEY, "false");
+ Configuration conf = new Configuration(ambariProperties);
+ Assert.assertFalse(conf.validateAgentHostnames());
+ Assert.assertEquals("false", conf.getConfigsMap().get(Configuration.SRVR_AGENT_HOSTNAME_VALIDATE_KEY));
+ }
+
+ /**
* ambari.properties doesn't contain "security.server.two_way_ssl" option
* @throws Exception
*/
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/CertificateManagerTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/CertificateManagerTest.java
new file mode 100644
index 0000000..f28f234
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/security/CertificateManagerTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.ambari.server.security;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import junit.framework.Assert;
+import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.state.stack.OsFamily;
+import org.easymock.EasyMockSupport;
+import org.easymock.IAnswer;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.easymock.EasyMock.expect;
+
+public class CertificateManagerTest extends EasyMockSupport {
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Test
+ public void testSignAgentCrt() throws Exception {
+ Injector injector = getInjector();
+
+ File directory = folder.newFolder();
+
+ String hostname = "host1.example.com";
+
+ Map<String, String> configurationMap = new HashMap<String, String>();
+ configurationMap.put(Configuration.SRVR_KSTR_DIR_KEY, directory.getAbsolutePath());
+ configurationMap.put(Configuration.SRVR_CRT_PASS_KEY, "server_cert_pass");
+ configurationMap.put(Configuration.SRVR_CRT_NAME_KEY, "server_cert_name");
+ configurationMap.put(Configuration.SRVR_KEY_NAME_KEY, "server_key_name");
+ configurationMap.put(Configuration.PASSPHRASE_KEY, "passphrase");
+
+ Configuration configuration = injector.getInstance(Configuration.class);
+ expect(configuration.validateAgentHostnames()).andReturn(true).once();
+ expect(configuration.getConfigsMap()).andReturn(configurationMap).anyTimes();
+
+ Method runCommand = CertificateManager.class.getDeclaredMethod("runCommand", String.class);
+
+ final File agentCrtFile = new File(directory, String.format("%s.crt", hostname));
+
+ String expectedCommand = String.format("openssl ca -config %s/ca.config -in %s/%s.csr -out %s -batch -passin pass:%s -keyfile %s/%s -cert %s/%s",
+ directory.getAbsolutePath(),
+ directory.getAbsolutePath(),
+ hostname,
+ agentCrtFile.getAbsolutePath(),
+ configurationMap.get(Configuration.SRVR_CRT_PASS_KEY),
+ directory.getAbsolutePath(),
+ configurationMap.get(Configuration.SRVR_KEY_NAME_KEY),
+ directory.getAbsolutePath(),
+ configurationMap.get(Configuration.SRVR_CRT_NAME_KEY));
+
+ CertificateManager certificateManager = createMockBuilder(CertificateManager.class)
+ .addMockedMethod(runCommand)
+ .createMock();
+ expect(certificateManager.runCommand(expectedCommand))
+ .andAnswer(new IAnswer<Integer>() {
+ @Override
+ public Integer answer() throws Throwable {
+ return (agentCrtFile.createNewFile()) ? 0 : 1;
+ }
+ })
+ .once();
+
+ injector.injectMembers(certificateManager);
+
+ replayAll();
+
+ SignCertResponse response = certificateManager.signAgentCrt(hostname, "crtContent", "passphrase");
+
+ verifyAll();
+
+ Assert.assertEquals(SignCertResponse.OK_STATUS, response.getResult());
+ }
+
+ @Test
+ public void testSignAgentCrtInvalidHostname() throws Exception {
+ Injector injector = getInjector();
+
+ Configuration configuration = injector.getInstance(Configuration.class);
+ expect(configuration.validateAgentHostnames()).andReturn(true).once();
+
+ replayAll();
+
+ CertificateManager certificateManager = new CertificateManager();
+ injector.injectMembers(certificateManager);
+
+ SignCertResponse response = certificateManager.signAgentCrt("hostname; echo \"hello\" > /tmp/hello.txt;", "crtContent", "passphrase");
+
+ verifyAll();
+
+ Assert.assertEquals(SignCertResponse.ERROR_STATUS, response.getResult());
+ Assert.assertEquals("The agent hostname is not a valid hostname", response.getMessage());
+ }
+
+ @Test
+ public void testSignAgentCrtBadPassphrase() throws Exception {
+ Injector injector = getInjector();
+
+ Configuration configuration = injector.getInstance(Configuration.class);
+ expect(configuration.validateAgentHostnames()).andReturn(true).once();
+ expect(configuration.getConfigsMap()).andReturn(Collections.singletonMap(Configuration.PASSPHRASE_KEY, "some_passphrase")).once();
+
+ replayAll();
+
+ CertificateManager certificateManager = new CertificateManager();
+ injector.injectMembers(certificateManager);
+
+ SignCertResponse response = certificateManager.signAgentCrt("host1.example.com", "crtContent", "passphrase");
+
+ verifyAll();
+
+ Assert.assertEquals(SignCertResponse.ERROR_STATUS, response.getResult());
+ Assert.assertEquals("Incorrect passphrase from the agent", response.getMessage());
+ }
+
+ @Test
+ public void testSignAgentCrtInvalidHostnameIgnoreBadPassphrase() throws Exception {
+ Injector injector = getInjector();
+
+ Configuration configuration = injector.getInstance(Configuration.class);
+ expect(configuration.validateAgentHostnames()).andReturn(false).once();
+ expect(configuration.getConfigsMap()).andReturn(Collections.singletonMap(Configuration.PASSPHRASE_KEY, "some_passphrase")).once();
+
+ replayAll();
+
+ CertificateManager certificateManager = new CertificateManager();
+ injector.injectMembers(certificateManager);
+
+ SignCertResponse response = certificateManager.signAgentCrt("hostname; echo \"hello\" > /tmp/hello.txt;", "crtContent", "passphrase");
+
+ verifyAll();
+
+ Assert.assertEquals(SignCertResponse.ERROR_STATUS, response.getResult());
+ Assert.assertEquals("Incorrect passphrase from the agent", response.getMessage());
+ }
+
+ private Injector getInjector() {
+ return Guice.createInjector(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
+ bind(Configuration.class).toInstance(createMock(Configuration.class));
+ }
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/utils/HostUtilsTest.java b/ambari-server/src/test/java/org/apache/ambari/server/utils/HostUtilsTest.java
new file mode 100644
index 0000000..341e214
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/utils/HostUtilsTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.ambari.server.utils;
+
+import junit.framework.Assert;
+import org.junit.Test;
+
+public class HostUtilsTest {
+ @Test
+ public void testIsValidHostname() throws Exception {
+
+ // Valid host names
+ Assert.assertTrue(HostUtils.isValidHostname("localhost"));
+ Assert.assertTrue(HostUtils.isValidHostname("localhost.localdomain"));
+ Assert.assertTrue(HostUtils.isValidHostname("host1.example.com"));
+ Assert.assertTrue(HostUtils.isValidHostname("Host1.eXample.coM"));
+ Assert.assertTrue(HostUtils.isValidHostname("host-name.example.com"));
+ Assert.assertTrue(HostUtils.isValidHostname("123.456.789"));
+ Assert.assertTrue(HostUtils.isValidHostname("host-123-name.ex4mpl3.c0m"));
+
+ // Invalid host names
+ Assert.assertFalse(HostUtils.isValidHostname("host_name.example.com"));
+ Assert.assertFalse(HostUtils.isValidHostname("host;name.example.com"));
+ Assert.assertFalse(HostUtils.isValidHostname("host?name.example.com"));
+ Assert.assertFalse(HostUtils.isValidHostname("host@name.example.com"));
+ Assert.assertFalse(HostUtils.isValidHostname("host=name.example.com"));
+ Assert.assertFalse(HostUtils.isValidHostname("host+name.example.com"));
+ Assert.assertFalse(HostUtils.isValidHostname("host)name).example.com"));
+ }
+
+}
\ No newline at end of file