Add an option to enable HSTS in druid services (#13489)

* Add an option to enable HSTS

* Fix code and add docs

* Deduplicate headers

* unused import

* Fix spelling
This commit is contained in:
Abhishek Agarwal 2023-01-10 22:31:51 +05:30 committed by GitHub
parent 2503095296
commit 17936e2920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 76 additions and 11 deletions

View File

@ -1823,6 +1823,7 @@ Druid uses Jetty to serve HTTP requests. Each query being processed consumes a s
|`druid.server.http.maxQueryTimeout`|Maximum allowed value (in milliseconds) for `timeout` parameter. See [query-context](../querying/query-context.md) to know more about `timeout`. Query is rejected if the query context `timeout` is greater than this value. |Long.MAX_VALUE| |`druid.server.http.maxQueryTimeout`|Maximum allowed value (in milliseconds) for `timeout` parameter. See [query-context](../querying/query-context.md) to know more about `timeout`. Query is rejected if the query context `timeout` is greater than this value. |Long.MAX_VALUE|
|`druid.server.http.maxRequestHeaderSize`|Maximum size of a request header in bytes. Larger headers consume more memory and can make a server more vulnerable to denial of service attacks. |8 * 1024| |`druid.server.http.maxRequestHeaderSize`|Maximum size of a request header in bytes. Larger headers consume more memory and can make a server more vulnerable to denial of service attacks. |8 * 1024|
|`druid.server.http.contentSecurityPolicy`|Content-Security-Policy header value to set on each non-POST response. Setting this property to an empty string, or omitting it, both result in the default `frame-ancestors: none` being set.|`frame-ancestors 'none'`| |`druid.server.http.contentSecurityPolicy`|Content-Security-Policy header value to set on each non-POST response. Setting this property to an empty string, or omitting it, both result in the default `frame-ancestors: none` being set.|`frame-ancestors 'none'`|
|`druid.server.http.enableHSTS`|If set to true, druid services will add strict transport security header `Strict-Transport-Security: max-age=63072000; includeSubDomains` to all HTTP responses|`false`|
##### Client Configuration ##### Client Configuration

View File

@ -131,6 +131,10 @@
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId> <artifactId>jetty-proxy</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.code.findbugs</groupId> <groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId> <artifactId>jsr305</artifactId>

View File

@ -67,7 +67,8 @@ public class ServerConfig
@NotNull List<String> allowedHttpMethods, @NotNull List<String> allowedHttpMethods,
boolean showDetailedJettyErrors, boolean showDetailedJettyErrors,
@NotNull ErrorResponseTransformStrategy errorResponseTransformStrategy, @NotNull ErrorResponseTransformStrategy errorResponseTransformStrategy,
@Nullable String contentSecurityPolicy @Nullable String contentSecurityPolicy,
boolean enableHSTS
) )
{ {
this.numThreads = numThreads; this.numThreads = numThreads;
@ -88,6 +89,7 @@ public class ServerConfig
this.showDetailedJettyErrors = showDetailedJettyErrors; this.showDetailedJettyErrors = showDetailedJettyErrors;
this.errorResponseTransformStrategy = errorResponseTransformStrategy; this.errorResponseTransformStrategy = errorResponseTransformStrategy;
this.contentSecurityPolicy = contentSecurityPolicy; this.contentSecurityPolicy = contentSecurityPolicy;
this.enableHSTS = enableHSTS;
} }
public ServerConfig() public ServerConfig()
@ -161,6 +163,9 @@ public class ServerConfig
@JsonProperty("contentSecurityPolicy") @JsonProperty("contentSecurityPolicy")
private String contentSecurityPolicy; private String contentSecurityPolicy;
@JsonProperty
private boolean enableHSTS = false;
@JsonProperty @JsonProperty
private boolean showDetailedJettyErrors = true; private boolean showDetailedJettyErrors = true;
@ -255,6 +260,11 @@ public class ServerConfig
return contentSecurityPolicy; return contentSecurityPolicy;
} }
public boolean isEnableHSTS()
{
return enableHSTS;
}
@Override @Override
public boolean equals(Object o) public boolean equals(Object o)
{ {
@ -282,7 +292,8 @@ public class ServerConfig
unannouncePropagationDelay.equals(that.unannouncePropagationDelay) && unannouncePropagationDelay.equals(that.unannouncePropagationDelay) &&
allowedHttpMethods.equals(that.allowedHttpMethods) && allowedHttpMethods.equals(that.allowedHttpMethods) &&
errorResponseTransformStrategy.equals(that.errorResponseTransformStrategy) && errorResponseTransformStrategy.equals(that.errorResponseTransformStrategy) &&
Objects.equals(contentSecurityPolicy, that.getContentSecurityPolicy()); Objects.equals(contentSecurityPolicy, that.getContentSecurityPolicy()) &&
enableHSTS == that.enableHSTS;
} }
@Override @Override
@ -306,7 +317,8 @@ public class ServerConfig
allowedHttpMethods, allowedHttpMethods,
errorResponseTransformStrategy, errorResponseTransformStrategy,
showDetailedJettyErrors, showDetailedJettyErrors,
contentSecurityPolicy contentSecurityPolicy,
enableHSTS
); );
} }
@ -332,6 +344,7 @@ public class ServerConfig
", errorResponseTransformStrategy=" + errorResponseTransformStrategy + ", errorResponseTransformStrategy=" + errorResponseTransformStrategy +
", showDetailedJettyErrors=" + showDetailedJettyErrors + ", showDetailedJettyErrors=" + showDetailedJettyErrors +
", contentSecurityPolicy=" + contentSecurityPolicy + ", contentSecurityPolicy=" + contentSecurityPolicy +
", enableHSTS=" + enableHSTS +
'}'; '}';
} }

