001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.http; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertNotEquals; 022import static org.junit.Assert.assertTrue; 023 024import java.io.ByteArrayInputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.nio.charset.StandardCharsets; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.List; 031import java.util.Map; 032import java.util.stream.Stream; 033import org.apache.hadoop.conf.Configuration; 034import org.apache.hadoop.fs.Path; 035import org.apache.hadoop.hbase.HBaseClassTestRule; 036import org.apache.hadoop.hbase.HBaseTestingUtil; 037import org.apache.hadoop.hbase.HConstants; 038import org.apache.hadoop.hbase.LocalHBaseCluster; 039import org.apache.hadoop.hbase.ServerName; 040import org.apache.hadoop.hbase.master.HMaster; 041import org.apache.hadoop.hbase.master.ServerManager; 042import org.apache.hadoop.hbase.testclassification.MiscTests; 043import org.apache.hadoop.hbase.testclassification.SmallTests; 044import org.apache.hadoop.hbase.util.CommonFSUtils; 045import org.apache.hadoop.hbase.util.TestServerHttpUtils; 046import org.junit.AfterClass; 047import org.junit.BeforeClass; 048import org.junit.ClassRule; 049import org.junit.Test; 050import org.junit.experimental.categories.Category; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053 054/** 055 * This class performs tests that ensure sensitive config values found in the HBase UI's Debug Dump 056 * are redacted. A config property name must follow one of the regex patterns specified in 057 * hadoop.security.sensitive-config-keys in order to have its value redacted. 058 */ 059@Category({ MiscTests.class, SmallTests.class }) 060public class TestDebugDumpRedaction { 061 private static final Logger LOG = LoggerFactory.getLogger(TestDebugDumpRedaction.class); 062 private static final HBaseTestingUtil UTIL = new HBaseTestingUtil(); 063 private static final String XML_CONFIGURATION_START_TAG = "<configuration>"; 064 private static final String XML_CONFIGURATION_END_TAG = "</configuration>"; 065 private static final int SUBSTRING_OFFSET = XML_CONFIGURATION_END_TAG.length(); 066 private static final String REDACTED_TEXT = "******"; 067 068 // These are typical configuration properties whose values we would want to see redacted. 069 private static final List<String> SENSITIVE_CONF_PROPERTIES = 070 Arrays.asList("hbase.zookeeper.property.ssl.trustStore.password", 071 "ssl.client.truststore.password", "hbase.rpc.tls.truststore.password", 072 "ssl.server.keystore.password", "fs.s3a.server-side-encryption.key", 073 "fs.s3a.encryption.algorithm", "fs.s3a.encryption.key", "fs.s3a.secret.key", 074 "fs.s3a.important.secret.key", "fs.s3a.session.key", "fs.s3a.secret.session.key", 075 "fs.s3a.session.token", "fs.s3a.secret.session.token", "fs.azure.account.key.importantKey", 076 "fs.azure.oauth2.token", "fs.adl.oauth2.token", "fs.gs.encryption.sensitive", 077 "fs.gs.proxy.important", "fs.gs.auth.sensitive.info", "sensitive.credential", 078 "oauth.important.secret", "oauth.important.password", "oauth.important.token", 079 "fs.adl.oauth2.access.token.provider.type", "hadoop.security.sensitive-config-keys"); 080 081 // These are not typical configuration properties whose values we would want to see redacted, 082 // but we are testing their redaction anyway because we want to see how the redaction behaves 083 // with booleans and ints. 084 private static final List<String> NON_SENSITIVE_KEYS_WITH_DEFAULT_VALUES = Arrays.asList( 085 "hbase.zookeeper.quorum", "hbase.cluster.distributed", "hbase.master.logcleaner.ttl", 086 "hbase.master.hfilecleaner.plugins", "hbase.master.infoserver.redirect", 087 "hbase.thrift.minWorkerThreads", "hbase.table.lock.enable"); 088 089 // We also want to verify the behavior for a string with value "null" and an empty string. 090 // (giving a config property an actual null value will throw an error) 091 private static final String NULL_CONFIG_KEY = "null.key"; 092 private static final String EMPTY_CONFIG_KEY = "empty.key"; 093 094 // Combine all properties we want to redact into one list 095 private static final List<String> REDACTED_PROPS = 096 Stream.of(SENSITIVE_CONF_PROPERTIES, NON_SENSITIVE_KEYS_WITH_DEFAULT_VALUES, 097 List.of(NULL_CONFIG_KEY, EMPTY_CONFIG_KEY)).flatMap(Collection::stream).toList(); 098 099 private static LocalHBaseCluster CLUSTER; 100 101 @ClassRule 102 public static final HBaseClassTestRule CLASS_RULE = 103 HBaseClassTestRule.forClass(TestDebugDumpRedaction.class); 104 105 @BeforeClass 106 public static void beforeClass() throws Exception { 107 Configuration conf = UTIL.getConfiguration(); 108 109 // Add various config properties with sensitive information that should be redacted 110 // when the Debug Dump is performed in the UI. These properties are following the 111 // regexes specified by the hadoop.security.sensitive-config-keys property. 112 for (String property : SENSITIVE_CONF_PROPERTIES) { 113 conf.set(property, "testPassword"); 114 } 115 116 // Also verify a null string and empty string will get redacted. 117 // Setting the config to use an actual null value throws an error. 118 conf.set(NULL_CONFIG_KEY, "null"); 119 conf.set(EMPTY_CONFIG_KEY, ""); 120 121 // Config properties following these regex patterns will have their values redacted in the 122 // Debug Dump 123 String sensitiveKeyRegexes = "secret$,password$,ssl.keystore.pass$," 124 + "fs.s3a.server-side-encryption.key,fs.s3a.*.server-side-encryption.key," 125 + "fs.s3a.encryption.algorithm,fs.s3a.encryption.key,fs.s3a.secret.key," 126 + "fs.s3a.*.secret.key,fs.s3a.session.key,fs.s3a.*.session.key,fs.s3a.session.token," 127 + "fs.s3a.*.session.token,fs.azure.account.key.*,fs.azure.oauth2.*,fs.adl.oauth2.*," 128 + "fs.gs.encryption.*,fs.gs.proxy.*,fs.gs.auth.*,credential$,oauth.*secret," 129 + "oauth.*password,oauth.*token,hadoop.security.sensitive-config-keys," 130 + String.join(",", NON_SENSITIVE_KEYS_WITH_DEFAULT_VALUES) + "," + NULL_CONFIG_KEY + "," 131 + EMPTY_CONFIG_KEY; 132 133 conf.set("hadoop.security.sensitive-config-keys", sensitiveKeyRegexes); 134 135 UTIL.startMiniZKCluster(); 136 137 UTIL.startMiniDFSCluster(1); 138 Path rootdir = UTIL.getDataTestDirOnTestFS("TestDebugDumpServlet"); 139 CommonFSUtils.setRootDir(conf, rootdir); 140 141 // The info servers do not run in tests by default. 142 // Set them to ephemeral ports so they will start 143 // setup configuration 144 conf.setInt(HConstants.MASTER_INFO_PORT, 0); 145 conf.setInt(HConstants.REGIONSERVER_INFO_PORT, 0); 146 147 CLUSTER = new LocalHBaseCluster(conf, 1); 148 CLUSTER.startup(); 149 CLUSTER.getActiveMaster().waitForMetaOnline(); 150 } 151 152 @AfterClass 153 public static void afterClass() throws Exception { 154 if (CLUSTER != null) { 155 CLUSTER.shutdown(); 156 CLUSTER.join(); 157 } 158 UTIL.shutdownMiniCluster(); 159 } 160 161 @Test 162 public void testMasterPasswordsAreRedacted() throws IOException { 163 String response = TestServerHttpUtils.getMasterPageContent(CLUSTER); 164 165 // Verify this is the master server's debug dump 166 assertTrue( 167 response.startsWith("Master status for " + CLUSTER.getActiveMaster().getServerName())); 168 169 verifyDebugDumpResponseConfig(response); 170 } 171 172 @Test 173 public void testRegionServerPasswordsAreRedacted() throws IOException { 174 HMaster master = CLUSTER.getActiveMaster(); 175 176 ServerManager serverManager = master.getServerManager(); 177 List<ServerName> onlineServersList = serverManager.getOnlineServersList(); 178 179 assertEquals(1, onlineServersList.size()); 180 181 ServerName regionServerName = onlineServersList.get(0); 182 int regionServerInfoPort = master.getRegionServerInfoPort(regionServerName); 183 String regionServerHostname = regionServerName.getHostname(); 184 185 String response = 186 TestServerHttpUtils.getRegionServerPageContent(regionServerHostname, regionServerInfoPort); 187 188 // Verify this is the region server's debug dump 189 assertTrue(response.startsWith("RegionServer status for " + regionServerName)); 190 191 verifyDebugDumpResponseConfig(response); 192 } 193 194 private void verifyDebugDumpResponseConfig(String response) throws IOException { 195 // Grab the server's config from the Debug Dump. 196 String xmlString = response.substring(response.indexOf(XML_CONFIGURATION_START_TAG), 197 response.indexOf(XML_CONFIGURATION_END_TAG) + SUBSTRING_OFFSET); 198 199 // Convert the XML string into an InputStream. 200 Configuration conf = new Configuration(false); 201 try (InputStream is = new ByteArrayInputStream(xmlString.getBytes(StandardCharsets.UTF_8))) { 202 // Add the InputStream as a resource to the Configuration object 203 conf.addResource(is, "DebugDumpXmlConfig"); 204 } 205 206 // Verify the expected properties had their values redacted. 207 for (String property : REDACTED_PROPS) { 208 LOG.info("Verifying property has been redacted: {}", property); 209 assertEquals("Expected " + property + " to have its value redacted", REDACTED_TEXT, 210 conf.get(property)); 211 } 212 213 // Verify all other props have not had their values redacted. 214 String propertyName; 215 for (Map.Entry<String, String> property : conf) { 216 propertyName = property.getKey(); 217 if (!REDACTED_PROPS.contains(propertyName)) { 218 LOG.info("Verifying {} property has not had its value redacted", propertyName); 219 assertNotEquals("Expected property " + propertyName + " to not have its value redacted", 220 REDACTED_TEXT, conf.get(propertyName)); 221 } 222 } 223 } 224}