Permalink
Browse files

IMPALA-1795: Add support for passwords for SSL private key files

This patch allows administrators to configure all Impala daemons with a
password for the private key file used to negotiate connections with
clients which present the corresponding public key. This private key is
obtained by running a user-supplied shell command and using the result.

The command is supplied by setting --ssl_private_key_password_cmd. The
output of the command is truncated to a maximum of 1024 bytes (this is a
limitation of RunShellProcess(), but should not be significant for this
use case), and then all trailing whitespace is trimmed (this is to avoid
unexpected trailing newlines etc. from shell output).

If the password is incorrect clients will be unable to connect to the
server, whether or not they have the correct public key. If the command
exits with an error, the server will not start.

Change-Id: Icc13933fdf50a6170c859989626da5772fe5040d
Reviewed-on: http://gerrit.cloudera.org:8080/623
Reviewed-by: Alex Behm <alex.behm@cloudera.com>
Tested-by: Internal Jenkins
  • Loading branch information...
henryr authored and Internal Jenkins committed Aug 11, 2015
1 parent d48d6e8 commit faeb9902075e32f53e74a3afa3d7e1d73b5a8fe9
@@ -42,6 +42,7 @@ DECLARE_bool(enable_webserver);
DECLARE_int32(state_store_subscriber_port);
DECLARE_string(ssl_server_certificate);
DECLARE_string(ssl_private_key);
DECLARE_string(ssl_private_key_password_cmd);
#include "common/names.h"
@@ -84,8 +85,8 @@ int main(int argc, char** argv) {
FLAGS_catalog_service_port, NULL, metrics.get(), 5);
if (!FLAGS_ssl_server_certificate.empty()) {
LOG(INFO) << "Enabling SSL for CatalogService";
EXIT_IF_ERROR(server->EnableSsl(
FLAGS_ssl_server_certificate, FLAGS_ssl_private_key));
EXIT_IF_ERROR(server->EnableSsl(FLAGS_ssl_server_certificate, FLAGS_ssl_private_key,
FLAGS_ssl_private_key_password_cmd));
}
EXIT_IF_ERROR(server->Start());
LOG(INFO) << "CatalogService started on port: " << FLAGS_catalog_service_port;
@@ -12,56 +12,114 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "testutil/in-process-servers.h"
#include "rpc/thrift-client.h"
#include "service/fe-support.h"
#include "service/impala-server.h"
#include <string>
#include <gtest/gtest.h>
#include "common/init.h"
#include "service/fe-support.h"
#include "gen-cpp/StatestoreService.h"
#include "gutil/strings/substitute.h"
#include "common/names.h"
using namespace impala;
using namespace strings;
using namespace apache::thrift;
DECLARE_string(ssl_server_certificate);
DECLARE_string(ssl_private_key);
DECLARE_string(ssl_client_ca_certificate);
DECLARE_int32(state_store_port);
DECLARE_int32(be_port);
DECLARE_int32(beeswax_port);
string IMPALA_HOME(getenv("IMPALA_HOME"));
const string& SERVER_CERT =
Substitute("$0/be/src/testutil/server-cert.pem", IMPALA_HOME);
const string& PRIVATE_KEY =
Substitute("$0/be/src/testutil/server-key.pem", IMPALA_HOME);
const string& PASSWORD_PROTECTED_PRIVATE_KEY =
Substitute("$0/be/src/testutil/server-key-password.pem", IMPALA_HOME);
/// Dummy server class (chosen because it has the smallest interface to implement) that
/// tests can use to start Thrift servers.
class DummyStatestoreService : public StatestoreServiceIf {
public:
virtual void RegisterSubscriber(TRegisterSubscriberResponse& response,
const TRegisterSubscriberRequest& request) {
}
};
shared_ptr<TProcessor> MakeProcessor() {
shared_ptr<DummyStatestoreService> service(new DummyStatestoreService());
return shared_ptr<TProcessor>(new StatestoreServiceProcessor(service));
}
TEST(SslTest, Connectivity) {
// Start a server using SSL and confirm that an SSL client can connect, while a non-SSL
// client cannot.
string impala_home(getenv("IMPALA_HOME"));
stringstream server_cert;
server_cert << impala_home << "/be/src/testutil/server-cert.pem";
FLAGS_ssl_server_certificate = server_cert.str();
FLAGS_ssl_client_ca_certificate = server_cert.str();
stringstream server_key;
server_key << impala_home << "/be/src/testutil/server-key.pem";
FLAGS_ssl_private_key = server_key.str();
// TODO: Revert to stack-allocated when IMPALA-618 is fixed.
InProcessImpalaServer* impala =
new InProcessImpalaServer("localhost", FLAGS_be_port, 0, 0, "", 0);
EXIT_IF_ERROR(
impala->StartWithClientServers(FLAGS_beeswax_port, FLAGS_beeswax_port + 1, false));
ThriftClient<ImpalaServiceClient> ssl_client(
"localhost", FLAGS_beeswax_port, "", NULL, true);
ThriftServer server("DummyStatestore", MakeProcessor(),
FLAGS_state_store_port + 1, NULL, NULL, 5);
EXPECT_TRUE(server.EnableSsl(SERVER_CERT, PRIVATE_KEY, "echo password").ok());
server.Start();
FLAGS_ssl_client_ca_certificate = SERVER_CERT;
ThriftClient<StatestoreServiceClient> ssl_client(
"localhost", FLAGS_state_store_port + 1, "", NULL, true);
EXPECT_TRUE(ssl_client.Open().ok());
TRegisterSubscriberResponse resp;
EXPECT_NO_THROW({
ssl_client.iface()->RegisterSubscriber(resp, TRegisterSubscriberRequest());
});
TPingImpalaServiceResp resp;
// Disable SSL for this client.
ThriftClient<StatestoreServiceClient> non_ssl_client(
"localhost", FLAGS_state_store_port + 1, "", NULL, false);
EXPECT_TRUE(ssl_client.Open().ok());
EXPECT_THROW(non_ssl_client.iface()->RegisterSubscriber(
resp, TRegisterSubscriberRequest()), TTransportException);
}
TEST(PasswordProtectedPemFile, CorrectOperation) {
// Require the server to execute a shell command to read the password to the private key
// file.
ThriftServer server("DummyStatestore", MakeProcessor(),
FLAGS_state_store_port + 1, NULL, NULL, 5);
EXPECT_TRUE(server.EnableSsl(
SERVER_CERT, PASSWORD_PROTECTED_PRIVATE_KEY, "echo password").ok());
server.Start();
FLAGS_ssl_client_ca_certificate = SERVER_CERT;
ThriftClient<StatestoreServiceClient> ssl_client(
"localhost", FLAGS_state_store_port + 1, "", NULL, true);
EXPECT_TRUE(ssl_client.Open().ok());
TRegisterSubscriberResponse resp;
EXPECT_NO_THROW({
ssl_client.iface()->PingImpalaService(resp);
ssl_client.iface()->RegisterSubscriber(resp, TRegisterSubscriberRequest());
});
}
TEST(PasswordProtectedPemFile, BadPassword) {
// Test failure when password to private key is wrong.
ThriftServer server("DummyStatestore", MakeProcessor(),
FLAGS_state_store_port + 2, NULL, NULL, 5);
EXPECT_TRUE(server.EnableSsl(
SERVER_CERT, PASSWORD_PROTECTED_PRIVATE_KEY, "echo wrongpassword").ok());
server.Start();
FLAGS_ssl_client_ca_certificate = SERVER_CERT;
ThriftClient<StatestoreServiceClient> ssl_client(
"localhost", FLAGS_state_store_port + 2, "", NULL, true);
EXPECT_FALSE(ssl_client.Open().ok());
}
ThriftClient<ImpalaServiceClient> non_ssl_client(
"localhost", FLAGS_beeswax_port, "", NULL, false);
EXPECT_TRUE(non_ssl_client.Open().ok());
EXPECT_THROW(non_ssl_client.iface()->PingImpalaService(resp), TTransportException);
TEST(PasswordProtectedPemFile, BadCommand) {
// Test failure when password command is badly formed.
ThriftServer server("DummyStatestore", MakeProcessor(),
FLAGS_state_store_port + 3, NULL, NULL, 5);
EXPECT_FALSE(server.EnableSsl(
SERVER_CERT, PASSWORD_PROTECTED_PRIVATE_KEY, "cmd-no-exist").ok());
}
int main(int argc, char** argv) {
@@ -36,12 +36,14 @@
#include "rpc/thrift-thread.h"
#include "util/debug-util.h"
#include "util/network-util.h"
#include "util/os-util.h"
#include "util/uid-util.h"
#include <sstream>
#include "common/names.h"
namespace posix_time = boost::posix_time;
using namespace boost::algorithm;
using boost::filesystem::exists;
using boost::get_system_time;
using boost::system_time;
@@ -303,19 +305,36 @@ ThriftServer::ThriftServer(const string& name, const shared_ptr<TProcessor>& pro
}
}
/// Factory subclass to override getPassword() which provides a password string to Thrift
/// to decrypt the private key file.
class ImpalaSslSocketFactory : public TSSLSocketFactory {
public:
ImpalaSslSocketFactory(const string& password) : password_(password) { }
protected:
virtual void getPassword(string& output, int size) {
output = password_;
if (output.size() > size) output.resize(size);
}
private:
/// The password string.
const string password_;
};
Status ThriftServer::CreateSocket(shared_ptr<TServerTransport>* socket) {
if (ssl_enabled()) {
// This 'factory' is only called once, since CreateSocket() is only called from
// Start()
shared_ptr<TSSLSocketFactory> socket_factory(new TSSLSocketFactory());
shared_ptr<TSSLSocketFactory> socket_factory(
new ImpalaSslSocketFactory(key_password_));
socket_factory->overrideDefaultPasswordCallback();
try {
socket_factory->loadCertificate(certificate_path_.c_str());
socket_factory->loadPrivateKey(private_key_path_.c_str());
socket->reset(new TSSLServerSocket(port_, socket_factory));
} catch (const TException& e) {
stringstream err_msg;
err_msg << "Could not create SSL socket: " << e.what();
return Status(err_msg.str());
return Status(TErrorCode::SSL_SOCKET_CREATION_FAILED, e.what());
}
return Status::OK();
} else {
@@ -324,27 +343,35 @@ Status ThriftServer::CreateSocket(shared_ptr<TServerTransport>* socket) {
}
}
Status ThriftServer::EnableSsl(const string& certificate, const string& private_key) {
Status ThriftServer::EnableSsl(const string& certificate, const string& private_key,
const string& pem_password_cmd) {
DCHECK(!started_);
if (certificate.empty()) return Status("SSL certificate path may not be blank");
if (private_key.empty()) return Status("SSL private key path may not be blank");
if (certificate.empty()) return Status(TErrorCode::SSL_CERTIFICATE_PATH_BLANK);
if (private_key.empty()) return Status(TErrorCode::SSL_PRIVATE_KEY_PATH_BLANK);
if (!exists(certificate)) {
stringstream err_msg;
err_msg << "Certificate file " << certificate << " does not exist";
return Status(err_msg.str());
return Status(TErrorCode::SSL_CERTIFICATE_NOT_FOUND, certificate);
}
// TODO: Consider warning if private key file is world-readable
if (!exists(private_key)) {
stringstream err_msg;
err_msg << "Private key file " << private_key << " does not exist";
return Status(err_msg.str());
return Status(TErrorCode::SSL_PRIVATE_KEY_NOT_FOUND, private_key);
}
ssl_enabled_ = true;
certificate_path_ = certificate;
private_key_path_ = private_key;
if (!pem_password_cmd.empty()) {
if (!RunShellProcess(pem_password_cmd, &key_password_)) {
return Status(TErrorCode::SSL_PASSWORD_CMD_FAILED, pem_password_cmd, key_password_);
} else {
trim_right(key_password_);
LOG(INFO) << "Command '" << pem_password_cmd << "' executed successfully, "
<< ".PEM password retrieved";
}
}
return Status::OK();
}
View
@@ -84,10 +84,14 @@ class ThriftServer {
AuthProvider* auth_provider = NULL, MetricGroup* metrics = NULL,
int num_worker_threads = DEFAULT_WORKER_THREADS, ServerType server_type = Threaded);
/// Enables secure access over SSL. Must be called before Start(). The arguments are
/// paths to certificate and private key files in .PEM format, respectively. If either
/// file does not exist, an error is returned.
Status EnableSsl(const std::string& certificate, const std::string& private_key);
/// Enables secure access over SSL. Must be called before Start(). The first two
/// arguments are paths to certificate and private key files in .PEM format,
/// respectively. If either file does not exist, an error is returned. The final
/// optional argument provides the command to run if a password is required to decrypt
/// the private key. It is invoked once, and the resulting password is used only for
/// password-protected .PEM files.
Status EnableSsl(const std::string& certificate, const std::string& private_key,
const std::string& pem_password_cmd = "");
int port() const { return port_; }
@@ -151,6 +155,9 @@ class ThriftServer {
/// Path to private key file in .PEM format
std::string private_key_path_;
/// Password string retrieved by running command in EnableSsl().
std::string key_password_;
/// How many worker threads to use to serve incoming requests
/// (requests are queued if no thread is immediately available)
int num_worker_threads_;
@@ -150,6 +150,13 @@ DEFINE_string(ssl_client_ca_certificate, "", "(Advanced) The full path to a cert
"used by Thrift clients to check the validity of a server certificate. May either be "
"a certificate for a third-party Certificate Authority, or a copy of the certificate "
"the client expects to receive from the server.");
DEFINE_string(ssl_private_key_password_cmd, "", "A Unix command whose output returns the "
"password used to decrypt the certificate private key file specified in "
"--ssl_private_key. If the .PEM key file is not password-protected, this command "
"will not be invoked. The output of the command will be truncated to 1024 bytes, and "
"then all trailing whitespace will be trimmed before it is used to decrypt the "
"private key");
DEFINE_int32(idle_session_timeout, 0, "The time, in seconds, that a session may be idle"
" for before it is closed (and all running queries cancelled) by Impala. If 0, idle"
@@ -1704,8 +1711,8 @@ Status CreateImpalaServer(ExecEnv* exec_env, int beeswax_port, int hs2_port, int
(*beeswax_server)->SetConnectionHandler(handler.get());
if (!FLAGS_ssl_server_certificate.empty()) {
LOG(INFO) << "Enabling SSL for Beeswax";
RETURN_IF_ERROR((*beeswax_server)->EnableSsl(
FLAGS_ssl_server_certificate, FLAGS_ssl_private_key));
RETURN_IF_ERROR((*beeswax_server)->EnableSsl(FLAGS_ssl_server_certificate,
FLAGS_ssl_private_key, FLAGS_ssl_private_key_password_cmd));
}
LOG(INFO) << "Impala Beeswax Service listening on " << beeswax_port;
@@ -1726,8 +1733,8 @@ Status CreateImpalaServer(ExecEnv* exec_env, int beeswax_port, int hs2_port, int
(*hs2_server)->SetConnectionHandler(handler.get());
if (!FLAGS_ssl_server_certificate.empty()) {
LOG(INFO) << "Enabling SSL for HiveServer2";
RETURN_IF_ERROR((*hs2_server)->EnableSsl(
FLAGS_ssl_server_certificate, FLAGS_ssl_private_key));
RETURN_IF_ERROR((*hs2_server)->EnableSsl(FLAGS_ssl_server_certificate,
FLAGS_ssl_private_key, FLAGS_ssl_private_key_password_cmd));
}
LOG(INFO) << "Impala HiveServer2 Service listening on " << hs2_port;
@@ -1746,8 +1753,8 @@ Status CreateImpalaServer(ExecEnv* exec_env, int beeswax_port, int hs2_port, int
exec_env->metrics(), FLAGS_be_service_threads);
if (!FLAGS_ssl_server_certificate.empty()) {
LOG(INFO) << "Enabling SSL for backend";
RETURN_IF_ERROR((*be_server)->EnableSsl(
FLAGS_ssl_server_certificate, FLAGS_ssl_private_key));
RETURN_IF_ERROR((*be_server)->EnableSsl(FLAGS_ssl_server_certificate,
FLAGS_ssl_private_key, FLAGS_ssl_private_key_password_cmd));
}
LOG(INFO) << "ImpalaInternalService listening on " << be_port;
@@ -37,6 +37,7 @@ DECLARE_bool(enable_webserver);
DECLARE_string(principal);
DECLARE_string(ssl_server_certificate);
DECLARE_string(ssl_private_key);
DECLARE_string(ssl_private_key_password_cmd);
#include "common/names.h"
@@ -79,8 +80,8 @@ int main(int argc, char** argv) {
FLAGS_state_store_port, NULL, metrics.get(), 5);
if (!FLAGS_ssl_server_certificate.empty()) {
LOG(INFO) << "Enabling SSL for Statestore";
EXIT_IF_ERROR(server->EnableSsl(
FLAGS_ssl_server_certificate, FLAGS_ssl_private_key));
EXIT_IF_ERROR(server->EnableSsl(FLAGS_ssl_server_certificate, FLAGS_ssl_private_key,
FLAGS_ssl_private_key_password_cmd));
}
EXIT_IF_ERROR(server->Start());
@@ -0,0 +1,30 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,34270C6CD1623E14
j2xGHUZ/lPkLPPAgnvdMcpkpHstFAIIyDw8KQHD3y4+J9B9ZX24XP34wEdit8eDF
Tm/ODX5eccn7t9gjkRU+rsF7L1JT+OoLkGwSI8KdUlVhncFUCGYWspSeVYLUl5Qa
ucbutzo0ael+zvOBXkc4ns/tvsIk3NYVz5qgcn1iw/87GN0Nqd1U2dwmClv8TP5m
4o641PEv7ozydD4+/yt4qc++UEgbZCNUfJYTG1dh2vnstCJgcGysS6f2Z6XiXLDW
PCpVfG81cDWh/pRJq7oa4neQXf/NP18OMBoUtJO7BPPjZtfuD/SbSuhaIGan16hT
RuVCl1ycJ2lZe428xTz2qbvBFli1RRMLtiV/qp8L8Db4hTeUWS4Sa3GonebYqJws
pe/r5/rr7Ph6YcK9nUCWfhAljwZ8aMSFO0LFKSYWgFHTlcnfEGucljJplsl7YzhV
bRkP7VL9p6ewIB6FuiIzcc64XSdflXal5PUkDUdIBig37T0GIHrqZPuWbJFbhiYk
//8c9W2J8Vl4o1kUHhwsxaXbWIUmmFN2im6zMOCVDt5TAkBeAfbr9IiUxlNg5Yb0
QGRoi8ykGJL3TFBtHVE5hn0OVW41eNDPKmMdlsjGYfU5ZaX2Qefg6uOrll/wI6nM
2vvc2VwPtcRbYh8lK+oGShPSaCUqK650UXkcmYAQgCXR+UpJEl5Yp+jAq7UsvkyM
43vBx0mfvRH2Q1m3TTJ2ka32lFjzaUd+jsy1Mub/CcIsfJhSMppghFJ/W8SeEOg/
viwyB7JoGD8ZhwEGL0asxF8PV2CFKy5AsbhKtmXmrMaPPomdarpheSX36CmH0gcv
K/nfsm1meWIBEKcTh05yedc4XuQLvKwHrrCLqlcrwmvaNAtRSt1ZrHb/rYfRmPX5
hasseOY64SqKCSj2rx6ssuElMFk+xAeC7rJgrR1E3lwW881GoFGWYwMlbdFqEtyO
2SN/YKtzLRzzPJ5K36PzS7OhJG+UOMCVYBWvNQ51BpQpHp25ZxUgGHqtKeO55FAI
HDfjObOyqQPavVxqlis6TGLsWmD9OrXM3dqTRia4AmXjhJ/qVsvFYOlMpSgk8Q9i
Wx7dvVwqH4+/SrrasXkzPgKcexueyMf2iK18lesr6jM5er7ZWxvMfeKaIaS76Grs
STt1Ykabd0eJ0tJVKF2lO9KYmwSWqhq+PVOWGZmHHeJ8jMXvJmxU+cymNMiLhqWH
E0skEnM4xaaU1vHGzh8Clcb3BGbh2gi6XoaACxe6HgSHeGeglcBQiO9IULkUS12w
DDudhZ8/7nyYVPW5916aavQ8gxLo4VDhHDMFf7Jd7xV9eHhWulExwB7B33u4G4Ea
niV+Gj+n1WFHaylPKLLZV6H34zGzkG0adnr3KMT81TuknErv2rb41/tAeJPT2YG9
fINyGX8+S7TPsRdH1a76qa9VGO5lKtDjZgTzBTXuj4PbT8OYwOO52OjSbCYK70Km
4xoe8Ylyte5OYdhNbYSMoV+pEO2xJ7WyjQzHhosbcXxDFmXveLy6y9H2qNcgGRYn
GFz+KH2108J0ENStYrPwkirejBlqSStsjtyWgDbyO/X8yJI0kRRik58lmUZ8A3ip
-----END RSA PRIVATE KEY-----
Oops, something went wrong.

0 comments on commit faeb990

Please sign in to comment.