]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: add ssl support to beast frontend
authorCasey Bodley <cbodley@redhat.com>
Tue, 6 Feb 2018 22:06:11 +0000 (17:06 -0500)
committerCasey Bodley <cbodley@redhat.com>
Thu, 5 Apr 2018 19:30:22 +0000 (15:30 -0400)
adds frontend options ssl_certificate, ssl_private_key, ssl_port, ssl_endpoint

Fixes: http://tracker.ceph.com/issues/22832
Signed-off-by: Casey Bodley <cbodley@redhat.com>
CMakeLists.txt
doc/radosgw/frontends.rst
src/include/config-h.in.cmake
src/rgw/CMakeLists.txt
src/rgw/rgw_asio_client.cc
src/rgw/rgw_asio_client.h
src/rgw/rgw_asio_frontend.cc

index addddbce489584bbb8bea2e3b9eae44c2084e441..34c5796a47dc931ccd4f17af16f4dc675cd48559 100644 (file)
@@ -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")
index 3973a5ded0fdbfe4863107ccb220732b45d0f558..71532675553a4d4f3feac5280d7fa290731dc907 100644 (file)
@@ -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
 ========
 
index b63b3b8439416258ee4ddbd7bb2a5ad9ca79d058..d621ac0e0173277c07d0b6728996f071da2960ea 100644 (file)
 /* 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 */
index c435bdcc306a70022f216063ef76c29f0c599e14..e28fa309f450fb0942e1ad666eafc83cc6fed996 100644 (file)
@@ -183,7 +183,9 @@ endif (WITH_RADOSGW_BEAST_FRONTEND)
 
 add_library(radosgw_a STATIC ${radosgw_srcs}
   $<TARGET_OBJECTS:civetweb_common_objs>)
-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)
index 29db397df56fc4c6035cbb1528a12c0f181f1bc4..c226173bba7695eedfb28f211f66cc89df625af4 100644 (file)
 
 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;
 }
index 2dac89b41775f2637b681b6a1d8ed463709dbc84..5a9957b46c6c7254fbc6e76600cfc5b599fca265 100644 (file)
@@ -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;
index 73c6fe2b497ba6443eee22257bcb8beaeea81024..b69ec795e695684f958ef61f9bd945ce2ec1c682 100644 (file)
 #include "rgw_asio_client.h"
 #include "rgw_asio_frontend.h"
 
+#ifdef WITH_RADOSGW_BEAST_OPENSSL
+#include <boost/asio/ssl.hpp>
+#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 <typename Stream>
 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 <typename Stream>
+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<size_t>::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> 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<tcp::socket&> 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()