You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by gi...@apache.org on 2018/09/19 16:56:21 UTC

[incubator-druid] branch master updated: Mutual TLS support (#6076)

This is an automated email from the ASF dual-hosted git repository.

gian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 8972244  Mutual TLS support (#6076)
8972244 is described below

commit 8972244c6817cf8e0276a7904776103e172e6be3
Author: Jonathan Wei <jo...@users.noreply.github.com>
AuthorDate: Wed Sep 19 09:56:15 2018 -0700

    Mutual TLS support (#6076)
    
    * Mutual TLS support
    
    * Kafka test fixes
    
    * TeamCity fix
    
    * Split integration tests
    
    * Use localhost DOCKER_IP
    
    * Increase server thread count
    
    * Increase SSL handshake timeouts
    
    * Add broken pipe retries, use injected client config params
    
    * PR comments, Rat license check exclusion
---
 .travis.yml                                        |  25 +-
 ci/travis_script_integration.sh                    |   2 +-
 ...ation.sh => travis_script_integration_part2.sh} |   2 +-
 .../extensions-core/simple-client-sslcontext.md    |  11 +
 docs/content/operations/tls-support.md             |  14 +-
 .../org/apache/druid/https/SSLClientConfig.java    |  52 +++
 .../org/apache/druid/https/SSLContextProvider.java |  20 +-
 integration-tests/.gitignore                       |   6 +
 integration-tests/docker/Dockerfile                |  41 +-
 integration-tests/docker/broker.conf               |  20 +
 integration-tests/docker/coordinator.conf          |  21 +
 integration-tests/docker/historical.conf           |  20 +
 integration-tests/docker/metadata-storage.conf     |   3 +-
 integration-tests/docker/middlemanager.conf        |  24 +-
 integration-tests/docker/overlord.conf             |  21 +
 ...{router.conf => router-no-client-auth-tls.conf} |  27 +-
 .../{router.conf => router-permissive-tls.conf}    |  27 +-
 integration-tests/docker/router.conf               |  20 +
 integration-tests/docker/run-mysql.sh              |   3 +
 .../tls/generate-client-certs-and-keystores.sh     |  21 +
 .../docker/tls/generate-expired-client-cert.sh     |  41 ++
 .../docker/tls/generate-good-client-cert.sh        |  44 ++
 .../tls/generate-incorrect-hostname-client-cert.sh |  40 ++
 .../generate-invalid-intermediate-client-cert.sh   |  76 ++++
 .../docker/tls/generate-root-certs.sh              |  13 +
 .../tls/generate-server-certs-and-keystores.sh     |  66 +++
 .../tls/generate-to-be-revoked-client-cert.sh      |  43 ++
 .../tls/generate-untrusted-root-client-cert.sh     |  42 ++
 .../tls/generate-valid-intermediate-client-cert.sh |  76 ++++
 integration-tests/docker/tls/root.cnf              |  35 ++
 integration-tests/docker/tls/root2.cnf             |  13 +
 integration-tests/pom.xml                          |  17 +
 integration-tests/run_cluster.sh                   |  35 +-
 .../druid/testing/ConfigFileConfigProvider.java    | 138 +++++-
 .../apache/druid/testing/DockerConfigProvider.java |  54 +++
 .../druid/testing/IntegrationTestingConfig.java    |  18 +
 .../clients/CoordinatorResourceTestClient.java     |  24 +
 .../clients/EventReceiverFirehoseTestClient.java   |   2 +-
 .../clients/OverlordResourceTestClient.java        |   2 +-
 .../org/apache/druid/testing/utils/RetryUtil.java  |   4 +-
 .../druid/testing/utils/TestQueryHelper.java       |  23 +-
 .../java/io/druid/tests/security/ITTLSTest.java    | 482 +++++++++++++++++++++
 .../druid/tests/hadoop/ITHadoopIndexTest.java      |   2 +-
 .../indexer/AbstractITRealtimeIndexTaskTest.java   |   4 +-
 .../druid/tests/indexer/ITCompactionTaskTest.java  |  52 ++-
 .../tests/indexer/ITKafkaIndexingServiceTest.java  |   4 +-
 .../apache/druid/tests/indexer/ITKafkaTest.java    |   6 +-
 .../druid/tests/indexer/ITUnionQueryTest.java      |  28 ++
 .../test/resources/indexer/kafka_index_task.json   |   2 +-
 integration-tests/stop_cluster.sh                  |   7 +-
 pom.xml                                            |   1 +
 .../org/apache/druid/server/StatusResource.java    |   7 +-
 .../druid/server/emitter/HttpEmitterModule.java    |  14 +-
 .../druid/server/http/OverlordProxyServlet.java    |  29 +-
 .../server/initialization/TLSServerConfig.java     |  62 +++
 .../initialization/jetty/JettyServerModule.java    |  34 ++
 .../org/apache/druid/server/security/TLSUtils.java | 177 +++++++-
 .../server/http/OverlordProxyServletTest.java      |   2 +-
 .../java/org/apache/druid/cli/CliCoordinator.java  |   3 +
 59 files changed, 1996 insertions(+), 106 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index c0e093d..8750ca4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -93,8 +93,8 @@ matrix:
       services:
         - docker
       env:
-        - NAME="integration test"
-        - DOCKER_IP=172.17.0.1
+        - NAME="integration test part 1"
+        - DOCKER_IP=127.0.0.1
       install:
         # Only errors will be shown with the -q option. This is to avoid generating too many logs which make travis build failed.
         - mvn install -q -ff -DskipTests -B
@@ -108,3 +108,24 @@ matrix:
           echo $v dmesg ======================== ;
           docker exec -it druid-$v sh -c 'dmesg | tail -3' ;
           done
+
+      # run integration tests
+    - sudo: required
+      services:
+        - docker
+      env:
+        - NAME="integration test part 2"
+        - DOCKER_IP=127.0.0.1
+      install:
+        # Only errors will be shown with the -q option. This is to avoid generating too many logs which make travis build failed.
+        - mvn install -q -ff -DskipTests -B
+      script:
+        - $TRAVIS_BUILD_DIR/ci/travis_script_integration_part2.sh
+      after_failure:
+        - for v in ~/shared/logs/*.log ; do
+          echo $v logtail ======================== ; tail -100 $v ;
+          done
+        - for v in broker middlemanager overlord router coordinator historical ; do
+          echo $v dmesg ======================== ;
+          docker exec -it druid-$v sh -c 'dmesg | tail -3' ;
+          done
diff --git a/ci/travis_script_integration.sh b/ci/travis_script_integration.sh
index 3d0ca62..b4bb1af 100755
--- a/ci/travis_script_integration.sh
+++ b/ci/travis_script_integration.sh
@@ -21,6 +21,6 @@ set -e
 
 pushd $TRAVIS_BUILD_DIR/integration-tests
 
-mvn verify -P integration-tests
+mvn verify -P integration-tests -Dit.test=ITAppenderatorDriverRealtimeIndexTaskTest,ITCompactionTaskTest,ITIndexerTest,ITKafkaIndexingServiceTest,ITKafkaTest,ITParallelIndexTest,ITRealtimeIndexTaskTest
 
 popd
diff --git a/ci/travis_script_integration.sh b/ci/travis_script_integration_part2.sh
similarity index 86%
copy from ci/travis_script_integration.sh
copy to ci/travis_script_integration_part2.sh
index 3d0ca62..61b3b9b 100755
--- a/ci/travis_script_integration.sh
+++ b/ci/travis_script_integration_part2.sh
@@ -21,6 +21,6 @@ set -e
 
 pushd $TRAVIS_BUILD_DIR/integration-tests
 
-mvn verify -P integration-tests
+mvn verify -P integration-tests -Dit.test=ITUnionQueryTest,ITTwitterQueryTest,ITWikipediaQueryTest,ITBasicAuthConfigurationTest,ITTLSTest
 
 popd
diff --git a/docs/content/development/extensions-core/simple-client-sslcontext.md b/docs/content/development/extensions-core/simple-client-sslcontext.md
index de7dfb1..31ae3d6 100644
--- a/docs/content/development/extensions-core/simple-client-sslcontext.md
+++ b/docs/content/development/extensions-core/simple-client-sslcontext.md
@@ -18,5 +18,16 @@ Java's SSL support, please refer to [this](http://docs.oracle.com/javase/8/docs/
 |`druid.client.https.trustStoreAlgorithm`|Algorithm to be used by TrustManager to validate certificate chains|`javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()`|no|
 |`druid.client.https.trustStorePassword`|The [Password Provider](../../operations/password-provider.html) or String password for the Trust Store.|none|yes|
 
+The following table contains optional parameters for supporting client certificate authentication:
+
+|Property|Description|Default|Required|
+|--------|-----------|-------|--------|
+|`druid.client.https.keyStorePath`|The file path or URL of the TLS/SSL Key store containing the client certificate that Druid will use when communicating with other Druid services. If this is null, the other properties in this table are ignored.|none|yes|
+|`druid.client.https.keyStoreType`|The type of the key store.|none|yes|
+|`druid.client.https.certAlias`|Alias of TLS client certificate in the keystore.|none|yes|
+|`druid.client.https.keyStorePassword`|The [Password Provider](../operations/password-provider.html) or String password for the Key Store.|none|no|
+|`druid.client.https.keyManagerFactoryAlgorithm`|Algorithm to use for creating KeyManager, more details [here](https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#KeyManager).|`javax.net.ssl.KeyManagerFactory.getDefaultAlgorithm()`|no|
+|`druid.client.https.keyManagerPassword`|The [Password Provider](../operations/password-provider.html) or String password for the Key Manager.|none|no|
+
 This [document](http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html) lists all the possible
 values for the above mentioned configs among others provided by Java implementation.
\ No newline at end of file
diff --git a/docs/content/operations/tls-support.md b/docs/content/operations/tls-support.md
index bc004b1..63da285 100644
--- a/docs/content/operations/tls-support.md
+++ b/docs/content/operations/tls-support.md
@@ -31,7 +31,19 @@ values for the below mentioned configs among others provided by Java implementat
 |`druid.server.https.certAlias`|Alias of TLS/SSL certificate for the connector.|none|yes|
 |`druid.server.https.keyStorePassword`|The [Password Provider](../operations/password-provider.html) or String password for the Key Store.|none|yes|
 
-Following table contains non-mandatory advanced configuration options, use caution.
+The following table contains configuration options related to client certificate authentication.
+
+|Property|Description|Default|Required|
+|--------|-----------|-------|--------|
+|`druid.server.https.requireClientCertificate`|If set to true, clients must identify themselves by providing a TLS certificate.  If `requireClientCertificate` is false, the rest of the options in this table are ignored.|false|no|
+|`druid.server.https.trustStoreType`|The type of the trust store containing certificates used to validate client certificates. Not needed if `requireClientCertificate` is false.|`java.security.KeyStore.getDefaultType()`|no|
+|`druid.server.https.trustStorePath`|The file path or URL of the trust store containing certificates used to validate client certificates. Not needed if `requireClientCertificate` is false.|none|yes, only if `requireClientCertificate` is true|
+|`druid.server.https.trustStoreAlgorithm`|Algorithm to be used by TrustManager to validate client certificate chains. Not needed if `requireClientCertificate` is false.|`javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm()`|no|
+|`druid.server.https.trustStorePassword`|The [Password Provider](../../operations/password-provider.html) or String password for the Trust Store.  Not needed if `requireClientCertificate` is false.|none|no|
+|`druid.server.https.validateHostnames`|If set to true, check that the client's hostname matches the CN/subjectAltNames in the client certificate.  Not used if `requireClientCertificate` is false.|true|no|
+|`druid.server.https.crlPath`|Specifies a path to a file containing static [Certificate Revocation Lists](https://en.wikipedia.org/wiki/Certificate_revocation_list), used to check if a client certificate has been revoked. Not used if `requireClientCertificate` is false.|null|no|
+
+The following table contains non-mandatory advanced configuration options, use caution.
 
 |Property|Description|Default|Required|
 |--------|-----------|-------|--------|
diff --git a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java
index 27761ef..b7061cb 100644
--- a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java
+++ b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLClientConfig.java
@@ -39,6 +39,24 @@ public class SSLClientConfig
   @JsonProperty("trustStorePassword")
   private PasswordProvider trustStorePasswordProvider;
 
+  @JsonProperty
+  private String keyStorePath;
+
+  @JsonProperty
+  private String keyStoreType;
+
+  @JsonProperty
+  private String certAlias;
+
+  @JsonProperty("keyStorePassword")
+  private PasswordProvider keyStorePasswordProvider;
+
+  @JsonProperty("keyManagerPassword")
+  private PasswordProvider keyManagerPasswordProvider;
+
+  @JsonProperty
+  private String keyManagerFactoryAlgorithm;
+
   public String getProtocol()
   {
     return protocol;
@@ -64,6 +82,36 @@ public class SSLClientConfig
     return trustStorePasswordProvider;
   }
 
+  public String getKeyStorePath()
+  {
+    return keyStorePath;
+  }
+
+  public String getKeyStoreType()
+  {
+    return keyStoreType;
+  }
+
+  public PasswordProvider getKeyStorePasswordProvider()
+  {
+    return keyStorePasswordProvider;
+  }
+
+  public String getCertAlias()
+  {
+    return certAlias;
+  }
+
+  public PasswordProvider getKeyManagerPasswordProvider()
+  {
+    return keyManagerPasswordProvider;
+  }
+
+  public String getKeyManagerFactoryAlgorithm()
+  {
+    return keyManagerFactoryAlgorithm;
+  }
+
   @Override
   public String toString()
   {
@@ -72,6 +120,10 @@ public class SSLClientConfig
            ", trustStoreType='" + trustStoreType + '\'' +
            ", trustStorePath='" + trustStorePath + '\'' +
            ", trustStoreAlgorithm='" + trustStoreAlgorithm + '\'' +
+           ", keyStorePath='" + keyStorePath + '\'' +
+           ", keyStoreType='" + keyStoreType + '\'' +
+           ", certAlias='" + certAlias + '\'' +
+           ", keyManagerFactoryAlgorithm='" + keyManagerFactoryAlgorithm + '\'' +
            '}';
   }
 }
diff --git a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java
index fc5dba4..826791d 100644
--- a/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java
+++ b/extensions-core/simple-client-sslcontext/src/main/java/org/apache/druid/https/SSLContextProvider.java
@@ -43,12 +43,18 @@ public class SSLContextProvider implements Provider<SSLContext>
   {
     log.info("Creating SslContext for https client using config [%s]", config);
 
-    return TLSUtils.createSSLContext(
-        config.getProtocol(),
-        config.getTrustStoreType(),
-        config.getTrustStorePath(),
-        config.getTrustStoreAlgorithm(),
-        config.getTrustStorePasswordProvider()
-    );
+    return new TLSUtils.ClientSSLContextBuilder()
+        .setProtocol(config.getProtocol())
+        .setTrustStoreType(config.getTrustStoreType())
+        .setTrustStorePath(config.getTrustStorePath())
+        .setTrustStoreAlgorithm(config.getTrustStoreAlgorithm())
+        .setTrustStorePasswordProvider(config.getTrustStorePasswordProvider())
+        .setKeyStoreType(config.getKeyStoreType())
+        .setKeyStorePath(config.getKeyStorePath())
+        .setKeyStoreAlgorithm(config.getKeyManagerFactoryAlgorithm())
+        .setCertAlias(config.getCertAlias())
+        .setKeyStorePasswordProvider(config.getKeyStorePasswordProvider())
+        .setKeyManagerFactoryPasswordProvider(config.getKeyManagerPasswordProvider())
+        .build();
   }
 }
diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore
new file mode 100644
index 0000000..aa0b095
--- /dev/null
+++ b/integration-tests/.gitignore
@@ -0,0 +1,6 @@
+client_tls/
+docker/docker_ip
+docker/tls/root.key
+docker/tls/root.pem
+docker/tls/untrusted_root.key
+docker/tls/untrusted_root.pem
\ No newline at end of file
diff --git a/integration-tests/docker/Dockerfile b/integration-tests/docker/Dockerfile
index e0f104f..5cbb54c 100644
--- a/integration-tests/docker/Dockerfile
+++ b/integration-tests/docker/Dockerfile
@@ -39,32 +39,43 @@ RUN find /var/lib/mysql -type f -exec touch {} \; && service mysql start \
 # Setup supervisord
 ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf
 
+# mysql
+ADD run-mysql.sh /run-mysql.sh
+
 # internal docker_ip:9092 endpoint is used to access Kafka from other Docker containers
 # external docker ip:9093 endpoint is used to access Kafka from test code
 # run this last to avoid rebuilding the image every time the ip changes
 ADD docker_ip docker_ip
-RUN perl -pi -e "s/#listeners=.*/listeners=INTERNAL:\/\/$(resolveip -s $HOSTNAME):9092,EXTERNAL:\/\/$(resolveip -s $HOSTNAME):9093/" /usr/local/kafka/config/server.properties
-RUN perl -pi -e "s/#advertised.listeners=.*/advertised.listeners=INTERNAL:\/\/$(resolveip -s $HOSTNAME):9092,EXTERNAL:\/\/$(cat docker_ip):9093/" /usr/local/kafka/config/server.properties
+RUN perl -pi -e "s/#listeners=.*/listeners=INTERNAL:\/\/172.172.172.2:9092,EXTERNAL:\/\/172.172.172.2:9093/" /usr/local/kafka/config/server.properties
+RUN perl -pi -e "s/#advertised.listeners=.*/advertised.listeners=INTERNAL:\/\/172.172.172.2:9092,EXTERNAL:\/\/$(cat docker_ip):9093/" /usr/local/kafka/config/server.properties
 RUN perl -pi -e "s/#listener.security.protocol.map=.*/listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT\ninter.broker.listener.name=INTERNAL/" /usr/local/kafka/config/server.properties
 RUN perl
 
+# Add directory with TLS support files
+ADD tls tls
+
+ADD client_tls client_tls
+
 # Expose ports:
-# - 8081: HTTP (coordinator)
-# - 8082: HTTP (broker)
-# - 8083: HTTP (historical)
-# - 8090: HTTP (overlord)
-# - 8091: HTTP (middlemanager)
+# - 8081, 8281: HTTP, HTTPS (coordinator)
+# - 8082, 8282: HTTP, HTTPS (broker)
+# - 8083, 8283: HTTP, HTTPS (historical)
+# - 8090, 8290: HTTP, HTTPS (overlord)
+# - 8091, 8291: HTTP, HTTPS (middlemanager)
 # - 3306: MySQL
 # - 2181 2888 3888: ZooKeeper
-# - 8100 8101 8102 8103 8104 : peon ports
-EXPOSE 8081
-EXPOSE 8082
-EXPOSE 8083
-EXPOSE 8090
-EXPOSE 8091
+# - 8100 8101 8102 8103 8104 8105 : peon ports
+# - 8300 8301 8302 8303 8304 8305 : peon HTTPS ports
+EXPOSE 8081 8281
+EXPOSE 8082 8282
+EXPOSE 8083 8283
+EXPOSE 8090 8290
+EXPOSE 8091 8291
 EXPOSE 3306
 EXPOSE 2181 2888 3888
-EXPOSE 8100 8101 8102 8103 8104
+EXPOSE 8100 8101 8102 8103 8104 8105
+EXPOSE 8300 8301 8302 8303 8304 8305
+EXPOSE 9092 9093
 
 WORKDIR /var/lib/druid
-ENTRYPOINT export HOST_IP="$(resolveip -s $HOSTNAME)" && exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
+ENTRYPOINT export HOST_IP="$(resolveip -s $HOSTNAME)" && /tls/generate-server-certs-and-keystores.sh && exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
diff --git a/integration-tests/docker/broker.conf b/integration-tests/docker/broker.conf
index 4510c6e..a83eb1c 100644
--- a/integration-tests/docker/broker.conf
+++ b/integration-tests/docker/broker.conf
@@ -36,6 +36,26 @@ command=java
   -Ddruid.auth.authorizer.basic.type=basic
   -Ddruid.sql.enable=true
   -Ddruid.sql.avatica.enable=true
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=true
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=true
+  -Ddruid.server.https.crlPath=/tls/revocations.crl
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server broker
 redirect_stderr=true
diff --git a/integration-tests/docker/coordinator.conf b/integration-tests/docker/coordinator.conf
index 099abf5..d6b6b59 100644
--- a/integration-tests/docker/coordinator.conf
+++ b/integration-tests/docker/coordinator.conf
@@ -9,6 +9,7 @@ command=java
   -Duser.timezone=UTC
   -Dfile.encoding=UTF-8
   -Ddruid.host=%(ENV_HOST_IP)s
+  -Ddruid.server.http.numThreads=100
   -Ddruid.metadata.storage.type=mysql
   -Ddruid.metadata.storage.connector.connectURI=jdbc:mysql://druid-metadata-storage/druid
   -Ddruid.metadata.storage.connector.user=druid
@@ -29,6 +30,26 @@ command=java
   -Ddruid.auth.authorizers="[\"basic\"]"
   -Ddruid.auth.authorizer.basic.type=basic
   -Ddruid.auth.unsecuredPaths="[\"/druid/coordinator/v1/loadqueue\"]"
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=true
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=true
+  -Ddruid.server.https.crlPath=/tls/revocations.crl
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server coordinator
 redirect_stderr=true
diff --git a/integration-tests/docker/historical.conf b/integration-tests/docker/historical.conf
index 6ff564c..5b3fd0e 100644
--- a/integration-tests/docker/historical.conf
+++ b/integration-tests/docker/historical.conf
@@ -32,6 +32,26 @@ command=java
   -Ddruid.escalator.authorizerName=basic
   -Ddruid.auth.authorizers="[\"basic\"]"
   -Ddruid.auth.authorizer.basic.type=basic
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=true
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=true
+  -Ddruid.server.https.crlPath=/tls/revocations.crl
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server historical
 redirect_stderr=true
diff --git a/integration-tests/docker/metadata-storage.conf b/integration-tests/docker/metadata-storage.conf
index eb60e21..8283774 100644
--- a/integration-tests/docker/metadata-storage.conf
+++ b/integration-tests/docker/metadata-storage.conf
@@ -1,6 +1,5 @@
 [program:mysql]
-command=/usr/bin/pidproxy /var/run/mysqld/mysqld.pid /usr/bin/mysqld_safe
-         --bind-address=0.0.0.0
+command=/run-mysql.sh
 user=mysql
 priority=0
 stdout_logfile=/shared/logs/mysql.log
diff --git a/integration-tests/docker/middlemanager.conf b/integration-tests/docker/middlemanager.conf
index e2f4f06..cf09ae8 100644
--- a/integration-tests/docker/middlemanager.conf
+++ b/integration-tests/docker/middlemanager.conf
@@ -9,11 +9,12 @@ command=java
   -Duser.timezone=UTC
   -Dfile.encoding=UTF-8
   -Ddruid.host=%(ENV_HOST_IP)s
+  -Ddruid.server.http.numThreads=100
   -Ddruid.zk.service.host=druid-zookeeper-kafka
   -Ddruid.worker.capacity=3
   -Ddruid.indexer.logs.directory=/shared/tasklogs
   -Ddruid.storage.storageDirectory=/shared/storage
-  -Ddruid.indexer.runner.javaOpts=-server -Xmx256m -Xms256m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
+  -Ddruid.indexer.runner.javaOpts="-server -Xmx256m -Xms256m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"
   -Ddruid.indexer.fork.property.druid.processing.buffer.sizeBytes=25000000
   -Ddruid.indexer.fork.property.druid.processing.numThreads=1
   -Ddruid.indexer.fork.server.http.numThreads=100
@@ -35,6 +36,27 @@ command=java
   -Ddruid.escalator.authorizerName=basic
   -Ddruid.auth.authorizers="[\"basic\"]"
   -Ddruid.auth.authorizer.basic.type=basic
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=true
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=true
+  -Ddruid.server.https.crlPath=/tls/revocations.crl
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
+  -Ddruid.startup.logging.logProperties=true
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server middleManager
 redirect_stderr=true
diff --git a/integration-tests/docker/overlord.conf b/integration-tests/docker/overlord.conf
index 97a48cc..293835a 100644
--- a/integration-tests/docker/overlord.conf
+++ b/integration-tests/docker/overlord.conf
@@ -9,6 +9,7 @@ command=java
   -Duser.timezone=UTC
   -Dfile.encoding=UTF-8
   -Ddruid.host=%(ENV_HOST_IP)s
+  -Ddruid.server.http.numThreads=100
   -Ddruid.metadata.storage.type=mysql
   -Ddruid.metadata.storage.connector.connectURI=jdbc:mysql://druid-metadata-storage/druid
   -Ddruid.metadata.storage.connector.user=druid
@@ -30,6 +31,26 @@ command=java
   -Ddruid.escalator.authorizerName=basic
   -Ddruid.auth.authorizers="[\"basic\"]"
   -Ddruid.auth.authorizer.basic.type=basic
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=true
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=true
+  -Ddruid.server.https.crlPath=/tls/revocations.crl
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server overlord
 redirect_stderr=true
diff --git a/integration-tests/docker/router.conf b/integration-tests/docker/router-no-client-auth-tls.conf
similarity index 50%
copy from integration-tests/docker/router.conf
copy to integration-tests/docker/router-no-client-auth-tls.conf
index 39923d9..f947aa3 100644
--- a/integration-tests/docker/router.conf
+++ b/integration-tests/docker/router-no-client-auth-tls.conf
@@ -1,4 +1,4 @@
-[program:druid-router]
+[program:druid-router-no-client-auth-tls]
 command=java
   -server
   -Xmx128m
@@ -8,6 +8,8 @@ command=java
   -Duser.timezone=UTC
   -Dfile.encoding=UTF-8
   -Ddruid.host=%(ENV_HOST_IP)s
+  -Ddruid.plaintextPort=8890
+  -Ddruid.tlsPort=9090
   -Ddruid.zk.service.host=druid-zookeeper-kafka
   -Ddruid.server.http.numThreads=100
   -Ddruid.lookup.numLookupLoadingThreads=1
@@ -16,7 +18,7 @@ command=java
   -Ddruid.auth.authenticator.basic.initialAdminPassword=priest
   -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
   -Ddruid.auth.authenticator.basic.authorizerName=basic
-  -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/router
+  -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/router-no-client-auth-tls
   -Ddruid.escalator.type=basic
   -Ddruid.escalator.internalClientUsername=druid_system
   -Ddruid.escalator.internalClientPassword=warlock
@@ -25,9 +27,28 @@ command=java
   -Ddruid.auth.authorizer.basic.type=basic
   -Ddruid.sql.enable=true
   -Ddruid.sql.avatica.enable=true
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=false
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=false
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server router
 redirect_stderr=true
 priority=100
 autorestart=false
-stdout_logfile=/shared/logs/router.log
+stdout_logfile=/shared/logs/router-no-client-auth-tls.log
diff --git a/integration-tests/docker/router.conf b/integration-tests/docker/router-permissive-tls.conf
similarity index 50%
copy from integration-tests/docker/router.conf
copy to integration-tests/docker/router-permissive-tls.conf
index 39923d9..1b6e159 100644
--- a/integration-tests/docker/router.conf
+++ b/integration-tests/docker/router-permissive-tls.conf
@@ -1,4 +1,4 @@
-[program:druid-router]
+[program:druid-router-permissive-tls]
 command=java
   -server
   -Xmx128m
@@ -8,6 +8,8 @@ command=java
   -Duser.timezone=UTC
   -Dfile.encoding=UTF-8
   -Ddruid.host=%(ENV_HOST_IP)s
+  -Ddruid.plaintextPort=8889
+  -Ddruid.tlsPort=9089
   -Ddruid.zk.service.host=druid-zookeeper-kafka
   -Ddruid.server.http.numThreads=100
   -Ddruid.lookup.numLookupLoadingThreads=1
@@ -16,7 +18,7 @@ command=java
   -Ddruid.auth.authenticator.basic.initialAdminPassword=priest
   -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
   -Ddruid.auth.authenticator.basic.authorizerName=basic
-  -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/router
+  -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/router-permissive-tls
   -Ddruid.escalator.type=basic
   -Ddruid.escalator.internalClientUsername=druid_system
   -Ddruid.escalator.internalClientPassword=warlock
@@ -25,9 +27,28 @@ command=java
   -Ddruid.auth.authorizer.basic.type=basic
   -Ddruid.sql.enable=true
   -Ddruid.sql.avatica.enable=true
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=true
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=false
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server router
 redirect_stderr=true
 priority=100
 autorestart=false
-stdout_logfile=/shared/logs/router.log
+stdout_logfile=/shared/logs/router-permissive-tls.log
diff --git a/integration-tests/docker/router.conf b/integration-tests/docker/router.conf
index 39923d9..a65c25e 100644
--- a/integration-tests/docker/router.conf
+++ b/integration-tests/docker/router.conf
@@ -25,6 +25,26 @@ command=java
   -Ddruid.auth.authorizer.basic.type=basic
   -Ddruid.sql.enable=true
   -Ddruid.sql.avatica.enable=true
+  -Ddruid.enableTlsPort=true
+  -Ddruid.server.https.keyStorePath=/tls/server.jks
+  -Ddruid.server.https.keyStoreType=jks
+  -Ddruid.server.https.certAlias=druid
+  -Ddruid.server.https.keyManagerPassword=druid123
+  -Ddruid.server.https.keyStorePassword=druid123
+  -Ddruid.server.https.requireClientCertificate=true
+  -Ddruid.server.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.server.https.trustStorePassword=druid123
+  -Ddruid.server.https.trustStoreAlgorithm=PKIX
+  -Ddruid.server.https.validateHostnames=true
+  -Ddruid.server.https.crlPath=/tls/revocations.crl
+  -Ddruid.client.https.trustStoreAlgorithm=PKIX
+  -Ddruid.client.https.protocol=TLSv1.2
+  -Ddruid.client.https.trustStorePath=/tls/truststore.jks
+  -Ddruid.client.https.trustStorePassword=druid123
+  -Ddruid.client.https.keyStorePath=/tls/server.jks
+  -Ddruid.client.https.certAlias=druid
+  -Ddruid.client.https.keyManagerPassword=druid123
+  -Ddruid.client.https.keyStorePassword=druid123
   -cp /shared/docker/lib/*
   org.apache.druid.cli.Main server router
 redirect_stderr=true
diff --git a/integration-tests/docker/run-mysql.sh b/integration-tests/docker/run-mysql.sh
new file mode 100755
index 0000000..266b652
--- /dev/null
+++ b/integration-tests/docker/run-mysql.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+find /var/lib/mysql -type f -exec touch {} \; && /usr/bin/pidproxy /var/run/mysqld/mysqld.pid /usr/bin/mysqld_safe --bind-address=0.0.0.0
\ No newline at end of file
diff --git a/integration-tests/docker/tls/generate-client-certs-and-keystores.sh b/integration-tests/docker/tls/generate-client-certs-and-keystores.sh
new file mode 100755
index 0000000..0fad801
--- /dev/null
+++ b/integration-tests/docker/tls/generate-client-certs-and-keystores.sh
@@ -0,0 +1,21 @@
+#!/bin/bash -eu
+
+./docker/tls/generate-root-certs.sh
+
+mkdir -p client_tls
+rm -f client_tls/*
+cp docker/tls/root.key client_tls/root.key
+cp docker/tls/root.pem client_tls/root.pem
+cp docker/tls/untrusted_root.key client_tls/untrusted_root.key
+cp docker/tls/untrusted_root.pem client_tls/untrusted_root.pem
+cd client_tls
+
+../docker/tls/generate-expired-client-cert.sh
+../docker/tls/generate-good-client-cert.sh
+../docker/tls/generate-incorrect-hostname-client-cert.sh
+../docker/tls/generate-invalid-intermediate-client-cert.sh
+../docker/tls/generate-to-be-revoked-client-cert.sh
+../docker/tls/generate-untrusted-root-client-cert.sh
+../docker/tls/generate-valid-intermediate-client-cert.sh
+
+
diff --git a/integration-tests/docker/tls/generate-expired-client-cert.sh b/integration-tests/docker/tls/generate-expired-client-cert.sh
new file mode 100755
index 0000000..f4d7236
--- /dev/null
+++ b/integration-tests/docker/tls/generate-expired-client-cert.sh
@@ -0,0 +1,41 @@
+#!/bin/bash -eu
+
+export DOCKER_HOST_IP=$(resolveip -s $HOSTNAME)
+
+cat <<EOT > expired_csr.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=integration-test@druid.io
+CN = localhost
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = ${DOCKER_HOST_IP}
+IP.2 = 127.0.0.1
+IP.3 = 172.172.172.1
+DNS.1 = ${HOSTNAME}
+DNS.2 = localhost
+EOT
+
+# Generate a client certificate for this machine
+openssl genrsa -out expired_client.key 1024 -sha256
+openssl req -new -out expired_client.csr -key expired_client.key -reqexts req_ext -config expired_csr.conf
+openssl x509 -req -days -3650 -in expired_client.csr -CA root.pem -CAkey root.key -set_serial 0x11111115 -out expired_client.pem -sha256 -extfile expired_csr.conf -extensions req_ext
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in expired_client.pem -inkey expired_client.key -out expired_client.p12 -name expired_client -CAfile root.pem -caname druid-it-root -password pass:druid123
+keytool -importkeystore -srckeystore expired_client.p12 -srcstoretype PKCS12 -destkeystore expired_client.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
diff --git a/integration-tests/docker/tls/generate-good-client-cert.sh b/integration-tests/docker/tls/generate-good-client-cert.sh
new file mode 100755
index 0000000..96a1bc0
--- /dev/null
+++ b/integration-tests/docker/tls/generate-good-client-cert.sh
@@ -0,0 +1,44 @@
+#!/bin/bash -eu
+
+export DOCKER_HOST_IP=$(resolveip -s $HOSTNAME)
+
+cat <<EOT > csr.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=integration-test@druid.io
+CN = localhost
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = ${DOCKER_HOST_IP}
+IP.2 = 127.0.0.1
+IP.3 = 172.172.172.1
+DNS.1 = ${HOSTNAME}
+DNS.2 = localhost
+EOT
+
+# Generate a client certificate for this machine
+openssl genrsa -out client.key 1024 -sha256
+openssl req -new -out client.csr -key client.key -reqexts req_ext -config csr.conf
+openssl x509 -req -days 3650 -in client.csr -CA root.pem -CAkey root.key -set_serial 0x11111111 -out client.pem -sha256 -extfile csr.conf -extensions req_ext
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in client.pem -inkey client.key -out client.p12 -name druid -CAfile root.pem -caname druid-it-root -password pass:druid123
+keytool -importkeystore -srckeystore client.p12 -srcstoretype PKCS12 -destkeystore client.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
+
+# Create a Java truststore with the imply test cluster root CA
+keytool -import -alias druid-it-root -keystore truststore.jks -file root.pem -storepass druid123 -noprompt
diff --git a/integration-tests/docker/tls/generate-incorrect-hostname-client-cert.sh b/integration-tests/docker/tls/generate-incorrect-hostname-client-cert.sh
new file mode 100755
index 0000000..5cb3022
--- /dev/null
+++ b/integration-tests/docker/tls/generate-incorrect-hostname-client-cert.sh
@@ -0,0 +1,40 @@
+#!/bin/bash -eu
+
+export DOCKER_HOST_IP=$(resolveip -s $HOSTNAME)
+
+
+# Generate a client cert with an incorrect hostname for testing
+cat <<EOT > invalid_hostname_csr.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=integration-test@druid.io
+CN = thisisprobablynottherighthostname
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+DNS.1 = thisisprobablywrongtoo
+
+EOT
+
+openssl genrsa -out invalid_hostname_client.key 1024 -sha256
+openssl req -new -out invalid_hostname_client.csr -key invalid_hostname_client.key -reqexts req_ext -config invalid_hostname_csr.conf
+openssl x509 -req -days 3650 -in invalid_hostname_client.csr -CA root.pem -CAkey root.key -set_serial 0x11111112 -out invalid_hostname_client.pem -sha256 -extfile invalid_hostname_csr.conf -extensions req_ext
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in invalid_hostname_client.pem -inkey invalid_hostname_client.key -out invalid_hostname_client.p12 -name invalid_hostname_client -CAfile root.pem -caname druid-it-root -password pass:druid123
+keytool -importkeystore -srckeystore invalid_hostname_client.p12 -srcstoretype PKCS12 -destkeystore invalid_hostname_client.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
+
diff --git a/integration-tests/docker/tls/generate-invalid-intermediate-client-cert.sh b/integration-tests/docker/tls/generate-invalid-intermediate-client-cert.sh
new file mode 100755
index 0000000..134898b
--- /dev/null
+++ b/integration-tests/docker/tls/generate-invalid-intermediate-client-cert.sh
@@ -0,0 +1,76 @@
+#!/bin/bash -eu
+
+export DOCKER_HOST_IP=$(resolveip -s $HOSTNAME)
+
+cat <<EOT > invalid_ca_intermediate.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=bad-intermediate@druid.io
+CN = badintermediate
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = 9.9.9.9
+EOT
+
+# Generate a bad intermediate certificate
+openssl genrsa -out invalid_ca_intermediate.key 1024 -sha256
+openssl req -new -out invalid_ca_intermediate.csr -key invalid_ca_intermediate.key -reqexts req_ext -config invalid_ca_intermediate.conf
+openssl x509 -req -days 3650 -in invalid_ca_intermediate.csr -CA root.pem -CAkey root.key -set_serial 0x33333331 -out invalid_ca_intermediate.pem -sha256 -extfile invalid_ca_intermediate.conf -extensions req_ext
+
+
+cat <<EOT > invalid_ca_client.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=basic-constraint-fail@druid.io
+CN = localhost
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = ${DOCKER_HOST_IP}
+IP.2 = 127.0.0.1
+IP.3 = 172.172.172.1
+DNS.1 = ${HOSTNAME}
+DNS.2 = localhost
+EOT
+
+# Generate a client certificate for this machine
+openssl genrsa -out invalid_ca_client.key 1024 -sha256
+openssl req -new -out invalid_ca_client.csr -key invalid_ca_client.key -reqexts req_ext -config invalid_ca_client.conf
+openssl x509 -req -days 3650 -in invalid_ca_client.csr -CA invalid_ca_intermediate.pem -CAkey invalid_ca_intermediate.key -set_serial 0x33333333 -out invalid_ca_client.pem -sha256 -extfile invalid_ca_client.conf -extensions req_ext
+
+# Append the signing cert
+printf "\n" >> invalid_ca_client.pem
+cat invalid_ca_intermediate.pem >> invalid_ca_client.pem
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in invalid_ca_client.pem -inkey invalid_ca_client.key -out invalid_ca_client.p12 -name invalid_ca_client -CAfile invalid_ca_intermediate.pem -caname druid-it-root -password pass:druid123
+keytool -importkeystore -srckeystore invalid_ca_client.p12 -srcstoretype PKCS12 -destkeystore invalid_ca_client.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
\ No newline at end of file
diff --git a/integration-tests/docker/tls/generate-root-certs.sh b/integration-tests/docker/tls/generate-root-certs.sh
new file mode 100755
index 0000000..8b14f27
--- /dev/null
+++ b/integration-tests/docker/tls/generate-root-certs.sh
@@ -0,0 +1,13 @@
+#!/bin/bash -eu
+
+rm -f root.key
+rm -f untrusted_root.key
+rm -f root.pem
+rm -f untrusted_root.pem
+
+openssl genrsa -out docker/tls/root.key 4096
+openssl genrsa -out docker/tls/untrusted_root.key 4096
+
+openssl req -config docker/tls/root.cnf -key docker/tls/root.key -new -x509 -days 3650 -sha256 -extensions v3_ca -out docker/tls/root.pem
+openssl req -config docker/tls/root.cnf -key docker/tls/untrusted_root.key -new -x509 -days 3650 -sha256 -extensions v3_ca -out docker/tls/untrusted_root.pem
+
diff --git a/integration-tests/docker/tls/generate-server-certs-and-keystores.sh b/integration-tests/docker/tls/generate-server-certs-and-keystores.sh
new file mode 100755
index 0000000..940d444
--- /dev/null
+++ b/integration-tests/docker/tls/generate-server-certs-and-keystores.sh
@@ -0,0 +1,66 @@
+#!/bin/bash -eu
+
+cd /tls
+
+rm -f cert_db.txt
+touch cert_db.txt
+
+export DOCKER_IP=$(cat /docker_ip)
+export MY_HOSTNAME=$(hostname)
+export MY_IP=$(hostname -i)
+
+cat <<EOT > csr.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=integration-test@druid.io
+CN = ${MY_IP}
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = ${DOCKER_IP}
+IP.2 = ${MY_IP}
+IP.3 = 127.0.0.1
+DNS.1 = ${MY_HOSTNAME}
+DNS.2 = localhost
+
+EOT
+
+# Generate a server certificate for this machine
+openssl genrsa -out server.key 1024 -sha256
+openssl req -new -out server.csr -key server.key -reqexts req_ext -config csr.conf
+openssl x509 -req -days 3650 -in server.csr -CA root.pem -CAkey root.key -set_serial 0x22222222 -out server.pem -sha256 -extfile csr.conf -extensions req_ext
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in server.pem -inkey server.key -out server.p12 -name druid -CAfile root.pem -caname druid-it-root -password pass:druid123
+keytool -importkeystore -srckeystore server.p12 -srcstoretype PKCS12 -destkeystore server.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
+
+# Create a Java truststore with the imply test cluster root CA
+keytool -import -alias druid-it-root -keystore truststore.jks -file root.pem -storepass druid123 -noprompt
+
+# Revoke one of the client certs
+openssl ca -revoke /client_tls/revoked_client.pem -config root.cnf -cert root.pem -keyfile root.key
+
+# Create the CRL
+openssl ca -gencrl -config root.cnf -cert root.pem -keyfile root.key -out /tls/revocations.crl
+
+# Generate empty CRLs for the intermediate cert test case
+rm -f cert_db2.txt
+touch cert_db2.txt
+openssl ca -gencrl -config root2.cnf -cert /client_tls/ca_intermediate.pem -keyfile /client_tls/ca_intermediate.key -out /tls/empty-revocations-intermediate.crl
+
+# Append CRLs
+cat empty-revocations-intermediate.crl >> revocations.crl
diff --git a/integration-tests/docker/tls/generate-to-be-revoked-client-cert.sh b/integration-tests/docker/tls/generate-to-be-revoked-client-cert.sh
new file mode 100755
index 0000000..ecb49bd
--- /dev/null
+++ b/integration-tests/docker/tls/generate-to-be-revoked-client-cert.sh
@@ -0,0 +1,43 @@
+#!/bin/bash -eu
+
+export DOCKER_HOST_IP=$(resolveip -s $HOSTNAME)
+
+# Generate a client cert that will be revoked
+cat <<EOT > revoked_csr.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=RevokedIntegrationTests
+emailAddress=revoked-it-cert@druid.io
+CN = localhost
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = ${DOCKER_HOST_IP}
+IP.2 = 127.0.0.1
+IP.3 = 172.172.172.1
+DNS.1 = ${HOSTNAME}
+DNS.2 = localhost
+
+EOT
+
+# Generate a client certificate for this machine
+openssl genrsa -out revoked_client.key 1024 -sha256
+openssl req -new -out revoked_client.csr -key revoked_client.key -reqexts req_ext -config revoked_csr.conf
+openssl x509 -req -days 3650 -in revoked_client.csr -CA root.pem -CAkey root.key -set_serial 0x11111113 -out revoked_client.pem -sha256 -extfile csr.conf -extensions req_ext
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in revoked_client.pem -inkey revoked_client.key -out revoked_client.p12 -name revoked_druid -CAfile root.pem -caname druid-it-root -password pass:druid123
+keytool -importkeystore -srckeystore revoked_client.p12 -srcstoretype PKCS12 -destkeystore revoked_client.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
diff --git a/integration-tests/docker/tls/generate-untrusted-root-client-cert.sh b/integration-tests/docker/tls/generate-untrusted-root-client-cert.sh
new file mode 100755
index 0000000..08ff13d
--- /dev/null
+++ b/integration-tests/docker/tls/generate-untrusted-root-client-cert.sh
@@ -0,0 +1,42 @@
+#!/bin/bash -eu
+
+export DOCKER_HOST_IP=$(resolveip -s $HOSTNAME)
+
+cat <<EOT > csr_another_root.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=integration-test@druid.io
+CN = localhost
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = ${DOCKER_HOST_IP}
+IP.2 = 127.0.0.1
+IP.3 = 172.172.172.1
+DNS.1 = ${HOSTNAME}
+DNS.2 = localhost
+EOT
+
+# Generate a client certificate for this machine
+openssl genrsa -out client_another_root.key 1024 -sha256
+openssl req -new -out client_another_root.csr -key client_another_root.key -reqexts req_ext -config csr_another_root.conf
+openssl x509 -req -days 3650 -in client_another_root.csr -CA untrusted_root.pem -CAkey untrusted_root.key -set_serial 0x11111114 -out client_another_root.pem -sha256 -extfile csr_another_root.conf -extensions req_ext
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in client_another_root.pem -inkey client_another_root.key -out client_another_root.p12 -name druid_another_root -CAfile untrusted_root.pem -caname druid-it-untrusted-root -password pass:druid123
+keytool -importkeystore -srckeystore client_another_root.p12 -srcstoretype PKCS12 -destkeystore client_another_root.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
+
diff --git a/integration-tests/docker/tls/generate-valid-intermediate-client-cert.sh b/integration-tests/docker/tls/generate-valid-intermediate-client-cert.sh
new file mode 100755
index 0000000..d76a4fc
--- /dev/null
+++ b/integration-tests/docker/tls/generate-valid-intermediate-client-cert.sh
@@ -0,0 +1,76 @@
+#!/bin/bash -eu
+
+export DOCKER_HOST_IP=$(resolveip -s $HOSTNAME)
+
+cat <<EOT > ca_intermediate.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=intermediate@druid.io
+CN = intermediate
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:TRUE,pathlen:1
+
+[ alt_names ]
+IP.1 = 9.9.9.9
+EOT
+
+# Generate an intermediate certificate
+openssl genrsa -out ca_intermediate.key 1024 -sha256
+openssl req -new -out ca_intermediate.csr -key ca_intermediate.key -reqexts req_ext -config ca_intermediate.conf
+openssl x509 -req -days 3650 -in ca_intermediate.csr -CA root.pem -CAkey root.key -set_serial 0x33333332 -out ca_intermediate.pem -sha256 -extfile ca_intermediate.conf -extensions req_ext
+
+
+cat <<EOT > intermediate_ca_client.conf
+[req]
+default_bits = 1024
+prompt = no
+default_md = sha256
+req_extensions = req_ext
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=intermediate-client@druid.io
+CN = localhost
+
+[ req_ext ]
+subjectAltName = @alt_names
+basicConstraints=CA:FALSE,pathlen:0
+
+[ alt_names ]
+IP.1 = ${DOCKER_HOST_IP}
+IP.2 = 127.0.0.1
+IP.3 = 172.172.172.1
+DNS.1 = ${HOSTNAME}
+DNS.2 = localhost
+EOT
+
+# Generate a client certificate for this machine
+openssl genrsa -out intermediate_ca_client.key 1024 -sha256
+openssl req -new -out intermediate_ca_client.csr -key intermediate_ca_client.key -reqexts req_ext -config intermediate_ca_client.conf
+openssl x509 -req -days 3650 -in intermediate_ca_client.csr -CA ca_intermediate.pem -CAkey ca_intermediate.key -set_serial 0x33333333 -out intermediate_ca_client.pem -sha256 -extfile intermediate_ca_client.conf -extensions req_ext
+
+# Append the signing cert
+printf "\n" >> intermediate_ca_client.pem
+cat ca_intermediate.pem >> intermediate_ca_client.pem
+
+# Create a Java keystore containing the generated certificate
+openssl pkcs12 -export -in intermediate_ca_client.pem -inkey intermediate_ca_client.key -out intermediate_ca_client.p12 -name intermediate_ca_client -CAfile ca_intermediate.pem -caname druid-it-root -password pass:druid123
+keytool -importkeystore -srckeystore intermediate_ca_client.p12 -srcstoretype PKCS12 -destkeystore intermediate_ca_client.jks -deststoretype JKS -srcstorepass druid123 -deststorepass druid123
\ No newline at end of file
diff --git a/integration-tests/docker/tls/root.cnf b/integration-tests/docker/tls/root.cnf
new file mode 100644
index 0000000..dd664bf
--- /dev/null
+++ b/integration-tests/docker/tls/root.cnf
@@ -0,0 +1,35 @@
+[ ca ]
+default_ca = CA_default
+
+[ CA_default ]
+database = /tls/cert_db.txt
+x509_extensions	= usr_cert
+name_opt = ca_default
+cert_opt = ca_default
+default_days = 365
+default_crl_days= 30
+default_md = default
+preserve = no
+policy = policy_match
+
+[req]
+default_bits = 4096
+prompt = no
+default_md = sha256
+req_extensions = v3_ca
+distinguished_name = dn
+
+[ dn ]
+C=DR
+ST=DR
+L=Druid City
+O=Druid
+OU=IntegrationTests
+emailAddress=integration-test@druid.io
+CN = itroot
+
+[ v3_ca ]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
\ No newline at end of file
diff --git a/integration-tests/docker/tls/root2.cnf b/integration-tests/docker/tls/root2.cnf
new file mode 100644
index 0000000..5173a87
--- /dev/null
+++ b/integration-tests/docker/tls/root2.cnf
@@ -0,0 +1,13 @@
+[ ca ]
+default_ca = CA_default
+
+[ CA_default ]
+database = /tls/cert_db2.txt
+x509_extensions	= usr_cert
+name_opt = ca_default
+cert_opt = ca_default
+default_days = 365
+default_crl_days= 30
+default_md = default
+preserve = no
+policy = policy_match
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 9c67b3e..2682e76 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -89,6 +89,11 @@
             <version>${project.parent.version}</version>
         </dependency>
         <dependency>
+            <groupId>org.apache.druid.extensions</groupId>
+            <artifactId>simple-client-sslcontext</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+        <dependency>
             <groupId>org.apache.druid</groupId>
             <artifactId>druid-services</artifactId>
             <version>${project.parent.version}</version>
@@ -227,6 +232,12 @@
                                 -Ddruid.test.config.dockerIp=${env.DOCKER_IP}
                                 -Ddruid.test.config.hadoopDir=${env.HADOOP_DIR}
                                 -Ddruid.zk.service.host=${env.DOCKER_IP}
+                                -Ddruid.client.https.trustStorePath=client_tls/truststore.jks
+                                -Ddruid.client.https.trustStorePassword=druid123
+                                -Ddruid.client.https.keyStorePath=client_tls/client.jks
+                                -Ddruid.client.https.certAlias=druid
+                                -Ddruid.client.https.keyManagerPassword=druid123
+                                -Ddruid.client.https.keyStorePassword=druid123
                             </argLine>
                             <suiteXmlFiles>
                                 <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
@@ -276,6 +287,12 @@
                                 -Dfile.encoding=UTF-8
                                 -Ddruid.test.config.type=configFile
                                 -Ddruid.test.config.configFile=${env.CONFIG_FILE}
+                                -Ddruid.client.https.trustStorePath=client_tls/truststore.jks
+                                -Ddruid.client.https.trustStorePassword=druid123
+                                -Ddruid.client.https.keyStorePath=client_tls/client.jks
+                                -Ddruid.client.https.certAlias=druid
+                                -Ddruid.client.https.keyManagerPassword=druid123
+                                -Ddruid.client.https.keyStorePassword=druid123
                             </argLine>
                             <suiteXmlFiles>
                                 <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
diff --git a/integration-tests/run_cluster.sh b/integration-tests/run_cluster.sh
index e5d3a25..a41a5b8 100755
--- a/integration-tests/run_cluster.sh
+++ b/integration-tests/run_cluster.sh
@@ -15,12 +15,14 @@
 # limitations under the License.
 
 # cleanup
-for node in druid-historical druid-coordinator druid-overlord druid-router druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage;
+for node in druid-historical druid-coordinator druid-overlord druid-router druid-router-permissive-tls druid-router-no-client-auth-tls druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage;
 do
 docker stop $node
 docker rm $node
 done
 
+docker network rm druid-it-net
+
 # environment variables
 DIR=$(cd $(dirname $0) && pwd)
 DOCKERDIR=$DIR/docker
@@ -31,6 +33,11 @@ RESOURCEDIR=$DIR/src/test/resources
 # so docker IP addr will be known during docker build
 echo ${DOCKER_IP:=127.0.0.1} > $DOCKERDIR/docker_ip
 
+# setup client keystore
+./docker/tls/generate-client-certs-and-keystores.sh
+rm -rf docker/client_tls
+cp -r client_tls docker/client_tls
+
 # Make directories if they dont exist
 mkdir -p $SHARED_DIR/logs
 mkdir -p $SHARED_DIR/tasklogs
@@ -40,29 +47,37 @@ rm -rf $SHARED_DIR/docker
 cp -R docker $SHARED_DIR/docker
 mvn -B dependency:copy-dependencies -DoutputDirectory=$SHARED_DIR/docker/lib
 
+docker network create --subnet=172.172.172.0/24 druid-it-net
+
 # Build Druid Cluster Image
 docker build -t druid/cluster $SHARED_DIR/docker
 
 # Start zookeeper and kafka
-docker run -d --privileged --name druid-zookeeper-kafka -p 2181:2181 -p 9092:9092 -p 9093:9093 -v $SHARED_DIR:/shared -v $DOCKERDIR/zookeeper.conf:$SUPERVISORDIR/zookeeper.conf -v $DOCKERDIR/kafka.conf:$SUPERVISORDIR/kafka.conf druid/cluster
+docker run -d --privileged --net druid-it-net --ip 172.172.172.2 --name druid-zookeeper-kafka -p 2181:2181 -p 9092:9092 -p 9093:9093 -v $SHARED_DIR:/shared -v $DOCKERDIR/zookeeper.conf:$SUPERVISORDIR/zookeeper.conf -v $DOCKERDIR/kafka.conf:$SUPERVISORDIR/kafka.conf druid/cluster
 
 # Start MYSQL 
-docker run -d --privileged --name druid-metadata-storage -v $SHARED_DIR:/shared -v $DOCKERDIR/metadata-storage.conf:$SUPERVISORDIR/metadata-storage.conf druid/cluster
+docker run -d --privileged --net druid-it-net --ip 172.172.172.3 --name druid-metadata-storage -v $SHARED_DIR:/shared -v $DOCKERDIR/metadata-storage.conf:$SUPERVISORDIR/metadata-storage.conf druid/cluster
 
 # Start Overlord
-docker run -d --privileged --name druid-overlord -p 8090:8090 -v $SHARED_DIR:/shared -v $DOCKERDIR/overlord.conf:$SUPERVISORDIR/overlord.conf --link druid-metadata-storage:druid-metadata-storage --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
+docker run -d --privileged --net druid-it-net --ip 172.172.172.4 --name druid-overlord -p 8090:8090 -p 8290:8290 -v $SHARED_DIR:/shared -v $DOCKERDIR/overlord.conf:$SUPERVISORDIR/overlord.conf --link druid-metadata-storage:druid-metadata-storage --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
 
 # Start Coordinator
-docker run -d --privileged --name druid-coordinator -p 8081:8081 -v $SHARED_DIR:/shared -v $DOCKERDIR/coordinator.conf:$SUPERVISORDIR/coordinator.conf --link druid-overlord:druid-overlord --link druid-metadata-storage:druid-metadata-storage --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
+docker run -d --privileged --net druid-it-net --ip 172.172.172.5 --name druid-coordinator -p 8081:8081 -p 8281:8281 -v $SHARED_DIR:/shared -v $DOCKERDIR/coordinator.conf:$SUPERVISORDIR/coordinator.conf --link druid-overlord:druid-overlord --link druid-metadata-storage:druid-metadata-storage --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
 
 # Start Historical 
-docker run -d --privileged --name druid-historical -p 8083:8083 -v $SHARED_DIR:/shared -v $DOCKERDIR/historical.conf:$SUPERVISORDIR/historical.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
+docker run -d --privileged --net druid-it-net --ip 172.172.172.6 --name druid-historical -p 8083:8083 -p 8283:8283 -v $SHARED_DIR:/shared -v $DOCKERDIR/historical.conf:$SUPERVISORDIR/historical.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
 
 # Start Middlemanger
-docker run -d --privileged --name druid-middlemanager -p 8091:8091 -p 8100:8100 -p 8101:8101 -p 8102:8102 -p 8103:8103 -p 8104:8104 -p 8105:8105 -v $RESOURCEDIR:/resources -v $SHARED_DIR:/shared -v $DOCKERDIR/middlemanager.conf:$SUPERVISORDIR/middlemanager.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-overlord:druid-overlord druid/cluster
+docker run -d --privileged --net druid-it-net --ip 172.172.172.7 --name druid-middlemanager -p 8091:8091 -p 8291:8291 -p 8100:8100 -p 8101:8101 -p 8102:8102 -p 8103:8103 -p 8104:8104 -p 8105:8105 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8303:8303 -p 8304:8304 -p 8305:8305 -v $RESOURCEDIR:/resources -v $SHARED_DIR:/shared -v $DOCKERDIR/middlemanager.conf:$SUPERVISORDIR/middlemanager.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-overlord:druid-overlord druid/cluster
 
 # Start Broker 
-docker run -d --privileged --name druid-broker -p 8082:8082 -v $SHARED_DIR:/shared -v $DOCKERDIR/broker.conf:$SUPERVISORDIR/broker.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-middlemanager:druid-middlemanager --link druid-historical:druid-historical druid/cluster
+docker run -d --privileged --net druid-it-net --ip 172.172.172.8 --name druid-broker -p 8082:8082 -p 8282:8282 -v $SHARED_DIR:/shared -v $DOCKERDIR/broker.conf:$SUPERVISORDIR/broker.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-middlemanager:druid-middlemanager --link druid-historical:druid-historical druid/cluster
+
+# Start Router
+docker run -d --privileged --net druid-it-net --ip 172.172.172.9 --name druid-router -p 8888:8888 -p 9088:9088 -v $SHARED_DIR:/shared -v $DOCKERDIR/router.conf:$SUPERVISORDIR/router.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-coordinator:druid-coordinator --link druid-broker:druid-broker druid/cluster
+
+# Start Router with permissive TLS settings (client auth enabled, no hostname verification, no revocation check)
+docker run -d --privileged --net druid-it-net --ip 172.172.172.10 --name druid-router-permissive-tls -p 8889:8889 -p 9089:9089 -v $SHARED_DIR:/shared -v $DOCKERDIR/router-permissive-tls.conf:$SUPERVISORDIR/router-permissive-tls.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-coordinator:druid-coordinator --link druid-broker:druid-broker druid/cluster
 
-# Start Router 
-docker run -d --privileged --name druid-router -p 8888:8888 -v $SHARED_DIR:/shared -v $DOCKERDIR/router.conf:$SUPERVISORDIR/router.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-coordinator:druid-coordinator --link druid-broker:druid-broker druid/cluster
+# Start Router with TLS but no client auth
+docker run -d --privileged --net druid-it-net --ip 172.172.172.11 --name druid-router-no-client-auth-tls -p 8890:8890 -p 9090:9090 -v $SHARED_DIR:/shared -v $DOCKERDIR/router-no-client-auth-tls.conf:$SUPERVISORDIR/router-no-client-auth-tls.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-coordinator:druid-coordinator --link druid-broker:druid-broker druid/cluster
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java b/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java
index 584e118..2780527 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/ConfigFileConfigProvider.java
@@ -38,6 +38,15 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide
   private String historicalUrl;
   private String coordinatorUrl;
   private String indexerUrl;
+  private String permissiveRouterUrl;
+  private String noClientAuthRouterUrl;
+  private String routerTLSUrl;
+  private String brokerTLSUrl;
+  private String historicalTLSUrl;
+  private String coordinatorTLSUrl;
+  private String indexerTLSUrl;
+  private String permissiveRouterTLSUrl;
+  private String noClientAuthRouterTLSUrl;
   private String middleManagerHost;
   private String zookeeperHosts;        // comma-separated list of host:port
   private String kafkaHost;
@@ -70,25 +79,89 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide
         routerUrl = StringUtils.format("http://%s:%s", routerHost, props.get("router_port"));
       }
     }
+    routerTLSUrl = props.get("router_tls_url");
+    if (routerTLSUrl == null) {
+      String routerHost = props.get("router_host");
+      if (null != routerHost) {
+        routerTLSUrl = StringUtils.format("https://%s:%s", routerHost, props.get("router_tls_port"));
+      }
+    }
+    permissiveRouterUrl = props.get("router_permissive_url");
+    if (permissiveRouterUrl == null) {
+      String permissiveRouterHost = props.get("router_permissive_host");
+      if (null != permissiveRouterHost) {
+        permissiveRouterUrl = StringUtils.format("http://%s:%s", permissiveRouterHost, props.get("router_permissive_port"));
+      }
+    }
+    permissiveRouterTLSUrl = props.get("router_permissive_tls_url");
+    if (permissiveRouterTLSUrl == null) {
+      String permissiveRouterHost = props.get("router_permissive_host");
+      if (null != permissiveRouterHost) {
+        permissiveRouterTLSUrl = StringUtils.format("https://%s:%s", permissiveRouterHost, props.get("router_permissive_tls_port"));
+      }
+    }
+    noClientAuthRouterUrl = props.get("router_no_client_auth_url");
+    if (noClientAuthRouterUrl == null) {
+      String noClientAuthRouterHost = props.get("router_no_client_auth_host");
+      if (null != noClientAuthRouterHost) {
+        noClientAuthRouterUrl = StringUtils.format("http://%s:%s", noClientAuthRouterHost, props.get("router_no_client_auth_port"));
+      }
+    }
+    noClientAuthRouterTLSUrl = props.get("router_no_client_auth_tls_url");
+    if (noClientAuthRouterTLSUrl == null) {
+      String noClientAuthRouterHost = props.get("router_no_client_auth_host");
+      if (null != noClientAuthRouterHost) {
+        noClientAuthRouterTLSUrl = StringUtils.format("https://%s:%s", noClientAuthRouterHost, props.get("router_no_client_auth_tls_port"));
+      }
+    }
     brokerUrl = props.get("broker_url");
     if (brokerUrl == null) {
       brokerUrl = StringUtils.format("http://%s:%s", props.get("broker_host"), props.get("broker_port"));
     }
-
+    brokerTLSUrl = props.get("broker_tls_url");
+    if (brokerTLSUrl == null) {
+      String brokerHost = props.get("broker_host");
+      if (null != brokerHost) {
+        brokerTLSUrl = StringUtils.format("https://%s:%s", brokerHost, props.get("broker_tls_port"));
+      }
+    }
+    
     historicalUrl = props.get("historical_url");
     if (historicalUrl == null) {
       historicalUrl = StringUtils.format("http://%s:%s", props.get("historical_host"), props.get("historical_port"));
     }
+    historicalTLSUrl = props.get("historical_tls_url");
+    if (historicalTLSUrl == null) {
+      String historicalHost = props.get("historical_host");
+      if (null != historicalHost) {
+        historicalTLSUrl = StringUtils.format("https://%s:%s", historicalHost, props.get("historical_tls_port"));
+      }
+    }
 
     coordinatorUrl = props.get("coordinator_url");
     if (coordinatorUrl == null) {
       coordinatorUrl = StringUtils.format("http://%s:%s", props.get("coordinator_host"), props.get("coordinator_port"));
     }
+    coordinatorTLSUrl = props.get("coordinator_tls_url");
+    if (coordinatorTLSUrl == null) {
+      String coordinatorHost = props.get("coordinator_host");
+      if (null != coordinatorHost) {
+        coordinatorTLSUrl = StringUtils.format("https://%s:%s", coordinatorHost, props.get("coordinator_tls_port"));
+      }
+    }
 
     indexerUrl = props.get("indexer_url");
     if (indexerUrl == null) {
       indexerUrl = StringUtils.format("http://%s:%s", props.get("indexer_host"), props.get("indexer_port"));
     }
+    indexerTLSUrl = props.get("indexer_tls_url");
+    if (indexerTLSUrl == null) {
+      String indexerHost = props.get("indexer_host");
+      if (null != indexerHost) {
+        indexerTLSUrl = StringUtils.format("https://%s:%s", indexerHost, props.get("indexer_tls_port"));
+      }
+    }
+    
     middleManagerHost = props.get("middlemanager_host");
 
     zookeeperHosts = props.get("zookeeper_hosts");
@@ -98,10 +171,11 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide
 
     password = props.get("password");
 
-    LOG.info("router: [%s]", routerUrl);
-    LOG.info("broker: [%s]", brokerUrl);
-    LOG.info("coordinator: [%s]", coordinatorUrl);
-    LOG.info("overlord: [%s]", indexerUrl);
+    LOG.info("router: [%s], [%s]", routerUrl, routerTLSUrl);
+    LOG.info("broker: [%s], [%s]", brokerUrl, brokerTLSUrl);
+    LOG.info("historical: [%s], [%s]", historicalUrl, historicalTLSUrl);
+    LOG.info("coordinator: [%s], [%s]", coordinatorUrl, coordinatorTLSUrl);
+    LOG.info("overlord: [%s], [%s]", indexerUrl, indexerTLSUrl);
     LOG.info("middle manager: [%s]", middleManagerHost);
     LOG.info("zookeepers: [%s]", zookeeperHosts);
     LOG.info("kafka: [%s]", kafkaHost);
@@ -121,30 +195,84 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide
       }
 
       @Override
+      public String getCoordinatorTLSUrl()
+      {
+        return coordinatorTLSUrl;
+      }
+
+      @Override
       public String getIndexerUrl()
       {
         return indexerUrl;
       }
 
       @Override
+      public String getIndexerTLSUrl()
+      {
+        return indexerTLSUrl;
+      }
+
+      @Override
       public String getRouterUrl()
       {
         return routerUrl;
       }
 
       @Override
+      public String getRouterTLSUrl()
+      {
+        return routerTLSUrl;
+      }
+
+      @Override
+      public String getPermissiveRouterUrl()
+      {
+        return permissiveRouterUrl;
+      }
+
+      @Override
+      public String getPermissiveRouterTLSUrl()
+      {
+        return permissiveRouterTLSUrl;
+      }
+
+      @Override
+      public String getNoClientAuthRouterUrl()
+      {
+        return noClientAuthRouterUrl;
+      }
+
+      @Override
+      public String getNoClientAuthRouterTLSUrl()
+      {
+        return noClientAuthRouterTLSUrl;
+      }
+
+      @Override
       public String getBrokerUrl()
       {
         return brokerUrl;
       }
 
       @Override
+      public String getBrokerTLSUrl()
+      {
+        return brokerTLSUrl;
+      }
+
+      @Override
       public String getHistoricalUrl()
       {
         return historicalUrl;
       }
 
       @Override
+      public String getHistoricalTLSUrl()
+      {
+        return historicalTLSUrl;
+      }
+
+      @Override
       public String getMiddleManagerHost()
       {
         return middleManagerHost;
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java b/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java
index 3cb333f..bb71bc3 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/DockerConfigProvider.java
@@ -49,30 +49,84 @@ public class DockerConfigProvider implements IntegrationTestingConfigProvider
       }
 
       @Override
+      public String getCoordinatorTLSUrl()
+      {
+        return "https://" + dockerIp + ":8281";
+      }
+
+      @Override
       public String getIndexerUrl()
       {
         return "http://" + dockerIp + ":8090";
       }
 
       @Override
+      public String getIndexerTLSUrl()
+      {
+        return "https://" + dockerIp + ":8290";
+      }
+
+      @Override
       public String getRouterUrl()
       {
         return "http://" + dockerIp + ":8888";
       }
 
       @Override
+      public String getRouterTLSUrl()
+      {
+        return "https://" + dockerIp + ":9088";
+      }
+
+      @Override
+      public String getPermissiveRouterUrl()
+      {
+        return "http://" + dockerIp + ":8889";
+      }
+
+      @Override
+      public String getPermissiveRouterTLSUrl()
+      {
+        return "https://" + dockerIp + ":9089";
+      }
+
+      @Override
+      public String getNoClientAuthRouterUrl()
+      {
+        return "http://" + dockerIp + ":8890";
+      }
+
+      @Override
+      public String getNoClientAuthRouterTLSUrl()
+      {
+        return "https://" + dockerIp + ":9090";
+      }
+
+      @Override
       public String getBrokerUrl()
       {
         return "http://" + dockerIp + ":8082";
       }
 
       @Override
+      public String getBrokerTLSUrl()
+      {
+        return "https://" + dockerIp + ":8282";
+      }
+
+      @Override
       public String getHistoricalUrl()
       {
         return "http://" + dockerIp + ":8083";
       }
 
       @Override
+      public String getHistoricalTLSUrl()
+      {
+        return "https://" + dockerIp + ":8283";
+      }
+
+      @Override
       public String getMiddleManagerHost()
       {
         return dockerIp;
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java b/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java
index de05013..2b72e58 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/IntegrationTestingConfig.java
@@ -27,14 +27,32 @@ public interface IntegrationTestingConfig
 {
   String getCoordinatorUrl();
 
+  String getCoordinatorTLSUrl();
+
   String getIndexerUrl();
 
+  String getIndexerTLSUrl();
+
   String getRouterUrl();
 
+  String getRouterTLSUrl();
+
+  String getPermissiveRouterUrl();
+
+  String getPermissiveRouterTLSUrl();
+
+  String getNoClientAuthRouterUrl();
+
+  String getNoClientAuthRouterTLSUrl();
+
   String getBrokerUrl();
 
+  String getBrokerTLSUrl();
+
   String getHistoricalUrl();
 
+  String getHistoricalTLSUrl();
+
   String getMiddleManagerHost();
 
   String getZookeeperHosts();
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/clients/CoordinatorResourceTestClient.java b/integration-tests/src/main/java/org/apache/druid/testing/clients/CoordinatorResourceTestClient.java
index 667febc..eff8989 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/clients/CoordinatorResourceTestClient.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/clients/CoordinatorResourceTestClient.java
@@ -70,6 +70,11 @@ public class CoordinatorResourceTestClient
     );
   }
 
+  private String getMetadataSegmentsURL(String dataSource)
+  {
+    return StringUtils.format("%smetadata/datasources/%s/segments", getCoordinatorURL(), dataSource);
+  }
+
   private String getIntervalsURL(String dataSource)
   {
     return StringUtils.format("%sdatasources/%s/intervals", getCoordinatorURL(), dataSource);
@@ -81,6 +86,25 @@ public class CoordinatorResourceTestClient
   }
 
   // return a list of the segment dates for the specified datasource
+  public List<String> getMetadataSegments(final String dataSource)
+  {
+    ArrayList<String> segments = null;
+    try {
+      StatusResponseHolder response = makeRequest(HttpMethod.GET, getMetadataSegmentsURL(dataSource));
+
+      segments = jsonMapper.readValue(
+          response.getContent(), new TypeReference<ArrayList<String>>()
+          {
+          }
+      );
+    }
+    catch (Exception e) {
+      throw Throwables.propagate(e);
+    }
+    return segments;
+  }
+
+  // return a list of the segment dates for the specified datasource
   public List<String> getSegmentIntervals(final String dataSource)
   {
     ArrayList<String> segments = null;
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/clients/EventReceiverFirehoseTestClient.java b/integration-tests/src/main/java/org/apache/druid/testing/clients/EventReceiverFirehoseTestClient.java
index a7b4d9a..5b01c9e 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/clients/EventReceiverFirehoseTestClient.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/clients/EventReceiverFirehoseTestClient.java
@@ -71,7 +71,7 @@ public class EventReceiverFirehoseTestClient
   private String getURL()
   {
     return StringUtils.format(
-        "http://%s/druid/worker/v1/chat/%s/push-events/",
+        "https://%s/druid/worker/v1/chat/%s/push-events/",
         host,
         chatID
     );
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/clients/OverlordResourceTestClient.java b/integration-tests/src/main/java/org/apache/druid/testing/clients/OverlordResourceTestClient.java
index 1f5d888..eee9eaa 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/clients/OverlordResourceTestClient.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/clients/OverlordResourceTestClient.java
@@ -175,7 +175,7 @@ public class OverlordResourceTestClient
 
   public void waitUntilTaskCompletes(final String taskID)
   {
-    waitUntilTaskCompletes(taskID, 60000, 10);
+    waitUntilTaskCompletes(taskID, 10000, 60);
   }
 
   public void waitUntilTaskCompletes(final String taskID, final int millisEach, final int numTimes)
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/utils/RetryUtil.java b/integration-tests/src/main/java/org/apache/druid/testing/utils/RetryUtil.java
index 6f3d70a..53888db 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/utils/RetryUtil.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/utils/RetryUtil.java
@@ -32,9 +32,9 @@ public class RetryUtil
 
   private static final Logger LOG = new Logger(RetryUtil.class);
 
-  public static int DEFAULT_RETRY_COUNT = 10;
+  public static int DEFAULT_RETRY_COUNT = 30;
 
-  public static long DEFAULT_RETRY_SLEEP = TimeUnit.SECONDS.toMillis(30);
+  public static long DEFAULT_RETRY_SLEEP = TimeUnit.SECONDS.toMillis(10);
 
   public static void retryUntilTrue(Callable<Boolean> callable, String task)
   {
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java b/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java
index 93692e4..649557d 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java
@@ -42,6 +42,9 @@ public class TestQueryHelper
   private final QueryResourceTestClient queryClient;
   private final ObjectMapper jsonMapper;
   private final String broker;
+  private final String brokerTLS;
+  private final String router;
+  private final String routerTLS;
 
   @Inject
   TestQueryHelper(
@@ -53,11 +56,17 @@ public class TestQueryHelper
     this.jsonMapper = jsonMapper;
     this.queryClient = queryClient;
     this.broker = config.getBrokerUrl();
+    this.brokerTLS = config.getBrokerTLSUrl();
+    this.router = config.getRouterUrl();
+    this.routerTLS = config.getRouterTLSUrl();
   }
 
   public void testQueriesFromFile(String filePath, int timesToRun) throws Exception
   {
-    testQueriesFromFile(getBrokerURL(), filePath, timesToRun);
+    testQueriesFromFile(getQueryURL(broker), filePath, timesToRun);
+    testQueriesFromFile(getQueryURL(brokerTLS), filePath, timesToRun);
+    testQueriesFromFile(getQueryURL(router), filePath, timesToRun);
+    testQueriesFromFile(getQueryURL(routerTLS), filePath, timesToRun);
   }
 
   public void testQueriesFromFile(String url, String filePath, int timesToRun) throws Exception
@@ -75,7 +84,10 @@ public class TestQueryHelper
 
   public void testQueriesFromString(String str, int timesToRun) throws Exception
   {
-    testQueriesFromString(getBrokerURL(), str, timesToRun);
+    testQueriesFromString(getQueryURL(broker), str, timesToRun);
+    testQueriesFromString(getQueryURL(brokerTLS), str, timesToRun);
+    testQueriesFromString(getQueryURL(router), str, timesToRun);
+    testQueriesFromString(getQueryURL(routerTLS), str, timesToRun);
   }
 
   public void testQueriesFromString(String url, String str, int timesToRun) throws Exception
@@ -93,6 +105,7 @@ public class TestQueryHelper
 
   private void testQueries(String url, List<QueryWithResults> queries, int timesToRun) throws Exception
   {
+    LOG.info("Running queries, url [%s]", url);
     for (int i = 0; i < timesToRun; i++) {
       LOG.info("Starting Iteration %d", i);
 
@@ -119,9 +132,9 @@ public class TestQueryHelper
     }
   }
 
-  private String getBrokerURL()
+  private String getQueryURL(String schemeAndHost)
   {
-    return StringUtils.format("%s/druid/v2?pretty", broker);
+    return StringUtils.format("%s/druid/v2?pretty", schemeAndHost);
   }
 
   @SuppressWarnings("unchecked")
@@ -138,7 +151,7 @@ public class TestQueryHelper
                                   .intervals(interval)
                                   .build();
 
-    List<Map<String, Object>> results = queryClient.query(getBrokerURL(), query);
+    List<Map<String, Object>> results = queryClient.query(getQueryURL(broker), query);
     if (results.isEmpty()) {
       return 0;
     } else {
diff --git a/integration-tests/src/test/java/io/druid/tests/security/ITTLSTest.java b/integration-tests/src/test/java/io/druid/tests/security/ITTLSTest.java
new file mode 100644
index 0000000..7759eff
--- /dev/null
+++ b/integration-tests/src/test/java/io/druid/tests/security/ITTLSTest.java
@@ -0,0 +1,482 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package io.druid.tests.security;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Throwables;
+import com.google.inject.Inject;
+import org.apache.druid.guice.annotations.Client;
+import org.apache.druid.guice.http.DruidHttpClientConfig;
+import org.apache.druid.guice.http.LifecycleUtils;
+import org.apache.druid.https.SSLClientConfig;
+import org.apache.druid.java.util.common.ISE;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.lifecycle.Lifecycle;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.java.util.http.client.CredentialedHttpClient;
+import org.apache.druid.java.util.http.client.HttpClient;
+import org.apache.druid.java.util.http.client.HttpClientConfig;
+import org.apache.druid.java.util.http.client.HttpClientInit;
+import org.apache.druid.java.util.http.client.Request;
+import org.apache.druid.java.util.http.client.auth.BasicCredentials;
+import org.apache.druid.java.util.http.client.response.StatusResponseHandler;
+import org.apache.druid.java.util.http.client.response.StatusResponseHolder;
+import org.apache.druid.server.security.TLSUtils;
+import org.apache.druid.testing.IntegrationTestingConfig;
+import org.apache.druid.testing.guice.DruidTestModuleFactory;
+import org.jboss.netty.handler.codec.http.HttpMethod;
+import org.jboss.netty.handler.codec.http.HttpResponseStatus;
+import org.joda.time.Duration;
+import org.testng.Assert;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+@Guice(moduleFactory = DruidTestModuleFactory.class)
+public class ITTLSTest
+{
+  private static final Logger LOG = new Logger(ITTLSTest.class);
+
+  private static final Duration SSL_HANDSHAKE_TIMEOUT = new Duration(30 * 1000);
+
+  private static final int MAX_BROKEN_PIPE_RETRIES = 30;
+
+  @Inject
+  IntegrationTestingConfig config;
+
+  @Inject
+  ObjectMapper jsonMapper;
+
+  @Inject
+  SSLClientConfig sslClientConfig;
+
+  @Inject
+  @Client
+  HttpClient httpClient;
+
+  @Inject
+  @Client
+  DruidHttpClientConfig httpClientConfig;
+
+  StatusResponseHandler responseHandler = new StatusResponseHandler(StandardCharsets.UTF_8);
+
+  @Test
+  public void testPlaintextAccess()
+  {
+    LOG.info("---------Testing resource access without TLS---------");
+    HttpClient adminClient = new CredentialedHttpClient(
+        new BasicCredentials("admin", "priest"),
+        httpClient
+    );
+    makeRequest(adminClient, HttpMethod.GET, config.getCoordinatorUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getIndexerUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getBrokerUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getHistoricalUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getRouterUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getPermissiveRouterUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getNoClientAuthRouterUrl() + "/status", null);
+  }
+
+  @Test
+  public void testTLSNodeAccess()
+  {
+    LOG.info("---------Testing resource access with TLS enabled---------");
+    HttpClient adminClient = new CredentialedHttpClient(
+        new BasicCredentials("admin", "priest"),
+        httpClient
+    );
+    makeRequest(adminClient, HttpMethod.GET, config.getCoordinatorTLSUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getIndexerTLSUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getBrokerTLSUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getHistoricalTLSUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getRouterTLSUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl() + "/status", null);
+    makeRequest(adminClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  @Test
+  public void testTLSNodeAccessWithIntermediate()
+  {
+    LOG.info("---------Testing TLS resource access with 3-part cert chain---------");
+    HttpClient intermediateCertClient = makeCustomHttpClient(
+        "client_tls/intermediate_ca_client.jks",
+        "intermediate_ca_client"
+    );
+    makeRequest(intermediateCertClient, HttpMethod.GET, config.getCoordinatorTLSUrl() + "/status", null);
+    makeRequest(intermediateCertClient, HttpMethod.GET, config.getIndexerTLSUrl() + "/status", null);
+    makeRequest(intermediateCertClient, HttpMethod.GET, config.getBrokerTLSUrl() + "/status", null);
+    makeRequest(intermediateCertClient, HttpMethod.GET, config.getHistoricalTLSUrl() + "/status", null);
+    makeRequest(intermediateCertClient, HttpMethod.GET, config.getRouterTLSUrl() + "/status", null);
+    makeRequest(intermediateCertClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl() + "/status", null);
+    makeRequest(intermediateCertClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  @Test
+  public void checkAccessWithNoCert()
+  {
+    LOG.info("---------Testing TLS resource access without a certificate---------");
+    HttpClient certlessClient = makeCertlessClient();
+    checkFailedAccessNoCert(certlessClient, HttpMethod.GET, config.getCoordinatorTLSUrl());
+    checkFailedAccessNoCert(certlessClient, HttpMethod.GET, config.getIndexerTLSUrl());
+    checkFailedAccessNoCert(certlessClient, HttpMethod.GET, config.getBrokerTLSUrl());
+    checkFailedAccessNoCert(certlessClient, HttpMethod.GET, config.getHistoricalTLSUrl());
+    checkFailedAccessNoCert(certlessClient, HttpMethod.GET, config.getRouterTLSUrl());
+    checkFailedAccessNoCert(certlessClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl());
+    makeRequest(certlessClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  @Test
+  public void checkAccessWithWrongHostname()
+  {
+    LOG.info("---------Testing TLS resource access when client certificate has non-matching hostnames---------");
+    HttpClient wrongHostnameClient = makeCustomHttpClient(
+        "client_tls/invalid_hostname_client.jks",
+        "invalid_hostname_client"
+    );
+    checkFailedAccessWrongHostname(wrongHostnameClient, HttpMethod.GET, config.getCoordinatorTLSUrl());
+    checkFailedAccessWrongHostname(wrongHostnameClient, HttpMethod.GET, config.getIndexerTLSUrl());
+    checkFailedAccessWrongHostname(wrongHostnameClient, HttpMethod.GET, config.getBrokerTLSUrl());
+    checkFailedAccessWrongHostname(wrongHostnameClient, HttpMethod.GET, config.getHistoricalTLSUrl());
+    checkFailedAccessWrongHostname(wrongHostnameClient, HttpMethod.GET, config.getRouterTLSUrl());
+    makeRequest(wrongHostnameClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl() + "/status", null);
+    makeRequest(wrongHostnameClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  @Test
+  public void checkAccessWithWrongRoot()
+  {
+    LOG.info("---------Testing TLS resource access when client certificate is signed by a non-trusted root CA---------");
+    HttpClient wrongRootClient = makeCustomHttpClient(
+        "client_tls/client_another_root.jks",
+        "druid_another_root"
+    );
+    checkFailedAccessWrongRoot(wrongRootClient, HttpMethod.GET, config.getCoordinatorTLSUrl());
+    checkFailedAccessWrongRoot(wrongRootClient, HttpMethod.GET, config.getIndexerTLSUrl());
+    checkFailedAccessWrongRoot(wrongRootClient, HttpMethod.GET, config.getBrokerTLSUrl());
+    checkFailedAccessWrongRoot(wrongRootClient, HttpMethod.GET, config.getHistoricalTLSUrl());
+    checkFailedAccessWrongRoot(wrongRootClient, HttpMethod.GET, config.getRouterTLSUrl());
+    checkFailedAccessWrongRoot(wrongRootClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl());
+    makeRequest(wrongRootClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  @Test
+  public void checkAccessWithRevokedCert()
+  {
+    LOG.info("---------Testing TLS resource access when client certificate has been revoked---------");
+    HttpClient revokedClient = makeCustomHttpClient(
+        "client_tls/revoked_client.jks",
+        "revoked_druid"
+    );
+    checkFailedAccessRevoked(revokedClient, HttpMethod.GET, config.getCoordinatorTLSUrl());
+    checkFailedAccessRevoked(revokedClient, HttpMethod.GET, config.getIndexerTLSUrl());
+    checkFailedAccessRevoked(revokedClient, HttpMethod.GET, config.getBrokerTLSUrl());
+    checkFailedAccessRevoked(revokedClient, HttpMethod.GET, config.getHistoricalTLSUrl());
+    checkFailedAccessRevoked(revokedClient, HttpMethod.GET, config.getRouterTLSUrl());
+    makeRequest(revokedClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl() + "/status", null);
+    makeRequest(revokedClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  @Test
+  public void checkAccessWithExpiredCert()
+  {
+    LOG.info("---------Testing TLS resource access when client certificate has expired---------");
+    HttpClient expiredClient = makeCustomHttpClient(
+        "client_tls/expired_client.jks",
+        "expired_client"
+    );
+    checkFailedAccessExpired(expiredClient, HttpMethod.GET, config.getCoordinatorTLSUrl());
+    checkFailedAccessExpired(expiredClient, HttpMethod.GET, config.getIndexerTLSUrl());
+    checkFailedAccessExpired(expiredClient, HttpMethod.GET, config.getBrokerTLSUrl());
+    checkFailedAccessExpired(expiredClient, HttpMethod.GET, config.getHistoricalTLSUrl());
+    checkFailedAccessExpired(expiredClient, HttpMethod.GET, config.getRouterTLSUrl());
+    checkFailedAccessExpired(expiredClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl());
+    makeRequest(expiredClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  @Test
+  public void checkAccessWithNotCASignedCert()
+  {
+    LOG.info(
+        "---------Testing TLS resource access when client certificate is signed by a non-CA intermediate cert---------");
+    HttpClient notCAClient = makeCustomHttpClient(
+        "client_tls/invalid_ca_client.jks",
+        "invalid_ca_client"
+    );
+    checkFailedAccessNotCA(notCAClient, HttpMethod.GET, config.getCoordinatorTLSUrl());
+    checkFailedAccessNotCA(notCAClient, HttpMethod.GET, config.getIndexerTLSUrl());
+    checkFailedAccessNotCA(notCAClient, HttpMethod.GET, config.getBrokerTLSUrl());
+    checkFailedAccessNotCA(notCAClient, HttpMethod.GET, config.getHistoricalTLSUrl());
+    checkFailedAccessNotCA(notCAClient, HttpMethod.GET, config.getRouterTLSUrl());
+    checkFailedAccessNotCA(notCAClient, HttpMethod.GET, config.getPermissiveRouterTLSUrl());
+    makeRequest(notCAClient, HttpMethod.GET, config.getNoClientAuthRouterTLSUrl() + "/status", null);
+  }
+
+  private void checkFailedAccessNoCert(HttpClient httpClient, HttpMethod method, String url)
+  {
+    checkFailedAccess(
+        httpClient,
+        method,
+        url + "/status",
+        "Certless",
+        SSLException.class,
+        "Received fatal alert: bad_certificate"
+    );
+  }
+
+  private void checkFailedAccessWrongHostname(HttpClient httpClient, HttpMethod method, String url)
+  {
+    checkFailedAccess(
+        httpClient,
+        method,
+        url + "/status",
+        "Wrong hostname",
+        SSLException.class,
+        "Received fatal alert: certificate_unknown"
+    );
+  }
+
+  private void checkFailedAccessWrongRoot(HttpClient httpClient, HttpMethod method, String url)
+  {
+    checkFailedAccess(
+        httpClient,
+        method,
+        url + "/status",
+        "Wrong root cert",
+        SSLException.class,
+        "Received fatal alert: certificate_unknown"
+    );
+  }
+
+  private void checkFailedAccessRevoked(HttpClient httpClient, HttpMethod method, String url)
+  {
+    checkFailedAccess(
+        httpClient,
+        method,
+        url + "/status",
+        "Revoked cert",
+        SSLException.class,
+        "Received fatal alert: certificate_unknown"
+    );
+  }
+
+  private void checkFailedAccessExpired(HttpClient httpClient, HttpMethod method, String url)
+  {
+    checkFailedAccess(
+        httpClient,
+        method,
+        url + "/status",
+        "Expired cert",
+        SSLException.class,
+        "Received fatal alert: certificate_unknown"
+    );
+  }
+
+  private void checkFailedAccessNotCA(HttpClient httpClient, HttpMethod method, String url)
+  {
+    checkFailedAccess(
+        httpClient,
+        method,
+        url + "/status",
+        "Cert signed by non-CA",
+        SSLException.class,
+        "Received fatal alert: certificate_unknown"
+    );
+  }
+
+  private HttpClientConfig.Builder getHttpClientConfigBuilder(SSLContext sslContext)
+  {
+    return HttpClientConfig
+        .builder()
+        .withNumConnections(httpClientConfig.getNumConnections())
+        .withReadTimeout(httpClientConfig.getReadTimeout())
+        .withWorkerCount(httpClientConfig.getNumMaxThreads())
+        .withCompressionCodec(
+            HttpClientConfig.CompressionCodec.valueOf(StringUtils.toUpperCase(httpClientConfig.getCompressionCodec()))
+        )
+        .withUnusedConnectionTimeoutDuration(httpClientConfig.getUnusedConnectionTimeout())
+        .withSslHandshakeTimeout(SSL_HANDSHAKE_TIMEOUT)
+        .withSslContext(sslContext);
+  }
+
+  private HttpClient makeCustomHttpClient(String keystorePath, String certAlias)
+  {
+    SSLContext intermediateClientSSLContext = new TLSUtils.ClientSSLContextBuilder()
+        .setProtocol(sslClientConfig.getProtocol())
+        .setTrustStoreType(sslClientConfig.getTrustStoreType())
+        .setTrustStorePath(sslClientConfig.getTrustStorePath())
+        .setTrustStoreAlgorithm(sslClientConfig.getTrustStoreAlgorithm())
+        .setTrustStorePasswordProvider(sslClientConfig.getTrustStorePasswordProvider())
+        .setKeyStoreType(sslClientConfig.getKeyStoreType())
+        .setKeyStorePath(keystorePath)
+        .setKeyStoreAlgorithm(sslClientConfig.getKeyManagerFactoryAlgorithm())
+        .setCertAlias(certAlias)
+        .setKeyStorePasswordProvider(sslClientConfig.getKeyStorePasswordProvider())
+        .setKeyManagerFactoryPasswordProvider(sslClientConfig.getKeyManagerPasswordProvider())
+        .build();
+
+    final HttpClientConfig.Builder builder = getHttpClientConfigBuilder(intermediateClientSSLContext);
+
+    final Lifecycle lifecycle = new Lifecycle();
+
+    HttpClient client = HttpClientInit.createClient(
+        builder.build(),
+        LifecycleUtils.asMmxLifecycle(lifecycle)
+    );
+
+    HttpClient adminClient = new CredentialedHttpClient(
+        new BasicCredentials("admin", "priest"),
+        client
+    );
+    return adminClient;
+  }
+
+  private HttpClient makeCertlessClient()
+  {
+    SSLContext certlessClientSSLContext = new TLSUtils.ClientSSLContextBuilder()
+        .setProtocol(sslClientConfig.getProtocol())
+        .setTrustStoreType(sslClientConfig.getTrustStoreType())
+        .setTrustStorePath(sslClientConfig.getTrustStorePath())
+        .setTrustStoreAlgorithm(sslClientConfig.getTrustStoreAlgorithm())
+        .setTrustStorePasswordProvider(sslClientConfig.getTrustStorePasswordProvider())
+        .build();
+
+    final HttpClientConfig.Builder builder = getHttpClientConfigBuilder(certlessClientSSLContext);
+
+    final Lifecycle lifecycle = new Lifecycle();
+
+    HttpClient client = HttpClientInit.createClient(
+        builder.build(),
+        LifecycleUtils.asMmxLifecycle(lifecycle)
+    );
+
+    HttpClient adminClient = new CredentialedHttpClient(
+        new BasicCredentials("admin", "priest"),
+        client
+    );
+    return adminClient;
+  }
+
+  private void checkFailedAccess(
+      HttpClient httpClient,
+      HttpMethod method,
+      String url,
+      String clientDesc,
+      Class expectedException,
+      String expectedExceptionMsg
+  )
+  {
+    int retries = 0;
+    while (true) {
+      try {
+        makeRequest(httpClient, method, url, null, -1);
+      }
+      catch (RuntimeException re) {
+        Throwable rootCause = Throwables.getRootCause(re);
+
+        if (rootCause instanceof IOException && "Broken pipe".equals(rootCause.getMessage())) {
+          if (retries > MAX_BROKEN_PIPE_RETRIES) {
+            Assert.fail(StringUtils.format(
+                "Broken pipe retries exhausted, test failed, did not get %s.",
+                expectedException
+            ));
+          } else {
+            retries += 1;
+            continue;
+          }
+        }
+
+        Assert.assertTrue(
+            expectedException.isInstance(rootCause),
+            StringUtils.format("Expected %s but found %s instead.", expectedException, rootCause)
+        );
+
+        Assert.assertEquals(
+            rootCause.getMessage(),
+            expectedExceptionMsg
+        );
+
+        LOG.info("%s client [%s] request failed as expected when accessing [%s]", clientDesc, method, url);
+        return;
+      }
+      Assert.fail(StringUtils.format("Test failed, did not get %s.", expectedException));
+    }
+  }
+
+  private StatusResponseHolder makeRequest(HttpClient httpClient, HttpMethod method, String url, byte[] content)
+  {
+    return makeRequest(httpClient, method, url, content, 4);
+  }
+
+  private StatusResponseHolder makeRequest(
+      HttpClient httpClient,
+      HttpMethod method,
+      String url,
+      byte[] content,
+      int maxRetries
+  )
+  {
+    try {
+      Request request = new Request(method, new URL(url));
+      if (content != null) {
+        request.setContent(MediaType.APPLICATION_JSON, content);
+      }
+      int retryCount = 0;
+
+      StatusResponseHolder response;
+
+      while (true) {
+        response = httpClient.go(
+            request,
+            responseHandler
+        ).get();
+
+        if (!response.getStatus().equals(HttpResponseStatus.OK)) {
+          String errMsg = StringUtils.format(
+              "Error while making request to url[%s] status[%s] content[%s]",
+              url,
+              response.getStatus(),
+              response.getContent()
+          );
+          if (retryCount > maxRetries) {
+            throw new ISE(errMsg);
+          } else {
+            LOG.error(errMsg);
+            LOG.error("retrying in 3000ms, retryCount: " + retryCount);
+            retryCount++;
+            Thread.sleep(3000);
+          }
+        } else {
+          LOG.info("[%s] request to [%s] succeeded.", method, url);
+          break;
+        }
+      }
+      return response;
+    }
+    catch (Exception e) {
+      throw Throwables.propagate(e);
+    }
+  }
+}
diff --git a/integration-tests/src/test/java/org/apache/druid/tests/hadoop/ITHadoopIndexTest.java b/integration-tests/src/test/java/org/apache/druid/tests/hadoop/ITHadoopIndexTest.java
index 5a8124e..ebfe22d 100644
--- a/integration-tests/src/test/java/org/apache/druid/tests/hadoop/ITHadoopIndexTest.java
+++ b/integration-tests/src/test/java/org/apache/druid/tests/hadoop/ITHadoopIndexTest.java
@@ -75,7 +75,7 @@ public class ITHadoopIndexTest extends AbstractIndexerTest
     try {
       final String taskID = indexer.submitTask(indexerSpec);
       LOG.info("TaskID for loading index task %s", taskID);
-      indexer.waitUntilTaskCompletes(taskID, 60000, 20);
+      indexer.waitUntilTaskCompletes(taskID, 10000, 120);
       RetryUtil.retryUntil(
           new Callable<Boolean>()
           {
diff --git a/integration-tests/src/test/java/org/apache/druid/tests/indexer/AbstractITRealtimeIndexTaskTest.java b/integration-tests/src/test/java/org/apache/druid/tests/indexer/AbstractITRealtimeIndexTaskTest.java
index eb72839..0995dba 100644
--- a/integration-tests/src/test/java/org/apache/druid/tests/indexer/AbstractITRealtimeIndexTaskTest.java
+++ b/integration-tests/src/test/java/org/apache/druid/tests/indexer/AbstractITRealtimeIndexTaskTest.java
@@ -142,8 +142,8 @@ public abstract class AbstractITRealtimeIndexTaskTest extends AbstractIndexerTes
             }
           },
           true,
-          60000,
-          10,
+          10000,
+          60,
           "Real-time generated segments loaded"
       );
 
diff --git a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITCompactionTaskTest.java b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITCompactionTaskTest.java
index 8ea14ed..2f9078a 100644
--- a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITCompactionTaskTest.java
+++ b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITCompactionTaskTest.java
@@ -42,6 +42,7 @@ public class ITCompactionTaskTest extends AbstractIndexerTest
   {
     loadData();
     final List<String> intervalsBeforeCompaction = coordinator.getSegmentIntervals(INDEX_DATASOURCE);
+    intervalsBeforeCompaction.sort(null);
     final String compactedInterval = "2013-08-31T00:00:00.000Z/2013-09-02T00:00:00.000Z";
     if (intervalsBeforeCompaction.contains(compactedInterval)) {
       throw new ISE("Containing a segment for the compacted interval[%s] before compaction", compactedInterval);
@@ -49,12 +50,14 @@ public class ITCompactionTaskTest extends AbstractIndexerTest
     try {
       queryHelper.testQueriesFromFile(INDEX_QUERIES_RESOURCE, 2);
       compactData(false);
+
+      // 4 segments across 2 days, compacted into 1 new segment (5 total)
+      checkCompactionFinished(5);
       queryHelper.testQueriesFromFile(INDEX_QUERIES_RESOURCE, 2);
 
-      final List<String> intervalsAfterCompaction = coordinator.getSegmentIntervals(INDEX_DATASOURCE);
-      if (!intervalsAfterCompaction.contains(compactedInterval)) {
-        throw new ISE("Compacted segment for interval[%s] does not exist", compactedInterval);
-      }
+      intervalsBeforeCompaction.add(compactedInterval);
+      intervalsBeforeCompaction.sort(null);
+      checkCompactionIntervals(intervalsBeforeCompaction);
     }
     finally {
       unloadAndKillData(INDEX_DATASOURCE);
@@ -66,21 +69,16 @@ public class ITCompactionTaskTest extends AbstractIndexerTest
   {
     loadData();
     final List<String> intervalsBeforeCompaction = coordinator.getSegmentIntervals(INDEX_DATASOURCE);
+    intervalsBeforeCompaction.sort(null);
     try {
       queryHelper.testQueriesFromFile(INDEX_QUERIES_RESOURCE, 2);
       compactData(true);
+
+      // 4 segments across 2 days, compacted into 2 new segments (6 total)
+      checkCompactionFinished(6);
       queryHelper.testQueriesFromFile(INDEX_QUERIES_RESOURCE, 2);
 
-      final List<String> intervalsAfterCompaction = coordinator.getSegmentIntervals(INDEX_DATASOURCE);
-      intervalsBeforeCompaction.sort(null);
-      intervalsAfterCompaction.sort(null);
-      if (!intervalsBeforeCompaction.equals(intervalsAfterCompaction)) {
-        throw new ISE(
-            "Intervals before compaction[%s] should be same with those after compaction[%s]",
-            intervalsBeforeCompaction,
-            intervalsAfterCompaction
-        );
-      }
+      checkCompactionIntervals(intervalsBeforeCompaction);
     }
     finally {
       unloadAndKillData(INDEX_DATASOURCE);
@@ -112,4 +110,30 @@ public class ITCompactionTaskTest extends AbstractIndexerTest
         "Segment Compaction"
     );
   }
+
+  private void checkCompactionFinished(int numExpectedSegments)
+  {
+    RetryUtil.retryUntilTrue(
+        () -> {
+          int metadataSegmentCount = coordinator.getMetadataSegments(INDEX_DATASOURCE).size();
+          LOG.info("Current metadata segment count: %d, expected: %d", metadataSegmentCount, numExpectedSegments);
+          return metadataSegmentCount == numExpectedSegments;
+        },
+        "Compaction segment count check"
+    );
+  }
+
+  private void checkCompactionIntervals(List<String> expectedIntervals)
+  {
+    RetryUtil.retryUntilTrue(
+        () -> {
+          final List<String> intervalsAfterCompaction = coordinator.getSegmentIntervals(INDEX_DATASOURCE);
+          intervalsAfterCompaction.sort(null);
+          System.out.println("AFTER: " + intervalsAfterCompaction);
+          System.out.println("EXPECTED: " + expectedIntervals);
+          return intervalsAfterCompaction.equals(expectedIntervals);
+        },
+        "Compaction interval check"
+    );
+  }
 }
diff --git a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaIndexingServiceTest.java b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaIndexingServiceTest.java
index 38b508c..7b59721 100644
--- a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaIndexingServiceTest.java
+++ b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaIndexingServiceTest.java
@@ -278,8 +278,8 @@ public class ITKafkaIndexingServiceTest extends AbstractIndexerTest
             }
           },
           true,
-          30000,
-          10,
+          10000,
+          30,
           "Real-time generated segments loaded"
       );
     }
diff --git a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaTest.java b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaTest.java
index efbf2c2..b9ddcfc 100644
--- a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaTest.java
+++ b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITKafkaTest.java
@@ -226,7 +226,7 @@ public class ITKafkaTest extends AbstractIndexerTest
     LOG.info("-------------SUBMITTED TASK");
 
     // wait for the task to finish
-    indexer.waitUntilTaskCompletes(taskID, 20000, 30);
+    indexer.waitUntilTaskCompletes(taskID, 10000, 60);
 
     // wait for segments to be handed off
     try {
@@ -240,8 +240,8 @@ public class ITKafkaTest extends AbstractIndexerTest
             }
           },
           true,
-          30000,
-          10,
+          10000,
+          30,
           "Real-time generated segments loaded"
       );
     }
diff --git a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITUnionQueryTest.java b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITUnionQueryTest.java
index 748da88..a1bfb4c 100644
--- a/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITUnionQueryTest.java
+++ b/integration-tests/src/test/java/org/apache/druid/tests/indexer/ITUnionQueryTest.java
@@ -25,18 +25,26 @@ import com.google.inject.Inject;
 import org.apache.druid.curator.discovery.ServerDiscoveryFactory;
 import org.apache.druid.curator.discovery.ServerDiscoverySelector;
 import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.java.util.http.client.HttpClient;
+import org.apache.druid.java.util.http.client.Request;
+import org.apache.druid.java.util.http.client.response.StatusResponseHandler;
+import org.apache.druid.java.util.http.client.response.StatusResponseHolder;
 import org.apache.druid.testing.IntegrationTestingConfig;
 import org.apache.druid.testing.clients.EventReceiverFirehoseTestClient;
 import org.apache.druid.testing.guice.DruidTestModuleFactory;
 import org.apache.druid.testing.guice.TestClient;
 import org.apache.druid.testing.utils.RetryUtil;
 import org.apache.druid.testing.utils.ServerDiscoveryUtil;
+import org.jboss.netty.handler.codec.http.HttpMethod;
+import org.jboss.netty.handler.codec.http.HttpResponseStatus;
 import org.joda.time.DateTime;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
 
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
@@ -173,6 +181,26 @@ public class ITUnionQueryTest extends AbstractIndexerTest
 
       LOG.info("Event Receiver Found at host [%s]", host);
 
+      LOG.info("Checking worker /status/health for [%s]", host);
+      final StatusResponseHandler handler = new StatusResponseHandler(StandardCharsets.UTF_8);
+      RetryUtil.retryUntilTrue(
+          () -> {
+            try {
+              StatusResponseHolder response = httpClient.go(
+                  new Request(HttpMethod.GET, new URL(StringUtils.format("https://%s/status/health", host))),
+                  handler
+              ).get();
+              return response.getStatus().equals(HttpResponseStatus.OK);
+            }
+            catch (Throwable e) {
+              LOG.error(e, "");
+              return false;
+            }
+          },
+          StringUtils.format("Checking /status/health for worker [%s]", host)
+      );
+      LOG.info("Finished checking worker /status/health for [%s], success", host);
+
       EventReceiverFirehoseTestClient client = new EventReceiverFirehoseTestClient(
           host,
           EVENT_RECEIVER_SERVICE_PREFIX + id,
diff --git a/integration-tests/src/test/resources/indexer/kafka_index_task.json b/integration-tests/src/test/resources/indexer/kafka_index_task.json
index f580d46..55e28c7 100644
--- a/integration-tests/src/test/resources/indexer/kafka_index_task.json
+++ b/integration-tests/src/test/resources/indexer/kafka_index_task.json
@@ -61,7 +61,7 @@
       "type" : "realtime",
       "maxRowsInMemory": 500000,
       "intermediatePersistPeriod": "PT3M",
-      "windowPeriod": "PT1M",
+      "windowPeriod": "PT150S",
       "basePersistDirectory": "/home/y/var/druid_state/kafka_test/realtime/basePersist"
     }
   }
diff --git a/integration-tests/stop_cluster.sh b/integration-tests/stop_cluster.sh
index fb09ec2..fe4ad20 100755
--- a/integration-tests/stop_cluster.sh
+++ b/integration-tests/stop_cluster.sh
@@ -14,8 +14,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-for node in druid-historical druid-coordinator druid-overlord druid-router druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage;
-do 
+for node in druid-historical druid-coordinator druid-overlord druid-router druid-router-permissive-tls druid-router-no-client-auth-tls druid-broker druid-middlemanager druid-zookeeper-kafka druid-metadata-storage;
+
+do
 docker stop $node
 docker rm $node
 done
+
+docker network rm druid-it-net
diff --git a/pom.xml b/pom.xml
index 4438a0d..ec3e593 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1106,6 +1106,7 @@
                         <exclude>**/tutorial/conf/**</exclude>
                         <exclude>**/derby.log</exclude>
                         <exclude>**/docker/**</exclude>
+                        <exclude>**/client_tls/**</exclude>
                         <!--IDE MODULE FILES-->
                         <exclude>**/*.iml</exclude>
                         <!--CRASH LOGS-->
diff --git a/server/src/main/java/org/apache/druid/server/StatusResource.java b/server/src/main/java/org/apache/druid/server/StatusResource.java
index 5d14ca7..b96d2bf 100644
--- a/server/src/main/java/org/apache/druid/server/StatusResource.java
+++ b/server/src/main/java/org/apache/druid/server/StatusResource.java
@@ -31,9 +31,11 @@ import org.apache.druid.server.http.security.ConfigResourceFilter;
 import org.apache.druid.server.http.security.StateResourceFilter;
 
 import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -47,7 +49,6 @@ import java.util.Set;
 @Path("/status")
 public class StatusResource
 {
-
   private final Properties properties;
 
   private final DruidServerConfig druidServerConfig;
@@ -73,7 +74,9 @@ public class StatusResource
   @GET
   @ResourceFilters(StateResourceFilter.class)
   @Produces(MediaType.APPLICATION_JSON)
-  public Status doGet()
+  public Status doGet(
+      @Context final HttpServletRequest req
+  )
   {
     return new Status(Initialization.getLoadedImplementations(DruidModule.class));
   }
diff --git a/server/src/main/java/org/apache/druid/server/emitter/HttpEmitterModule.java b/server/src/main/java/org/apache/druid/server/emitter/HttpEmitterModule.java
index 3c3bca1..3517014 100644
--- a/server/src/main/java/org/apache/druid/server/emitter/HttpEmitterModule.java
+++ b/server/src/main/java/org/apache/druid/server/emitter/HttpEmitterModule.java
@@ -126,13 +126,13 @@ public class HttpEmitterModule implements Module
       }
     } else if (sslConfig.getTrustStorePath() != null) {
       log.info("Creating SSLContext for HttpEmitter client using config [%s]", sslConfig);
-      effectiveSSLContext = TLSUtils.createSSLContext(
-          sslConfig.getProtocol(),
-          sslConfig.getTrustStoreType(),
-          sslConfig.getTrustStorePath(),
-          sslConfig.getTrustStoreAlgorithm(),
-          sslConfig.getTrustStorePasswordProvider()
-      );
+      effectiveSSLContext = new TLSUtils.ClientSSLContextBuilder()
+          .setProtocol(sslConfig.getProtocol())
+          .setTrustStoreType(sslConfig.getTrustStoreType())
+          .setTrustStorePath(sslConfig.getTrustStorePath())
+          .setTrustStoreAlgorithm(sslConfig.getTrustStoreAlgorithm())
+          .setTrustStorePasswordProvider(sslConfig.getTrustStorePasswordProvider())
+          .build();
     } else {
       effectiveSSLContext = sslContext;
     }
diff --git a/server/src/main/java/org/apache/druid/server/http/OverlordProxyServlet.java b/server/src/main/java/org/apache/druid/server/http/OverlordProxyServlet.java
index 9f6b3ee..0b4a12d 100644
--- a/server/src/main/java/org/apache/druid/server/http/OverlordProxyServlet.java
+++ b/server/src/main/java/org/apache/druid/server/http/OverlordProxyServlet.java
@@ -23,12 +23,17 @@ import com.google.common.base.Throwables;
 import com.google.inject.Inject;
 import org.apache.druid.client.indexing.IndexingService;
 import org.apache.druid.discovery.DruidLeaderClient;
+import org.apache.druid.guice.annotations.Global;
+import org.apache.druid.guice.http.DruidHttpClientConfig;
 import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.server.security.AuthConfig;
+import com.google.inject.Provider;
+import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.proxy.ProxyServlet;
 
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.net.URI;
@@ -40,13 +45,19 @@ import java.net.URISyntaxException;
 public class OverlordProxyServlet extends ProxyServlet
 {
   private final DruidLeaderClient druidLeaderClient;
+  private final Provider<HttpClient> httpClientProvider;
+  private final DruidHttpClientConfig httpClientConfig;
 
   @Inject
   OverlordProxyServlet(
-      @IndexingService DruidLeaderClient druidLeaderClient
+      @IndexingService DruidLeaderClient druidLeaderClient,
+      @Global Provider<HttpClient> httpClientProvider,
+      @Global DruidHttpClientConfig httpClientConfig
   )
   {
     this.druidLeaderClient = druidLeaderClient;
+    this.httpClientProvider = httpClientProvider;
+    this.httpClientConfig = httpClientConfig;
   }
 
   @Override
@@ -72,6 +83,22 @@ public class OverlordProxyServlet extends ProxyServlet
   }
 
   @Override
+  protected HttpClient newHttpClient()
+  {
+    return httpClientProvider.get();
+  }
+
+  @Override
+  protected HttpClient createHttpClient() throws ServletException
+  {
+    HttpClient client = super.createHttpClient();
+    // override timeout set in ProxyServlet.createHttpClient
+    setTimeout(httpClientConfig.getReadTimeout().getMillis());
+    return client;
+  }
+
+
+  @Override
   protected void sendProxyRequest(
       HttpServletRequest clientRequest,
       HttpServletResponse proxyResponse,
diff --git a/server/src/main/java/org/apache/druid/server/initialization/TLSServerConfig.java b/server/src/main/java/org/apache/druid/server/initialization/TLSServerConfig.java
index 6ea2c12..4e8a625 100644
--- a/server/src/main/java/org/apache/druid/server/initialization/TLSServerConfig.java
+++ b/server/src/main/java/org/apache/druid/server/initialization/TLSServerConfig.java
@@ -55,6 +55,27 @@ public class TLSServerConfig
   @JsonProperty
   private List<String> excludeProtocols;
 
+  @JsonProperty
+  private boolean requireClientCertificate = false;
+
+  @JsonProperty
+  private String trustStoreType;
+
+  @JsonProperty
+  private String trustStorePath;
+
+  @JsonProperty
+  private String trustStoreAlgorithm;
+
+  @JsonProperty("trustStorePassword")
+  private PasswordProvider trustStorePasswordProvider;
+
+  @JsonProperty
+  private boolean validateHostnames = true;
+
+  @JsonProperty
+  private String crlPath;
+
   public String getKeyStorePath()
   {
     return keyStorePath;
@@ -105,6 +126,41 @@ public class TLSServerConfig
     return excludeProtocols;
   }
 
+  public boolean isRequireClientCertificate()
+  {
+    return requireClientCertificate;
+  }
+
+  public String getTrustStoreType()
+  {
+    return trustStoreType;
+  }
+
+  public String getTrustStorePath()
+  {
+    return trustStorePath;
+  }
+
+  public String getTrustStoreAlgorithm()
+  {
+    return trustStoreAlgorithm;
+  }
+
+  public PasswordProvider getTrustStorePasswordProvider()
+  {
+    return trustStorePasswordProvider;
+  }
+
+  public boolean isValidateHostnames()
+  {
+    return validateHostnames;
+  }
+
+  public String getCrlPath()
+  {
+    return crlPath;
+  }
+
   @Override
   public String toString()
   {
@@ -117,6 +173,12 @@ public class TLSServerConfig
            ", excludeCipherSuites=" + excludeCipherSuites +
            ", includeProtocols=" + includeProtocols +
            ", excludeProtocols=" + excludeProtocols +
+           ", requireClientCertificate=" + requireClientCertificate +
+           ", trustStoreType='" + trustStoreType + '\'' +
+           ", trustStorePath='" + trustStorePath + '\'' +
+           ", trustStoreAlgorithm='" + trustStoreAlgorithm + '\'' +
+           ", validateHostnames='" + validateHostnames + '\'' +
+           ", crlPath='" + crlPath + '\'' +
            '}';
   }
 }
diff --git a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
index ee613d1..269bb2c 100644
--- a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
+++ b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyServerModule.java
@@ -75,6 +75,8 @@ import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
 
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLEngine;
+import javax.net.ssl.TrustManagerFactory;
+import java.security.KeyStore;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -231,6 +233,7 @@ public class JettyServerModule extends JerseyServletModule
       if (sslContextFactoryBinding == null) {
         // Never trust all certificates by default
         sslContextFactory = new SslContextFactory(false);
+
         sslContextFactory.setKeyStorePath(tlsServerConfig.getKeyStorePath());
         sslContextFactory.setKeyStoreType(tlsServerConfig.getKeyStoreType());
         sslContextFactory.setKeyStorePassword(tlsServerConfig.getKeyStorePasswordProvider().getPassword());
@@ -256,6 +259,37 @@ public class JettyServerModule extends JerseyServletModule
           sslContextFactory.setExcludeProtocols(
               tlsServerConfig.getExcludeProtocols().toArray(new String[0]));
         }
+
+        sslContextFactory.setNeedClientAuth(tlsServerConfig.isRequireClientCertificate());
+        if (tlsServerConfig.isRequireClientCertificate()) {
+          if (tlsServerConfig.getCrlPath() != null) {
+            // setValidatePeerCerts is used just to enable revocation checking using a static CRL file.
+            // Certificate validation is always performed when client certificates are required.
+            sslContextFactory.setValidatePeerCerts(true);
+            sslContextFactory.setCrlPath(tlsServerConfig.getCrlPath());
+          }
+          if (tlsServerConfig.isValidateHostnames()) {
+            sslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
+          }
+          if (tlsServerConfig.getTrustStorePath() != null) {
+            sslContextFactory.setTrustStorePath(tlsServerConfig.getTrustStorePath());
+            sslContextFactory.setTrustStoreType(
+                tlsServerConfig.getTrustStoreType() == null
+                ? KeyStore.getDefaultType()
+                : tlsServerConfig.getTrustStoreType()
+            );
+            sslContextFactory.setTrustManagerFactoryAlgorithm(
+                tlsServerConfig.getTrustStoreAlgorithm() == null
+                ? TrustManagerFactory.getDefaultAlgorithm()
+                : tlsServerConfig.getTrustStoreAlgorithm()
+            );
+            sslContextFactory.setTrustStorePassword(
+                tlsServerConfig.getTrustStorePasswordProvider() == null
+                ? null
+                : tlsServerConfig.getTrustStorePasswordProvider().getPassword()
+            );
+          }
+        }
       } else {
         sslContextFactory = sslContextFactoryBinding.getProvider().get();
       }
