Skip to content

Commit

Permalink
Basic implementation of TLS for memcached.
Browse files Browse the repository at this point in the history
Most of the work done by Tharanga. Some commits squashed in by
dormando. Also reviewed by dormando.

Tested, working, but experimental implementation of TLS for memcached.

Enable with ./configure --enable-tls

Requires OpenSSL 1.1.0 or better.

See `memcached -h` output for usage.
  • Loading branch information
tharanga authored and dormando committed Apr 16, 2019
1 parent d2dcfff commit ee1cfe3
Show file tree
Hide file tree
Showing 30 changed files with 1,630 additions and 91 deletions.
11 changes: 7 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
sudo: required
dist: trusty
dist: xenial
language: cpp

compiler:
- gcc

install:
- sudo apt-get update -y
- sudo apt-get install -y build-essential automake1.11 autoconf libevent-dev libseccomp-dev git
- sudo apt-get install -y build-essential automake1.11 autoconf libevent-dev libseccomp-dev git tar wget libio-socket-ssl-perl
- wget https://www.openssl.org/source/openssl-1.1.0g.tar.gz && tar xzvf openssl-1.1.0g.tar.gz
- cd openssl-1.1.0g && ./config -Wl,--enable-new-dtags,-rpath,'$(LIBRPATH)' && make && sudo make install && openssl version && cd ../
- rm -rf openssl-1.1.0g && rm openssl-1.1.0g.tar.gz*

script:
- ./autogen.sh
- ./configure --enable-seccomp
- ./configure --enable-seccomp --enable-tls
- make -j
- make test

- make test_tls
24 changes: 24 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ memcached_SOURCES += extstore.c extstore.h \
slab_automove_extstore.c slab_automove_extstore.h
endif

if ENABLE_TLS
memcached_SOURCES += tls.c tls.h
endif

memcached_debug_SOURCES = $(memcached_SOURCES)
memcached_CPPFLAGS = -DNDEBUG
memcached_debug_LDADD = @PROFILER_LDFLAGS@
Expand Down Expand Up @@ -102,9 +106,29 @@ EXTRA_DIST = doc scripts t memcached.spec memcached_dtrace.d version.m4 README.m

MOSTLYCLEANFILES = *.gcov *.gcno *.gcda *.tcov

if ENABLE_TLS
test_tls:
$(MAKE) SSL_TEST=1 test

test_basic_tls:
@if test $(SSL_TEST)1 != 1; then \
echo "Running basic tests with TLS"; \
$(srcdir)/testapp; \
prove $(srcdir)/t/binary.t $(srcdir)/t/getset.t $(srcdir)/t/ssl*; \
echo "Finished running basic TLS tests"; \
else \
echo "Set SSL_TEST=1 to enable TLS tests"; \
fi
endif