View File

@ -163,7 +163,8 @@ public class CliIndexerServerModule implements Module
oldConfig.getAllowedHttpMethods(), oldConfig.getAllowedHttpMethods(),
oldConfig.isShowDetailedJettyErrors(), oldConfig.isShowDetailedJettyErrors(),
oldConfig.getErrorResponseTransformStrategy(), oldConfig.getErrorResponseTransformStrategy(),
oldConfig.getContentSecurityPolicy() oldConfig.getContentSecurityPolicy(),
oldConfig.isEnableHSTS()
); );
} }
} }

View File

@ -23,8 +23,12 @@ import com.google.inject.Injector;
import com.google.inject.Key; import com.google.inject.Key;
import com.google.inject.TypeLiteral; import com.google.inject.TypeLiteral;
import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.ISE;
import org.apache.druid.server.initialization.ServerConfig;
import org.apache.druid.server.security.AllowHttpMethodsResourceFilter; import org.apache.druid.server.security.AllowHttpMethodsResourceFilter;
import org.eclipse.jetty.rewrite.handler.HeaderPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterHolder;
@ -32,6 +36,7 @@ import org.eclipse.jetty.servlet.FilterMapping;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -122,4 +127,25 @@ public class JettyServerInitUtils
null null
); );
} }
public static void maybeAddHSTSPatternRule(ServerConfig serverConfig, RewriteHandler rewriteHandler)
{
if (serverConfig.isEnableHSTS()) {
rewriteHandler.addRule(getHSTSHeaderPattern());
}
}
public static void maybeAddHSTSRewriteHandler(ServerConfig serverConfig, HandlerList handlerList)
{
if (serverConfig.isEnableHSTS()) {
RewriteHandler rewriteHandler = new RewriteHandler();
rewriteHandler.addRule(getHSTSHeaderPattern());
handlerList.addHandler(rewriteHandler);
}
}
private static HeaderPatternRule getHSTSHeaderPattern()
{
return new HeaderPatternRule("*", "Strict-Transport-Security", "max-age=63072000; includeSubDomains");
}
} }

View File

