This is an automated email from the ASF dual-hosted git repository. alexey pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/kudu.git
commit b65b95af3c90964ee706967f570bff99897dcec8 Author: Alexey Serbin <ale...@apache.org> AuthorDate: Thu May 18 11:42:30 2023 -0700 [master] add /ipki-ca-cert webserver end-point This patch introduces a new /ipki-ca-cert end-point for the master's embedded webserver to provide information on the IPKI CA certificate used by a Kudu cluster. This information is useful in JWT-based authentication scenarios. The missing part of importing certificates into Kudu client's chain of trusted TLS certificates will be addressed in follow-up patches. Change-Id: I833d934cc1d916dba243a05aa96926d3b540b70d Reviewed-on: http://gerrit.cloudera.org:8080/19906 Tested-by: Alexey Serbin <ale...@apache.org> Reviewed-by: Abhishek Chennaka <achenn...@cloudera.com> --- src/kudu/integration-tests/security-itest.cc | 93 ++++++++++++++++++++++ .../integration-tests/webserver-crawl-itest.cc | 16 +++- src/kudu/master/master_cert_authority.h | 7 +- src/kudu/master/master_path_handlers.cc | 42 +++++++++- src/kudu/master/master_path_handlers.h | 2 + src/kudu/mini-cluster/webui_checker.h | 1 + 6 files changed, 153 insertions(+), 8 deletions(-) diff --git a/src/kudu/integration-tests/security-itest.cc b/src/kudu/integration-tests/security-itest.cc index d3ddad2d3..5e22745c4 100644 --- a/src/kudu/integration-tests/security-itest.cc +++ b/src/kudu/integration-tests/security-itest.cc @@ -55,6 +55,7 @@ #include "kudu/ranger-kms/mini_ranger_kms.h" #include "kudu/rpc/messenger.h" #include "kudu/rpc/rpc_controller.h" +#include "kudu/security/cert.h" #include "kudu/security/kinit_context.h" #include "kudu/security/test/mini_kdc.h" #include "kudu/security/test/test_certs.h" @@ -66,11 +67,14 @@ #include "kudu/tserver/tserver.pb.h" #include "kudu/tserver/tserver_service.pb.h" #include "kudu/tserver/tserver_service.proxy.h" +#include "kudu/util/curl_util.h" #include "kudu/util/env.h" +#include "kudu/util/faststring.h" #include "kudu/util/mini_oidc.h" #include "kudu/util/monotime.h" #include "kudu/util/net/net_util.h" #include "kudu/util/net/sockaddr.h" +#include "kudu/util/openssl_util.h" #include "kudu/util/path_util.h" #include "kudu/util/random.h" #include "kudu/util/random_util.h" @@ -930,6 +934,95 @@ TEST_F(SecurityITest, TestEncryptionWithKMSIntegrationMultipleServers) { SmokeTestCluster(client, /*transactional=*/false); } +TEST_F(SecurityITest, IPKICACert) { + SKIP_IF_SLOW_NOT_ALLOWED(); + + // Need to test the functionality for both leader and follower masters. + cluster_opts_.num_masters = 3; + // No need to involve tablet servers in this scenario. + cluster_opts_.num_tablet_servers = 0; + + ASSERT_OK(StartCluster()); + + shared_ptr<KuduClient> client; + ASSERT_OK(cluster_->CreateClient(nullptr, &client)); + + string authn_creds; + ASSERT_OK(client->ExportAuthenticationCredentials(&authn_creds)); + client::AuthenticationCredentialsPB pb; + ASSERT_TRUE(pb.ParseFromString(authn_creds)); + ASSERT_EQ(1, pb.ca_cert_ders_size()); + + security::Cert ca_cert_client; + ASSERT_OK(ca_cert_client.FromString(pb.ca_cert_ders(0), + security::DataFormat::DER)); + string ca_cert_client_pem; + ASSERT_OK(ca_cert_client.ToString(&ca_cert_client_pem, + security::DataFormat::PEM)); + + const auto fetch_ipki_ca = [c = cluster_.get()](int master_idx, string* out) { + const auto& http_hp = c->master(master_idx)->bound_http_hostport(); + string url = Substitute("http://$0/ipki-ca-cert", http_hp.ToString()); + EasyCurl curl; + faststring dst; + auto res = curl.FetchURL(url, &dst); + *out = dst.ToString(); + return res; + }; + + int leader_master_idx; + ASSERT_OK(cluster_->GetLeaderMasterIndex(&leader_master_idx)); + string str; + ASSERT_OK(fetch_ipki_ca(leader_master_idx, &str)); + security::Cert ca_cert; + ASSERT_OK(ca_cert.FromString(str, security::DataFormat::PEM)); + + // Using (string --> security::Cert --> string) conversion chain to compare + // canonical representations of the CA certificates in PEM format. + string ca_cert_str; + ASSERT_OK(ca_cert.ToString(&ca_cert_str, security::DataFormat::PEM)); + ASSERT_EQ(ca_cert_client_pem, ca_cert_str); + + const auto count_valid_certs = [&](size_t* res) { + size_t count = 0; + for (auto i = 0; i < cluster_->num_masters(); ++i) { + string str; + auto s = fetch_ipki_ca(i, &str); + ASSERT_TRUE(s.ok() || s.IsRemoteError()); + if (s.IsRemoteError()) { + // If there wasn't a CA cert in the output, there should had been + // an error reported. + ASSERT_NE(string::npos, str.find("ERROR: ")); + continue; + } + security::Cert ca_cert; + ASSERT_OK(ca_cert.FromString(str, security::DataFormat::PEM)); + + string ca_cert_str; + ASSERT_OK(ca_cert.ToString(&ca_cert_str, security::DataFormat::PEM)); + ASSERT_EQ(ca_cert_client_pem, ca_cert_str); + ++count; + } + *res = count; + }; + + // The IPKI has been certainly initialized at leader master since the client + // was able to successfully connect to the cluster (see above). + { + size_t count = 0; + NO_FATALS(count_valid_certs(&count)); + ASSERT_GE(count, 1); + } + + // At some point, all the followers should have loaded the CA information + // generated by the leader master upon the very first startup. + ASSERT_EVENTUALLY([&] { + size_t count = 0; + NO_FATALS(count_valid_certs(&count)); + ASSERT_EQ(cluster_opts_.num_masters, count); + }); +} + class EncryptionPolicyTest : public SecurityITest, public ::testing::WithParamInterface<tuple< diff --git a/src/kudu/integration-tests/webserver-crawl-itest.cc b/src/kudu/integration-tests/webserver-crawl-itest.cc index 707d539d5..32e2c1042 100644 --- a/src/kudu/integration-tests/webserver-crawl-itest.cc +++ b/src/kudu/integration-tests/webserver-crawl-itest.cc @@ -17,6 +17,7 @@ #include <algorithm> #include <deque> +#include <functional> #include <ostream> #include <string> #include <tuple> @@ -282,7 +283,6 @@ TEST_P(WebserverCrawlITest, TestAllWebPages) { curl.set_verify_peer(false); } - faststring response; vector<string> headers; if (impersonate_knox) { // Pretend we're Knox when communicating with the web UI. @@ -296,10 +296,18 @@ TEST_P(WebserverCrawlITest, TestAllWebPages) { int ret = FindNth(url, '/', 3); string host = ret == string::npos ? url : url.substr(0, ret); - // Every link should be reachable. + // Every link should be reachable, but some URLs are allowed to return + // non-2xx status codes temporarily (e.g., 503 Service Unavailable). SCOPED_TRACE(url); - ASSERT_OK(curl.FetchURL(url, &response, headers)); - string resp_str = response.ToString(); + faststring response; + if (const auto s = curl.FetchURL(url, &response, headers); !s.ok()) { + ASSERT_TRUE(s.IsRemoteError()) << s.ToString(); + ASSERT_STR_MATCHES(s.ToString(), "HTTP [[:digit:]]{3}$"); + ASSERT_EVENTUALLY([&] { + ASSERT_OK(curl.FetchURL(url, &response, headers)); + }); + } + const string resp_str = response.ToString(); SCOPED_TRACE(resp_str); gq::CDocument page; diff --git a/src/kudu/master/master_cert_authority.h b/src/kudu/master/master_cert_authority.h index a1a279fd0..0039e0085 100644 --- a/src/kudu/master/master_cert_authority.h +++ b/src/kudu/master/master_cert_authority.h @@ -66,6 +66,9 @@ class MasterCertAuthority { // authority with the information read from the system table. Status Init(std::unique_ptr<security::PrivateKey> key, std::unique_ptr<security::Cert> cert); + bool IsInitialized() const { + return !!ca_cert_; + } // Sign the given CSR 'csr_der' provided by a server in the cluster. // The authenticated user should be passed in 'caller'. The cert contents @@ -90,12 +93,12 @@ class MasterCertAuthority { // This can be sent to participants in the cluster so they can add it to // their trust stores. const std::string& ca_cert_der() const { - CHECK(ca_cert_) << "must Init()"; + DCHECK(IsInitialized()) << "must Init()"; return ca_cert_der_; } const security::Cert& ca_cert() const { - CHECK(ca_cert_) << "must Init()"; + DCHECK(IsInitialized()) << "must Init()"; return *ca_cert_; } diff --git a/src/kudu/master/master_path_handlers.cc b/src/kudu/master/master_path_handlers.cc index c7bc02816..31f78a689 100644 --- a/src/kudu/master/master_path_handlers.cc +++ b/src/kudu/master/master_path_handlers.cc @@ -49,15 +49,18 @@ #include "kudu/gutil/strings/human_readable.h" #include "kudu/gutil/strings/join.h" #include "kudu/gutil/strings/numbers.h" +#include "kudu/gutil/strings/strip.h" #include "kudu/gutil/strings/substitute.h" #include "kudu/gutil/walltime.h" #include "kudu/master/catalog_manager.h" #include "kudu/master/master.h" #include "kudu/master/master.pb.h" +#include "kudu/master/master_cert_authority.h" #include "kudu/master/sys_catalog.h" #include "kudu/master/table_metrics.h" #include "kudu/master/ts_descriptor.h" #include "kudu/master/ts_manager.h" +#include "kudu/security/cert.h" #include "kudu/server/monitored_task.h" #include "kudu/server/rpc_server.h" #include "kudu/server/webui_util.h" @@ -68,6 +71,7 @@ #include "kudu/util/metrics.h" #include "kudu/util/monotime.h" #include "kudu/util/net/net_util.h" +#include "kudu/util/openssl_util.h" #include "kudu/util/pb_util.h" #include "kudu/util/string_case.h" #include "kudu/util/url-coding.h" @@ -601,6 +605,34 @@ void MasterPathHandlers::HandleMasters(const Webserver::WebRequest& /*req*/, } } +void MasterPathHandlers::HandleIpkiCaCert( + const Webserver::WebRequest& /*req*/, + Webserver::PrerenderedWebResponse* resp) { + ostringstream& out = resp->output; + if (!master_->catalog_manager()->IsInitialized()) { + resp->status_code = HttpStatusCode::ServiceUnavailable; + out << "ERROR: CatalogManager is not running"; + return; + } + const auto* ca = master_->cert_authority(); + if (!ca || !ca->IsInitialized()) { + resp->status_code = HttpStatusCode::ServiceUnavailable; + out << "ERROR: IPKI CA isn't initialized"; + return; + } + const auto& cert = ca->ca_cert(); + string cert_str; + if (auto s = cert.ToString(&cert_str, security::DataFormat::PEM); !s.ok()) { + auto err = s.CloneAndPrepend("could not convert CA cert to PEM format"); + LOG(ERROR) << err.ToString(); + resp->status_code = HttpStatusCode::InternalServerError; + out << "ERROR: " << err.ToString(); + return; + } + RemoveExtraWhitespace(&cert_str); + out << cert_str; +} + namespace { // Visitor for the catalog table which dumps tables and tablets in a JSON format. This @@ -789,8 +821,8 @@ void MasterPathHandlers::HandleDumpEntities(const Webserver::WebRequest& /*req*/ } Status MasterPathHandlers::Register(Webserver* server) { - bool is_styled = true; - bool is_on_nav_bar = true; + constexpr const bool is_styled = true; + constexpr const bool is_on_nav_bar = true; server->RegisterPathHandler( "/tablet-servers", "Tablet Servers", [this](const Webserver::WebRequest& req, Webserver::WebResponse* resp) { @@ -815,6 +847,12 @@ Status MasterPathHandlers::Register(Webserver* server) { this->HandleMasters(req, resp); }, is_styled, is_on_nav_bar); + server->RegisterPrerenderedPathHandler( + "/ipki-ca-cert", "IPKI CA certificate", + [this](const Webserver::WebRequest& req, Webserver::PrerenderedWebResponse* resp) { + this->HandleIpkiCaCert(req, resp); + }, + false /*is_styled*/, true /*is_on_nav_bar*/); server->RegisterPrerenderedPathHandler( "/dump-entities", "Dump Entities", [this](const Webserver::WebRequest& req, Webserver::PrerenderedWebResponse* resp) { diff --git a/src/kudu/master/master_path_handlers.h b/src/kudu/master/master_path_handlers.h index 186e6d312..feced1417 100644 --- a/src/kudu/master/master_path_handlers.h +++ b/src/kudu/master/master_path_handlers.h @@ -52,6 +52,8 @@ class MasterPathHandlers { Webserver::WebResponse* resp); void HandleMasters(const Webserver::WebRequest& req, Webserver::WebResponse* resp); + void HandleIpkiCaCert(const Webserver::WebRequest& req, + Webserver::PrerenderedWebResponse* resp); void HandleDumpEntities(const Webserver::WebRequest& req, Webserver::PrerenderedWebResponse* resp); diff --git a/src/kudu/mini-cluster/webui_checker.h b/src/kudu/mini-cluster/webui_checker.h index 315fe684d..9f368a216 100644 --- a/src/kudu/mini-cluster/webui_checker.h +++ b/src/kudu/mini-cluster/webui_checker.h @@ -36,6 +36,7 @@ class PeriodicWebUIChecker { const std::string& tablet_id = "", const std::vector<std::string>& master_pages = { "/dump-entities", + "/ipki-ca-cert", "/masters", "/mem-trackers", "/metrics",