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


The following commit(s) were added to refs/heads/master by this push:
     new 61d229678 KUDU-3448 Add support for encrypting existing keys
61d229678 is described below

commit 61d2296783a8392763f088d65a03376eb3e6463b
Author: Attila Bukor <abu...@apache.org>
AuthorDate: Mon Jun 12 18:57:18 2023 +0200

    KUDU-3448 Add support for encrypting existing keys
    
    On an existing cluster before KUDU-3448, the IPKI and TSK private keys
    were stored in clear text. With KUDU-3448, it is now possible to encrypt
    these keys, but without this commit, it's not possible to use this
    feature in an existing cluster.
    
    This commit introduces a fallback when trying to decrypt the stored
    private keys, so that if that fails, it tries to read it without
    decrypting it.
    
    If it succeeds to read the IPKI private key, it encrypts it using the
    password, and rewrites it in the sys-catalog table. It does no such
    thing with the TSK, as they will be rolled out soon anyway, but it
    encrypts the new keys, so it's still not possible to go back from
    encrypted TSKs after a new key has been generated.
    
    This commit doesn't make it possible to rotate the IPKI key.
    
    Change-Id: Ide6ec4fb86325897f2b011aee9643d276044279d
    Reviewed-on: http://gerrit.cloudera.org:8080/20050
    Reviewed-by: Alexey Serbin <ale...@apache.org>
    Tested-by: Alexey Serbin <ale...@apache.org>
---
 src/kudu/integration-tests/security-itest.cc | 58 ++++++++++++++++++++++++++++
 src/kudu/master/catalog_manager.cc           | 50 ++++++++++++++++++++++++
 src/kudu/master/catalog_manager.h            |  6 +++
 src/kudu/master/sys_catalog.cc               | 16 +++++++-
 src/kudu/master/sys_catalog.h                | 12 ++++++
 src/kudu/security/token_signing_key.cc       | 15 ++++++-
 6 files changed, 153 insertions(+), 4 deletions(-)

diff --git a/src/kudu/integration-tests/security-itest.cc 
b/src/kudu/integration-tests/security-itest.cc
index 7cb2bde4a..adea89025 100644
--- a/src/kudu/integration-tests/security-itest.cc
+++ b/src/kudu/integration-tests/security-itest.cc
@@ -1157,6 +1157,64 @@ TEST_F(SecurityITest, IPKICACertWebServerEndPoints) {
   ASSERT_EQ(ca_cert_str_1, ca_cert_str_2);
 }
 
