Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MINOR] Ensure support for Redis 6 with ACLs and TLS #250

Merged
merged 1 commit into from
Mar 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,20 @@ Get Started

Ready to contribute? Here's how to set up PySOA for local development.

1. Fork the ``pysoa`` repository on GitHub.
2. Clone your fork locally::
1. Ensure that Lua 5.2 or newer and its development headers are installed on your local system using one of the
following techniques (or equivalent) based on your system. PySOA does not use Lua in your services, but PySOA's own
tests use Lua when mocking Redis::

$ brew install lua # macOS (see https://brew.sh/)
$ apt-get install lua5.2 liblua5.2-dev # Ubuntu
$ yum install lua lua-devel # CentOS

2. Fork the ``pysoa`` repository on GitHub.
3. Clone your fork locally::

$ git clone [email protected]:your_name_here/pysoa.git

3. Create Python 2.7 and 3.7 virtualenvs (you should ``pip install virtualenvwrapper`` on your system if you have not
4. Create Python 2.7 and 3.7 virtualenvs (you should ``pip install virtualenvwrapper`` on your system if you have not
already) for installing PySOA dependencies::

$ mkvirtualenv2 pysoa2
Expand All @@ -67,18 +75,18 @@ Ready to contribute? Here's how to set up PySOA for local development.
(pysoa3) $ pip install -e .[testing]
(pysoa3) $ deactivate

4. Make sure the tests pass on master before making any changes; otherwise, you might have an environment issue::
5. Make sure the tests pass on master before making any changes; otherwise, you might have an environment issue::

(pysoa2) $ ./test.sh
(pysoa3) $ ./test.sh

5. Create a branch for local development::
6. Create a branch for local development::

$ git checkout -b name-of-your-bugfix-or-feature

Now you can make your changes locally.

5. As you make changes, and when you are done making changes, regularly check that Flake8 and MyPy analysis and all of
7. As you make changes, and when you are done making changes, regularly check that Flake8 and MyPy analysis and all of
the tests pass. You should also include new tests or assertions to validate your new or changed code::

# this command runs unit and integration tests, Flake8 analysis, and MyPy analysis
Expand All @@ -102,11 +110,11 @@ Ready to contribute? Here's how to set up PySOA for local development.

$ ./tox.sh

6. When you think you're ready to commit, run ``isort`` to organize your imports::
8. When you think you're ready to commit, run ``isort`` to organize your imports::

$ isort

7. Commit your changes and push your branch to GitHub::
9. Commit your changes and push your branch to GitHub::

$ git add -A
$ git commit -m "[PATCH] Your detailed description of your changes"
Expand All @@ -119,7 +127,7 @@ Ready to contribute? Here's how to set up PySOA for local development.
``[MAJOR]`` changes, as they will not be released until the next major milestone, which could be as much as a year
away.

8. Submit a pull request through the GitHub website.
10. Submit a pull request through the GitHub website.

Pull Request Guidelines
-----------------------
Expand Down
204 changes: 202 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Python 3 and ``str`` in Python 2). It is an error to interchange these, and will
possibly even client-side exceptions.

.. contents:: Contents
:local:
:depth: 3
:backlinks: none