test: memcached-debug sizes testapp
$(srcdir)/sizes
$(srcdir)/testapp
if ENABLE_TLS
@if test $(SSL_TEST)1 = 1; then \
$(MAKE) SSL_TEST=1 test_basic_tls; \
fi
endif
@if test -n "${PARALLEL}"; then \
prove $(srcdir)/t -j ${PARALLEL}; \
else \
Expand Down
97 changes: 97 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ AC_ARG_ENABLE(sasl_pwdb,
AS_IF([test "x$enable_sasl_pwdb" = "xyes"],
[enable_sasl=yes ])

AC_ARG_ENABLE(tls,
[AS_HELP_STRING([--enable-tls], [Enable Transport Layer Security EXPERIMENTAL ])])


dnl **********************************************************************
Expand Down Expand Up @@ -190,6 +192,10 @@ if test "x$enable_extstore" = "xyes"; then
AC_DEFINE([EXTSTORE],1,[Set to nonzero if you want to enable extstore])
fi

if test "x$enable_tls" = "xyes"; then
AC_DEFINE([TLS],1,[Set to nonzero if you want to enable TLS])
fi

if test "x$enable_arm_crc32" = "xyes"; then
AC_DEFINE([ARM_CRC32],1,[Set to nonzero if you want to enable ARMv8 crc32])
fi
Expand All @@ -199,6 +205,8 @@ AM_CONDITIONAL([DTRACE_INSTRUMENT_OBJ],[test "$dtrace_instrument_obj" = "yes"])
AM_CONDITIONAL([ENABLE_SASL],[test "$enable_sasl" = "yes"])
AM_CONDITIONAL([ENABLE_EXTSTORE],[test "$enable_extstore" = "yes"])
AM_CONDITIONAL([ENABLE_ARM_CRC32],[test "$enable_arm_crc32" = "yes"])
AM_CONDITIONAL([ENABLE_TLS],[test "$enable_tls" = "yes"])


AC_SUBST(DTRACE)
AC_SUBST(DTRACEFLAGS)
Expand Down Expand Up @@ -354,6 +362,95 @@ if test $ac_cv_libevent_dir != "(system)"; then
fi
fi

trylibssldir=""
AC_ARG_WITH(libssl,
[ --with-libssl=PATH Specify path to libssl installation ],
[
if test "x$withval" != "xno" ; then
trylibssldir=$withval
fi
]
)

dnl ----------------------------------------------------------------------------
dnl libssl detection. swiped from libevent. modified for openssl detection.

OPENSSL_URL=https://www.openssl.org/
if test "x$enable_tls" = "xyes"; then
AC_CACHE_CHECK([for libssl directory], ac_cv_libssl_dir, [
saved_LIBS="$LIBS"
saved_LDFLAGS="$LDFLAGS"
saved_CPPFLAGS="$CPPFLAGS"
le_found=no
for ledir in $trylibssldir "" $prefix /usr/local ; do
LDFLAGS="$saved_LDFLAGS"
LIBS="-lssl -lcrypto $saved_LIBS"
# Skip the directory if it isn't there.
if test ! -z "$ledir" -a ! -d "$ledir" ; then
continue;
fi
if test ! -z "$ledir" ; then
if test -d "$ledir/lib" ; then
LDFLAGS="-L$ledir/lib $LDFLAGS"
else
LDFLAGS="-L$ledir $LDFLAGS"
fi
if test -d "$ledir/include" ; then
CPPFLAGS="-I$ledir/include $CPPFLAGS"
else
CPPFLAGS="-I$ledir $CPPFLAGS"
fi
fi
# Can I compile and link it?
AC_TRY_LINK([#include <sys/time.h>
#include <sys/types.h>
#include <assert.h>
#include <openssl/ssl.h>], [ SSL_CTX* ssl_ctx = SSL_CTX_new(TLS_server_method());
assert(OPENSSL_VERSION_NUMBER >= 0x10100000L);],
[ libssl_linked=yes ], [ libssl_linked=no ])
if test $libssl_linked = yes; then
if test ! -z "$ledir" ; then
ac_cv_libssl_dir=$ledir
_myos=`echo $target_os | cut -f 1 -d .`
AS_IF(test "$SUNCC" = "yes" -o "x$_myos" = "xsolaris2",
[saved_LDFLAGS="$saved_LDFLAGS -Wl,-R$ledir/lib"],
[AS_IF(test "$GCC" = "yes",
[saved_LDFLAGS="$saved_LDFLAGS -Wl,-rpath,$ledir/lib"])])
else
ac_cv_libssl_dir="(system)"
fi
le_found=yes
break
fi
done
LIBS="$saved_LIBS"
LDFLAGS="$saved_LDFLAGS"
CPPFLAGS="$saved_CPPFLAGS"
if test $le_found = no ; then
AC_MSG_ERROR([libssl (at least version 1.1.0) is required. You can get it from $OPENSSL_URL
If it's already installed, specify its path using --with-libssl=/dir/
])
fi
])
LIBS="-lssl -lcrypto $LIBS"
if test $ac_cv_libssl_dir != "(system)"; then
if test -d "$ac_cv_libssl_dir/lib" ; then
LDFLAGS="-L$ac_cv_libssl_dir/lib $LDFLAGS"
le_libdir="$ac_cv_libssl_dir/lib"
else
LDFLAGS="-L$ac_cv_libssl_dir $LDFLAGS"
le_libdir="$ac_cv_libssl_dir"
fi
if test -d "$ac_cv_libssl_dir/include" ; then
CPPFLAGS="-I$ac_cv_libssl_dir/include $CPPFLAGS"
else
CPPFLAGS="-I$ac_cv_libssl_dir $CPPFLAGS"
fi
fi
fi

dnl ----------------------------------------------------------------------------

AC_SEARCH_LIBS(umem_cache_create, umem)
Expand Down
4 changes: 2 additions & 2 deletions crawler.c
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ static int lru_crawler_poll(crawler_client_t *c) {

if (to_poll[0].revents & POLLIN) {
char buf[1];
int res = read(c->sfd, buf, 1);
int res = ((conn*)c->c)->read(c->c, buf, 1);
if (res == 0 || (res == -1 && (errno != EAGAIN && errno != EWOULDBLOCK))) {
lru_crawler_close_client(c);
return -1;
Expand All @@ -304,7 +304,7 @@ static int lru_crawler_poll(crawler_client_t *c) {
lru_crawler_close_client(c);
return -1;
} else if (to_poll[0].revents & POLLOUT) {
int total = write(c->sfd, data, data_size);
int total = ((conn*)c->c)->write(c->c, data, data_size);
if (total == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
lru_crawler_close_client(c);
Expand Down
133 changes: 133 additions & 0 deletions doc/tls.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
Securing Memcached with TLS

Requirements
------------
We are required to encrypt Memcached network traffic as we deploy our servers in public cloud
environments. We decided to implement SSL/TLS for TCP at the network layer of Memcached
using OpenSSL libraries. This provides following benefits with the expense of added latency
and reduced throughput (to be quantified).

# Encryption :Data is encrypted on the wire between Memcached client and server.
# Authentication : Optionally, both server and client authenticate each other.
# Integrity: Data is not tampered or altered when transmitted between client and server

Following are a few additional features.
# Certificate refresh: when the server gets a new certificate, new connections
will use new certificates without a need of re-starting the server process.

# Multiple ports with and without TLS : by default all TCP ports are secured. Optionally we can setup
the server to secure a specific TCP port.

Note that initial implementation does not support session resumption or renegotiation.

Design
------
We experimented two options for implementing TLS, with SSL buffered events and directly using
OpenSSL API.

Bufferevents can use the OpenSSL library to implement SSL/TLS. Our experiment used
a socket-based bufferevent that tells OpenSSL to communicate with the network directly over.
Unlike a worker thread sets callback on the socket, this uses a “bufferevent” object for
callbacks. Memcached still has to setup the SSL Context but SSL handshake and object
management is done via the “bufferevent_” API. While this was fairly easy to implement,
we noticed a higher memory usage as we don’t have much control over allocating evbuffer
objects in bufferevents. More over there is a discussion on removing the libevent dependency
from Memcached; hence this option was not chosen.

OpenSSL library provides APIs for us to directly read/write from a socket. With this option,
we create an SSL Context and many SSL objects. The SSL Context object, created at the process level,
holds certificates, a private key, and options regarding the TLS protocol and algorithms.
SSL objects, created at the connection level, represents SSL sessions. SSL objects are responsible
for encryption, and session handshake among other things.

There are two ways to do network IO over TLS, either only use SSL_read/SSL_write with a network socket or
use the API along with an output/input buffer pair. These buffers are referred as BIO
(Basic Input Output) buffers.

We started with the first option, create SSL objects with the socket and only interact with SSL_read/SSL_write.

+------+ +-----+
|......|--> read(fd) --> BIO_write(rbio) -->|.....|--> SSL_read(ssl) --> IN
|......| |.....|
|.sock.| |.SSL.|
|......| |.....|
|......|<-- write(fd) <-- BIO_read(wbio) <--|.....|<-- SSL_write(ssl) <-- OUT
+------+ +-----+
| | | |
|<-------------------------------->| |<------------------->|
| encrypted bytes | | unencrypted bytes |

Figure 1 : Network sockets, BIO buffers and SSL_read/SSL_write

(reference: https://gist.github.com/darrenjs/4645f115d10aa4b5cebf57483ec82eca)

Memcached uses non blocking sockets and implements a rather complex state machine for
network IO. A listener thread does the TCP handshake and initiates the SSL handshake after
creating an SSL object based on the SSL Context object of the server. If there are no
fatal errors, the listener thread hands over the socket to a worker thread. A worker completes
the SSL handshake.

----------- ----------------------
| |
Client | | Memcached Server
| |
| |---------------------
| | Listener thread |
| TCP connect | |
|---------------------> | (accept) |
| ClientHello | |
|---------------------> | (SSL_accept) |
| | |
| ServerHello and | |
| Certificate, | |
| ServerHelloDone | |
| <---------------------| |
| |---------------------
| | |
| | V
| |-------------------
| | Worker thread |
| ClientKeyExchange, | |
| ChangeCipherSpec, | |
| Finished | |
|---------------------> | (SSL_read) |
| | |
| | |
| NewSessionTicket, | |
| ChangeCipherSpec, | |
| Finished | |
| <---------------------| |
| | |
| Memcached request/ | |
| response | |
| <-------------------> | (SSL_read/ |
| | SSL_write) |
----------- -------------------------

Figure 2 : The initial SSL handshake


Setting-up callbacks when the socket is ready for reading/writing is the same
for both TLS and non-TLS connections. When the socket is ready, the state machine kicks off
and issues a SSL_read/ SSL_write. Note that we implement a SSL_sendmsg wrapper on top of
SSL_write to simulate the sendmsg API.
This way we don't explicitly use BIO buffers or do BIO_write/BIO_read, but let OpenSSL
library to do it on our behalf. Existing state machine takes care of reading the correct amount
of bytes and do the error handling when needed.

As a best practice, server certificates and keys are periodically refreshed by the PKI.
When this happens we want server to use the new certificate without restarting the process.
Memcached is a cache and restarting servers affects the latency of applications. We implement
the automatic certificate refresh through a command. Upon receiving the "refresh_certs" command,
the server reloads the certificates and key to the SSL Context object. Existing connection won't be
interrupted but new connections will use the new certificate.

We understand not all users want to use TLS or have the OpenSSL dependency. Therefore
it's an optional module at the compile time. We can build a TLS capable Memcached server with
"./configure --enable-tls". Once the server is built with TLS support, we can enabled it with
"-Z" flag or "--enable-ssl". Certificate (-o ssl_chain_cert) and (-o ssl_key) are required
parameters while others are optional. Supported options can be listed through "memcached -h".

Developers need to have libio-socket-ssl-perl installed for running unit tests. When the server is
built with TLS support, we can use "test_tls" make target to run all existing tests over TLS and some
additional TLS specific tests. The minimum required OpenSSL version is 1.1.0g.
4 changes: 2 additions & 2 deletions logger.c
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ static int logger_thread_poll_watchers(int force_poll, int watcher) {
*/
if (watchers_pollfds[nfd].revents & POLLIN) {
char buf[1];
int res = read(w->sfd, buf, 1);
int res = ((conn*)w->c)->read(w->c, buf, 1);
if (res == 0 || (res == -1 && (errno != EAGAIN && errno != EWOULDBLOCK))) {
L_DEBUG("LOGGER: watcher closed remotely\n");
logger_thread_close_watcher(w);
Expand All @@ -464,7 +464,7 @@ static int logger_thread_poll_watchers(int force_poll, int watcher) {
total = fwrite(data, 1, data_size, stderr);
break;
case LOGGER_WATCHER_CLIENT:
total = write(w->sfd, data, data_size);
total = ((conn*)w->c)->write(w->c, data, data_size);
break;
}

Expand Down
Loading

0 comments on commit ee1cfe3

Please sign in to comment.