+TEST_F(SecurityITest, TestEnableAndDisableEncryptedIPKICert) {
+  SKIP_IF_SLOW_NOT_ALLOWED();
+  ASSERT_OK(StartCluster());
+
+  shared_ptr<KuduClient> client;
+  KuduClientBuilder b;
+  ASSERT_OK(cluster_->CreateClient(&b, &client));
+  NO_FATALS(SmokeTestCluster(client, /*transactional=*/false));
+
+  for (auto i = 0; i < cluster_->num_masters(); ++i) {
+    
cluster_->master(i)->mutable_flags()->emplace_back("--ipki_private_key_password_cmd=echo
 foo");
+  }
+  cluster_->Shutdown();
+  ASSERT_OK(cluster_->Restart());
+
+  NO_FATALS(SmokeTestCluster(client, /*transactional=*/false));
+
+  for (auto i = 0; i < cluster_->num_masters(); ++i) {
+    cluster_->master(i)->mutable_flags()->pop_back();
+  }
+  cluster_->Shutdown();
+  // the master should fail to start, which means the tablet servers won't be
+  // able to connect to it and the mini cluster will time out waiting for the
+  // cluster to start up.
+  ASSERT_TRUE(cluster_->Restart().IsTimedOut());
+}
+
+TEST_F(SecurityITest, TestEnableAndDisableEncryptedTSK) {
+  SKIP_IF_SLOW_NOT_ALLOWED();
+  
cluster_opts_.extra_master_flags.emplace_back("--authn_token_validity_seconds=20");
+  
cluster_opts_.extra_master_flags.emplace_back("--authz_token_validity_seconds=20");
+  cluster_opts_.extra_master_flags.emplace_back("--tsk_rotation_seconds=20");
+  ASSERT_OK(StartCluster());
+
+  shared_ptr<KuduClient> client;
+  KuduClientBuilder b;
+  ASSERT_OK(cluster_->CreateClient(&b, &client));
+  NO_FATALS(SmokeTestCluster(client, /*transactional=*/false));
+
+  for (auto i = 0; i < cluster_->num_masters(); ++i) {
+    
cluster_->master(i)->mutable_flags()->emplace_back("--tsk_private_key_password_cmd=echo
 foo");
+  }
+  cluster_->Shutdown();
+  SleepFor(MonoDelta::FromSeconds(20));
+  ASSERT_OK(cluster_->Restart());
+  NO_FATALS(SmokeTestCluster(client, /*transactional=*/false));
+
+  for (auto i = 0; i < cluster_->num_masters(); ++i) {
+    cluster_->master(i)->mutable_flags()->pop_back();
+  }
+  cluster_->Shutdown();
+  SleepFor(MonoDelta::FromSeconds(20));
+  // the master should fail to start, which means the tablet servers won't be
+  // able to connect to it and the mini cluster will time out waiting for the
+  // cluster to start up.
+  ASSERT_TRUE(cluster_->Restart().IsTimedOut());
+}
+
 class EncryptionPolicyTest :
     public SecurityITest,
     public ::testing::WithParamInterface<tuple<
diff --git a/src/kudu/master/catalog_manager.cc 
b/src/kudu/master/catalog_manager.cc
index ef56e428d..cf138364b 100644
--- a/src/kudu/master/catalog_manager.cc
+++ b/src/kudu/master/catalog_manager.cc
@@ -1261,6 +1261,15 @@ Status CatalogManager::InitCertAuthority() {
     // should not run without a CA certificate.
     return InitCertAuthorityWith(std::move(key), std::move(cert));
   }
+  // If a cluster existed before KUDU-3448, or the
+  // --ipki_private_key_password_cmd was not set on a cluster, the IPKI private
+  // key is stored in cleartext. In this case, if the administrator adds this
+  // flag to the existing cluster, we need to read the cleartext private key,
+  // encrypt it, and overwrite the cleartext key with the encrypted version.
+  if (s.IsRuntimeError() && !ipki_private_key_password_.empty()) {
+    RETURN_NOT_OK(LoadAndEncryptCertAuthorityInfo(&key, &cert));
+    return InitCertAuthorityWith(std::move(key), std::move(cert));
+  }
 
   return s;
 }
@@ -1336,6 +1345,47 @@ Status 
CatalogManager::LoadCertAuthorityInfo(unique_ptr<PrivateKey>* key,
   return Status::OK();
 }
 
+Status CatalogManager::LoadAndEncryptCertAuthorityInfo(unique_ptr<PrivateKey>* 
key,
+                                                       unique_ptr<Cert>* cert) 
{
+  leader_lock_.AssertAcquiredForWriting();
+
+  
MAYBE_INJECT_RANDOM_LATENCY(FLAGS_catalog_manager_inject_latency_load_ca_info_ms);
+
+  SysCertAuthorityEntryPB info;
+  RETURN_NOT_OK(sys_catalog_->GetCertAuthorityEntry(&info));
+
+  unique_ptr<PrivateKey> ca_private_key(new PrivateKey);
+  unique_ptr<Cert> ca_cert(new Cert);
+
+  RETURN_NOT_OK_PREPEND(ca_private_key->FromString(info.private_key(), 
DataFormat::DER),
+      "could not load IPKI private key");
+
+  LOG(INFO) << "IPKI private key was stored in cleartext, attempting to 
encrypt it.";
+
+  RETURN_NOT_OK_PREPEND(ca_private_key->ToEncryptedString(
+        info.mutable_private_key(), DataFormat::DER,
+        [&](string* password){ *password = ipki_private_key_password_;
+          return Status::OK();
+        }),
+      "private key was stored in cleartext, reencrypting with the provided "
+      "password failed.");
+
+  LOG(INFO) << "IPKI private key successfully encrypted.";
+
+  RETURN_NOT_OK_PREPEND(sys_catalog_->UpdateCertAuthorityEntry(info),
+                        "IPKI certificate couldn't be written to syscatalog");
+
+  RETURN_NOT_OK(ca_cert->FromString(
+      info.certificate(), DataFormat::DER));
+  // Extra sanity check.
+  RETURN_NOT_OK(ca_cert->CheckKeyMatch(*ca_private_key));
+
+  key->swap(ca_private_key);
+  cert->swap(ca_cert);
+
+  return Status::OK();
+}
+
 // Store cluster ID into the system table.
 Status CatalogManager::StoreClusterId(const string& cluster_id) {
   leader_lock_.AssertAcquiredForWriting();
diff --git a/src/kudu/master/catalog_manager.h 
b/src/kudu/master/catalog_manager.h
index bf819de9c..159a7a243 100644
--- a/src/kudu/master/catalog_manager.h
+++ b/src/kudu/master/catalog_manager.h
@@ -1100,6 +1100,12 @@ class CatalogManager : public 
tserver::TabletReplicaLookupIf {
   Status LoadCertAuthorityInfo(std::unique_ptr<security::PrivateKey>* key,
                                std::unique_ptr<security::Cert>* cert);
 
+  // Same as above, but it doesn't try to decrypt it even if
+  // ipki_private_key_password_ is set, and if it's able to load it, then it
+  // encrypts it using the above password and rewrites the system table entry.
+  Status 
LoadAndEncryptCertAuthorityInfo(std::unique_ptr<security::PrivateKey>* key,
+                                         std::unique_ptr<security::Cert>* 
cert);
+
   // Store cluster ID into the system table.
   Status StoreClusterId(const std::string& cluster_id);
 
diff --git a/src/kudu/master/sys_catalog.cc b/src/kudu/master/sys_catalog.cc
index 1c346cacf..545dfad0b 100644
--- a/src/kudu/master/sys_catalog.cc
+++ b/src/kudu/master/sys_catalog.cc
@@ -25,6 +25,7 @@
 #include <optional>
 #include <ostream>
 #include <string>
+#include <type_traits>
 #include <utility>
 #include <vector>
 
@@ -50,6 +51,7 @@
 #include "kudu/consensus/consensus_peers.h"
 #include "kudu/consensus/log.h"
 #include "kudu/consensus/log_anchor_registry.h"
+#include "kudu/consensus/opid.pb.h"
 #include "kudu/consensus/opid_util.h"
 #include "kudu/consensus/quorum_util.h"
 #include "kudu/consensus/raft_consensus.h"
@@ -183,7 +185,6 @@ bool ArePBsEqual(const google::protobuf::Message& prev_pb,
 
 } // anonymous namespace
 
-
 SysCatalogTable::SysCatalogTable(Master* master,
                                  ElectedLeaderCallback leader_cb)
     : metric_registry_(master->metric_registry()),
@@ -910,6 +911,17 @@ Status SysCatalogTable::AddClusterIdEntry(
 
 Status SysCatalogTable::AddCertAuthorityEntry(
     const SysCertAuthorityEntryPB& entry) {
+  return AddOrUpdateCertAuthorityEntry(entry, RowOperationsPB::INSERT);
+}
+
+Status SysCatalogTable::UpdateCertAuthorityEntry(
+    const SysCertAuthorityEntryPB& entry) {
+  return AddOrUpdateCertAuthorityEntry(entry, RowOperationsPB::UPDATE);
+}
+
+Status SysCatalogTable::AddOrUpdateCertAuthorityEntry(
+    const SysCertAuthorityEntryPB& entry,
+    RowOperationsPB::Type op) {
   WriteRequestPB req;
   req.set_tablet_id(kSysCatalogTabletId);
   RETURN_NOT_OK(SchemaToPB(schema_, req.mutable_schema()));
@@ -922,7 +934,7 @@ Status SysCatalogTable::AddCertAuthorityEntry(
   CHECK_OK(row.SetStringNoCopy(kSysCatalogTableColId, 
kSysCertAuthorityEntryId));
   CHECK_OK(row.SetStringNoCopy(kSysCatalogTableColMetadata, metadata_buf));
   RowOperationsPBEncoder enc(req.mutable_row_operations());
-  enc.Add(RowOperationsPB::INSERT, row);
+  enc.Add(op, row);
 
   return SyncWrite(req);
 }
diff --git a/src/kudu/master/sys_catalog.h b/src/kudu/master/sys_catalog.h
index b4d6dd11e..79a697afc 100644
--- a/src/kudu/master/sys_catalog.h
+++ b/src/kudu/master/sys_catalog.h
@@ -26,6 +26,7 @@
 
 #include <gtest/gtest_prod.h>
 
+#include "kudu/common/row_operations.pb.h"
 #include "kudu/common/schema.h"
 #include "kudu/consensus/metadata.pb.h"
 #include "kudu/gutil/macros.h"
@@ -267,6 +268,12 @@ class SysCatalogTable {
   // There should be no more than one CA entry in the system table.
   Status AddCertAuthorityEntry(const SysCertAuthorityEntryPB& entry);
 
+  // Update an existing root CA entry (private key and certificate) in the
+  // system table.
+  //
+  // There should be no more than one CA entry in the system table.
+  Status UpdateCertAuthorityEntry(const SysCertAuthorityEntryPB& entry);
+
   // Add TSK (Token Signing Key) entry into the system table.
   Status AddTskEntry(const SysTskEntryPB& entry);
 
@@ -393,6 +400,11 @@ class SysCatalogTable {
   // Overwrite (upsert) the latest event ID in the table with the provided ID.
   void ReqSetNotificationLogEventId(tserver::WriteRequestPB* req, int64_t 
event_id);
 
+  // Add or update root CA entry (private key and certificate) into the system 
table.
+  // There should be no more than one CA entry in the system table.
+  Status AddOrUpdateCertAuthorityEntry(const SysCertAuthorityEntryPB& entry,
+                                       RowOperationsPB::Type op);
+
   static std::string TskSeqNumberToEntryId(int64_t seq_number);
 
   static int64_t TskEntryIdToSeqNumber(const std::string& entry_id);
diff --git a/src/kudu/security/token_signing_key.cc 
b/src/kudu/security/token_signing_key.cc
index 20667ce30..a3cda42e4 100644
--- a/src/kudu/security/token_signing_key.cc
+++ b/src/kudu/security/token_signing_key.cc
@@ -65,12 +65,23 @@ TokenSigningPrivateKey::TokenSigningPrivateKey(
   if (password.empty()) {
     CHECK_OK(key_->FromString(pb.rsa_key_der(), DataFormat::DER));
   } else {
-    CHECK_OK(key_->FromEncryptedString(pb.rsa_key_der(), DataFormat::DER,
+    Status s = key_->FromEncryptedString(pb.rsa_key_der(), DataFormat::DER,
           [&](string* p) {
             *p = password;
             return Status::OK();
           }
-    ));
+    );
+
+    // It's possible that we have old TSKs from before
+    // --tsk_private_key_password_cmd has been added to the master and they're
+    // still stored in cleartext. In this case, we retry to load the TSK 
without
+    // password. There's no need to re-encrypt the existing private keys, as
+    // they'll be rolled out soon anyway and the new ones will be encrypted.
+    if (s.IsRuntimeError()) {
+      CHECK_OK(key_->FromString(pb.rsa_key_der(), DataFormat::DER));
+    } else {
+      CHECK_OK(s);
+    }
   }
   private_key_der_ = pb.rsa_key_der();
   key_seq_num_ = pb.key_seq_num();

Reply via email to