@ -37,6 +37,7 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
@ -48,7 +49,9 @@ import java.util.Set;
*/ */
public class StandardResponseHeaderFilterHolder implements ServletFilterHolder public class StandardResponseHeaderFilterHolder implements ServletFilterHolder
{ {
private static final Set<String> STANDARD_HEADERS = ImmutableSet.of("Cache-Control", "Content-Security-Policy"); private static final Set<String> STANDARD_HEADERS = ImmutableSet.of("Cache-Control",
"Content-Security-Policy",
"Strict-Transport-Security");
private static final String DEFAULT_CONTENT_SECURITY_POLICY = "frame-ancestors 'none'"; private static final String DEFAULT_CONTENT_SECURITY_POLICY = "frame-ancestors 'none'";
private final String contentSecurityPolicy; private final String contentSecurityPolicy;

View File

@ -42,6 +42,7 @@ public class ServerConfigTest
ServerConfig defaultConfig2 = OBJECT_MAPPER.readValue(defaultConfigJson, ServerConfig.class); ServerConfig defaultConfig2 = OBJECT_MAPPER.readValue(defaultConfigJson, ServerConfig.class);
Assert.assertEquals(defaultConfig, defaultConfig2); Assert.assertEquals(defaultConfig, defaultConfig2);
Assert.assertFalse(defaultConfig2.isEnableForwardedRequestCustomizer()); Assert.assertFalse(defaultConfig2.isEnableForwardedRequestCustomizer());
Assert.assertFalse(defaultConfig2.isEnableHSTS());
ServerConfig modifiedConfig = new ServerConfig( ServerConfig modifiedConfig = new ServerConfig(
999, 999,
@ -61,7 +62,8 @@ public class ServerConfigTest
ImmutableList.of(HttpMethod.OPTIONS), ImmutableList.of(HttpMethod.OPTIONS),
true, true,
new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of(".*")), new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of(".*")),
"my-cool-policy" "my-cool-policy",
true
); );
String modifiedConfigJson = OBJECT_MAPPER.writeValueAsString(modifiedConfig); String modifiedConfigJson = OBJECT_MAPPER.writeValueAsString(modifiedConfig);
ServerConfig modifiedConfig2 = OBJECT_MAPPER.readValue(modifiedConfigJson, ServerConfig.class); ServerConfig modifiedConfig2 = OBJECT_MAPPER.readValue(modifiedConfigJson, ServerConfig.class);
@ -74,6 +76,7 @@ public class ServerConfigTest
Assert.assertTrue(modifiedConfig2.getAllowedHttpMethods().contains(HttpMethod.OPTIONS)); Assert.assertTrue(modifiedConfig2.getAllowedHttpMethods().contains(HttpMethod.OPTIONS));
Assert.assertEquals("my-cool-policy", modifiedConfig.getContentSecurityPolicy()); Assert.assertEquals("my-cool-policy", modifiedConfig.getContentSecurityPolicy());
Assert.assertEquals("my-cool-policy", modifiedConfig2.getContentSecurityPolicy()); Assert.assertEquals("my-cool-policy", modifiedConfig2.getContentSecurityPolicy());
Assert.assertTrue(modifiedConfig2.isEnableHSTS());
} }
@Test @Test

View File

