| // 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. |
| |
| #include "kudu/security/test/mini_kdc.h" |
| |
| #include <csignal> |
| #include <stdlib.h> |
| |
| #include <limits> |
| #include <map> |
| #include <memory> |
| #include <string> |
| |
| #include <glog/logging.h> |
| |
| #include "kudu/gutil/gscoped_ptr.h" |
| #include "kudu/gutil/strings/numbers.h" |
| #include "kudu/gutil/strings/split.h" |
| #include "kudu/gutil/strings/strip.h" |
| #include "kudu/gutil/strings/substitute.h" |
| #include "kudu/util/env.h" |
| #include "kudu/util/monotime.h" |
| #include "kudu/util/path_util.h" |
| #include "kudu/util/stopwatch.h" |
| #include "kudu/util/subprocess.h" |
| |
| // test_util.h has a dependancy on gmock which Impala doesn't depend on, so we rewrite |
| // parts of this code that use test_util members. |
| //#include "kudu/util/test_util.h" |
| |
| using std::map; |
| using std::string; |
| using std::unique_ptr; |
| using strings::Substitute; |
| |
| namespace kudu { |
| |
| string MiniKdcOptions::ToString() const { |
| return strings::Substitute("{ realm: $0, port: $1, data_root: $2 }", realm, port, data_root); |
| } |
| |
| MiniKdc::MiniKdc() |
| : MiniKdc(MiniKdcOptions()) { |
| } |
| |
| MiniKdc::MiniKdc(const MiniKdcOptions& options) |
| : options_(options) { |
| if (options_.realm.empty()) { |
| options_.realm = "KRBTEST.COM"; |
| } |
| if (options_.data_root.empty()) { |
| // We hardcode "/tmp" here since the original function which initializes a random test |
| // directory (GetTestDataDirectory()), depends on gmock. |
| options_.data_root = JoinPathSegments("/tmp", "krb5kdc"); |
| } |
| if (options_.renew_lifetime.empty()) { |
| options_.renew_lifetime = "7d"; |
| } |
| if (options_.ticket_lifetime.empty()) { |
| options_.ticket_lifetime = "24h"; |
| } |
| } |
| |
| MiniKdc::~MiniKdc() { |
| if (kdc_process_) { |
| WARN_NOT_OK(Stop(), "Unable to stop MiniKdc"); |
| } |
| } |
| |
| map<string, string> MiniKdc::GetEnvVars() const { |
| return { |
| {"KRB5_CONFIG", JoinPathSegments(options_.data_root, "krb5.conf")}, |
| {"KRB5_KDC_PROFILE", JoinPathSegments(options_.data_root, "kdc.conf")}, |
| {"KRB5CCNAME", JoinPathSegments(options_.data_root, "krb5cc")} |
| }; |
| } |
| |
| vector<string> MiniKdc::MakeArgv(const vector<string>& in_argv) { |
| vector<string> real_argv = { "env" }; |
| for (const auto& p : GetEnvVars()) { |
| real_argv.push_back(Substitute("$0=$1", p.first, p.second)); |
| } |
| for (const string& a : in_argv) { |
| real_argv.push_back(a); |
| } |
| return real_argv; |
| } |
| |
| namespace { |
| // Attempts to find the path to the specified Kerberos binary, storing it in 'path'. |
| Status GetBinaryPath(const string& binary, |
| const vector<string>& search, |
| string* path) { |
| string p; |
| |
| // First, check specified locations which are sometimes not on the PATH. |
| // This is necessary to check first so that the system Heimdal kerberos |
| // binaries won't be found first on OS X. |
| for (const auto& location : search) { |
| p = JoinPathSegments(location, binary); |
| if (Env::Default()->FileExists(p)) { |
| *path = p; |
| return Status::OK(); |
| } |
| } |
| |
| // Next check if the binary is on the PATH. |
| Status s = Subprocess::Call({ "which", binary }, "", &p); |
| if (s.ok()) { |
| StripTrailingNewline(&p); |
| *path = p; |
| return Status::OK(); |
| } |
| |
| return Status::NotFound("Unable to find binary", binary); |
| } |
| |
| // Attempts to find the path to the specified Kerberos binary, storing it in 'path'. |
| Status GetBinaryPath(const string& binary, string* path) { |
| static const vector<string> kCommonLocations = { |
| "/usr/local/opt/krb5/sbin", // Homebrew |
| "/usr/local/opt/krb5/bin", // Homebrew |
| "/opt/local/sbin", // Macports |
| "/opt/local/bin", // Macports |
| "/usr/lib/mit/sbin", // SLES |
| "/usr/sbin", // Linux |
| }; |
| return GetBinaryPath(binary, kCommonLocations, path); |
| } |
| } // namespace |
| |
| |
| Status MiniKdc::Start() { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, "starting KDC"); |
| CHECK(!kdc_process_); |
| VLOG(1) << "Starting Kerberos KDC: " << options_.ToString(); |
| |
| if (!Env::Default()->FileExists(options_.data_root)) { |
| VLOG(1) << "Creating KDC database and configuration files"; |
| RETURN_NOT_OK(Env::Default()->CreateDir(options_.data_root)); |
| |
| RETURN_NOT_OK(CreateKdcConf()); |
| RETURN_NOT_OK(CreateKrb5Conf()); |
| |
| // Create the KDC database using the kdb5_util tool. |
| string kdb5_util_bin; |
| RETURN_NOT_OK(GetBinaryPath("kdb5_util", &kdb5_util_bin)); |
| |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ |
| kdb5_util_bin, "create", |
| "-s", // Stash the master password. |
| "-P", "masterpw", // Set a password. |
| "-W", // Use weak entropy (since we don't need real security). |
| }))); |
| } |
| |
| // Start the Kerberos KDC. |
| string krb5kdc_bin; |
| RETURN_NOT_OK(GetBinaryPath("krb5kdc", &krb5kdc_bin)); |
| |
| kdc_process_.reset(new Subprocess( |
| MakeArgv({ |
| krb5kdc_bin, |
| "-n", // Do not daemonize. |
| }))); |
| |
| RETURN_NOT_OK(kdc_process_->Start()); |
| |
| const bool need_config_update = (options_.port == 0); |
| // Wait for KDC to start listening on its ports and commencing operation. |
| RETURN_NOT_OK(WaitForKdcPorts()); |
| |
| if (need_config_update) { |
| // If we asked for an ephemeral port, grab the actual ports and |
| // rewrite the configuration so that clients can connect. |
| RETURN_NOT_OK(CreateKrb5Conf()); |
| RETURN_NOT_OK(CreateKdcConf()); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::Stop() { |
| CHECK(kdc_process_); |
| VLOG(1) << "Stopping KDC"; |
| unique_ptr<Subprocess> proc(kdc_process_.release()); |
| RETURN_NOT_OK(proc->Kill(SIGKILL)); |
| RETURN_NOT_OK(proc->Wait()); |
| |
| return Status::OK(); |
| } |
| |
| // Creates a kdc.conf file according to the provided options. |
| Status MiniKdc::CreateKdcConf() const { |
| static const string kFileTemplate = R"( |
| [kdcdefaults] |
| kdc_ports = $2 |
| kdc_tcp_ports = "" |
| |
| [realms] |
| $1 = { |
| acl_file = $0/kadm5.acl |
| admin_keytab = $0/kadm5.keytab |
| database_name = $0/principal |
| key_stash_file = $0/.k5.$1 |
| max_renewable_life = 7d 0h 0m 0s |
| } |
| )"; |
| string file_contents = strings::Substitute(kFileTemplate, options_.data_root, |
| options_.realm, options_.port); |
| return WriteStringToFile(Env::Default(), file_contents, |
| JoinPathSegments(options_.data_root, "kdc.conf")); |
| } |
| |
| // Creates a krb5.conf file according to the provided options. |
| Status MiniKdc::CreateKrb5Conf() const { |
| static const string kFileTemplate = R"( |
| [logging] |
| kdc = FILE:/dev/stderr |
| |
| [libdefaults] |
| default_realm = $1 |
| dns_lookup_kdc = false |
| dns_lookup_realm = false |
| forwardable = true |
| renew_lifetime = $2 |
| ticket_lifetime = $3 |
| |
| # In miniclusters, we start daemons on local loopback IPs that |
| # have no reverse DNS entries. So, disable reverse DNS. |
| rdns = false |
| |
| # The server side will start its GSSAPI server using the local FQDN. |
| # However, in tests, we connect to it via a non-matching loopback IP. |
| # This enables us to connect despite that mismatch. |
| ignore_acceptor_hostname = true |
| |
| [realms] |
| $1 = { |
| kdc = 127.0.0.1:$0 |
| # This super-arcane syntax can be found documented in various Hadoop |
| # vendors' security guides and very briefly in the MIT krb5 docs. |
| # Basically, this one says to map anyone coming in as foo@OTHERREALM.COM |
| # and map them to a local user 'other-foo' |
| auth_to_local = RULE:[1:other-$$1@$$0](.*@OTHERREALM.COM$$)s/@.*// |
| } |
| )"; |
| string file_contents = strings::Substitute(kFileTemplate, options_.port, options_.realm, |
| options_.renew_lifetime, options_.ticket_lifetime); |
| return WriteStringToFile(Env::Default(), file_contents, |
| JoinPathSegments(options_.data_root, "krb5.conf")); |
| } |
| |
| Status MiniKdc::WaitForKdcPorts() { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("waiting for KDC ports")); |
| // We have to use 'lsof' to figure out which ports the KDC bound to if we |
| // requested ephemeral ones. The KDC doesn't log the bound port or expose it |
| // in any other fashion, and re-implementing lsof involves parsing a lot of |
| // files in /proc/. So, requiring lsof for tests and parsing its output seems |
| // more straight-forward. We call lsof in a loop in case the kdc is slow to |
| // bind to the ports. |
| |
| string lsof; |
| RETURN_NOT_OK(GetBinaryPath("lsof", {"/sbin", "/usr/sbin"}, &lsof)); |
| |
| const vector<string> cmd = { |
| lsof, "-wbnP", "-Ffn", |
| "-p", std::to_string(kdc_process_->pid()), |
| "-a", "-i", "4UDP"}; |
| |
| string lsof_out; |
| for (int i = 1; ; i++) { |
| lsof_out.clear(); |
| Status s = Subprocess::Call(cmd, "", &lsof_out); |
| |
| if (s.ok()) { |
| StripTrailingNewline(&lsof_out); |
| break; |
| } else if (i > 10) { |
| return s; |
| } |
| |
| SleepFor(MonoDelta::FromMilliseconds(i * i)); |
| } |
| |
| // The '-Ffn' flag gets lsof to output something like: |
| // p19730 |
| // f123 |
| // n*:41254 |
| // The first line is the pid. We ignore it. |
| // The second line is the file descriptor number. We ignore it. |
| // The third line has the bind address and port. |
| vector<string> lines = strings::Split(lsof_out, "\n"); |
| int32_t port = -1; |
| if (lines.size() != 3 || |
| lines[2].substr(0, 3) != "n*:" || |
| !safe_strto32(lines[2].substr(3), &port) || |
| port <= 0) { |
| return Status::RuntimeError("unexpected lsof output", lsof_out); |
| } |
| CHECK(port > 0 && port < std::numeric_limits<uint16_t>::max()) |
| << "parsed invalid port: " << port; |
| VLOG(1) << "Determined bound KDC port: " << port; |
| if (options_.port == 0) { |
| options_.port = port; |
| } else { |
| // Sanity check: if KDC's port is already established, it's supposed to be |
| // written into the configuration files, so the process must bind to the |
| // already established port. |
| CHECK(options_.port == port); |
| } |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::CreateUserPrincipal(const string& username) { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("creating user principal $0", username)); |
| string kadmin; |
| RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ |
| kadmin, "-q", Substitute("add_principal -pw $0 $0", username)}))); |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::CreateServiceKeytab(const string& spn, |
| string* path) { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("creating service keytab for $0", spn)); |
| string kt_path = spn; |
| StripString(&kt_path, "/", '_'); |
| kt_path = JoinPathSegments(options_.data_root, kt_path) + ".keytab"; |
| |
| string kadmin; |
| RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ |
| kadmin, "-q", Substitute("add_principal -randkey $0", spn)}))); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ |
| kadmin, "-q", Substitute("ktadd -k $0 $1", kt_path, spn)}))); |
| *path = kt_path; |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::Kinit(const string& username) { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("kinit for $0", username)); |
| string kinit; |
| RETURN_NOT_OK(GetBinaryPath("kinit", &kinit)); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ kinit, username }), username)); |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::Kdestroy() { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, "kdestroy"); |
| string kdestroy; |
| RETURN_NOT_OK(GetBinaryPath("kdestroy", &kdestroy)); |
| return Subprocess::Call(MakeArgv({ kdestroy, "-A" })); |
| } |
| |
| Status MiniKdc::Klist(string* output) { |
| string klist; |
| RETURN_NOT_OK(GetBinaryPath("klist", &klist)); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-A" }), "", output)); |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::KlistKeytab(const string& keytab_path, string* output) { |
| string klist; |
| RETURN_NOT_OK(GetBinaryPath("klist", &klist)); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ klist, "-k", keytab_path }), "", output)); |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::SetKrb5Environment() const { |
| if (!kdc_process_) { |
| return Status::IllegalState("KDC not started"); |
| } |
| for (const auto& p : GetEnvVars()) { |
| CHECK_ERR(setenv(p.first.c_str(), p.second.c_str(), 1 /*overwrite*/)); |
| } |
| |
| return Status::OK(); |
| } |
| |
| } // namespace kudu |