Expand Down Expand Up @@ -619,7 +620,7 @@ field (``destination_field``) to each object that has a source field (``bar_id`
],
},
}


Client exceptions
*****************
Expand Down Expand Up @@ -767,7 +768,16 @@ Redis Gateway Transport

The ``transport.redis_gateway`` module provides a transport implementation that uses Redis (in standard or Sentinel
mode) for sending and receiving messages. This is the recommended transport for use with PySOA, as it provides a
convenient and performant backend for asynchronous service requests.
convenient and performant backend for asynchronous service requests. A single Redis server running on a ``c5.xlarge``
EC2 instance has been tested to handle about 10,000 PySOA requests and responses per second at about 50% CPU usage
and about 16,000 PySOA requests and responses per second at about 80% CPU usage. A cluster of three masters of that
size can easily handle about 45,000 requests and responses per second.

The PySOA Redis Gateway Transport is tested and certified against Redis 5 and Redis 6 and is commonly known to work
with Redis 3 and 4 (though those versions are no longer supported). PySOA 2.0 will require Redis 6. Currently, the
transport supports the `Redis Python library <https://pypi.org/project/redis/>`_ versions 2.10+ and 3.4+ and does not
support versions older than 2.10 or any of the 3.0.x-3.3.x versions. PySOA 2.0 will require at least version 3.5 and
may require an even-newer version (this has not been decided yet).


Standard and Sentinel modes
Expand All @@ -787,6 +797,7 @@ multiple masters, operations will proceed as follow:
pick a master to which to send the response, based on the queue name to which it is supposed to send that response,
such that it will always send to the same master on which the client is "listening."


Configuration
-------------

Expand Down Expand Up @@ -835,6 +846,195 @@ The Redis Gateway transport takes the following extra keyword arguments for conf
response logging.


Redis Authentication Support
----------------------------

Both the Standard and Sentinel modes of the Redis Gateway transport support Redis authentication, and both traditional
password-only authentication and the new ACL user-password authentication added to Redis 6 are supported. ACL
user-password authentication requires at least Redis 6 and at least version 3.4.1 of the
`Redis Python library <https://pypi.org/project/redis/>`_. Password-only authentication works with Redis 5 and at least
version 2.10.0 of the Redis Python library.

Before proceeding with password-only authentication, be sure you have read and understand the
`Redis "Authentication feature" documentation <https://redis.io/topics/security#authentication-feature>`_. For ACLs,
be sure you have read and understand the `Redis "ACL" documentation <https://redis.io/topics/acl>`_.

To configure password-only authentication, specify it in a ``connection_kwargs`` item within the
``backend_layer_kwargs`` argument (above) of either the standard or Sentinel transports:

.. code-block:: python

...
'backend_layer_kwargs': {
...
'connection_kwargs': {
...
'password': 'the_super_secret_redis_password',
...
},
...
},
...

Configuring ACL user-password authentication is nearly identical, supplementing ``password`` with ``username``:

.. code-block:: python

...
'backend_layer_kwargs': {
...
'connection_kwargs': {
...
'username': 'service_transport_user',
'password': 'the_service_transport_user_password',
...
},
...
},
...


Redis TLS Support
-----------------

The Standard mode of the Redis Gateway Transport supports TLS when communicating with the Redis server, and the
Sentinel mode supports TLS when communicating with the Redis server only, with the Sentinel server only, or both,
depending on your setup.

Before proceeding with TLS, be sure you have read and understand the
`Redis "TLS Support" documentation <https://redis.io/topics/encryption>`_.

Using TLS on Standard mode is straightforward and can be demonstrated with following example ``connection_kwargs``:

.. code-block:: python

...
'backend_layer_kwargs': {
...
'connection_kwargs': {
...
'ssl': True,
'ssl_ca_certs': '/path/to/ca.crt',
'ssl_certfile': '/path/to/redis.crt',
'ssl_keyfile': '/path/to/redis.key',
...
},
...
},
...

In this example, the client is supporting TLS on the server and also proving a client certificate, the default
Redis TLS configuration. ``ssl_ca_certs`` is the PEM-encoded file containing the Certificate Authority certificates
used to sign both the server certificate and the client certificate. ``ssl_certfile`` is the PEM-encoded file
containing the client certificate, and ``ssl_keyfile`` is the TLS key used to generate the client certificate. If you
have configured Redis with ``tls-auth-clients no`` to disable client certificates, you do not need the ``ssl_certfile``
and ``ssl_keyfile`` arguments, but you still need the ``ssl_ca_certs`` arguments.

TLS authentication when Sentinel is involved is considerably more involved:

* If TLS is enabled on your master and replicas but Sentinel does not talk to those servers using TLS, your service and
clients will also not be able to talk to Redis using TLS. The Redis Gateway Transport asks Sentinel to give it the
address for the current Redis master. If Sentinel is talking to the Redis master on a non-TLS address, it will give
the transport that non-TLS address.
* If TLS is enabled on your master and replicas and Sentinel talks to those servers using TLS (``sentinel.conf``
contains ``tls-replication yes`` and ``sentinel monitor`` specifies the TLS port), but Sentinel itself is not
listening on a TLS port (``tls-port`` is not in ``sentinel.conf``), your service and clients will be able to talk to
Redis over TLS but will not be able to talk to Sentinel over TLS. This configuration is actually fine—sensitive data
that your services transact is not transmitted over the Sentinel connection; it is transmitted only over the Redis
connection.
* If TLS is enabled on your master and replicas and Sentinel talks to those servers using TLS (``sentinel.conf``
contains ``tls-replication yes`` and ``sentinel monitor`` specifies the TLS port) and Sentinel itself is also
configured to listen on a TLS port (``sentinel.conf`` contains ``tls-port`` and related config options), your service
and clients can talk to both Redis and Sentinel over TLS. This is the most-secure configuration (it prevents a MitM
attack from redirecting your services and clients to a malicious Redis server).

The example below demonstrates the third condition, where the transport can use TLS to talk to both Redis and Sentinel:

.. code-block:: python

...
'backend_layer_kwargs': {
...
'connection_kwargs': {
...
'ssl': True,
'ssl_ca_certs': '/path/to/ca.crt',
'ssl_certfile': '/path/to/redis.crt',
'ssl_keyfile': '/path/to/redis.key',
...
},
'sentinel_kwargs': {
...
'ssl': True,
'ssl_ca_certs': '/path/to/ca.crt',
'ssl_certfile': '/path/to/redis.crt',
'ssl_keyfile': '/path/to/redis.key',
...
},
...
},
...

As above, this example demonstrates both TLS and client authentication certificates. You can omit ``ssl_certfile`` and
``ssl_keyfile`` if Redis is configured with ``tls-auth-clients no``. If you want to talk only to Redis over TLS and
not Sentinel, you can omit all of the ``ssl*`` arguments from ``sentinel_kwargs``.

Note that TLSv1.2 is supported basically universally, but TLSv1.3 requires your Python services and clients to be
running on a system with at lest OpenSSL 1.1.0 installed. Python running on systems with older versions of OpenSSL will
not be able to connect to Redis servers with TLSv1.3. As such, take care when configuring the ``tls-protocols`` option
in your Redis configuration.


Timeouts and Keep-Alive
-----------------------

The Standard and Sentinel modes of the Redis Gateway Transport automatically configure sane defaults for timeouts to
ensure proper operation of your transports. However, you may wish to override those. The following demonstrates the
*default* values for the Standard mode:

.. code-block:: python

...
'backend_layer_kwargs': {
...
'connection_kwargs': {
...
'socket_connect_timeout': 5.0, # seconds
'socket_keepalive': True,
...
},
...
},
...

Specify those keys with your own values in your configuration to override those defaults. The following demonstrates
the *default* values for the Sentinel mode:

.. code-block:: python

...
'backend_layer_kwargs': {
...
'connection_kwargs': {
...
'socket_connect_timeout': 5.0, # seconds
'socket_keepalive': True,
...
},
'sentinel_kwargs': {
...
'socket_connect_timeout': 5.0, # seconds
'socket_timeout': 5.0, # seconds
'socket_keepalive': True,
...
},
...
},
...

Specify those keys with your own values in your configuration to override those defaults.


Middleware
++++++++++

Expand Down
1 change: 1 addition & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ with PySOA services. This document describes these libraries and techniques. As
are unicode strings (the default in Python 3; use ``from __future__ import unicode_literals`` for Python 2).

.. contents:: Contents
:local:
:depth: 3
:backlinks: none

Expand Down
30 changes: 28 additions & 2 deletions functional.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ fi
set -e

mkdir -p tests/functional/run/redis
for r in 4 5
for r in 5 6
do
cp -f "tests/functional/docker/redis/redis${r}-master.conf" "tests/functional/run/redis/redis${r}-standalone.conf"
cp -f "tests/functional/docker/redis/redis${r}-standalone.conf" "tests/functional/run/redis/redis${r}-standalone.conf"
cp -f "tests/functional/docker/redis/redis${r}-master.conf" "tests/functional/run/redis/redis${r}-master.conf"
for i in 1 2 3
do
Expand All @@ -41,6 +41,32 @@ do
chmod -v 0666 tests/functional/run/redis/*
done

if [[ ! -f tests/functional/run/tls/ca.crt ]] || [[ ! -f tests/functional/run/tls/redis.key ]] || [[ ! -f tests/functional/run/tls/redis.crt ]]
then
rm -rf tests/functional/run/tls
mkdir -p tests/functional/run/tls
openssl genrsa -out tests/functional/run/tls/ca.key 4096
openssl req \
-x509 -new -nodes -sha256 \
-key tests/functional/run/tls/ca.key \
-days 3650 \
-subj '/O=Redis Test/CN=Certificate Authority' \
-out tests/functional/run/tls/ca.crt
openssl genrsa -out tests/functional/run/tls/redis.key 2048
openssl req \
-new -sha256 \
-key tests/functional/run/tls/redis.key \
-subj '/O=Redis Test/CN=Server' | \
openssl x509 \
-req -sha256 \
-CA tests/functional/run/tls/ca.crt \
-CAkey tests/functional/run/tls/ca.key \
-CAserial tests/functional/run/tls/ca.txt \
-CAcreateserial \
-days 365 \
-out tests/functional/run/tls/redis.crt
fi

set -x

docker build --tag pysoa-test-mysql --file tests/functional/docker/Dockerfile-mysql . &
Expand Down
Loading