| // 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 <cstdlib> |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include <glog/logging.h> |
| |
| #include "kudu/gutil/map-util.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/scoped_cleanup.h" |
| #include "kudu/util/slice.h" |
| #include "kudu/util/stopwatch.h" |
| #include "kudu/util/subprocess.h" |
| #include "kudu/util/test_util.h" |
| |
| using std::map; |
| using std::string; |
| using std::unique_ptr; |
| using std::vector; |
| using strings::Substitute; |
| |
| namespace kudu { |
| |
| string MiniKdcOptions::ToString() const { |
| return strings::Substitute("{ realm: $0, data_root: $1, port: $2, " |
| "ticket_lifetime: $3, renew_lifetime: $4 }", |
| realm, data_root, port, ticket_lifetime, renew_lifetime); |
| } |
| |
| MiniKdc::MiniKdc() |
| : MiniKdc(MiniKdcOptions()) { |
| } |
| |
| MiniKdc::MiniKdc(MiniKdcOptions options) |
| : options_(std::move(options)) { |
| if (options_.realm.empty()) { |
| options_.realm = "KRBTEST.COM"; |
| } |
| if (options_.data_root.empty()) { |
| options_.data_root = JoinPathSegments(GetTestDataDirectory(), "krb5kdc"); |
| } |
| if (options_.ticket_lifetime.empty()) { |
| options_.ticket_lifetime = "24h"; |
| } |
| if (options_.renew_lifetime.empty()) { |
| options_.renew_lifetime = "7d"; |
| } |
| } |
| |
| 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")}, |
| // Enable the workaround for MIT krb5 1.10 bugs from krb5_realm_override.cc. |
| {"KUDU_ENABLE_KRB5_REALM_FIX", "yes"} |
| }; |
| } |
| |
| 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, string* path) { |
| static const vector<string> kCommonLocations = { |
| "/usr/local/opt/krb5/sbin", // Homebrew |
| "/usr/local/opt/krb5/bin", // Homebrew |
| "/opt/homebrew/opt/krb5/sbin", // Homebrew arm |
| "/opt/homebrew/opt/krb5/bin", // Homebrew arm |
| "/opt/local/sbin", // Macports |
| "/opt/local/bin", // Macports |
| "/usr/lib/mit/sbin", // SLES |
| "/usr/sbin", // Linux |
| }; |
| return FindExecutable(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 |
| // with a wildcard binding. |
| RETURN_NOT_OK(WaitForUdpBind( |
| kdc_process_->pid(), &options_.port, {}, MonoDelta::FromSeconds(1))); |
| |
| 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() { |
| if (!kdc_process_) { |
| return Status::OK(); |
| } |
| 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 |
| |
| # Disable aes256 since Java does not support it without JCE. Java is only |
| # one of several minicluster consumers, but disabling aes256 doesn't |
| # appreciably hurt Kudu code coverage, so we disable it universally. |
| # |
| # For more details, see: |
| # https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/jgss-features.html |
| default_tkt_enctypes = aes128-cts des3-cbc-sha1 |
| default_tgs_enctypes = aes128-cts des3-cbc-sha1 |
| permitted_enctypes = aes128-cts des3-cbc-sha1 |
| |
| # 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::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::RandomizePrincipalKey(const string& spn) { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("randomizing key for $0", spn)); |
| string kadmin; |
| RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ |
| kadmin, "-q", Substitute("change_password -randkey $0", spn)}))); |
| return Status::OK(); |
| } |
| |
| Status MiniKdc::CreateKeytabForExistingPrincipal(const string& spn) { |
| SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, Substitute("creating keytab for $0", spn)); |
| string kt_path = GetKeytabPathForPrincipal(spn); |
| string kadmin; |
| RETURN_NOT_OK(GetBinaryPath("kadmin.local", &kadmin)); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ |
| kadmin, "-q", Substitute("xst -norandkey -k $0 $1", kt_path, spn)}))); |
| return Status::OK(); |
| } |
| |
| string MiniKdc::GetKeytabPathForPrincipal(const string& spn) const { |
| string kt_path = spn; |
| StripString(&kt_path, "/", '_'); |
| return JoinPathSegments(options_.data_root, kt_path) + ".keytab"; |
| } |
| |
| 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)); |
| unique_ptr<WritableFile> tmp_cc_file; |
| string tmp_cc_path; |
| string tmp_username = username; |
| StripString(&tmp_username, "/", '_'); |
| const auto tmp_template = Substitute("kinit-temp-$0.XXXXXX", tmp_username); |
| WritableFileOptions opts; |
| opts.is_sensitive = false; |
| RETURN_NOT_OK_PREPEND(Env::Default()->NewTempWritableFile( |
| opts, |
| JoinPathSegments(options_.data_root, tmp_template), |
| &tmp_cc_path, &tmp_cc_file), |
| "could not create temporary file"); |
| auto delete_tmp_cc = MakeScopedCleanup([&]() { |
| WARN_NOT_OK(Env::Default()->DeleteFile(tmp_cc_path), |
| "could not delete file " + tmp_cc_path); |
| }); |
| RETURN_NOT_OK(Subprocess::Call(MakeArgv({ kinit, "-c", tmp_cc_path, username }), username)); |
| const auto env_vars_map = GetEnvVars(); |
| const auto& ccache_path = FindOrDie(env_vars_map, "KRB5CCNAME"); |
| RETURN_NOT_OK_PREPEND(Env::Default()->RenameFile(tmp_cc_path, ccache_path), |
| "could not move new file into place"); |
| delete_tmp_cc.cancel(); |
| 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 |