[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 <alexey@apache.org>
Reviewed-by: Abhishek Chennaka <achennaka@cloudera.com>
diff --git a/src/kudu/integration-tests/security-itest.cc b/src/kudu/integration-tests/security-itest.cc
index d3ddad2..5e22745 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 @@
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 707d539..32e2c10 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 @@
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 @@
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 a1a279f..0039e00 100644
--- a/src/kudu/master/master_cert_authority.h
+++ b/src/kudu/master/master_cert_authority.h
@@ -66,6 +66,9 @@
// 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 @@
// 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 c7bc028..31f78a6 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::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 @@
}
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) {
@@ -816,6 +848,12 @@
},
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) {
this->HandleDumpEntities(req, resp);
diff --git a/src/kudu/master/master_path_handlers.h b/src/kudu/master/master_path_handlers.h
index 186e6d3..feced14 100644
--- a/src/kudu/master/master_path_handlers.h
+++ b/src/kudu/master/master_path_handlers.h
@@ -52,6 +52,8 @@
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 315fe68..9f368a2 100644
--- a/src/kudu/mini-cluster/webui_checker.h
+++ b/src/kudu/mini-cluster/webui_checker.h
@@ -36,6 +36,7 @@
const std::string& tablet_id = "",
const std::vector<std::string>& master_pages = {
"/dump-entities",
+ "/ipki-ca-cert",
"/masters",
"/mem-trackers",
"/metrics",