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