common/protobuf/kudu/rpc/server_negotiation.cc (770 lines of code) (raw):

// 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/rpc/server_negotiation.h" #include <sasl/sasl.h> #include <sys/socket.h> #include <algorithm> #include <cstdint> #include <cstdlib> #include <functional> #include <memory> #include <mutex> #include <ostream> #include <set> #include <string> #include <boost/optional/optional.hpp> #include <gflags/gflags.h> #include <glog/logging.h> #include "kudu/gutil/macros.h" #include "kudu/gutil/map-util.h" #include "kudu/gutil/strings/split.h" #include "kudu/gutil/strings/stringpiece.h" #include "kudu/gutil/strings/substitute.h" #include "kudu/rpc/blocking_ops.h" #include "kudu/rpc/constants.h" #include "kudu/rpc/rpc_verification_util.h" #include "kudu/rpc/serialization.h" #include "kudu/security/cert.h" #include "kudu/security/crypto.h" #include "kudu/security/init.h" #include "kudu/security/tls_context.h" #include "kudu/security/tls_handshake.h" #include "kudu/security/token.pb.h" #include "kudu/security/token_verifier.h" #include "kudu/util/faststring.h" #include "kudu/util/fault_injection.h" #include "kudu/util/flag_tags.h" #include "kudu/util/logging.h" #include "kudu/util/net/net_util.h" #include "kudu/util/net/sockaddr.h" #include "kudu/util/net/socket.h" #include "kudu/util/slice.h" #include "kudu/util/trace.h" #if defined(__APPLE__) // Almost all functions in the SASL API are marked as deprecated // since macOS 10.11. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif // #if defined(__APPLE__) using kudu::security::RpcEncryption; using std::set; using std::string; using std::unique_ptr; using std::vector; // Fault injection flags. DEFINE_double(rpc_inject_invalid_authn_token_ratio, 0, "If set higher than 0, AuthenticateByToken() randomly injects " "errors replying with FATAL_INVALID_AUTHENTICATION_TOKEN code. " "The flag's value corresponds to the probability of the fault " "injection event. Used for only for tests."); TAG_FLAG(rpc_inject_invalid_authn_token_ratio, runtime); TAG_FLAG(rpc_inject_invalid_authn_token_ratio, unsafe); DEFINE_double(rpc_inject_invalid_channel_bindings_ratio, 0, "The ratio of injection of invalid channel bindings during " "connection negotiation. This is a test-only flag."); TAG_FLAG(rpc_inject_invalid_channel_bindings_ratio, runtime); TAG_FLAG(rpc_inject_invalid_channel_bindings_ratio, unsafe); DEFINE_bool(rpc_send_channel_bindings, true, "Whether to send channel bindings in NegotiatePB response as " "prescribed by RFC 5929. This is a test-only flag."); TAG_FLAG(rpc_send_channel_bindings, runtime); TAG_FLAG(rpc_send_channel_bindings, unsafe); DECLARE_bool(rpc_encrypt_loopback_connections); DEFINE_string(trusted_subnets, "127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16", "A trusted subnet whitelist. If set explicitly, all unauthenticated " "or unencrypted connections are prohibited except the ones from the " "specified address blocks. Otherwise, private network (127.0.0.0/8, etc.) " "and local subnets of all local network interfaces will be used. Set it " "to '0.0.0.0/0' to allow unauthenticated/unencrypted connections from all " "remote IP addresses. However, if network access is not otherwise restricted " "by a firewall, malicious users may be able to gain unauthorized access."); TAG_FLAG(trusted_subnets, advanced); TAG_FLAG(trusted_subnets, evolving); static bool ValidateTrustedSubnets(const char* /*flagname*/, const string& value) { if (value.empty()) { return true; } for (const auto& t : strings::Split(value, ",", strings::SkipEmpty())) { kudu::Network network; kudu::Status s = network.ParseCIDRString(t.ToString()); if (!s.ok()) { LOG(ERROR) << "Invalid subnet address: " << t << ". Subnet must be specified in CIDR notation."; return false; } } return true; } DEFINE_validator(trusted_subnets, &ValidateTrustedSubnets); namespace kudu { namespace rpc { namespace { vector<Network>* g_trusted_subnets = nullptr; } // anonymous namespace static int ServerNegotiationGetoptCb(ServerNegotiation* server_negotiation, const char* plugin_name, const char* option, const char** result, unsigned* len) { return server_negotiation->GetOptionCb(plugin_name, option, result, len); } static int ServerNegotiationPlainAuthCb(sasl_conn_t* conn, ServerNegotiation* server_negotiation, const char* user, const char* pass, unsigned passlen, struct propctx* propctx) { return server_negotiation->PlainAuthCb(conn, user, pass, passlen, propctx); } ServerNegotiation::ServerNegotiation(unique_ptr<Socket> socket, const security::TlsContext* tls_context, const security::TokenVerifier* token_verifier, RpcEncryption encryption, bool encrypt_loopback, std::string sasl_proto_name) : socket_(std::move(socket)), helper_(SaslHelper::SERVER), tls_context_(tls_context), tls_handshake_(security::TlsHandshakeType::SERVER), encryption_(encryption), tls_negotiated_(false), encrypt_loopback_(encrypt_loopback), token_verifier_(token_verifier), negotiated_authn_(AuthenticationType::INVALID), negotiated_mech_(SaslMechanism::INVALID), sasl_proto_name_(std::move(sasl_proto_name)), deadline_(MonoTime::Max()) { callbacks_.push_back(SaslBuildCallback(SASL_CB_GETOPT, reinterpret_cast<int (*)()>(&ServerNegotiationGetoptCb), this)); callbacks_.push_back(SaslBuildCallback(SASL_CB_SERVER_USERDB_CHECKPASS, reinterpret_cast<int (*)()>(&ServerNegotiationPlainAuthCb), this)); callbacks_.push_back(SaslBuildCallback(SASL_CB_LIST_END, nullptr, nullptr)); } Status ServerNegotiation::EnablePlain() { return helper_.EnablePlain(); } Status ServerNegotiation::EnableGSSAPI() { return helper_.EnableGSSAPI(); } SaslMechanism::Type ServerNegotiation::negotiated_mechanism() const { return negotiated_mech_; } void ServerNegotiation::set_server_fqdn(const string& domain_name) { helper_.set_server_fqdn(domain_name); } void ServerNegotiation::set_deadline(const MonoTime& deadline) { deadline_ = deadline; } Status ServerNegotiation::Negotiate() { TRACE("Beginning negotiation"); // Wait until starting negotiation to check that the socket, tls_context, and // token_verifier are not null, since they do not need to be set for // PreflightCheckGSSAPI. DCHECK(socket_); DCHECK(tls_context_); DCHECK(token_verifier_); // Ensure we can use blocking calls on the socket during negotiation. RETURN_NOT_OK(CheckInBlockingMode(socket_.get())); faststring recv_buf; // Step 1: Read the connection header. RETURN_NOT_OK(ValidateConnectionHeader(&recv_buf)); { // Step 2: Receive and respond to the NEGOTIATE step message. NegotiatePB request; RETURN_NOT_OK(RecvNegotiatePB(&request, &recv_buf)); RETURN_NOT_OK(HandleNegotiate(request)); TRACE("Negotiated authn=$0", AuthenticationTypeToString(negotiated_authn_)); } // Step 3: if both ends support TLS, do a TLS handshake. if (encryption_ != RpcEncryption::DISABLED && tls_context_->has_cert() && ContainsKey(client_features_, TLS)) { RETURN_NOT_OK(tls_context_->InitiateHandshake(&tls_handshake_)); if (negotiated_authn_ != AuthenticationType::CERTIFICATE) { // The server does not need to verify the client's certificate unless it's // being used for authentication. tls_handshake_.set_verification_mode(security::TlsVerificationMode::VERIFY_NONE); } while (true) { NegotiatePB request; RETURN_NOT_OK(RecvNegotiatePB(&request, &recv_buf)); Status s = HandleTlsHandshake(request); if (s.ok()) { break; } if (!s.IsIncomplete()) { return s; } } tls_negotiated_ = true; } // Rejects any connection from public routable IPs if encryption // is disabled. See KUDU-1875. if (!tls_negotiated_) { Sockaddr addr; RETURN_NOT_OK(socket_->GetPeerAddress(&addr)); if (!IsTrustedConnection(addr)) { // Receives client response before sending error // message, even though the response is never used, // to avoid risk condition that connection gets // closed before client receives server's error // message. NegotiatePB request; RETURN_NOT_OK(RecvNegotiatePB(&request, &recv_buf)); Status s = Status::NotAuthorized("unencrypted connections from publicly routable " "IPs are prohibited. See --trusted_subnets flag " "for more information.", addr.ToString()); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } } // Step 4: Authentication switch (negotiated_authn_) { case AuthenticationType::SASL: RETURN_NOT_OK(AuthenticateBySasl(&recv_buf)); break; case AuthenticationType::TOKEN: RETURN_NOT_OK(AuthenticateByToken(&recv_buf)); break; case AuthenticationType::CERTIFICATE: RETURN_NOT_OK(AuthenticateByCertificate()); break; case AuthenticationType::INVALID: LOG(FATAL) << "unreachable"; } // Step 5: Receive connection context. RETURN_NOT_OK(RecvConnectionContext(&recv_buf)); TRACE("Negotiation successful"); return Status::OK(); } Status ServerNegotiation::PreflightCheckGSSAPI(const std::string& sasl_proto_name) { // TODO(todd): the error messages that come from this function on el6 // are relatively useless due to the following krb5 bug: // http://krbdev.mit.edu/rt/Ticket/Display.html?id=6973 // This may not be useful anymore given the keytab login that happens // in security/init.cc. // Initialize a ServerNegotiation with a null socket, and enable // only GSSAPI. // // We aren't going to actually send/receive any messages, but // this makes it easier to reuse the initialization code. ServerNegotiation server( nullptr, nullptr, nullptr, RpcEncryption::OPTIONAL, false, sasl_proto_name); Status s = server.EnableGSSAPI(); if (!s.ok()) { return Status::RuntimeError(s.message()); } RETURN_NOT_OK(server.InitSaslServer()); // Start the SASL server as if we were accepting a connection. const char* server_out = nullptr; // ignored uint32_t server_out_len = 0; s = WrapSaslCall(server.sasl_conn_.get(), [&]() { return sasl_server_start( server.sasl_conn_.get(), kSaslMechGSSAPI, "", 0, // Pass a 0-length token. &server_out, &server_out_len); }, "calling sasl_server_start()"); // We expect 'Incomplete' status to indicate that the first step of negotiation // was correct. if (s.IsIncomplete()) return Status::OK(); string err_msg = s.message().ToString(); if (err_msg == "Permission denied") { // For bad keytab permissions, we get a rather vague message. So, // we make it more specific for better usability. err_msg = "error accessing keytab: " + err_msg; } return Status::RuntimeError(err_msg); } Status ServerNegotiation::RecvNegotiatePB(NegotiatePB* msg, faststring* recv_buf) { RequestHeader header; Slice param_buf; RETURN_NOT_OK(ReceiveFramedMessageBlocking( socket_.get(), recv_buf, &header, &param_buf, deadline_)); Status s = helper_.CheckNegotiateCallId(header.call_id()); if (!s.ok()) { RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_INVALID_RPC_HEADER, s)); return s; } s = helper_.ParseNegotiatePB(param_buf, msg); if (!s.ok()) { RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_DESERIALIZING_REQUEST, s)); return s; } TRACE("Received $0 NegotiatePB request", NegotiatePB::NegotiateStep_Name(msg->step())); return Status::OK(); } Status ServerNegotiation::SendNegotiatePB(const NegotiatePB& msg) { ResponseHeader header; header.set_call_id(kNegotiateCallId); DCHECK(socket_); DCHECK(msg.IsInitialized()) << "message must be initialized"; DCHECK(msg.has_step()) << "message must have a step"; TRACE("Sending $0 NegotiatePB response", NegotiatePB::NegotiateStep_Name(msg.step())); return SendFramedMessageBlocking(socket_.get(), header, msg, deadline_); } Status ServerNegotiation::SendError(ErrorStatusPB::RpcErrorCodePB code, const Status& err) { DCHECK(!err.ok()); // Create header with negotiation-specific callId ResponseHeader header; header.set_call_id(kNegotiateCallId); header.set_is_error(true); // Get RPC error code from Status object ErrorStatusPB msg; msg.set_code(code); msg.set_message(err.ToString()); TRACE("Sending RPC error: $0: $1", ErrorStatusPB::RpcErrorCodePB_Name(code), err.ToString()); return SendFramedMessageBlocking(socket_.get(), header, msg, deadline_); } Status ServerNegotiation::ValidateConnectionHeader(faststring* recv_buf) { TRACE("Waiting for connection header"); size_t num_read; const size_t conn_header_len = kMagicNumberLength + kHeaderFlagsLength; recv_buf->resize(conn_header_len); RETURN_NOT_OK(socket_->BlockingRecv(recv_buf->data(), conn_header_len, &num_read, deadline_)); DCHECK_EQ(conn_header_len, num_read); RETURN_NOT_OK(serialization::ValidateConnHeader(*recv_buf)); TRACE("Connection header received"); return Status::OK(); } // calls sasl_server_init() and sasl_server_new() Status ServerNegotiation::InitSaslServer() { // TODO(unknown): Support security flags. unsigned secflags = 0; sasl_conn_t* sasl_conn = nullptr; const char* server_fqdn = helper_.server_fqdn(); // If not explicitly set, use the host's FQDN here. // SASL handles this itself if we pass null, but in a buggy way[1] that fails // if the FQDN is >64 characters. // // [1] https://github.com/cyrusimap/cyrus-sasl/issues/583 string default_server_fqdn; if (!server_fqdn) { RETURN_NOT_OK_PREPEND(GetFQDN(&default_server_fqdn), "could not determine own FQDN"); server_fqdn = default_server_fqdn.c_str(); } static constexpr const char* const kDesc = "creating new SASL server"; //static constexpr const char* const kDesc = "create " RETURN_NOT_OK_PREPEND(WrapSaslCall(nullptr /* no conn */, [&]() { return sasl_server_new( // Registered name of the service using SASL. Required. sasl_proto_name_.c_str(), // The fully qualified domain name of this server. server_fqdn, // Permits multiple user realms on server. NULL == use default. nullptr, // Local and remote IP address strings. We don't use any mechanisms // which need these. nullptr, nullptr, // Connection-specific callbacks. &callbacks_[0], // Security flags. secflags, &sasl_conn); }, kDesc), string(kDesc) + " failed"); sasl_conn_.reset(sasl_conn); return Status::OK(); } Status ServerNegotiation::HandleNegotiate(const NegotiatePB& request) { if (request.step() != NegotiatePB::NEGOTIATE) { Status s = Status::NotAuthorized("expected NEGOTIATE step", NegotiatePB::NegotiateStep_Name(request.step())); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } TRACE("Received NEGOTIATE request from client"); // Fill in the set of features supported by the client. for (int flag : request.supported_features()) { // We only add the features that our local build knows about. RpcFeatureFlag feature_flag = RpcFeatureFlag_IsValid(flag) ? static_cast<RpcFeatureFlag>(flag) : UNKNOWN; if (feature_flag != UNKNOWN) { client_features_.insert(feature_flag); } } if (encryption_ == RpcEncryption::REQUIRED && !ContainsKey(client_features_, RpcFeatureFlag::TLS)) { Status s = Status::NotAuthorized("client does not support required TLS encryption"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } // Find the set of mutually supported authentication types. set<AuthenticationType> authn_types; if (request.authn_types().empty()) { // If the client doesn't send any support authentication types, we assume // support for SASL. This preserves backwards compatibility with clients who // don't support security features. authn_types.insert(AuthenticationType::SASL); } else { for (const auto& type : request.authn_types()) { switch (type.type_case()) { case AuthenticationTypePB::kSasl: authn_types.insert(AuthenticationType::SASL); break; case AuthenticationTypePB::kToken: authn_types.insert(AuthenticationType::TOKEN); break; case AuthenticationTypePB::kCertificate: // We only provide authenticated TLS if the certificates are generated // by the internal CA. if (!tls_context_->is_external_cert()) { authn_types.insert(AuthenticationType::CERTIFICATE); } break; case AuthenticationTypePB::TYPE_NOT_SET: { Sockaddr addr; RETURN_NOT_OK(socket_->GetPeerAddress(&addr)); KLOG_EVERY_N_SECS(WARNING, 60) << "client supports unknown authentication type, consider updating server, address: " << addr.ToString(); break; } } } if (authn_types.empty()) { Status s = Status::NotSupported("no mutually supported authentication types"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } } if (encryption_ != RpcEncryption::DISABLED && ContainsKey(authn_types, AuthenticationType::CERTIFICATE) && tls_context_->has_signed_cert()) { // If the client supports it and we are locally configured with TLS and have // a CA-signed cert, choose cert authn. // TODO(KUDU-1924): consider adding the fingerprint of the CA cert which signed // the client's cert to the authentication message. negotiated_authn_ = AuthenticationType::CERTIFICATE; } else if (ContainsKey(authn_types, AuthenticationType::TOKEN) && token_verifier_->GetMaxKnownKeySequenceNumber() >= 0 && encryption_ != RpcEncryption::DISABLED && tls_context_->has_signed_cert()) { // If the client supports it, we have a TSK to verify the client's token, // and we have a signed-cert so the client can verify us, choose token authn. // TODO(KUDU-1924): consider adding the TSK sequence number to the authentication // message. negotiated_authn_ = AuthenticationType::TOKEN; } else { // Otherwise we always can fallback to SASL. DCHECK(ContainsKey(authn_types, AuthenticationType::SASL)); negotiated_authn_ = AuthenticationType::SASL; } // Fill in the NEGOTIATE step response for the client. NegotiatePB response; response.set_step(NegotiatePB::NEGOTIATE); // Tell the client which features we support. server_features_ = kSupportedServerRpcFeatureFlags; if (tls_context_->has_cert() && encryption_ != RpcEncryption::DISABLED) { server_features_.insert(TLS); // If the remote peer is local, then we allow using TLS for authentication // without encryption or integrity. if (socket_->IsLoopbackConnection() && !encrypt_loopback_) { server_features_.insert(TLS_AUTHENTICATION_ONLY); } } for (RpcFeatureFlag feature : server_features_) { response.add_supported_features(feature); } switch (negotiated_authn_) { case AuthenticationType::CERTIFICATE: response.add_authn_types()->mutable_certificate(); break; case AuthenticationType::TOKEN: response.add_authn_types()->mutable_token(); break; case AuthenticationType::SASL: { response.add_authn_types()->mutable_sasl(); const set<SaslMechanism::Type>& server_mechs = helper_.EnabledMechs(); if (PREDICT_FALSE(server_mechs.empty())) { // This will happen if no mechanisms are enabled before calling Init() Status s = Status::NotAuthorized("SASL server mechanism list is empty!"); LOG(ERROR) << s.ToString(); TRACE("Sending FATAL_UNAUTHORIZED response to client"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } for (auto mechanism : server_mechs) { response.add_sasl_mechanisms()->set_mechanism(SaslMechanism::name_of(mechanism)); } break; } case AuthenticationType::INVALID: LOG(FATAL) << "unreachable"; } return SendNegotiatePB(response); } Status ServerNegotiation::HandleTlsHandshake(const NegotiatePB& request) { if (PREDICT_FALSE(request.step() != NegotiatePB::TLS_HANDSHAKE)) { Status s = Status::NotAuthorized("expected TLS_HANDSHAKE step", NegotiatePB::NegotiateStep_Name(request.step())); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } if (PREDICT_FALSE(!request.has_tls_handshake())) { Status s = Status::NotAuthorized( "No TLS handshake token in TLS_HANDSHAKE request from client"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } string token; const Status s = tls_handshake_.Continue(request.tls_handshake(), &token); if (PREDICT_FALSE(!s.IsIncomplete() && !s.ok())) { RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } const bool needs_extra_step = tls_handshake_.NeedsExtraStep(s, token); if (needs_extra_step) { RETURN_NOT_OK(SendTlsHandshake(std::move(token))); } // Check that the handshake step didn't produce an error. It also propagates // any non-OK status. RETURN_NOT_OK(s); if (!needs_extra_step && !token.empty()) { DCHECK(s.ok()); DCHECK(!token.empty()); tls_handshake_.StorePendingData(std::move(token)); } // TLS handshake is finished. if (ContainsKey(server_features_, TLS_AUTHENTICATION_ONLY) && ContainsKey(client_features_, TLS_AUTHENTICATION_ONLY)) { TRACE("Negotiated auth-only $0 with cipher $1", tls_handshake_.GetProtocol(), tls_handshake_.GetCipherDescription()); return tls_handshake_.FinishNoWrap(*socket_); } TRACE("Negotiated $0 with cipher $1", tls_handshake_.GetProtocol(), tls_handshake_.GetCipherDescription()); return tls_handshake_.Finish(&socket_); } Status ServerNegotiation::SendTlsHandshake(string tls_token) { NegotiatePB msg; msg.set_step(NegotiatePB::TLS_HANDSHAKE); msg.mutable_tls_handshake()->swap(tls_token); return SendNegotiatePB(msg); } Status ServerNegotiation::AuthenticateBySasl(faststring* recv_buf) { RETURN_NOT_OK(InitSaslServer()); NegotiatePB request; RETURN_NOT_OK(RecvNegotiatePB(&request, recv_buf)); Status s = HandleSaslInitiate(request); while (s.IsIncomplete()) { RETURN_NOT_OK(RecvNegotiatePB(&request, recv_buf)); s = HandleSaslResponse(request); } RETURN_NOT_OK(s); const char* c_username = nullptr; int rc = sasl_getprop(sasl_conn_.get(), SASL_USERNAME, reinterpret_cast<const void**>(&c_username)); // We expect that SASL_USERNAME will always get set. CHECK(rc == SASL_OK && c_username != nullptr) << "No username on authenticated connection"; if (negotiated_mech_ == SaslMechanism::GSSAPI) { // The SASL library doesn't include the user's realm in the username if it's the // same realm as the default realm of the server. So, we pass it back through the // Kerberos library to add back the realm if necessary. string principal = c_username; RETURN_NOT_OK_PREPEND(security::CanonicalizeKrb5Principal(&principal), "could not canonicalize krb5 principal"); // Map the principal to the corresponding local username. For example, admins // can set up mappings so that joe@REMOTEREALM becomes something like 'remote-joe' // locally for the purposes of group mapping, ACLs, etc. string local_name; RETURN_NOT_OK_PREPEND(security::MapPrincipalToLocalName(principal, &local_name), strings::Substitute("could not map krb5 principal '$0' to username", principal)); authenticated_user_.SetAuthenticatedByKerberos(std::move(local_name), std::move(principal)); } else { authenticated_user_.SetUnauthenticated(c_username); } return Status::OK(); } Status ServerNegotiation::AuthenticateByToken(faststring* recv_buf) { // Sanity check that TLS has been negotiated. Receiving the token on an // unencrypted channel is a big no-no. CHECK(tls_negotiated_); // Receive the token from the client. NegotiatePB pb; RETURN_NOT_OK(RecvNegotiatePB(&pb, recv_buf)); if (pb.step() != NegotiatePB::TOKEN_EXCHANGE) { Status s = Status::NotAuthorized("expected TOKEN_EXCHANGE step", NegotiatePB::NegotiateStep_Name(pb.step())); } if (!pb.has_authn_token()) { Status s = Status::NotAuthorized("TOKEN_EXCHANGE message must include an authentication token"); } // TODO(KUDU-1924): propagate the specific token verification failure back to the client, // so it knows how to intelligently retry. security::TokenPB token; auto verification_result = token_verifier_->VerifyTokenSignature(pb.authn_token(), &token); ErrorStatusPB::RpcErrorCodePB error; Status s = ParseTokenVerificationResult(verification_result, ErrorStatusPB::FATAL_INVALID_AUTHENTICATION_TOKEN, &error); if (!s.ok()) { RETURN_NOT_OK(SendError(error, s)); return s; } if (!token.has_authn()) { Status s = Status::NotAuthorized("non-authentication token presented for authentication"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } if (!token.authn().has_username()) { // This is a runtime error because there should be no way a client could // get a signed authn token without a subject. Status s = Status::RuntimeError("authentication token has no username"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_INVALID_AUTHENTICATION_TOKEN, s)); return s; } if (PREDICT_FALSE(FLAGS_rpc_inject_invalid_authn_token_ratio > 0)) { security::TokenVerificationResult res; int sel = rand() % 4; switch (sel) { case 0: res = security::TokenVerificationResult::INVALID_TOKEN; break; case 1: res = security::TokenVerificationResult::INVALID_SIGNATURE; break; case 2: res = security::TokenVerificationResult::EXPIRED_TOKEN; break; case 3: res = security::TokenVerificationResult::EXPIRED_SIGNING_KEY; break; default: LOG(FATAL) << "unreachable"; } if (kudu::fault_injection::MaybeTrue(FLAGS_rpc_inject_invalid_authn_token_ratio)) { Status s = Status::NotAuthorized(TokenVerificationResultToString(res)); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_INVALID_AUTHENTICATION_TOKEN, s)); return s; } } authenticated_user_.SetAuthenticatedByToken(token.authn().username()); // Respond with success message. pb.Clear(); pb.set_step(NegotiatePB::TOKEN_EXCHANGE); return SendNegotiatePB(pb); } Status ServerNegotiation::AuthenticateByCertificate() { // Sanity check that TLS has been negotiated. Cert-based authentication is // only possible with TLS. CHECK(tls_negotiated_); // Grab the subject from the client's cert. security::Cert cert; RETURN_NOT_OK(tls_handshake_.GetRemoteCert(&cert)); boost::optional<string> user_id = cert.UserId(); boost::optional<string> principal = cert.KuduKerberosPrincipal(); if (!user_id) { Status s = Status::NotAuthorized("did not find expected X509 userId extension in cert"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_INVALID_AUTHENTICATION_TOKEN, s)); return s; } authenticated_user_.SetAuthenticatedByClientCert(*user_id, std::move(principal)); return Status::OK(); } Status ServerNegotiation::HandleSaslInitiate(const NegotiatePB& request) { if (PREDICT_FALSE(request.step() != NegotiatePB::SASL_INITIATE)) { Status s = Status::NotAuthorized("expected SASL_INITIATE step", NegotiatePB::NegotiateStep_Name(request.step())); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } TRACE("Received SASL_INITIATE request from client"); if (request.sasl_mechanisms_size() != 1) { Status s = Status::NotAuthorized( "SASL_INITIATE request must include exactly one SASL mechanism, found", std::to_string(request.sasl_mechanisms_size())); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } const string& mechanism = request.sasl_mechanisms(0).mechanism(); TRACE("Client requested to use mechanism: $0", mechanism); negotiated_mech_ = SaslMechanism::value_of(mechanism); // Rejects any connection from public routable IPs if authentication mechanism // is plain. See KUDU-1875. if (negotiated_mech_ == SaslMechanism::PLAIN) { Sockaddr addr; RETURN_NOT_OK(socket_->GetPeerAddress(&addr)); if (!IsTrustedConnection(addr)) { Status s = Status::NotAuthorized("unauthenticated connections from publicly " "routable IPs are prohibited. See " "--trusted_subnets flag for more information.", addr.ToString()); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } } // If the negotiated mechanism is GSSAPI (Kerberos), configure SASL to use // integrity protection so that the channel bindings and nonce can be // verified. if (negotiated_mech_ == SaslMechanism::GSSAPI) { RETURN_NOT_OK(EnableProtection(sasl_conn_.get(), SaslProtection::kIntegrity)); } const char* server_out = nullptr; uint32_t server_out_len = 0; static constexpr const char* const kDesc = "calling sasl_server_start()"; TRACE(kDesc); Status s = WrapSaslCall(sasl_conn_.get(), [&]() { return sasl_server_start( sasl_conn_.get(), // The SASL connection context created by init() mechanism.c_str(), // The mechanism requested by the client. request.token().c_str(), // Optional string the client gave us. request.token().length(), // Client string len. &server_out, // The output of the SASL library, might not be NULL terminated &server_out_len); // Output len. }, kDesc); if (PREDICT_FALSE(!s.ok() && !s.IsIncomplete())) { RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } // We have a valid mechanism match if (s.ok()) { DCHECK(server_out_len == 0); RETURN_NOT_OK(SendSaslSuccess()); } else { // s.IsIncomplete() (equivalent to SASL_CONTINUE) RETURN_NOT_OK(SendSaslChallenge(server_out, server_out_len)); } return s; } Status ServerNegotiation::HandleSaslResponse(const NegotiatePB& request) { if (PREDICT_FALSE(request.step() != NegotiatePB::SASL_RESPONSE)) { Status s = Status::NotAuthorized("expected SASL_RESPONSE step", NegotiatePB::NegotiateStep_Name(request.step())); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } TRACE("Received SASL_RESPONSE request from client"); if (!request.has_token()) { Status s = Status::NotAuthorized("no token in SASL_RESPONSE from client"); RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } static constexpr const char* const kDesc = "calling sasl_server_step()"; const char* server_out = nullptr; uint32_t server_out_len = 0; TRACE(kDesc); Status s = WrapSaslCall(sasl_conn_.get(), [&]() { return sasl_server_step( sasl_conn_.get(), // The SASL connection context created by init() request.token().c_str(), // Optional string the client gave us request.token().length(), // Client string len &server_out, // The output of the SASL library, might not be NULL terminated &server_out_len); // Output len }, kDesc); if (s.ok()) { DCHECK(server_out_len == 0); return SendSaslSuccess(); } if (s.IsIncomplete()) { return SendSaslChallenge(server_out, server_out_len); } RETURN_NOT_OK(SendError(ErrorStatusPB::FATAL_UNAUTHORIZED, s)); return s; } Status ServerNegotiation::SendSaslChallenge(const char* challenge, unsigned clen) { NegotiatePB response; response.set_step(NegotiatePB::SASL_CHALLENGE); response.mutable_token()->assign(challenge, clen); RETURN_NOT_OK(SendNegotiatePB(response)); return Status::Incomplete(""); } Status ServerNegotiation::SendSaslSuccess() { NegotiatePB response; response.set_step(NegotiatePB::SASL_SUCCESS); if (negotiated_mech_ == SaslMechanism::GSSAPI) { // Send a nonce to the client. nonce_ = string(); RETURN_NOT_OK(security::GenerateNonce(nonce_.get_ptr())); response.set_nonce(*nonce_); if (tls_negotiated_ && PREDICT_TRUE(FLAGS_rpc_send_channel_bindings)) { // Send the channel bindings to the client. security::Cert cert; RETURN_NOT_OK(tls_handshake_.GetLocalCert(&cert)); string plaintext_channel_bindings; RETURN_NOT_OK(cert.GetServerEndPointChannelBindings(&plaintext_channel_bindings)); if (kudu::fault_injection::MaybeTrue( FLAGS_rpc_inject_invalid_channel_bindings_ratio)) { DCHECK_GT(plaintext_channel_bindings.size(), 0); plaintext_channel_bindings[0] += 1; } Slice ciphertext; RETURN_NOT_OK(SaslEncode(sasl_conn_.get(), plaintext_channel_bindings, &ciphertext)); *response.mutable_channel_bindings() = ciphertext.ToString(); } } return SendNegotiatePB(response); } Status ServerNegotiation::RecvConnectionContext(faststring* recv_buf) { TRACE("Waiting for connection context"); RequestHeader header; Slice param_buf; RETURN_NOT_OK(ReceiveFramedMessageBlocking( socket_.get(), recv_buf, &header, &param_buf, deadline_)); DCHECK(header.IsInitialized()); if (header.call_id() != kConnectionContextCallId) { return Status::NotAuthorized("expected ConnectionContext callid, received", std::to_string(header.call_id())); } ConnectionContextPB conn_context; if (!conn_context.ParseFromArray(param_buf.data(), param_buf.size())) { return Status::NotAuthorized("invalid ConnectionContextPB message, missing fields", conn_context.InitializationErrorString()); } if (nonce_) { Status s; // Validate that the client returned the correct SASL protected nonce. if (!conn_context.has_encoded_nonce()) { return Status::NotAuthorized("ConnectionContextPB wrapped nonce missing"); } Slice decoded_nonce; s = SaslDecode(sasl_conn_.get(), conn_context.encoded_nonce(), &decoded_nonce); if (!s.ok()) { return Status::NotAuthorized("failed to decode nonce", s.message()); } if (*nonce_ != decoded_nonce) { Sockaddr addr; RETURN_NOT_OK(socket_->GetPeerAddress(&addr)); LOG(WARNING) << "Received an invalid connection nonce from client " << addr.ToString() << ", this could indicate a replay attack"; return Status::NotAuthorized("nonce mismatch"); } } return Status::OK(); } int ServerNegotiation::GetOptionCb(const char* plugin_name, const char* option, const char** result, unsigned* len) { return helper_.GetOptionCb(plugin_name, option, result, len); } int ServerNegotiation::PlainAuthCb(sasl_conn_t* /*conn*/, const char* user, const char* /*pass*/, unsigned /*passlen*/, struct propctx* /*propctx*/) { TRACE("Received PLAIN auth, user=$0", user); if (PREDICT_FALSE(!helper_.IsPlainEnabled())) { LOG(DFATAL) << "Password authentication callback called while PLAIN auth disabled"; return SASL_BADPARAM; } // We always allow PLAIN authentication to succeed. return SASL_OK; } bool ServerNegotiation::IsTrustedConnection(const Sockaddr& addr) { if (addr.family() == AF_UNIX) return true; static std::once_flag once; std::call_once(once, [] { g_trusted_subnets = new vector<Network>(); CHECK_OK(Network::ParseCIDRStrings(FLAGS_trusted_subnets, g_trusted_subnets)); // If --trusted_subnets is not set explicitly, local subnets of all local network // interfaces as well as the default private subnets will be used. if (google::GetCommandLineFlagInfoOrDie("trusted_subnets").is_default) { std::vector<Network> local_networks; WARN_NOT_OK(GetLocalNetworks(&local_networks), "Unable to get local networks."); g_trusted_subnets->insert(g_trusted_subnets->end(), local_networks.begin(), local_networks.end()); } }); return std::any_of(g_trusted_subnets->begin(), g_trusted_subnets->end(), [&](const Network& t) { return t.WithinNetwork(addr); }); } } // namespace rpc } // namespace kudu #if defined(__APPLE__) #pragma GCC diagnostic pop #endif // #if defined(__APPLE__)