Skip to content

Commit bbd0930

Browse files
authored
Add whitelist to watcher HttpClient (#36817)
This adds a configurable whitelist to the HTTP client in watcher. By default every URL is allowed to retain BWC. A dynamically configurable setting named "xpack.http.whitelist" was added that allows to configure an array of URLs, which can also contain simple regexes. Closes #29937
1 parent 37493c2 commit bbd0930

File tree

10 files changed

+286
-35
lines changed

10 files changed

+286
-35
lines changed

docs/reference/settings/notification-settings.asciidoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ request is aborted.
6464
Specifies the maximum size an HTTP response is allowed to have, defaults to
6565
`10mb`, the maximum configurable value is `50mb`.
6666

67+
`xpack.http.whitelist`::
68+
A list of URLs, that the internal HTTP client is allowed to connect to. This
69+
client is used in the HTTP input, the webhook, the slack, pagerduty, hipchat
70+
and jira actions. This setting can be updated dynamically. It defaults to `*`
71+
allowing everything. Note: If you configure this setting and you are using one
72+
of the slack/pagerduty/hipchat actions, you have to ensure that the
73+
corresponding endpoints are whitelisted as well.
74+
6775
[[ssl-notification-settings]]
6876
:ssl-prefix: xpack.http
6977
:component: {watcher}

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
273273
new WatcherIndexTemplateRegistry(clusterService, threadPool, client);
274274

275275
// http client
276-
httpClient = new HttpClient(settings, getSslService(), cryptoService);
276+
httpClient = new HttpClient(settings, getSslService(), cryptoService, clusterService);
277277

278278
// notification
279279
EmailService emailService = new EmailService(settings, cryptoService, clusterService.getClusterSettings());

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpClient.java

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import org.apache.http.Header;
99
import org.apache.http.HttpHeaders;
1010
import org.apache.http.HttpHost;
11+
import org.apache.http.HttpRequestInterceptor;
1112
import org.apache.http.NameValuePair;
13+
import org.apache.http.ProtocolException;
1214
import org.apache.http.auth.AuthScope;
1315
import org.apache.http.auth.Credentials;
1416
import org.apache.http.auth.UsernamePasswordCredentials;
@@ -19,6 +21,7 @@
1921
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
2022
import org.apache.http.client.methods.HttpHead;
2123
import org.apache.http.client.methods.HttpRequestBase;
24+
import org.apache.http.client.methods.HttpRequestWrapper;
2225
import org.apache.http.client.protocol.HttpClientContext;
2326
import org.apache.http.client.utils.URIUtils;
2427
import org.apache.http.client.utils.URLEncodedUtils;
@@ -31,11 +34,20 @@
3134
import org.apache.http.impl.client.BasicAuthCache;
3235
import org.apache.http.impl.client.BasicCredentialsProvider;
3336
import org.apache.http.impl.client.CloseableHttpClient;
37+
import org.apache.http.impl.client.DefaultRedirectStrategy;
3438
import org.apache.http.impl.client.HttpClientBuilder;
3539
import org.apache.http.message.BasicNameValuePair;
40+
import org.apache.http.protocol.HttpContext;
3641
import org.apache.logging.log4j.LogManager;
3742
import org.apache.logging.log4j.Logger;
43+
import org.apache.lucene.util.automaton.Automaton;
44+
import org.apache.lucene.util.automaton.CharacterRunAutomaton;
45+
import org.apache.lucene.util.automaton.MinimizationOperations;
46+
import org.apache.lucene.util.automaton.Operations;
47+
import org.elasticsearch.ElasticsearchException;
48+
import org.elasticsearch.cluster.service.ClusterService;
3849
import org.elasticsearch.common.Strings;
50+
import org.elasticsearch.common.regex.Regex;
3951
import org.elasticsearch.common.settings.Settings;
4052
import org.elasticsearch.common.unit.ByteSizeValue;
4153
import org.elasticsearch.common.unit.TimeValue;
@@ -59,6 +71,7 @@
5971
import java.util.HashMap;
6072
import java.util.List;
6173
import java.util.Map;
74+
import java.util.concurrent.atomic.AtomicReference;
6275

6376
public class HttpClient implements Closeable {
6477

@@ -69,20 +82,29 @@ public class HttpClient implements Closeable {
6982
private static final int MAX_CONNECTIONS = 500;
7083
private static final Logger logger = LogManager.getLogger(HttpClient.class);
7184

85+
private final AtomicReference<CharacterRunAutomaton> whitelistAutomaton = new AtomicReference<>();
7286
private final CloseableHttpClient client;
7387
private final HttpProxy settingsProxy;
7488
private final TimeValue defaultConnectionTimeout;
7589
private final TimeValue defaultReadTimeout;
7690
private final ByteSizeValue maxResponseSize;
7791
private final CryptoService cryptoService;
92+
private final SSLService sslService;
7893

79-
public HttpClient(Settings settings, SSLService sslService, CryptoService cryptoService) {
94+
public HttpClient(Settings settings, SSLService sslService, CryptoService cryptoService, ClusterService clusterService) {
8095
this.defaultConnectionTimeout = HttpSettings.CONNECTION_TIMEOUT.get(settings);
8196
this.defaultReadTimeout = HttpSettings.READ_TIMEOUT.get(settings);
8297
this.maxResponseSize = HttpSettings.MAX_HTTP_RESPONSE_SIZE.get(settings);
8398
this.settingsProxy = getProxyFromSettings(settings);
8499
this.cryptoService = cryptoService;
100+
this.sslService = sslService;
85101

102+
setWhitelistAutomaton(HttpSettings.HOSTS_WHITELIST.get(settings));
103+
clusterService.getClusterSettings().addSettingsUpdateConsumer(HttpSettings.HOSTS_WHITELIST, this::setWhitelistAutomaton);
104+
this.client = createHttpClient();
105+
}
106+
107+
private CloseableHttpClient createHttpClient() {
86108
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
87109

88110
// ssl setup
@@ -95,8 +117,48 @@ public HttpClient(Settings settings, SSLService sslService, CryptoService crypto
95117
clientBuilder.evictExpiredConnections();
96118
clientBuilder.setMaxConnPerRoute(MAX_CONNECTIONS);
97119
clientBuilder.setMaxConnTotal(MAX_CONNECTIONS);
120+
clientBuilder.setRedirectStrategy(new DefaultRedirectStrategy() {
121+
@Override
122+
public boolean isRedirected(org.apache.http.HttpRequest request, org.apache.http.HttpResponse response,
123+
HttpContext context) throws ProtocolException {
124+
boolean isRedirected = super.isRedirected(request, response, context);
125+
if (isRedirected) {
126+
String host = response.getHeaders("Location")[0].getValue();
127+
if (isWhitelisted(host) == false) {
128+
throw new ElasticsearchException("host [" + host + "] is not whitelisted in setting [" +
129+
HttpSettings.HOSTS_WHITELIST.getKey() + "], will not redirect");
130+
}
131+
}
132+
133+
return isRedirected;
134+
}
135+
});
136+
137+
clientBuilder.addInterceptorFirst((HttpRequestInterceptor) (request, context) -> {
138+
if (request instanceof HttpRequestWrapper == false) {
139+
throw new ElasticsearchException("unable to check request [{}/{}] for white listing", request,
140+
request.getClass().getName());
141+
}
142+
143+
HttpRequestWrapper wrapper = ((HttpRequestWrapper) request);
144+
final String host;
145+
if (wrapper.getTarget() != null) {
146+
host = wrapper.getTarget().toURI();
147+
} else {
148+
host = wrapper.getOriginal().getRequestLine().getUri();
149+
}
98150

99-
client = clientBuilder.build();
151+
if (isWhitelisted(host) == false) {
152+
throw new ElasticsearchException("host [" + host + "] is not whitelisted in setting [" +
153+
HttpSettings.HOSTS_WHITELIST.getKey() + "], will not connect");
154+
}
155+
});
156+
157+
return clientBuilder.build();
158+
}
159+
160+
private void setWhitelistAutomaton(List<String> whiteListedHosts) {
161+
whitelistAutomaton.set(createAutomaton(whiteListedHosts));
100162
}
101163

102164
public HttpResponse execute(HttpRequest request) throws IOException {
@@ -285,6 +347,24 @@ final class HttpMethodWithEntity extends HttpEntityEnclosingRequestBase {
285347
public String getMethod() {
286348
return methodName;
287349
}
350+
288351
}
289352

353+
private boolean isWhitelisted(String host) {
354+
return whitelistAutomaton.get().run(host);
355+
}
356+
357+
private static final CharacterRunAutomaton MATCH_ALL_AUTOMATON = new CharacterRunAutomaton(Regex.simpleMatchToAutomaton("*"));
358+
// visible for testing
359+
static CharacterRunAutomaton createAutomaton(List<String> whiteListedHosts) {
360+
if (whiteListedHosts.isEmpty()) {
361+
// the default is to accept everything, this should change in the next major version, being 8.0
362+
// we could emit depreciation warning here, if the whitelist is empty
363+
return MATCH_ALL_AUTOMATON;
364+
}
365+
366+
Automaton whiteListAutomaton = Regex.simpleMatchToAutomaton(whiteListedHosts.toArray(Strings.EMPTY_ARRAY));
367+
whiteListAutomaton = MinimizationOperations.minimize(whiteListAutomaton, Operations.DEFAULT_MAX_DETERMINIZED_STATES);
368+
return new CharacterRunAutomaton(whiteListAutomaton);
369+
}
290370
}

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpRequest.java

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.HashMap;
3636
import java.util.Map;
3737
import java.util.Objects;
38+
import java.util.stream.Collectors;
3839

3940
import static java.util.Collections.emptyMap;
4041
import static java.util.Collections.unmodifiableMap;
@@ -154,10 +155,8 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params toX
154155
builder.field(Field.PARAMS.getPreferredName(), this.params);
155156
}
156157
if (headers.isEmpty() == false) {
157-
if (WatcherParams.hideSecrets(toXContentParams) && headers.containsKey("Authorization")) {
158-
Map<String, String> sanitizedHeaders = new HashMap<>(headers);
159-
sanitizedHeaders.put("Authorization", WatcherXContentParser.REDACTED_PASSWORD);
160-
builder.field(Field.HEADERS.getPreferredName(), sanitizedHeaders);
158+
if (WatcherParams.hideSecrets(toXContentParams)) {
159+
builder.field(Field.HEADERS.getPreferredName(), sanitizeHeaders(headers));
161160
} else {
162161
builder.field(Field.HEADERS.getPreferredName(), headers);
163162
}
@@ -184,6 +183,15 @@ public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params toX
184183
return builder.endObject();
185184
}
186185

186+
private Map<String, String> sanitizeHeaders(Map<String, String> headers) {
187+
if (headers.containsKey("Authorization") == false) {
188+
return headers;
189+
}
190+
Map<String, String> sanitizedHeaders = new HashMap<>(headers);
191+
sanitizedHeaders.put("Authorization", WatcherXContentParser.REDACTED_PASSWORD);
192+
return sanitizedHeaders;
193+
}
194+
187195
@Override
188196
public boolean equals(Object o) {
189197
if (this == o) return true;
@@ -220,16 +228,9 @@ public String toString() {
220228
sb.append("port=[").append(port).append("], ");
221229
sb.append("path=[").append(path).append("], ");
222230
if (!headers.isEmpty()) {
223-
sb.append(", headers=[");
224-
boolean first = true;
225-
for (Map.Entry<String, String> header : headers.entrySet()) {
226-
if (!first) {
227-
sb.append(", ");
228-
}
229-
sb.append("[").append(header.getKey()).append(": ").append(header.getValue()).append("]");
230-
first = false;
231-
}
232-
sb.append("], ");
231+
sb.append(sanitizeHeaders(headers).entrySet().stream()
232+
.map(header -> header.getKey() + ": " + header.getValue())
233+
.collect(Collectors.joining(", ", "headers=[", "], ")));
233234
}
234235
if (auth != null) {
235236
sb.append("auth=[").append(BasicAuth.TYPE).append("], ");

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpSettings.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings;
1414

1515
import java.util.ArrayList;
16+
import java.util.Collections;
1617
import java.util.List;
18+
import java.util.function.Function;
1719

1820
/**
1921
* Handles the configuration and parsing of settings for the <code>xpack.http.</code> prefix
@@ -36,6 +38,8 @@ public class HttpSettings {
3638
static final Setting<String> PROXY_HOST = Setting.simpleString(PROXY_HOST_KEY, Property.NodeScope);
3739
static final Setting<String> PROXY_SCHEME = Setting.simpleString(PROXY_SCHEME_KEY, Scheme::parse, Property.NodeScope);
3840
static final Setting<Integer> PROXY_PORT = Setting.intSetting(PROXY_PORT_KEY, 0, 0, 0xFFFF, Property.NodeScope);
41+
static final Setting<List<String>> HOSTS_WHITELIST = Setting.listSetting("xpack.http.whitelist", Collections.singletonList("*"),
42+
Function.identity(), Property.NodeScope, Property.Dynamic);
3943

4044
static final Setting<ByteSizeValue> MAX_HTTP_RESPONSE_SIZE = Setting.byteSizeSetting("xpack.http.max_response_size",
4145
new ByteSizeValue(10, ByteSizeUnit.MB), // default
@@ -54,6 +58,7 @@ public static List<? extends Setting<?>> getSettings() {
5458
settings.add(PROXY_PORT);
5559
settings.add(PROXY_SCHEME);
5660
settings.add(MAX_HTTP_RESPONSE_SIZE);
61+
settings.add(HOSTS_WHITELIST);
5762
return settings;
5863
}
5964

x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/webhook/WebhookActionTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds;
4949
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
50+
import static org.elasticsearch.xpack.watcher.common.http.HttpClientTests.mockClusterService;
5051
import static org.hamcrest.CoreMatchers.instanceOf;
5152
import static org.hamcrest.CoreMatchers.notNullValue;
5253
import static org.hamcrest.Matchers.containsString;
@@ -214,7 +215,8 @@ private WebhookActionFactory webhookFactory(HttpClient client) {
214215
public void testThatSelectingProxyWorks() throws Exception {
215216
Environment environment = TestEnvironment.newEnvironment(Settings.builder().put("path.home", createTempDir()).build());
216217

217-
try (HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment.settings(), environment), null);
218+
try (HttpClient httpClient = new HttpClient(Settings.EMPTY, new SSLService(environment.settings(), environment), null,
219+
mockClusterService());
218220
MockWebServer proxyServer = new MockWebServer()) {
219221
proxyServer.start();
220222
proxyServer.enqueue(new MockResponse().setResponseCode(200).setBody("fullProxiedContent"));

0 commit comments

Comments
 (0)