Skip to content

Commit 313dacc

Browse files
authored
Require every UI API endpoint to declare its authorization (#9004)
* Require every UI API endpoint to declare its authorization * Prevent credential mask from overriding local config
1 parent d892aa2 commit 313dacc

10 files changed

Lines changed: 489 additions & 2 deletions

File tree

storm-client/src/jvm/org/apache/storm/Config.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,7 @@ public class Config extends HashMap<String, Object> {
12401240
* The password of the keystore that the nimbus TLS server uses.
12411241
*/
12421242
@IsString
1243+
@Password
12431244
public static final String NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD = "nimbus.thrift.tls.server.keystore.password";
12441245

12451246
/**
@@ -1258,6 +1259,7 @@ public class Config extends HashMap<String, Object> {
12581259
* The password of the truststore that the nimbus TLS server uses.
12591260
*/
12601261
@IsString
1262+
@Password
12611263
public static final String NIMBUS_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD = "nimbus.thrift.tls.server.truststore.password";
12621264

12631265
/**
@@ -1272,6 +1274,7 @@ public class Config extends HashMap<String, Object> {
12721274
* The password of the keystore that the nimbus TLS client uses.
12731275
*/
12741276
@IsString
1277+
@Password
12751278
public static final String NIMBUS_THRIFT_TLS_CLIENT_KEYSTORE_PASSWORD = "nimbus.thrift.tls.client.keystore.password";
12761279

12771280
/**
@@ -1296,6 +1299,7 @@ public class Config extends HashMap<String, Object> {
12961299
* The password of the truststore that the nimbus TLS client uses.
12971300
*/
12981301
@IsString
1302+
@Password
12991303
public static final String NIMBUS_THRIFT_TLS_CLIENT_TRUSTSTORE_PASSWORD = "nimbus.thrift.tls.client.truststore.password";
13001304

13011305
/**
@@ -1351,6 +1355,7 @@ public class Config extends HashMap<String, Object> {
13511355
* The password of the keystore that the supervisor TLS server uses.
13521356
*/
13531357
@IsString
1358+
@Password
13541359
public static final String SUPERVISOR_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD = "supervisor.thrift.tls.server.keystore.password";
13551360

13561361
/**
@@ -1363,6 +1368,7 @@ public class Config extends HashMap<String, Object> {
13631368
* The password of the truststore that the supervisor TLS server uses.
13641369
*/
13651370
@IsString
1371+
@Password
13661372
public static final String SUPERVISOR_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD = "supervisor.thrift.tls.server.truststore.password";
13671373

13681374
/**
@@ -1375,6 +1381,7 @@ public class Config extends HashMap<String, Object> {
13751381
* The password of the keystore that the supervisor TLS client uses.
13761382
*/
13771383
@IsString
1384+
@Password
13781385
public static final String SUPERVISOR_THRIFT_TLS_CLIENT_KEYSTORE_PASSWORD = "supervisor.thrift.tls.client.keystore.password";
13791386

13801387
/**
@@ -1387,6 +1394,7 @@ public class Config extends HashMap<String, Object> {
13871394
* The password of the truststore that the supervisor TLS client uses.
13881395
*/
13891396
@IsString
1397+
@Password
13901398
public static final String SUPERVISOR_THRIFT_TLS_CLIENT_TRUSTSTORE_PASSWORD = "supervisor.thrift.tls.client.truststore.password";
13911399

13921400
/**
@@ -1534,6 +1542,7 @@ public class Config extends HashMap<String, Object> {
15341542
* Keystore password for ZooKeeper client connection over SSL.
15351543
*/
15361544
@IsString
1545+
@Password
15371546
public static final String STORM_ZOOKEEPER_SSL_KEYSTORE_PASSWORD = "storm.zookeeper.ssl.keystore.password";
15381547
/**
15391548
* Truststore location for ZooKeeper client connection over SSL.
@@ -1544,6 +1553,7 @@ public class Config extends HashMap<String, Object> {
15441553
* Truststore password for ZooKeeper client connection over SSL.
15451554
*/
15461555
@IsString
1556+
@Password
15471557
public static final String STORM_ZOOKEEPER_SSL_TRUSTSTORE_PASSWORD = "storm.zookeeper.ssl.truststore.password";
15481558
/**
15491559
* Enable or disable hostname verification.
@@ -1735,6 +1745,7 @@ public class Config extends HashMap<String, Object> {
17351745
* deny access from workers.
17361746
*/
17371747
@IsString
1748+
@Password
17381749
public static final String STORM_ZOOKEEPER_AUTH_PAYLOAD = "storm.zookeeper.auth.payload";
17391750
/**
17401751
* What Network Topography detection classes should we use. Given a list of supervisor hostnames (or IP addresses), this class would
@@ -1925,6 +1936,7 @@ public class Config extends HashMap<String, Object> {
19251936
* Netty based messaging: Specifies the truststore password when TLS is enabled.
19261937
*/
19271938
@IsString
1939+
@Password
19281940
public static final String STORM_MESSAGING_NETTY_TLS_TRUSTSTORE_PASSWORD = "storm.messaging.netty.tls.truststore.password";
19291941

19301942
/**
@@ -1937,6 +1949,7 @@ public class Config extends HashMap<String, Object> {
19371949
* Netty based messaging: Specifies the keystore password when TLS is enabled.
19381950
*/
19391951
@IsString
1952+
@Password
19401953
public static final String STORM_MESSAGING_NETTY_TLS_KEYSTORE_PASSWORD = "storm.messaging.netty.tls.keystore.password";
19411954

19421955
/**
@@ -1949,6 +1962,7 @@ public class Config extends HashMap<String, Object> {
19491962
* Netty based messaging: Specifies the client truststore password when TLS is enabled.
19501963
*/
19511964
@IsString
1965+
@Password
19521966
public static final String STORM_MESSAGING_NETTY_TLS_CLIENT_TRUSTSTORE_PASSWORD =
19531967
"storm.messaging.netty.tls.client.truststore.password";
19541968

@@ -1962,6 +1976,7 @@ public class Config extends HashMap<String, Object> {
19621976
* Netty based messaging: Specifies the client keystore password when TLS is enabled.
19631977
*/
19641978
@IsString
1979+
@Password
19651980
public static final String STORM_MESSAGING_NETTY_TLS_CLIENT_KEYSTORE_PASSWORD =
19661981
"storm.messaging.netty.tls.client.keystore.password";
19671982

storm-client/src/jvm/org/apache/storm/utils/ConfigUtils.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Random;
2626
import java.util.Set;
2727
import java.util.function.BooleanSupplier;
28+
import java.util.regex.Pattern;
2829
import java.util.stream.Collectors;
2930

3031
import org.apache.storm.Config;
@@ -42,6 +43,7 @@ public class ConfigUtils {
4243
public static final double RFC1889_ALPHA = 1.0 / 16.0;
4344

4445
private static final Set<String> passwordConfigKeys = new HashSet<>();
46+
private static final Pattern CREDENTIAL_KEY_NAME = Pattern.compile("(?i)(password|passwd|secret)");
4547

4648
static {
4749
for (Class<?> clazz : ConfigValidation.getConfigClasses()) {
@@ -89,6 +91,41 @@ public Object transformEntry(String key, Object value) {
8991
return Maps.transformEntries(conf, maskPasswords);
9092
}
9193

94+
/**
95+
* Mask credential values before a config map is served over an API. This covers what
96+
* {@link #maskPasswords(Map)} covers, plus string values whose key name denotes a secret: plugins read their
97+
* own keys straight out of the config map, so those keys are declared by no annotated field and the annotation
98+
* scan cannot see them. Only string values are considered, so timeouts and class lists whose names merely
99+
* mention credentials keep their value.
100+
*
101+
* @param conf the config to mask
102+
* @return a view of the config with credential values replaced
103+
*/
104+
public static Map<String, Object> maskCredentials(final Map<String, Object> conf) {
105+
Maps.EntryTransformer<String, Object, Object> maskCredentials = new Maps.EntryTransformer<String, Object, Object>() {
106+
@Override
107+
public Object transformEntry(String key, Object value) {
108+
if (passwordConfigKeys.contains(key)) {
109+
return "*****";
110+
}
111+
return value instanceof String && CREDENTIAL_KEY_NAME.matcher(key).find() ? "*****" : value;
112+
}
113+
};
114+
return Maps.transformEntries(conf, maskCredentials);
115+
}
116+
117+
/**
118+
* Whether a config key holds a credential, and therefore whether {@link #maskCredentials(Map)} would replace its
119+
* value. Callers that read a config back from a daemon use this to tell which entries carry no usable value and
120+
* must be taken from their own configuration instead.
121+
*
122+
* @param key the config key
123+
* @return true when the key denotes a credential
124+
*/
125+
public static boolean isCredentialKey(String key) {
126+
return passwordConfigKeys.contains(key) || CREDENTIAL_KEY_NAME.matcher(key).find();
127+
}
128+
92129
public static boolean isLocalMode(Map<String, Object> conf) {
93130
String mode = (String) conf.get(Config.STORM_CLUSTER_MODE);
94131
if (mode != null) {

storm-client/test/jvm/org/apache/storm/utils/ConfigUtilsTest.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,118 @@ public void upstreamFeedbackFreqSecs_rejectsNegative() {
195195
assertThrows(IllegalArgumentException.class, () -> ConfigUtils.upstreamFeedbackFreqSecs(
196196
mockMap(Config.TOPOLOGY_UPSTREAM_FEEDBACK_FREQ_SECS, -1)));
197197
}
198+
199+
@Test
200+
public void maskPasswords_masksClusterZookeeperCredentials() {
201+
Map<String, Object> conf = new HashMap<>();
202+
conf.put(Config.STORM_ZOOKEEPER_AUTH_PAYLOAD, "zk-user:zk-secret");
203+
conf.put(Config.STORM_ZOOKEEPER_TOPOLOGY_AUTH_PAYLOAD, "topo-user:topo-secret");
204+
205+
Map<String, Object> masked = ConfigUtils.maskPasswords(conf);
206+
207+
assertEquals("*****", masked.get(Config.STORM_ZOOKEEPER_AUTH_PAYLOAD));
208+
assertEquals("*****", masked.get(Config.STORM_ZOOKEEPER_TOPOLOGY_AUTH_PAYLOAD));
209+
}
210+
211+
@Test
212+
public void maskPasswords_masksThriftTlsStorePasswords() {
213+
Map<String, Object> conf = new HashMap<>();
214+
conf.put(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD, "nimbus-ks");
215+
conf.put(Config.NIMBUS_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD, "nimbus-ts");
216+
conf.put(Config.NIMBUS_THRIFT_TLS_CLIENT_KEYSTORE_PASSWORD, "client-ks");
217+
conf.put(Config.NIMBUS_THRIFT_TLS_CLIENT_TRUSTSTORE_PASSWORD, "client-ts");
218+
conf.put(Config.SUPERVISOR_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD, "sup-ks");
219+
conf.put(Config.SUPERVISOR_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD, "sup-ts");
220+
221+
Map<String, Object> masked = ConfigUtils.maskPasswords(conf);
222+
223+
assertEquals("*****", masked.get(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD));
224+
assertEquals("*****", masked.get(Config.NIMBUS_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD));
225+
assertEquals("*****", masked.get(Config.NIMBUS_THRIFT_TLS_CLIENT_KEYSTORE_PASSWORD));
226+
assertEquals("*****", masked.get(Config.NIMBUS_THRIFT_TLS_CLIENT_TRUSTSTORE_PASSWORD));
227+
assertEquals("*****", masked.get(Config.SUPERVISOR_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD));
228+
assertEquals("*****", masked.get(Config.SUPERVISOR_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD));
229+
}
230+
231+
@Test
232+
public void maskPasswords_masksZookeeperAndNettyTlsStorePasswords() {
233+
Map<String, Object> conf = new HashMap<>();
234+
conf.put(Config.STORM_ZOOKEEPER_SSL_KEYSTORE_PASSWORD, "zk-ks");
235+
conf.put(Config.STORM_ZOOKEEPER_SSL_TRUSTSTORE_PASSWORD, "zk-ts");
236+
conf.put(Config.STORM_MESSAGING_NETTY_TLS_KEYSTORE_PASSWORD, "netty-ks");
237+
conf.put(Config.STORM_MESSAGING_NETTY_TLS_TRUSTSTORE_PASSWORD, "netty-ts");
238+
239+
Map<String, Object> masked = ConfigUtils.maskPasswords(conf);
240+
241+
assertEquals("*****", masked.get(Config.STORM_ZOOKEEPER_SSL_KEYSTORE_PASSWORD));
242+
assertEquals("*****", masked.get(Config.STORM_ZOOKEEPER_SSL_TRUSTSTORE_PASSWORD));
243+
assertEquals("*****", masked.get(Config.STORM_MESSAGING_NETTY_TLS_KEYSTORE_PASSWORD));
244+
assertEquals("*****", masked.get(Config.STORM_MESSAGING_NETTY_TLS_TRUSTSTORE_PASSWORD));
245+
}
246+
247+
@Test
248+
public void maskCredentials_masksKeysThatOnlyPluginsDeclare() {
249+
Map<String, Object> conf = new HashMap<>();
250+
conf.put("storm.daemon.metrics.reporter.plugin.prometheus.basic_auth_password", "plugin-secret");
251+
conf.put("storm.zookeeper.auth.password", "zk-pass");
252+
conf.put("some.plugin.shared_secret", "shared");
253+
254+
Map<String, Object> masked = ConfigUtils.maskCredentials(conf);
255+
256+
assertEquals("*****", masked.get("storm.daemon.metrics.reporter.plugin.prometheus.basic_auth_password"));
257+
assertEquals("*****", masked.get("storm.zookeeper.auth.password"));
258+
assertEquals("*****", masked.get("some.plugin.shared_secret"));
259+
}
260+
261+
@Test
262+
public void maskCredentials_masksTheAnnotatedKeysAsWell() {
263+
Map<String, Object> conf = new HashMap<>();
264+
conf.put(Config.STORM_ZOOKEEPER_AUTH_PAYLOAD, "zk-user:zk-secret");
265+
266+
assertEquals("*****", ConfigUtils.maskCredentials(conf).get(Config.STORM_ZOOKEEPER_AUTH_PAYLOAD));
267+
}
268+
269+
@Test
270+
public void maskCredentials_leavesNonStringValuesAlone() {
271+
Map<String, Object> conf = new HashMap<>();
272+
conf.put("task.credentials.poll.secs", 30);
273+
conf.put("nimbus.credential.renewers.freq.secs", 600);
274+
conf.put("topology.auto-credentials", Collections.singletonList("org.example.AutoCreds"));
275+
conf.put("nimbus.seeds", Collections.singletonList("nimbus1"));
276+
277+
Map<String, Object> masked = ConfigUtils.maskCredentials(conf);
278+
279+
assertEquals(30, masked.get("task.credentials.poll.secs"));
280+
assertEquals(600, masked.get("nimbus.credential.renewers.freq.secs"));
281+
assertEquals(Collections.singletonList("org.example.AutoCreds"), masked.get("topology.auto-credentials"));
282+
assertEquals(Collections.singletonList("nimbus1"), masked.get("nimbus.seeds"));
283+
}
284+
285+
@Test
286+
public void isCredentialKey_recognisesAnnotatedAndPluginDeclaredKeys() {
287+
assertTrue(ConfigUtils.isCredentialKey(Config.STORM_ZOOKEEPER_AUTH_PAYLOAD));
288+
assertTrue(ConfigUtils.isCredentialKey(Config.NIMBUS_THRIFT_TLS_CLIENT_KEYSTORE_PASSWORD));
289+
assertTrue(ConfigUtils.isCredentialKey("storm.daemon.metrics.reporter.plugin.prometheus.basic_auth_password"));
290+
assertTrue(ConfigUtils.isCredentialKey("some.plugin.shared_secret"));
291+
}
292+
293+
@Test
294+
public void isCredentialKey_ignoresKeysThatOnlyMentionCredentials() {
295+
assertFalse(ConfigUtils.isCredentialKey("task.credentials.poll.secs"));
296+
assertFalse(ConfigUtils.isCredentialKey(Config.TOPOLOGY_AUTO_CREDENTIALS));
297+
assertFalse(ConfigUtils.isCredentialKey(Config.NIMBUS_THRIFT_TLS_CLIENT_KEYSTORE_PATH));
298+
assertFalse(ConfigUtils.isCredentialKey(Config.TOPOLOGY_NAME));
299+
}
300+
301+
@Test
302+
public void maskPasswords_keepsOrdinaryValues() {
303+
Map<String, Object> conf = new HashMap<>();
304+
conf.put(Config.STORM_ZOOKEEPER_SERVERS, Collections.singletonList("zk1"));
305+
conf.put(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PATH, "/etc/storm/nimbus.jks");
306+
307+
Map<String, Object> masked = ConfigUtils.maskPasswords(conf);
308+
309+
assertEquals(Collections.singletonList("zk1"), masked.get(Config.STORM_ZOOKEEPER_SERVERS));
310+
assertEquals("/etc/storm/nimbus.jks", masked.get(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PATH));
311+
}
198312
}

storm-core/src/jvm/org/apache/storm/command/UploadCredentials.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.storm.StormSubmitter;
2525
import org.apache.storm.generated.Nimbus;
2626
import org.apache.storm.generated.TopologySummary;
27+
import org.apache.storm.utils.ConfigUtils;
2728
import org.apache.storm.utils.NimbusClient;
2829
import org.apache.storm.utils.Utils;
2930
import org.slf4j.Logger;
@@ -113,6 +114,9 @@ public static void main(String[] args) throws Exception {
113114
*/
114115
topologyConf.remove("java.security.auth.login.config");
115116
topologyConf.remove(Config.NIMBUS_THRIFT_CLIENT_USE_TLS);
117+
// Nimbus masks credentials before serving a conf, so these entries hold no usable value here.
118+
// Dropping them lets the client's own configuration supply them, e.g. TLS store passwords.
119+
topologyConf.keySet().removeIf(ConfigUtils::isCredentialKey);
116120

117121
boolean throwExceptionForEmptyCreds = (boolean) cl.get("e");
118122
boolean hasCreds = StormSubmitter.pushCredentials(topologyName, topologyConf, credentialsMap, (String) cl.get("u"));

storm-server/src/main/java/org/apache/storm/daemon/nimbus/Nimbus.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4468,7 +4468,7 @@ public String getNimbusConf() throws AuthorizationException, TException {
44684468
try {
44694469
getNimbusConfCalls.mark();
44704470
checkAuthorization(null, null, "getNimbusConf");
4471-
return JSONValue.toJSONString(conf);
4471+
return JSONValue.toJSONString(ConfigUtils.maskCredentials(conf));
44724472
} catch (Exception e) {
44734473
LOG.warn("get nimbus conf exception.", e);
44744474
if (e instanceof TException) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.storm.daemon.nimbus;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
import net.minidev.json.JSONValue;
25+
import org.apache.storm.Config;
26+
import org.apache.storm.DaemonConfig;
27+
import org.apache.storm.LocalCluster;
28+
import org.junit.jupiter.api.Test;
29+
30+
import static org.junit.jupiter.api.Assertions.assertEquals;
31+
32+
public class NimbusGetNimbusConfTest {
33+
34+
private static final String MASKED = "*****";
35+
36+
@SuppressWarnings("unchecked")
37+
private static Map<String, Object> parse(String json) {
38+
return (Map<String, Object>) JSONValue.parse(json);
39+
}
40+
41+
@Test
42+
public void getNimbusConfMasksCredentialsAndKeepsOtherValues() throws Exception {
43+
Map<String, Object> daemonConf = new HashMap<>();
44+
daemonConf.put(DaemonConfig.NIMBUS_AUTHORIZER, "org.apache.storm.security.auth.authorizer.NoopAuthorizer");
45+
daemonConf.put(DaemonConfig.SUPERVISOR_AUTHORIZER, "org.apache.storm.security.auth.authorizer.NoopAuthorizer");
46+
daemonConf.put(Config.STORM_ZOOKEEPER_AUTH_PAYLOAD, "zk-user:zk-secret");
47+
daemonConf.put(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD, "nimbus-keystore-secret");
48+
daemonConf.put(DaemonConfig.UI_HTTPS_KEYSTORE_PASSWORD, "ui-keystore-secret");
49+
daemonConf.put(Config.STORM_ZOOKEEPER_SSL_KEYSTORE_PASSWORD, "zk-ssl-keystore-secret");
50+
daemonConf.put("storm.daemon.metrics.reporter.plugin.prometheus.basic_auth_password", "plugin-secret");
51+
daemonConf.put(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PATH, "/etc/storm/nimbus.jks");
52+
53+
try (LocalCluster cluster = new LocalCluster.Builder().withDaemonConf(daemonConf).build()) {
54+
Map<String, Object> served = parse(cluster.getNimbus().getNimbusConf());
55+
56+
assertEquals(MASKED, served.get(Config.STORM_ZOOKEEPER_AUTH_PAYLOAD),
57+
"the cluster ZooKeeper auth payload should be masked");
58+
assertEquals(MASKED, served.get(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD),
59+
"thrift TLS store passwords should be masked");
60+
assertEquals(MASKED, served.get(DaemonConfig.UI_HTTPS_KEYSTORE_PASSWORD),
61+
"UI keystore passwords should be masked");
62+
63+
assertEquals(MASKED, served.get(Config.STORM_ZOOKEEPER_SSL_KEYSTORE_PASSWORD),
64+
"ZooKeeper TLS store passwords should be masked");
65+
assertEquals(MASKED, served.get("storm.daemon.metrics.reporter.plugin.prometheus.basic_auth_password"),
66+
"credential keys that only a plugin declares should be masked too");
67+
68+
assertEquals("/etc/storm/nimbus.jks", served.get(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PATH),
69+
"non-credential values should be served unchanged");
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)