From 247e074338c15e0a8c2596aa687597bcce76b2f6 Mon Sep 17 00:00:00 2001 From: Casey Bodley Date: Tue, 6 Feb 2018 17:06:11 -0500 Subject: [PATCH] rgw: add ssl support to beast frontend adds frontend options ssl_certificate, ssl_private_key, ssl_port, ssl_endpoint Fixes: http://tracker.ceph.com/issues/22832 Signed-off-by: Casey Bodley --- CMakeLists.txt | 13 ++- doc/radosgw/frontends.rst | 22 ++++- src/include/config-h.in.cmake | 3 + src/rgw/CMakeLists.txt | 8 +- src/rgw/rgw_asio_client.cc | 8 +- src/rgw/rgw_asio_client.h | 3 +- src/rgw/rgw_asio_frontend.cc | 176 ++++++++++++++++++++++++++++++---- 7 files changed, 202 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index addddbce48958..34c5796a47dc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -384,6 +384,8 @@ endif() option(WITH_RADOSGW "Rados Gateway is enabled" ON) option(WITH_RADOSGW_FCGI_FRONTEND "Rados Gateway's FCGI frontend is enabled" OFF) option(WITH_RADOSGW_BEAST_FRONTEND "Rados Gateway's Beast frontend is enabled" ON) +option(WITH_RADOSGW_BEAST_OPENSSL "Rados Gateway's Beast frontend uses OpenSSL" ON) + if(WITH_RADOSGW) find_package(EXPAT REQUIRED) if(WITH_RADOSGW_FCGI_FRONTEND) @@ -393,10 +395,7 @@ if(WITH_RADOSGW) message(WARNING "disabling WITH_RADOSGW_BEAST_FRONTEND, which depends on WITH_BOOST_CONTEXT") set(WITH_RADOSGW_BEAST_FRONTEND OFF) endif() -endif(WITH_RADOSGW) - -if (WITH_RADOSGW) # https://curl.haxx.se/docs/install.html mentions the # configure flags for various ssl backends execute_process( @@ -409,7 +408,13 @@ if (WITH_RADOSGW) if (CURL_CONFIG_ERRORS) message(WARNING "unable to run curl-config; rgw cannot make ssl requests to external systems reliably") endif() - find_package(OpenSSL) + + if (WITH_RADOSGW_BEAST_FRONTEND AND WITH_RADOSGW_BEAST_OPENSSL) + find_package(OpenSSL REQUIRED) + else() + find_package(OpenSSL) + endif() + if (OPENSSL_FOUND) if (NOT NO_CURL_SSL_LINK) message(STATUS "libcurl is linked with openssl: explicitly setting locks") diff --git a/doc/radosgw/frontends.rst b/doc/radosgw/frontends.rst index 3973a5ded0fdb..71532675553a4 100644 --- a/doc/radosgw/frontends.rst +++ b/doc/radosgw/frontends.rst @@ -18,7 +18,7 @@ and the Boost.Asio library for asynchronous network i/o. Options ------- -``port`` +``port`` and ``ssl_port`` :Description: Sets the listening port number. Can be specified multiple times as in ``port=80 port=8000``. @@ -27,7 +27,7 @@ Options :Default: ``80`` -``endpoint`` +``endpoint`` and ``ssl_endpoint`` :Description: Sets the listening address in the form ``address[:port]``, where the address is an IPv4 address string in dotted decimal @@ -39,6 +39,24 @@ Options :Default: None +``ssl_certificate`` + +:Description: Path to the SSL certificate file used for SSL-enabled endpoints. + +:Type: String +:Default: None + + +``ssl_private_key`` + +:Description: Optional path to the private key file used for SSL-enabled + endpoints. If one is not given, the ``ssl_certificate`` file + is used as the private key. + +:Type: String +:Default: None + + Civetweb ======== diff --git a/src/include/config-h.in.cmake b/src/include/config-h.in.cmake index b63b3b8439416..d621ac0e01732 100644 --- a/src/include/config-h.in.cmake +++ b/src/include/config-h.in.cmake @@ -336,4 +336,7 @@ /* Defined if boost::context is available */ #cmakedefine HAVE_BOOST_CONTEXT +/* Defined if OpenSSL is available for the rgw beast frontend */ +#cmakedefine WITH_RADOSGW_BEAST_OPENSSL + #endif /* CONFIG_H */ diff --git a/src/rgw/CMakeLists.txt b/src/rgw/CMakeLists.txt index c435bdcc306a7..e28fa309f450f 100644 --- a/src/rgw/CMakeLists.txt +++ b/src/rgw/CMakeLists.txt @@ -183,7 +183,9 @@ endif (WITH_RADOSGW_BEAST_FRONTEND) add_library(radosgw_a STATIC ${radosgw_srcs} $) -target_link_libraries(radosgw_a rgw_a ${SSL_LIBRARIES}) +if (WITH_RADOSGW_BEAST_FRONTEND AND WITH_RADOSGW_BEAST_OPENSSL) + target_link_libraries(radosgw_a rgw_a ${SSL_LIBRARIES}) +endif() add_executable(radosgw rgw_main.cc) target_link_libraries(radosgw radosgw_a librados @@ -199,6 +201,10 @@ add_dependencies(radosgw cls_rgw cls_lock cls_refcount cls_version cls_replica_log cls_user) install(TARGETS radosgw DESTINATION bin) +if (WITH_RADOSGW_BEAST_FRONTEND) + target_link_libraries(radosgw_a ${OPENSSL_LIBRARIES}) +endif() + set(radosgw_admin_srcs rgw_admin.cc rgw_orphan.cc) diff --git a/src/rgw/rgw_asio_client.cc b/src/rgw/rgw_asio_client.cc index 29db397df56fc..c226173bba769 100644 --- a/src/rgw/rgw_asio_client.cc +++ b/src/rgw/rgw_asio_client.cc @@ -11,10 +11,10 @@ using namespace rgw::asio; -ClientIO::ClientIO(parser_type& parser, +ClientIO::ClientIO(parser_type& parser, bool is_ssl, const endpoint_type& local_endpoint, const endpoint_type& remote_endpoint) - : parser(parser), + : parser(parser), is_ssl(is_ssl), local_endpoint(local_endpoint), remote_endpoint(remote_endpoint), txbuf(*this) @@ -82,8 +82,10 @@ int ClientIO::init_env(CephContext *cct) char port_buf[16]; snprintf(port_buf, sizeof(port_buf), "%d", local_endpoint.port()); env.set("SERVER_PORT", port_buf); + if (is_ssl) { + env.set("SERVER_PORT_SECURE", port_buf); + } env.set("REMOTE_ADDR", remote_endpoint.address().to_string()); - // TODO: set SERVER_PORT_SECURE if using ssl // TODO: set REMOTE_USER if authenticated return 0; } diff --git a/src/rgw/rgw_asio_client.h b/src/rgw/rgw_asio_client.h index 2dac89b41775f..5a9957b46c6c7 100644 --- a/src/rgw/rgw_asio_client.h +++ b/src/rgw/rgw_asio_client.h @@ -21,6 +21,7 @@ class ClientIO : public io::RestfulClient, protected: parser_type& parser; private: + const bool is_ssl; using endpoint_type = boost::asio::ip::tcp::endpoint; endpoint_type local_endpoint; endpoint_type remote_endpoint; @@ -30,7 +31,7 @@ class ClientIO : public io::RestfulClient, rgw::io::StaticOutputBufferer<> txbuf; public: - ClientIO(parser_type& parser, + ClientIO(parser_type& parser, bool is_ssl, const endpoint_type& local_endpoint, const endpoint_type& remote_endpoint); ~ClientIO() override; diff --git a/src/rgw/rgw_asio_frontend.cc b/src/rgw/rgw_asio_frontend.cc index 73c6fe2b497ba..b69ec795e6956 100644 --- a/src/rgw/rgw_asio_frontend.cc +++ b/src/rgw/rgw_asio_frontend.cc @@ -13,6 +13,10 @@ #include "rgw_asio_client.h" #include "rgw_asio_frontend.h" +#ifdef WITH_RADOSGW_BEAST_OPENSSL +#include +#endif + #define dout_subsys ceph_subsys_rgw namespace { @@ -62,16 +66,20 @@ void Pauser::wait() using tcp = boost::asio::ip::tcp; namespace beast = boost::beast; +#ifdef WITH_RADOSGW_BEAST_OPENSSL +namespace ssl = boost::asio::ssl; +#endif +template class StreamIO : public rgw::asio::ClientIO { - tcp::socket& stream; + Stream& stream; beast::flat_buffer& buffer; public: - StreamIO(tcp::socket& stream, rgw::asio::parser_type& parser, - beast::flat_buffer& buffer, + StreamIO(Stream& stream, rgw::asio::parser_type& parser, + beast::flat_buffer& buffer, bool is_ssl, const tcp::endpoint& local_endpoint, const tcp::endpoint& remote_endpoint) - : ClientIO(parser, local_endpoint, remote_endpoint), + : ClientIO(parser, is_ssl, local_endpoint, remote_endpoint), stream(stream), buffer(buffer) {} @@ -107,7 +115,10 @@ class StreamIO : public rgw::asio::ClientIO { } }; -void handle_connection(RGWProcessEnv& env, tcp::socket& socket, +template +void handle_connection(RGWProcessEnv& env, Stream& stream, + beast::flat_buffer& buffer, bool is_ssl, + boost::system::error_code& ec, boost::asio::yield_context yield) { // limit header to 4k, since we read it all into a single flat_buffer @@ -116,10 +127,8 @@ void handle_connection(RGWProcessEnv& env, tcp::socket& socket, static constexpr size_t body_limit = std::numeric_limits::max(); auto cct = env.store->ctx(); - boost::system::error_code ec; - beast::flat_buffer buffer; - // read messages from the socket until eof + // read messages from the stream until eof for (;;) { // configure the parser rgw::asio::parser_type parser; @@ -127,8 +136,11 @@ void handle_connection(RGWProcessEnv& env, tcp::socket& socket, parser.body_limit(body_limit); // parse the header - beast::http::async_read_header(socket, buffer, parser, yield[ec]); + beast::http::async_read_header(stream, buffer, parser, yield[ec]); if (ec == boost::asio::error::connection_reset || +#ifdef WITH_RADOSGW_BEAST_OPENSSL + ec == ssl::error::stream_truncated || +#endif ec == beast::http::error::end_of_stream) { return; } @@ -139,7 +151,7 @@ void handle_connection(RGWProcessEnv& env, tcp::socket& socket, response.result(beast::http::status::bad_request); response.version(message.version() == 10 ? 10 : 11); response.prepare_payload(); - beast::http::async_write(socket, response, yield[ec]); + beast::http::async_write(stream, response, yield[ec]); if (ec) { ldout(cct, 5) << "failed to write response: " << ec.message() << dendl; } @@ -150,7 +162,8 @@ void handle_connection(RGWProcessEnv& env, tcp::socket& socket, // process the request RGWRequest req{env.store->get_new_req_id()}; - StreamIO real_client{socket, parser, buffer, + auto& socket = stream.lowest_layer(); + StreamIO real_client{stream, parser, buffer, is_ssl, socket.local_endpoint(), socket.remote_endpoint()}; @@ -176,7 +189,7 @@ void handle_connection(RGWProcessEnv& env, tcp::socket& socket, body.size = discard_buffer.size(); body.data = discard_buffer.data(); - beast::http::async_read_some(socket, buffer, parser, yield[ec]); + beast::http::async_read_some(stream, buffer, parser, yield[ec]); if (ec == boost::asio::error::connection_reset) { return; } @@ -193,11 +206,16 @@ class AsioFrontend { RGWProcessEnv env; RGWFrontendConfig* conf; boost::asio::io_service service; +#ifdef WITH_RADOSGW_BEAST_OPENSSL + boost::optional ssl_context; + int init_ssl(); +#endif struct Listener { tcp::endpoint endpoint; tcp::acceptor acceptor; tcp::socket socket; + bool use_ssl = false; Listener(boost::asio::io_service& service) : acceptor(service), socket(service) {} @@ -260,9 +278,16 @@ int AsioFrontend::init() boost::system::error_code ec; auto& config = conf->get_config_map(); +#ifdef WITH_RADOSGW_BEAST_OPENSSL + int r = init_ssl(); + if (r < 0) { + return r; + } +#endif + // parse endpoints - auto range = config.equal_range("port"); - for (auto i = range.first; i != range.second; ++i) { + auto ports = config.equal_range("port"); + for (auto i = ports.first; i != ports.second; ++i) { auto port = parse_port(i->second.c_str(), ec); if (ec) { lderr(ctx()) << "failed to parse port=" << i->second << dendl; @@ -272,8 +297,8 @@ int AsioFrontend::init() listeners.back().endpoint.port(port); } - range = config.equal_range("endpoint"); - for (auto i = range.first; i != range.second; ++i) { + auto endpoints = config.equal_range("endpoint"); + for (auto i = endpoints.first; i != endpoints.second; ++i) { auto endpoint = parse_endpoint(i->second, ec); if (ec) { lderr(ctx()) << "failed to parse endpoint=" << i->second << dendl; @@ -308,6 +333,88 @@ int AsioFrontend::init() return 0; } +#ifdef WITH_RADOSGW_BEAST_OPENSSL +int AsioFrontend::init_ssl() +{ + boost::system::error_code ec; + auto& config = conf->get_config_map(); + + // ssl configuration + auto cert = config.find("ssl_certificate"); + const bool have_cert = cert != config.end(); + if (have_cert) { + // only initialize the ssl context if it's going to be used + ssl_context = boost::in_place(ssl::context::tls); + } + + auto key = config.find("ssl_private_key"); + const bool have_private_key = key != config.end(); + if (have_private_key) { + if (!have_cert) { + lderr(ctx()) << "no ssl_certificate configured for ssl_private_key" << dendl; + return -EINVAL; + } + ssl_context->use_private_key_file(key->second, ssl::context::pem, ec); + if (ec) { + lderr(ctx()) << "failed to add ssl_private_key=" << key->second + << ": " << ec.message() << dendl; + return -ec.value(); + } + } + if (have_cert) { + ssl_context->use_certificate_chain_file(cert->second, ec); + if (ec) { + lderr(ctx()) << "failed to use ssl_certificate=" << cert->second + << ": " << ec.message() << dendl; + return -ec.value(); + } + if (!have_private_key) { + // attempt to use it as a private key if a separate one wasn't provided + ssl_context->use_private_key_file(cert->second, ssl::context::pem, ec); + if (ec) { + lderr(ctx()) << "failed to use ssl_certificate=" << cert->second + << " as a private key: " << ec.message() << dendl; + return -ec.value(); + } + } + } + + // parse ssl endpoints + auto ports = config.equal_range("ssl_port"); + for (auto i = ports.first; i != ports.second; ++i) { + if (!have_cert) { + lderr(ctx()) << "no ssl_certificate configured for ssl_port" << dendl; + return -EINVAL; + } + auto port = parse_port(i->second.c_str(), ec); + if (ec) { + lderr(ctx()) << "failed to parse ssl_port=" << i->second << dendl; + return -ec.value(); + } + listeners.emplace_back(service); + listeners.back().endpoint.port(port); + listeners.back().use_ssl = true; + } + + auto endpoints = config.equal_range("ssl_endpoint"); + for (auto i = endpoints.first; i != endpoints.second; ++i) { + if (!have_cert) { + lderr(ctx()) << "no ssl_certificate configured for ssl_endpoint" << dendl; + return -EINVAL; + } + auto endpoint = parse_endpoint(i->second, ec); + if (ec) { + lderr(ctx()) << "failed to parse ssl_endpoint=" << i->second << dendl; + return -ec.value(); + } + listeners.emplace_back(service); + listeners.back().endpoint = endpoint; + listeners.back().use_ssl = true; + } + return 0; +} +#endif // WITH_RADOSGW_BEAST_OPENSSL + void AsioFrontend::accept(Listener& l, boost::system::error_code ec) { if (!l.acceptor.is_open()) { @@ -324,10 +431,39 @@ void AsioFrontend::accept(Listener& l, boost::system::error_code ec) }); // spawn a coroutine to handle the connection - boost::asio::spawn(service, - [this, socket=std::move(socket)] (boost::asio::yield_context yield) mutable { - handle_connection(env, socket, yield); - }); +#ifdef WITH_RADOSGW_BEAST_OPENSSL + if (l.use_ssl) { + boost::asio::spawn(service, + [this, s=std::move(socket)] (boost::asio::yield_context yield) mutable { + // wrap the socket in an ssl stream + ssl::stream stream{s, *ssl_context}; + beast::flat_buffer buffer; + // do ssl handshake + boost::system::error_code ec; + auto bytes = stream.async_handshake(ssl::stream_base::server, + buffer.data(), yield[ec]); + if (ec) { + ldout(ctx(), 1) << "ssl handshake failed: " << ec.message() << dendl; + return; + } + buffer.consume(bytes); + handle_connection(env, stream, buffer, true, ec, yield); + if (!ec) { + // ssl shutdown (ignoring errors) + stream.async_shutdown(yield[ec]); + } + }); + } else { +#else + { +#endif // WITH_RADOSGW_BEAST_OPENSSL + boost::asio::spawn(service, + [this, s=std::move(socket)] (boost::asio::yield_context yield) mutable { + beast::flat_buffer buffer; + boost::system::error_code ec; + handle_connection(env, s, buffer, false, ec, yield); + }); + } } int AsioFrontend::run() -- 2.39.5