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}