You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kudu.apache.org by ab...@apache.org on 2023/04/14 14:54:00 UTC

[kudu] 02/03: [jwt] Verify JWKS URL server TLS certificate by default

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

abukor pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kudu.git

commit 8595384de007963181cca59b0248b85169f18792
Author: Zoltan Chovan <zc...@cloudera.com>
AuthorDate: Mon Apr 3 15:38:50 2023 +0200

    [jwt] Verify JWKS URL server TLS certificate by default
    
    This commit is to pull IMPALA-11922 code into the Kudu jwt handling,
    with some modifications.
    
    This change introduces:
      1. verification of JWKS server TLS certificate by default
      2. jwks_verify_server_certificate Kudu startup flag
    
    Instead of introducing a new flag such as 'jwks_ca_certificate' the
    already existing 'trusted_certificate_file' flag is reused.
    
    The TLS certificate verification is not used in unit-tests, however
    security-itest is set up with the verification enabled.
    
    Change-Id: I0fd7b53d651786bbe57642dd14cd477055b80c78
    Reviewed-on: http://gerrit.cloudera.org:8080/19709
    Reviewed-by: Attila Bukor <ab...@apache.org>
    Tested-by: Kudu Jenkins
---
 src/kudu/integration-tests/CMakeLists.txt      |   3 +-
 src/kudu/integration-tests/security-itest.cc   | 127 +++++++++++++++++++++
 src/kudu/mini-cluster/CMakeLists.txt           |   1 +
 src/kudu/mini-cluster/external_mini_cluster.cc |   7 +-
 src/kudu/security/test/test_certs.cc           | 150 +++++++++++++++++++++++++
 src/kudu/security/test/test_certs.h            |   7 ++
 src/kudu/server/server_base.cc                 |  17 ++-
 src/kudu/util/jwt-util-internal.h              |  11 +-
 src/kudu/util/jwt-util-test.cc                 |  54 ++++-----
 src/kudu/util/jwt-util.cc                      |  38 +++++--
 src/kudu/util/jwt-util.h                       |  19 +++-
 src/kudu/util/mini_oidc.cc                     |  32 ++++--
 src/kudu/util/mini_oidc.h                      |   7 ++
 13 files changed, 421 insertions(+), 52 deletions(-)

diff --git a/src/kudu/integration-tests/CMakeLists.txt b/src/kudu/integration-tests/CMakeLists.txt
index 0e7ca24b5..c390871c0 100644
--- a/src/kudu/integration-tests/CMakeLists.txt
+++ b/src/kudu/integration-tests/CMakeLists.txt
@@ -48,8 +48,7 @@ target_link_libraries(itest_util
   kudu_fs
   kudu_test_util
   kudu_tools_test_util
-  kudu_tools_util
-  security_test_util)
+  kudu_tools_util)
 add_dependencies(itest_util
   kudu-master
   kudu-tserver)
diff --git a/src/kudu/integration-tests/security-itest.cc b/src/kudu/integration-tests/security-itest.cc
index cdc642f18..0b77adbb9 100644
--- a/src/kudu/integration-tests/security-itest.cc
+++ b/src/kudu/integration-tests/security-itest.cc
@@ -57,6 +57,7 @@
 #include "kudu/rpc/rpc_controller.h"
 #include "kudu/security/kinit_context.h"
 #include "kudu/security/test/mini_kdc.h"
+#include "kudu/security/test/test_certs.h"
 #include "kudu/security/token.pb.h"
 #include "kudu/server/server_base.pb.h"
 #include "kudu/server/server_base.proxy.h"
@@ -512,6 +513,7 @@ void GetFullBinaryPath(string* binary) {
   (*binary) = JoinPathSegments(DirName(exe), *binary);
 }
 
+
 TEST_F(SecurityITest, TestJwtMiniCluster) {
   SKIP_IF_SLOW_NOT_ALLOWED();
 
@@ -527,6 +529,22 @@ TEST_F(SecurityITest, TestJwtMiniCluster) {
     { kInvalidAccount, false },
   };
   oidc_opts.lifetime_ms = kLifetimeMs;
+
+  // Set up certificates for the JWKS server
+  string ca_certificate_file;
+  string private_key_file;
+  string certificate_file;
+  ASSERT_OK(kudu::security::CreateTestSSLCertWithChainSignedByRoot(GetTestDataDirectory(),
+                                                                   &certificate_file,
+                                                                   &private_key_file,
+                                                                   &ca_certificate_file));
+  // set the certs and private key for the jwks webserver
+  oidc_opts.private_key_file = private_key_file;
+  oidc_opts.server_certificate = certificate_file;
+  // set the ca_cert (jwks certificate verification is enabled by default)
+  cluster_opts_.extra_master_flags.push_back(Substitute("--trusted_certificate_file=$0",
+                                                        ca_certificate_file));
+
   cluster_opts_.mini_oidc_options = std::move(oidc_opts);
   ASSERT_OK(StartCluster());
   const auto* const kSubject = "kudu-user";
@@ -585,6 +603,115 @@ TEST_F(SecurityITest, TestJwtMiniCluster) {
   }
 }
 