@ -122,6 +122,7 @@ import org.apache.druid.server.security.Authenticator;
import org.apache.druid.server.security.AuthenticatorMapper; import org.apache.druid.server.security.AuthenticatorMapper;
import org.apache.druid.tasklogs.TaskLogStreamer; import org.apache.druid.tasklogs.TaskLogStreamer;
import org.apache.druid.tasklogs.TaskLogs; import org.apache.druid.tasklogs.TaskLogs;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.HandlerList;
@ -449,10 +450,13 @@ public class CliOverlord extends ServerRunnable
root.addFilter(GuiceFilter.class, "/druid-ext/*", null); root.addFilter(GuiceFilter.class, "/druid-ext/*", null);
RewriteHandler rewriteHandler = WebConsoleJettyServerInitializer.createWebConsoleRewriteHandler();
JettyServerInitUtils.maybeAddHSTSPatternRule(serverConfig, rewriteHandler);
HandlerList handlerList = new HandlerList(); HandlerList handlerList = new HandlerList();
handlerList.setHandlers( handlerList.setHandlers(
new Handler[]{ new Handler[]{
WebConsoleJettyServerInitializer.createWebConsoleRewriteHandler(), rewriteHandler,
JettyServerInitUtils.getJettyRequestLogHandler(), JettyServerInitUtils.getJettyRequestLogHandler(),
JettyServerInitUtils.wrapWithDefaultGzipHandler( JettyServerInitUtils.wrapWithDefaultGzipHandler(
root, root,

View File

@ -35,6 +35,7 @@ import org.apache.druid.server.security.AuthConfig;
import org.apache.druid.server.security.AuthenticationUtils; import org.apache.druid.server.security.AuthenticationUtils;
import org.apache.druid.server.security.Authenticator; import org.apache.druid.server.security.Authenticator;
import org.apache.druid.server.security.AuthenticatorMapper; import org.apache.druid.server.security.AuthenticatorMapper;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.HandlerList;
@ -131,10 +132,13 @@ class CoordinatorJettyServerInitializer implements JettyServerInitializer
root.addServlet(new ServletHolder(injector.getInstance(OverlordProxyServlet.class)), "/druid/indexer/*"); root.addServlet(new ServletHolder(injector.getInstance(OverlordProxyServlet.class)), "/druid/indexer/*");
} }
RewriteHandler rewriteHandler = WebConsoleJettyServerInitializer.createWebConsoleRewriteHandler();
JettyServerInitUtils.maybeAddHSTSPatternRule(serverConfig, rewriteHandler);
HandlerList handlerList = new HandlerList(); HandlerList handlerList = new HandlerList();
handlerList.setHandlers( handlerList.setHandlers(
new Handler[]{ new Handler[]{
WebConsoleJettyServerInitializer.createWebConsoleRewriteHandler(), rewriteHandler,
JettyServerInitUtils.getJettyRequestLogHandler(), JettyServerInitUtils.getJettyRequestLogHandler(),
JettyServerInitUtils.wrapWithDefaultGzipHandler( JettyServerInitUtils.wrapWithDefaultGzipHandler(
root, root,

View File

@ -108,6 +108,7 @@ class MiddleManagerJettyServerInitializer implements JettyServerInitializer
new DefaultHandler() new DefaultHandler()
} }
); );
JettyServerInitUtils.maybeAddHSTSRewriteHandler(serverConfig, handlerList);
server.setHandler(handlerList); server.setHandler(handlerList);
} }
} }

View File

@ -135,6 +135,8 @@ public class QueryJettyServerInitializer implements JettyServerInitializer
handlerList.addHandler(handler); handlerList.addHandler(handler);
} }
JettyServerInitUtils.maybeAddHSTSRewriteHandler(serverConfig, handlerList);
// Add Gzip handler at the very end // Add Gzip handler at the very end
handlerList.addHandler(JettyServerInitUtils.wrapWithDefaultGzipHandler( handlerList.addHandler(JettyServerInitUtils.wrapWithDefaultGzipHandler(
root, root,

View File

@ -41,6 +41,7 @@ import org.apache.druid.server.security.Authenticator;
import org.apache.druid.server.security.AuthenticatorMapper; import org.apache.druid.server.security.AuthenticatorMapper;
import org.apache.druid.sql.avatica.DruidAvaticaJsonHandler; import org.apache.druid.sql.avatica.DruidAvaticaJsonHandler;
import org.apache.druid.sql.avatica.DruidAvaticaProtobufHandler; import org.apache.druid.sql.avatica.DruidAvaticaProtobufHandler;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.HandlerList;
@ -146,10 +147,13 @@ public class RouterJettyServerInitializer implements JettyServerInitializer
root.addFilter(GuiceFilter.class, "/druid/router/*", null); root.addFilter(GuiceFilter.class, "/druid/router/*", null);
root.addFilter(GuiceFilter.class, "/druid-ext/*", null); root.addFilter(GuiceFilter.class, "/druid-ext/*", null);
RewriteHandler rewriteHandler = WebConsoleJettyServerInitializer.createWebConsoleRewriteHandler();
JettyServerInitUtils.maybeAddHSTSPatternRule(serverConfig, rewriteHandler);
final HandlerList handlerList = new HandlerList(); final HandlerList handlerList = new HandlerList();
handlerList.setHandlers( handlerList.setHandlers(
new Handler[]{ new Handler[]{
WebConsoleJettyServerInitializer.createWebConsoleRewriteHandler(), rewriteHandler,
JettyServerInitUtils.getJettyRequestLogHandler(), JettyServerInitUtils.getJettyRequestLogHandler(),
JettyServerInitUtils.wrapWithDefaultGzipHandler( JettyServerInitUtils.wrapWithDefaultGzipHandler(
root, root,

View File

@ -24,7 +24,6 @@ import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.server.security.AuthenticationUtils; import org.apache.druid.server.security.AuthenticationUtils;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule; import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler; import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
@ -57,7 +56,7 @@ class WebConsoleJettyServerInitializer
AuthenticationUtils.addNoopAuthorizationFilters(root, UNAUTHORIZED_PATHS_FOR_UI); AuthenticationUtils.addNoopAuthorizationFilters(root, UNAUTHORIZED_PATHS_FOR_UI);
} }
static Handler createWebConsoleRewriteHandler() static RewriteHandler createWebConsoleRewriteHandler()
{ {
// redirect all legacy web consoles to current unified web console // redirect all legacy web consoles to current unified web console
RewriteHandler rewrite = new RewriteHandler(); RewriteHandler rewrite = new RewriteHandler();