diff --git a/server/src/main/java/org/apache/druid/server/security/TLSUtils.java b/server/src/main/java/org/apache/druid/server/security/TLSUtils.java
index 733af5e..691a0ae 100644
--- a/server/src/main/java/org/apache/druid/server/security/TLSUtils.java
+++ b/server/src/main/java/org/apache/druid/server/security/TLSUtils.java
@@ -19,48 +19,205 @@
 
 package org.apache.druid.server.security;
 
+import com.google.common.base.Preconditions;
 import com.google.common.base.Throwables;
 import org.apache.druid.metadata.PasswordProvider;
 
+import org.eclipse.jetty.util.ssl.AliasedX509ExtendedKeyManager;
+
+import javax.annotation.Nullable;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedKeyManager;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.security.KeyManagementException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
 
 public class TLSUtils
 {
+  public static class ClientSSLContextBuilder
+  {
+    private String protocol;
+    private String trustStoreType;
+    private String trustStorePath;
+    private String trustStoreAlgorithm;
+    private PasswordProvider trustStorePasswordProvider;
+    private String keyStoreType;
+    private String keyStorePath;
+    private String keyStoreAlgorithm;
+    private String certAlias;
+    private PasswordProvider keyStorePasswordProvider;
+    private PasswordProvider keyManagerFactoryPasswordProvider;
+
+    public ClientSSLContextBuilder setProtocol(String protocol)
+    {
+      this.protocol = protocol;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setTrustStoreType(String trustStoreType)
+    {
+      this.trustStoreType = trustStoreType;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setTrustStorePath(String trustStorePath)
+    {
+      this.trustStorePath = trustStorePath;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setTrustStoreAlgorithm(String trustStoreAlgorithm)
+    {
+      this.trustStoreAlgorithm = trustStoreAlgorithm;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setTrustStorePasswordProvider(PasswordProvider trustStorePasswordProvider)
+    {
+      this.trustStorePasswordProvider = trustStorePasswordProvider;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setKeyStoreType(String keyStoreType)
+    {
+      this.keyStoreType = keyStoreType;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setKeyStorePath(String keyStorePath)
+    {
+      this.keyStorePath = keyStorePath;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setKeyStoreAlgorithm(String keyStoreAlgorithm)
+    {
+      this.keyStoreAlgorithm = keyStoreAlgorithm;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setCertAlias(String certAlias)
+    {
+      this.certAlias = certAlias;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setKeyStorePasswordProvider(PasswordProvider keyStorePasswordProvider)
+    {
+      this.keyStorePasswordProvider = keyStorePasswordProvider;
+      return this;
+    }
+
+    public ClientSSLContextBuilder setKeyManagerFactoryPasswordProvider(PasswordProvider keyManagerFactoryPasswordProvider)
+    {
+      this.keyManagerFactoryPasswordProvider = keyManagerFactoryPasswordProvider;
+      return this;
+    }
+
+    public SSLContext build()
+    {
+      Preconditions.checkNotNull(trustStorePath, "must specify a trustStorePath");
+
+      return createSSLContext(
+        protocol,
+        trustStoreType,
+        trustStorePath,
+        trustStoreAlgorithm,
+        trustStorePasswordProvider,
+        keyStoreType,
+        keyStorePath,
+        keyStoreAlgorithm,
+        certAlias,
+        keyStorePasswordProvider,
+        keyManagerFactoryPasswordProvider
+      );
+    }
+  }
+
   public static SSLContext createSSLContext(
-      String protocol,
-      String trustStoreType,
+      @Nullable String protocol,
+      @Nullable String trustStoreType,
       String trustStorePath,
-      String trustStoreAlgorithm,
-      PasswordProvider trustStorePasswordProvider
+      @Nullable String trustStoreAlgorithm,
+      @Nullable PasswordProvider trustStorePasswordProvider,
+      @Nullable String keyStoreType,
+      @Nullable String keyStorePath,
+      @Nullable String keyStoreAlgorithm,
+      @Nullable String certAlias,
+      @Nullable PasswordProvider keyStorePasswordProvider,
+      @Nullable PasswordProvider keyManagerFactoryPasswordProvider
   )
   {
     SSLContext sslContext = null;
     try {
       sslContext = SSLContext.getInstance(protocol == null ? "TLSv1.2" : protocol);
-      KeyStore keyStore = KeyStore.getInstance(trustStoreType == null
+      KeyStore trustStore = KeyStore.getInstance(trustStoreType == null
                                                ? KeyStore.getDefaultType()
                                                : trustStoreType);
-      keyStore.load(
+      trustStore.load(
           new FileInputStream(trustStorePath),
-          trustStorePasswordProvider.getPassword().toCharArray()
+          trustStorePasswordProvider == null ? null : trustStorePasswordProvider.getPassword().toCharArray()
       );
       TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm == null
                                                                                 ? TrustManagerFactory.getDefaultAlgorithm()
                                                                                 : trustStoreAlgorithm);
-      trustManagerFactory.init(keyStore);
-      sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+      trustManagerFactory.init(trustStore);
+
+
+      KeyManager[] keyManagers;
+      if (keyStorePath != null) {
+        KeyStore keyStore = KeyStore.getInstance(keyStoreType == null
+                                                 ? KeyStore.getDefaultType()
+                                                 : keyStoreType);
+        keyStore.load(
+            new FileInputStream(keyStorePath),
+            keyStorePasswordProvider == null ? null : keyStorePasswordProvider.getPassword().toCharArray()
+        );
+
+        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
+            keyStoreAlgorithm == null ?
+            KeyManagerFactory.getDefaultAlgorithm() : keyStoreAlgorithm
+        );
+        keyManagerFactory.init(
+            keyStore,
+            keyManagerFactoryPasswordProvider == null ? null : keyManagerFactoryPasswordProvider.getPassword().toCharArray()
+        );
+        keyManagers = createAliasedKeyManagers(keyManagerFactory.getKeyManagers(), certAlias);
+      } else {
+        keyManagers = null;
+      }
+
+      sslContext.init(
+          keyManagers,
+          trustManagerFactory.getTrustManagers(),
+          null
+      );
     }
-    catch (CertificateException | KeyManagementException | IOException | KeyStoreException | NoSuchAlgorithmException e) {
+    catch (CertificateException | KeyManagementException | IOException | KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
       Throwables.propagate(e);
     }
     return sslContext;
   }
+
+  // Use Jetty's aliased KeyManager for consistency between server/client TLS configs
+  private static KeyManager[] createAliasedKeyManagers(KeyManager[] delegates, String certAlias)
+  {
+    KeyManager[] aliasedManagers = new KeyManager[delegates.length];
+    for (int i = 0; i < delegates.length; i++) {
+      if (delegates[i] instanceof X509ExtendedKeyManager) {
+        aliasedManagers[i] = new AliasedX509ExtendedKeyManager((X509ExtendedKeyManager) delegates[i], certAlias);
+      } else {
+        aliasedManagers[i] = delegates[i];
+      }
+    }
+    return aliasedManagers;
+  }
 }
diff --git a/server/src/test/java/org/apache/druid/server/http/OverlordProxyServletTest.java b/server/src/test/java/org/apache/druid/server/http/OverlordProxyServletTest.java
index 0a3dd93..dd364b7 100644
--- a/server/src/test/java/org/apache/druid/server/http/OverlordProxyServletTest.java
+++ b/server/src/test/java/org/apache/druid/server/http/OverlordProxyServletTest.java
@@ -41,7 +41,7 @@ public class OverlordProxyServletTest
 
     EasyMock.replay(druidLeaderClient, request);
 
-    URI uri = URI.create(new OverlordProxyServlet(druidLeaderClient).rewriteTarget(request));
+    URI uri = URI.create(new OverlordProxyServlet(druidLeaderClient, null, null).rewriteTarget(request));
     Assert.assertEquals("https://overlord:port/druid/overlord/worker?param1=test&param2=test2", uri.toString());
   }
 
diff --git a/services/src/main/java/org/apache/druid/cli/CliCoordinator.java b/services/src/main/java/org/apache/druid/cli/CliCoordinator.java
index 5503c85..750a9fa 100644
--- a/services/src/main/java/org/apache/druid/cli/CliCoordinator.java
+++ b/services/src/main/java/org/apache/druid/cli/CliCoordinator.java
@@ -45,6 +45,7 @@ import org.apache.druid.guice.LifecycleModule;
 import org.apache.druid.guice.ManageLifecycle;
 import org.apache.druid.guice.annotations.CoordinatorIndexingServiceHelper;
 import org.apache.druid.guice.annotations.EscalatedGlobal;
+import org.apache.druid.guice.http.JettyHttpClientModule;
 import org.apache.druid.java.util.common.concurrent.ScheduledExecutorFactory;
 import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.java.util.http.client.HttpClient;
@@ -127,6 +128,8 @@ public class CliCoordinator extends ServerRunnable
   {
     List<Module> modules = new ArrayList<>();
 
+    modules.add(JettyHttpClientModule.global());
+
     modules.add(
         new Module()
         {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org