+TEST_F(SecurityITest, TestJwtMiniClusterWithInvalidCert) {
+  cluster_opts_.enable_kerberos = false;
+  cluster_opts_.num_tablet_servers = 0;
+  cluster_opts_.enable_client_jwt = true;
+  MiniOidcOptions oidc_opts;
+  const auto* const kValidAccount = "valid";
+  const uint64_t kLifetimeMs = 1000;
+  oidc_opts.account_ids = {
+    { kValidAccount, true }
+  };
+  oidc_opts.lifetime_ms = kLifetimeMs;
+  const auto* const kSubject = "kudu-user";
+
+  // Set up certificates for the JWKS server
+  string ca_certificate_file;
+  string private_key_file;
+  string certificate_file;
+
+  ASSERT_OK(kudu::security::CreateTestSSLExpiredCertWithChainSignedByRoot(GetTestDataDirectory(),
+                                                                   &certificate_file,
+                                                                   &private_key_file,
+                                                                   &ca_certificate_file));
+
+  // set the certs and private key for the jwks webserver
+  oidc_opts.private_key_file = private_key_file;
+  oidc_opts.server_certificate = certificate_file;
+  // set the ca_cert (jwks certificate verification is enabled by default)
+  cluster_opts_.extra_master_flags.push_back(Substitute("--trusted_certificate_file=$0",
+                                                        ca_certificate_file));
+
+  cluster_opts_.mini_oidc_options = std::move(oidc_opts);
+  ASSERT_OK(StartCluster());
+
+  {
+    KuduClientBuilder client_builder;
+    client::AuthenticationCredentialsPB pb;
+    security::JwtRawPB jwt = security::JwtRawPB();
+    *jwt.mutable_jwt_data() = cluster_->oidc()->CreateJwt(kValidAccount, kSubject, true);
+    *pb.mutable_jwt() = std::move(jwt);
+    string creds;
+    CHECK(pb.SerializeToString(&creds));
+
+    for (auto i = 0; i < cluster_->num_masters(); ++i) {
+      client_builder.add_master_server_addr(cluster_->master(i)->bound_rpc_addr().ToString());
+    }
+    client_builder.import_authentication_credentials(creds);
+    client_builder.require_authentication(true);
+
+    shared_ptr<KuduClient> client;
+
+    Status s = client_builder.Build(&client);
+    ASSERT_FALSE(s.ok());
+    ASSERT_STR_CONTAINS(s.ToString(),
+                        "SSL certificate problem: unable to get local issuer certificate");
+  }
+}
+
+TEST_F(SecurityITest, TestJwtMiniClusterWithUntrustedCert) {
+  cluster_opts_.enable_kerberos = false;
+  cluster_opts_.num_tablet_servers = 0;
+  cluster_opts_.enable_client_jwt = true;
+  MiniOidcOptions oidc_opts;
+  const auto* const kValidAccount = "valid";
+  const uint64_t kLifetimeMs = 1000;
+  oidc_opts.account_ids = {
+    { kValidAccount, true }
+  };
+  oidc_opts.lifetime_ms = kLifetimeMs;
+  const auto* const kSubject = "kudu-user";
+
+  // Set up certificates for the JWKS server
+  string ca_certificate_file;
+  string private_key_file;
+  string certificate_file;
+  ASSERT_OK(kudu::security::CreateTestSSLCertWithChainSignedByRoot(GetTestDataDirectory(),
+                                                                   &certificate_file,
+                                                                   &private_key_file,
+                                                                   &ca_certificate_file));
+  // set the certs and private key for the jwks webserver
+  // jwks certificate verification is enabled by default, so we won't have to set it
+  oidc_opts.private_key_file = private_key_file;
+  oidc_opts.server_certificate = certificate_file;
+
+  cluster_opts_.mini_oidc_options = std::move(oidc_opts);
+  ASSERT_OK(StartCluster());
+
+  {
+    KuduClientBuilder client_builder;
+    client::AuthenticationCredentialsPB pb;
+    security::JwtRawPB jwt = security::JwtRawPB();
+    *jwt.mutable_jwt_data() = cluster_->oidc()->CreateJwt(kValidAccount, kSubject, true);
+    *pb.mutable_jwt() = std::move(jwt);
+    string creds;
+    CHECK(pb.SerializeToString(&creds));
+
+    for (auto i = 0; i < cluster_->num_masters(); ++i) {
+      client_builder.add_master_server_addr(cluster_->master(i)->bound_rpc_addr().ToString());
+    }
+    client_builder.import_authentication_credentials(creds);
+    client_builder.require_authentication(true);
+
+    shared_ptr<KuduClient> client;
+
+    Status s = client_builder.Build(&client);
+    ASSERT_FALSE(s.ok());
+    ASSERT_STR_CONTAINS(s.ToString(), "SSL peer certificate or SSH remote key was not OK");
+  }
+}
+
 TEST_F(SecurityITest, TestWorldReadableKeytab) {
   const string credentials_name = GetTestPath("insecure.keytab");
   NO_FATALS(CreateWorldReadableFile(credentials_name));
diff --git a/src/kudu/mini-cluster/CMakeLists.txt b/src/kudu/mini-cluster/CMakeLists.txt
index f10497a0d..7748795e6 100644
--- a/src/kudu/mini-cluster/CMakeLists.txt
+++ b/src/kudu/mini-cluster/CMakeLists.txt
@@ -45,6 +45,7 @@ set(MINI_CLUSTER_LIBS
   tserver
   tserver_proto
   tserver_service_proto
+  security_test_util
   wire_protocol_proto)
 
 if (NOT NO_CHRONY)
diff --git a/src/kudu/mini-cluster/external_mini_cluster.cc b/src/kudu/mini-cluster/external_mini_cluster.cc
index 948508e35..0e61192dc 100644
--- a/src/kudu/mini-cluster/external_mini_cluster.cc
+++ b/src/kudu/mini-cluster/external_mini_cluster.cc
@@ -276,8 +276,13 @@ Status ExternalMiniCluster::Start() {
   std::shared_ptr<JwtVerifier> jwt_verifier = nullptr;
   if (opts_.enable_client_jwt) {
     oidc_.reset(new MiniOidc(opts_.mini_oidc_options));
+    // Set up certificates for the JWKS server
     RETURN_NOT_OK_PREPEND(oidc_->Start(), "Failed to start OIDC endpoints");
-    jwt_verifier = std::make_shared<PerAccountKeyBasedJwtVerifier>(oidc_->url());
+    jwt_verifier =
+        std::make_shared<PerAccountKeyBasedJwtVerifier>(oidc_->url(),
+                                                        true,
+                                                        opts_.mini_oidc_options.server_certificate);
+
   }
 
   RETURN_NOT_OK_PREPEND(
diff --git a/src/kudu/security/test/test_certs.cc b/src/kudu/security/test/test_certs.cc
index 321a4ef59..d322e317c 100644
--- a/src/kudu/security/test/test_certs.cc
+++ b/src/kudu/security/test/test_certs.cc
@@ -971,5 +971,155 @@ KH5H1VGmllMdZDHOamHHKA8mEDI4eAKY3HoOS4rfioT8Tks=
   return Status::OK();
 }
 
+// These certificates are the same as used in CreateTestSSLCertWithChainSignedByRoot,
+// except the ca_cert, which is replaced with an expired version.
+Status CreateTestSSLExpiredCertWithChainSignedByRoot(const string& dir,
+                                              string* cert_file,
+                                              string* key_file,
+                                              string* expired_ca_cert_file) {
+
+   const char* kCert = R"(
+-----BEGIN CERTIFICATE-----
+MIIFizCCA3OgAwIBAgICEAAwDQYJKoZIhvcNAQEFBQAwUTEXMBUGA1UEAwwOSW50
+ZXJtZWRpYXRlQ0ExCzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzENMAsGA1UECgwE
+QWNtZTENMAsGA1UECwwES3VkdTAeFw0xNzA4MTEyMTM4MDZaFw00NDEyMjYyMTM4
+MDZaMEwxEjAQBgNVBAMMCWxvY2FsaG9zdDELMAkGA1UECAwCQ0ExCzAJBgNVBAYT
+AlVTMQ0wCwYDVQQKDARBY21lMQ0wCwYDVQQLDARLdWR1MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAqevNYH73n4kARZtMsHRucdKmqVd/xxztMlK5VOor
+ERUBhKVVOw3kpDrN9z80ldIkpOrtrfE7Ame/nA9v4k6P3minPEm1qCA/kvaAodtT
+4HjAkrPc+fto6VO6+aUV6l+ckAV/79lOuc7AutNlvvPtBQQcgOKvlNUSRKwM7ndy
+dO4ZAa+uP9Wtsd0gl8b5F3P8vwevD3a0+iDvwSd3pi2s/BeVgRwvOxJzud8ipZ/A
+ZmZN8Df9nHw5lsqLdNnqHXjTVCNXLnYXQC4gKU56fzyZL595liuefyQxiGY+dCCn
+CpqlSsHboJVC/F3OaQi3xVRTB5l2Nwb149EIadwCF0OulZCuYljJ5y9H2bECXEjP
+e5aOdz9d8W3/T7p9vBKWctToeCpqKXUd+8RPudh0D0sUHuwQ4u4S1K6X+eK+gGhT
+HOnPwt+P8ytG0M463z5Gh9feW9ZDIYoiFckheFBAHxsgDWhjYpFmYireLLXMbyaM
+s5v/AxPNRAsx3vAAd0M0vGOpdgEJ9V1MsKmxkPO/tDC3zmnv6uJhtJfrOAKxwiGC
+fDe4IoSC6H5fTxeAgw6BG5onS1UPLADL8NA/M1y8qiSCZS/5S0cHoJp5AxDfZSSR
+O49ispjqcONRwckcRJ5Pbl0IA+wGyg2DuI9LaqS5kKWp5AE8VCLPz7yepDkRnnjO
+3m8CAwEAAaNyMHAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUZBZLZZaUfyIK/8B7
+GIIWDqeEvDgwHwYDVR0jBBgwFoAU8KctfaqAq0887CHqDsIC0Rkg7oQwCwYDVR0P
+BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBBQUAA4ICAQA3
+XJXk9CbzdZUQugPI43LY88g+WjbTJfc/KtPSkHN3GjBBh8C0He7A2tp6Xj/LELmx
+crq62FzcFBnq8/iSdFITaYWRo0V/mXlpv2cpPebtwqbARCXUHGvF4/dGk/kw7uK/
+ohZJbeNySuQmQ5SQyfTdVA30Z0OSZ4jp24jC8uME7L8XOcFDgCRw01QNOISpi/5J
+BqeuFihmu/odYMHiEJdCXqe+4qIFfTh0mbgQ57l/geZm0K8uCEiOdTzSMoO8YdO2
+tm6EGNnc4yrVywjIHIvSy6YtNzd4ZM1a1CkEfPvGwe/wI1DI/zl3aJ721kcMPken
+rgEA4xXTPh6gZNMELIGZfu/mOTCFObe8rrh4QSaW4L+xa/VrLEnQRxuXAYGnmDWF
+e79aA+uXdS4+3OysNgEf4qDBt/ZquS/31DBdfJ59VfXWxp2yxMcGhcfiOdnx2Jy5
+KO8wdpXJA/7uwTJzsjLrIgfZnserOiBwE4luaHhDmKDGNVQvhkMq5tdtMdzuwn3/
+n6P1UwbFPiRGIzEAo0SSC1PRT8phv+5y0B1+gcj/peFymZVE+gRcrv9irVQqUpAY
+Lo9xrClAJ2xx4Ouz1GprKPoHdVyqtgcLXN4Oyi8Tehu96Zf6GytSEfTXsbQp+GgR
+TGRhKnDySjPhLp/uObfVwioyuAyA5mVCwjsZ/cvUUA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+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
+Wd40Cr+wAdHKN6t/oransoxu0EZ3HcSOI1umFg==
+-----END CERTIFICATE-----
+)";
+  const char* kKey = R"(
+-----BEGIN PRIVATE KEY-----
+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
+QSLJ/7+Bn9NwsoDIaegA7vbs2BrKqg==
+-----END PRIVATE KEY-----
+)";
+
+  *cert_file = JoinPathSegments(dir, "test.cert");
+  *key_file = JoinPathSegments(dir, "test.key");
+  *expired_ca_cert_file = JoinPathSegments(dir, "testchainca.cert");
+
+  RETURN_NOT_OK(WriteStringToFile(Env::Default(), kCert, *cert_file));
+  RETURN_NOT_OK(WriteStringToFile(Env::Default(), kKey, *key_file));
+  RETURN_NOT_OK(WriteStringToFile(Env::Default(), kCaExpiredCert, *expired_ca_cert_file));
+
+  return Status::OK();
+}
+
 } // namespace security
 } // namespace kudu
