blob: 12abe5c7ca030207206023f43f3a5bdbbf8dcedd [file] [log] [blame]
// 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