diff --git a/src/kudu/security/test/test_certs.h b/src/kudu/security/test/test_certs.h
index 7767cb249..8c571ce34 100644
--- a/src/kudu/security/test/test_certs.h
+++ b/src/kudu/security/test/test_certs.h
@@ -82,5 +82,12 @@ Status CreateTestSSLCertWithChainSignedByRoot(const std::string& dir,
                                               std::string* key_file,
                                               std::string* ca_cert_file);
 
+// Same as the CreateTestSSLCertWithPlainKey() except that the 'ca_cert_file' contains
+// an expired certificate.
+Status CreateTestSSLExpiredCertWithChainSignedByRoot(const std::string& dir,
+                                              std::string* cert_file,
+                                              std::string* key_file,
+                                              std::string* expired_ca_cert_file);
+
 } // namespace security
 } // namespace kudu
diff --git a/src/kudu/server/server_base.cc b/src/kudu/server/server_base.cc
index 51045eee0..baf877b2f 100644
--- a/src/kudu/server/server_base.cc
+++ b/src/kudu/server/server_base.cc
@@ -274,6 +274,17 @@ DEFINE_string(jwks_url, "",
     "URL of the JSON Web Key Set (JWKS) for JWT verification.");
 TAG_FLAG(jwks_url, experimental);
 
+// Enables retrieving the JWKS URL with verifying the presented TLS certificate
+// from the server.
+DEFINE_bool(jwks_verify_server_certificate, true,
+            "Specifies if the TLS certificate of the JWKS server is verified when retrieving "
+            "the JWKS from the specified JWKS URL. A certificate is considered valid if a "
+            "trust chain can be established for it, and the certificate has a common name or "
+            "SAN that matches the server's hostname. This should only be set to false for "
+            "development / testing.");
+TAG_FLAG(jwks_verify_server_certificate, experimental);
+TAG_FLAG(jwks_verify_server_certificate, unsafe);
+
 DEFINE_string(jwks_discovery_endpoint_base, "",
               "Base URL of the Discovery Endpoint that points to a JSON Web Key Set "
               "(JWKS) for JWT verification. Additional query parameters, like 'accountId', "
@@ -320,6 +331,7 @@ DECLARE_string(log_filename);
 DECLARE_string(keytab_file);
 DECLARE_string(principal);
 DECLARE_string(time_source);
+DECLARE_string(trusted_certificate_file);
 
 METRIC_DECLARE_gauge_size(merged_entities_count_of_server);
 METRIC_DEFINE_gauge_int64(server, uptime,
@@ -800,7 +812,10 @@ Status ServerBase::Init() {
   shared_ptr<JwtVerifier> jwt_verifier = nullptr;
   if (FLAGS_enable_jwt_token_auth) {
     if (!FLAGS_jwks_url.empty()) {
-      jwt_verifier = std::make_shared<PerAccountKeyBasedJwtVerifier>(FLAGS_jwks_url);
+      jwt_verifier =
+          std::make_shared<PerAccountKeyBasedJwtVerifier>(FLAGS_jwks_url,
+                                                          FLAGS_jwks_verify_server_certificate,
+                                                          FLAGS_trusted_certificate_file);
     } else if (!FLAGS_jwks_file_path.empty()) {
       jwt_verifier = std::make_shared<KeyBasedJwtVerifier>(FLAGS_jwks_file_path, true);
     } else {
diff --git a/src/kudu/util/jwt-util-internal.h b/src/kudu/util/jwt-util-internal.h
index 38c499d24..01be925ba 100644
--- a/src/kudu/util/jwt-util-internal.h
+++ b/src/kudu/util/jwt-util-internal.h
@@ -257,8 +257,9 @@ class JWKSSnapshot final {
   // Download JWKS JSON file from the given URL, then load the public keys if the
   // checksum of JWKS object is changed. If no keys were given in the URL, the internal
   // maps will be empty.
-  Status LoadKeysFromUrl(
-      const std::string& jwks_url, uint64_t cur_jwks_checksum, bool* is_changed);
+  Status LoadKeysFromUrl(const std::string& jwks_url, bool jwks_verify_server_certificate,
+                         uint64_t cur_jwks_hash, bool* is_changed);
+
 
   // Look up the key ID in the internal key maps and returns the key if the lookup was
   // successful, otherwise return nullptr.
@@ -339,7 +340,8 @@ class JWKSMgr {
   // the internal maps will be empty.
   // If the given jwks_uri is a URL, start a working thread which will periodically
   // checks the JWKS URL for updates. This provides support for key rotation.
-  Status Init(const std::string& jwks_uri, bool is_local_file);
+  Status Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+              bool is_local_file);
 
   // Returns a read only snapshot of the current JWKS. This function should be called
   // after calling Init().
@@ -361,6 +363,9 @@ class JWKSMgr {
   // JWKS URI.
   std::string jwks_uri_;
 
+  // JWKS insecure TLS
+  bool jwks_verify_server_certificate_;
+
   // The snapshot of the current JWKS. When the checksum of downloaded JWKS json object
   // has been changed, the public keys will be reloaded and the content of this pointer
   // will be atomically swapped.
diff --git a/src/kudu/util/jwt-util-test.cc b/src/kudu/util/jwt-util-test.cc
index ddc210f18..b7d181cd0 100644
--- a/src/kudu/util/jwt-util-test.cc
+++ b/src/kudu/util/jwt-util-test.cc
@@ -100,7 +100,7 @@ TEST(JwtUtilTest, LoadJwksFile) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_FALSE(jwks->IsEmpty());
@@ -131,7 +131,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
       "  ]"
       "}"));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file->Filename(), true);
+  Status status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_STR_CONTAINS(status.ToString(), "parsing key #0")
       << " Actual error: " << status.ToString();
@@ -150,7 +150,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
       "      \"n\": \"sttddbg-_yjXzcFpbMJB1fIFam9lQBeXWbTqzJwbuFbspHMsRowa8FaPw\","
       "      \"e\": \"AQAB\""
       "}"));
-  status = jwt_helper.Init(jwks_file->Filename(), true);
+  status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_STR_CONTAINS(status.ToString(), "Missing a comma or ']' after an array element")
       << " Actual error: " << status.ToString();
@@ -159,7 +159,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
   jwks_file.reset(new TempTestDataFile(
       Substitute(kJwksRsaFileFormat, "", "RS256", kRsaPubKeyJwkN, kRsaPubKeyJwkE,
           "", "RS256", kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE)));
-  status = jwt_helper.Init(jwks_file->Filename(), true);
+  status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_STR_CONTAINS(status.ToString(), "parsing key #0")
       << " Actual error: " << status.ToString();
@@ -169,7 +169,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
   // JWKS with empty key value.
   jwks_file.reset(new TempTestDataFile(
       Substitute(kJwksRsaFileFormat, kKid1, "RS256", "", "", kKid2, "RS256", "", "")));
-  status = jwt_helper.Init(jwks_file->Filename(), true);
+  status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_STR_CONTAINS(status.ToString(), "parsing key #0")
       << " Actual error: " << status.ToString();
@@ -184,7 +184,7 @@ TEST(JwtUtilTest, VerifyJwtHS256) {
   TempTestDataFile jwks_file(
       Substitute(kJwksHsFileFormat, kKid1, "HS256", shared_secret));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   EXPECT_OK(status);
@@ -224,7 +224,7 @@ TEST(JwtUtilTest, VerifyJwtHS384) {
   TempTestDataFile jwks_file(
       Substitute(kJwksHsFileFormat, kKid1, "HS384", shared_secret));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(1, jwks->GetHSKeyNum());
@@ -263,7 +263,7 @@ TEST(JwtUtilTest, VerifyJwtHS512) {
   TempTestDataFile jwks_file(
       Substitute(kJwksHsFileFormat, kKid1, "HS512", shared_secret));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   EXPECT_OK(status);
@@ -301,7 +301,7 @@ TEST(JwtUtilTest, VerifyJwtRS256) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -354,7 +354,7 @@ TEST(JwtUtilTest, VerifyJwtRS384) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS384", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -392,7 +392,7 @@ TEST(JwtUtilTest, VerifyJwtRS512) {
       kRsa512PubKeyJwkN, kRsa512PubKeyJwkE, kKid2, "RS512",
       kRsa512InvalidPubKeyJwkN, kRsa512PubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -430,7 +430,7 @@ TEST(JwtUtilTest, VerifyJwtPS256) {
       kRsa1024PubKeyJwkN, kRsa1024PubKeyJwkE, kKid2, "PS256",
       kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -468,7 +468,7 @@ TEST(JwtUtilTest, VerifyJwtPS384) {
       kRsa2048PubKeyJwkN, kRsa2048PubKeyJwkE, kKid2, "PS384",
       kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -506,7 +506,7 @@ TEST(JwtUtilTest, VerifyJwtPS512) {
       kRsa4096PubKeyJwkN, kRsa4096PubKeyJwkE, kKid2, "PS512",
       kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -543,7 +543,7 @@ TEST(JwtUtilTest, VerifyJwtES256) {
   TempTestDataFile jwks_file(Substitute(kJwksEcFileFormat, kKid1, "P-256",
       kEcdsa256PubKeyJwkX, kEcdsa256PubKeyJwkY));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(1, jwks->GetECPublicKeyNum());
@@ -588,7 +588,7 @@ TEST(JwtUtilTest, VerifyJwtES384) {
   TempTestDataFile jwks_file(Substitute(kJwksEcFileFormat, kKid1, "P-384",
       kEcdsa384PubKeyJwkX, kEcdsa384PubKeyJwkY));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(1, jwks->GetECPublicKeyNum());
@@ -625,7 +625,7 @@ TEST(JwtUtilTest, VerifyJwtES512) {
   TempTestDataFile jwks_file(Substitute(kJwksEcFileFormat, kKid1, "P-521",
       kEcdsa521PubKeyJwkX, kEcdsa521PubKeyJwkY));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(1, jwks->GetECPublicKeyNum());
@@ -683,7 +683,7 @@ TEST(JwtUtilTest, VerifyJwtFailMismatchingAlgorithms) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token, but set mismatching algorithm.
@@ -712,7 +712,7 @@ TEST(JwtUtilTest, VerifyJwtFailKeyNotFound) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token with a key ID which can not be found in JWKS.
@@ -740,7 +740,7 @@ TEST(JwtUtilTest, VerifyJwtTokenWithoutKeyId) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token without key ID.
@@ -764,7 +764,7 @@ TEST(JwtUtilTest, VerifyJwtFailTokenWithoutKeyId) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token without key ID.
@@ -787,7 +787,7 @@ TEST(JwtUtilTest, VerifyJwtFailTokenWithoutSignature) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token without signature.
@@ -809,7 +809,7 @@ TEST(JwtUtilTest, VerifyJwtFailExpiredToken) {
       kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
       kRsaPubKeyJwkE));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token and sign it with RS256.
@@ -947,7 +947,9 @@ TEST(JwtUtilTest, VerifyOIDCDiscoveryEndpoint) {
   };
   MiniOidc oidc(std::move(opts));
   ASSERT_OK(oidc.Start());
-  const PerAccountKeyBasedJwtVerifier jwt_verifier(oidc.url());
+  const PerAccountKeyBasedJwtVerifier jwt_verifier(oidc.url(),
+                                                   /*jwks_verify_server_certificate*/ false,
+                                                   /*jwks_ca_certificate*/ "");
 
   // Create and verify a token on the happy path.
   const string kSubject = "kudu";
@@ -980,7 +982,9 @@ TEST(JwtUtilTest, VerifyJWKSDiscoveryEndpointMultipleClients) {
   };
   MiniOidc oidc(std::move(opts));
   ASSERT_OK(oidc.Start());
-  PerAccountKeyBasedJwtVerifier jwt_verifier(oidc.url());
+  PerAccountKeyBasedJwtVerifier jwt_verifier(oidc.url(),
+                                             /*jwks_verify_server_certificate*/ false,
+                                             /*jwks_ca_certificate*/ "");
 
   {
     const string kSubject = "kudu";
diff --git a/src/kudu/util/jwt-util.cc b/src/kudu/util/jwt-util.cc
index 6ecd51cd5..5fd7c582b 100644
--- a/src/kudu/util/jwt-util.cc
+++ b/src/kudu/util/jwt-util.cc
@@ -602,14 +602,16 @@ Status JWKSSnapshot::LoadKeysFromFile(const string& jwks_file_path) {
 
 // Download JWKS from the given URL with Kudu's EasyCurl wrapper.
 Status JWKSSnapshot::LoadKeysFromUrl(
-    const std::string& jwks_url, uint64_t cur_jwks_checksum, bool* is_changed) {
+    const std::string& jwks_url, bool jwks_verify_server_certificate, uint64_t cur_jwks_checksum,
+    bool* is_changed) {
   kudu::EasyCurl curl;
   kudu::faststring dst;
   *is_changed = false;
 
   curl.set_timeout(
       kudu::MonoDelta::FromMilliseconds(static_cast<int64_t>(FLAGS_jwks_pulling_timeout_s) * 1000));
-  curl.set_verify_peer(false);
+  curl.set_verify_peer(jwks_verify_server_certificate);
+
   // TODO support CurlAuthType by calling kudu::EasyCurl::set_auth().
   RETURN_NOT_OK_PREPEND(curl.FetchURL(jwks_url, &dst),
       Substitute("Error downloading JWKS from '$0'", jwks_url));
@@ -694,15 +696,19 @@ JWKSMgr::~JWKSMgr() {
   if (jwks_update_thread_ != nullptr) jwks_update_thread_->Join();
 }
 
-Status JWKSMgr::Init(const std::string& jwks_uri, bool is_local_file) {
+Status JWKSMgr::Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+                     bool is_local_file) {
   jwks_uri_ = jwks_uri;
+  jwks_verify_server_certificate_ = jwks_verify_server_certificate;
   std::shared_ptr<JWKSSnapshot> new_jwks = std::make_shared<JWKSSnapshot>();
   if (is_local_file) {
     RETURN_NOT_OK_PREPEND(new_jwks->LoadKeysFromFile(jwks_uri), "Failed to load JWKS");
     SetJWKSSnapshot(new_jwks);
   } else {
     bool is_changed = false;
-    RETURN_NOT_OK_PREPEND(new_jwks->LoadKeysFromUrl(jwks_uri, current_jwks_checksum_, &is_changed),
+    RETURN_NOT_OK_PREPEND(new_jwks->LoadKeysFromUrl(jwks_uri, jwks_verify_server_certificate,
+                                                    current_jwks_checksum_,
+                                                    &is_changed),
                           "Failed to load JWKS");
     DCHECK(is_changed);
     if (is_changed) SetJWKSSnapshot(new_jwks);
@@ -734,7 +740,8 @@ void JWKSMgr::UpdateJWKSThread() {
     new_jwks = std::make_shared<JWKSSnapshot>();
     bool is_changed = false;
     Status status =
-        new_jwks->LoadKeysFromUrl(jwks_uri_, current_jwks_checksum_, &is_changed);
+        new_jwks->LoadKeysFromUrl(jwks_uri_, jwks_verify_server_certificate_,
+                                  current_jwks_checksum_, &is_changed);
     if (!status.ok()) {
       LOG(WARNING) << "Failed to update JWKS: " << status.ToString();
     } else if (is_changed) {
@@ -781,9 +788,17 @@ void JWTHelper::TokenDeleter::operator()(JWTHelper::JWTDecodedToken* token) cons
   delete token;
 }
 
-Status JWTHelper::Init(const std::string& jwks_uri, bool is_local_file) {
+Status JWTHelper::Init(const std::string& jwks_file_path) {
+  return Init(jwks_file_path,
+              /*jwks_verify_server_certificate*/ false,
+              /*is_local_file*/ true);
+}
+
+Status JWTHelper::Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+                       bool is_local_file) {
   jwks_mgr_.reset(new JWKSMgr());
-  RETURN_NOT_OK(jwks_mgr_->Init(jwks_uri, is_local_file));
+  RETURN_NOT_OK(jwks_mgr_->Init(jwks_uri, jwks_verify_server_certificate,
+                                is_local_file));
   if (!initialized_) initialized_ = true;
   return Status::OK();
 }
@@ -931,7 +946,7 @@ Status JWTHelper::GetCustomClaimUsername(const JWTDecodedToken* decoded_token,
 }
 
 Status KeyBasedJwtVerifier::Init() {
-  return jwt_->Init(jwks_uri_, is_local_file_);
+  return jwt_->Init(jwks_uri_, /*jwks_verify_server_certificate*/ false, is_local_file_);
 }
 
 Status KeyBasedJwtVerifier::VerifyToken(const string& bytes_raw, string* subject) const {
@@ -1004,7 +1019,9 @@ Status PerAccountKeyBasedJwtVerifier::JWTHelperForToken(const JWTHelper::JWTDeco
   // accounts, as it creates a JWKS refresh thread for each account. Group the
   // refreshes into a single thread or threadpool.
   auto new_helper = std::make_shared<JWTHelper>();
-  RETURN_NOT_OK_PREPEND(new_helper->Init(jwks_uri, /*is_local_file*/ false),
+  RETURN_NOT_OK_PREPEND(new_helper->Init(jwks_uri,
+                                         jwks_verify_server_certificate_,
+                                         /*is_local_file*/ false),
                         "Error initializing JWT helper");
 
   {
@@ -1019,7 +1036,8 @@ Status PerAccountKeyBasedJwtVerifier::JWTHelperForToken(const JWTHelper::JWTDeco
 Status PerAccountKeyBasedJwtVerifier::Init() {
   for (auto& [account_id, verifier] : jwt_by_account_id_) {
     RETURN_NOT_OK(verifier->Init(Substitute("$0?accountId=$1", oidc_uri_, account_id),
-                   /*is_local_file*/false));
+                                            jwks_verify_server_certificate_,
+                                            /*is_local_file*/ false));
   }
   return Status::OK();
 }
diff --git a/src/kudu/util/jwt-util.h b/src/kudu/util/jwt-util.h
index e25bbd8d4..daba84eb1 100644
--- a/src/kudu/util/jwt-util.h
+++ b/src/kudu/util/jwt-util.h
@@ -64,7 +64,12 @@ class JWTHelper {
 
   // Load JWKS from a given local JSON file or URL. Returns an error if problems were
   // encountered.
-  Status Init(const std::string& jwks_uri, bool is_local_file);
+  Status Init(const std::string& jwks_uri);
+
+  // Load JWKS from a given local JSON file or URL. Returns an error if problems were
+  // encountered.
+  Status Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+              bool is_local_file);
 
   // Decode the given JWT token. The decoding result is stored in decoded_token_out.
   // Return Status::OK if the decoding is successful.
@@ -117,8 +122,11 @@ class KeyBasedJwtVerifier : public JwtVerifier {
 
 class PerAccountKeyBasedJwtVerifier : public JwtVerifier {
  public:
-  explicit PerAccountKeyBasedJwtVerifier(std::string oidc_uri)
-      : oidc_uri_(std::move(oidc_uri)) {}
+  explicit PerAccountKeyBasedJwtVerifier(std::string oidc_uri, bool jwks_verify_server_certificate,
+                                         const std::string jwks_ca_certificate)
+      : oidc_uri_(std::move(oidc_uri)),
+        jwks_verify_server_certificate_(jwks_verify_server_certificate),
+        jwks_ca_certificate_(jwks_ca_certificate) {}
 
   ~PerAccountKeyBasedJwtVerifier() override = default;
 
@@ -132,6 +140,11 @@ class PerAccountKeyBasedJwtVerifier : public JwtVerifier {
   Status JWTHelperForToken(const JWTHelper::JWTDecodedToken& token, JWTHelper** helper) const;
 
   const std::string oidc_uri_;
+
+  const bool jwks_verify_server_certificate_;
+
+  const std::string jwks_ca_certificate_;
+
   // Marked as mutable so that PerAccountKeyBasedJwtVerifier::JWTHelperForToken is able to emplace
   // new JWTHelpers in it.
   mutable std::unordered_map<std::string, std::shared_ptr<JWTHelper>> jwt_by_account_id_;
diff --git a/src/kudu/util/mini_oidc.cc b/src/kudu/util/mini_oidc.cc
index 1672f33a5..a6296d3a5 100644
--- a/src/kudu/util/mini_oidc.cc
+++ b/src/kudu/util/mini_oidc.cc
@@ -23,6 +23,7 @@
 #include <utility>
 #include <vector>
 
+#include <glog/logging.h>
 #include <jwt-cpp/jwt.h>
 #include <jwt-cpp/traits/kazuho-picojson/defaults.h>
 #include <jwt-cpp/traits/kazuho-picojson/traits.h>
@@ -100,6 +101,10 @@ Status MiniOidc::Start() {
   // we've been configured to server.
   WebserverOptions jwks_opts;
   jwks_opts.port = 0;
+  jwks_opts.bind_interface = "localhost";
+  jwks_opts.certificate_file = options_.server_certificate;
+  jwks_opts.private_key_file = options_.private_key_file;
+
   jwks_server_.reset(new Webserver(jwks_opts));
 
   for (const auto& [account_id, valid] : options_.account_ids) {
@@ -121,16 +126,26 @@ Status MiniOidc::Start() {
         /*is_styled*/ false,
         /*is_on_nav_bar*/ false);
   }
+  LOG(INFO) << "Starting JWKS server";
   RETURN_NOT_OK(jwks_server_->Start());
-  vector<Sockaddr> bound_addrs;
+  vector<Sockaddr> advertised_addrs;
   Sockaddr addr;
-  RETURN_NOT_OK(jwks_server_->GetBoundAddresses(&bound_addrs));
-  RETURN_NOT_OK(addr.ParseString(bound_addrs[0].host(), bound_addrs[0].port()));
-  const string jwks_url = Substitute("http://$0/jwks", addr.ToString());
+  RETURN_NOT_OK(jwks_server_->GetAdvertisedAddresses(&advertised_addrs));
+  // calling ParseString() to verify the address components
+  RETURN_NOT_OK(addr.ParseString(advertised_addrs[0].host(), advertised_addrs[0].port()));
+  string protocol = "https";
+  if (jwks_opts.certificate_file.empty() && jwks_opts.password_file.empty()) {
+    protocol = "http";
+  }
+
+  const string jwks_url = Substitute("$0://localhost:$1/jwks",
+                                     protocol,
+                                     advertised_addrs[0].port());
 
   // Now start the OIDC Discovery server that points to the JWKS endpoints.
   WebserverOptions oidc_opts;
   oidc_opts.port = 0;
+  oidc_opts.bind_interface = "localhost";
   oidc_server_.reset(new Webserver(oidc_opts));
   oidc_server_->RegisterPrerenderedPathHandler(
       "/.well-known/openid-configuration",
@@ -142,10 +157,13 @@ Status MiniOidc::Start() {
       },
       /*is_styled*/ false,
       /*is_on_nav_bar*/ false);
+
+  LOG(INFO) << "Starting OIDC Discovery server";
   RETURN_NOT_OK(oidc_server_->Start());
-  bound_addrs.clear();
-  RETURN_NOT_OK(oidc_server_->GetBoundAddresses(&bound_addrs));
-  RETURN_NOT_OK(addr.ParseString(bound_addrs[0].host(), bound_addrs[0].port()));
+  advertised_addrs.clear();
+  RETURN_NOT_OK(oidc_server_->GetAdvertisedAddresses(&advertised_addrs));
+  // calling ParseString() to verify the address components
+  RETURN_NOT_OK(addr.ParseString(advertised_addrs[0].host(), advertised_addrs[0].port()));
   oidc_url_ = Substitute("http://$0/.well-known/openid-configuration", addr.ToString());
   return Status::OK();
 }
diff --git a/src/kudu/util/mini_oidc.h b/src/kudu/util/mini_oidc.h
index 74dfbfefd..b4eff0d0e 100644
--- a/src/kudu/util/mini_oidc.h
+++ b/src/kudu/util/mini_oidc.h
@@ -39,6 +39,13 @@ struct MiniOidcOptions {
 
   // Maps account IDs to add to whether or not to create JWKS with invalid keys.
   std::unordered_map<std::string, bool> account_ids;
+
+  // String that contains the server_certificate that is used to establish secure
+  // https connection to the JWKS server.
+  std::string server_certificate;
+
+  // The private key belonging to the server certificate
+  std::string private_key_file;
 };
 
 // Serves the following endpoints for testing a cluster: