Secure Redis
Redis philosophy is that security is a second class citizen and should be implemented by a different layer. A recent post about this topic can be found here.
However, some of us have to abide by regulations like PCI DSS or HIPAA that impose strict restrictions on how communications should be done.
Our target will be a highly available redis setup with secure communication between nodes and its clients.
Topology


Secure Communications
For the purpose of securing redis communications in the most transparent way, this setup will use ipsec in transport mode:


Install required packages (debian):
apt-get install ipsec-tools racoon
Edit file /etc/ipsec-tools.conf (configure a mesh):
Start the service:
service setkey start
Edit the file /etc/racoon/racoon.conf:
Edit the file /etc/racoon/psk.txt:
Start the racoon service:
service racoon restart
If everything is ok we should be able to ping redis-1 from redis-2:
$ ping 192.168.205.10
PING 192.168.205.10 (192.168.205.10) 56(84) bytes of data.
64 bytes from 192.168.205.10: icmp_seq=1 ttl=64 time=0.058 ms
64 bytes from 192.168.205.10: icmp_seq=2 ttl=64 time=0.050 ms
64 bytes from 192.168.205.10: icmp_seq=3 ttl=64 time=0.048 ms
Check with tcpdump that information is being encapsulated:
3:45:50.132031 IP (tos 0x0, ttl 64, id 22902, offset 0, flags [DF], proto ESP (50), length 104)
192.168.205.10 > 192.168.205.11: ESP(spi=0x0957a438,seq=0xaa5), length 84
13:45:50.223091 IP (tos 0x0, ttl 64, id 17277, offset 0, flags [DF], proto ESP (50), length 104)
192.168.205.10 > 192.168.205.11: ESP(spi=0x0957a438,seq=0xaa6), length 84
13:45:50.223268 IP (tos 0x0, ttl 64, id 37527, offset 0, flags [DF], proto ESP (50), length 232)
192.168.205.10 > 192.168.205.11: ESP(spi=0x0957a438,seq=0xaa7), length 212
Benchmark the connection without vpn:
$ iperf -c 192.168.205.10 -t 60
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Client connecting to 192.168.205.10, TCP port 5001
TCP window size: 93.5 KByte (default)
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
[ 3] local 192.168.205.11 port 33313 connected with 192.168.205.10 port 5001
[ ID] Interval Transfer Bandwidth
[ 3] 0.0–60.0 sec 6.74 GBytes 965 Mbits/sec
Benchmark the connection with vpn:
$ iperf -c 192.168.205.10 -t 60
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Client connecting to 192.168.205.10, TCP port 5001
TCP window size: 85.0 KByte (default)
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
[ 3] local 192.168.205.11 port 36091 connected with 192.168.205.10 port 5001
[ ID] Interval Transfer Bandwidth
[ 3] 0.0–60.2 sec 357 MBytes 49.7 Mbits/sec
Well that’s disappointing. I believe the culprit is related to the lack of support for the AES-NI instructions or with MTU:
AVX2 or AES-NI instructions are not detected.
eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
Let's move on for now.
Configuring Redis Server
Redis’s configuration is quite simple, we can ignore underlying network configuration.
Install redis-server:
apt-get install redis-server
Edit redis-1 /etc/redis/redis.conf:
bind 127.0.0.1 192.168.205.10
requirepass password
port 6379
Edit redis-2 /etc/redis/redis.conf:
bind 127.0.0.1 192.168.205.11
port 6379
slaveof 192.168.205.10 6379
masterauth password
requirepass password
Edit redis-3 /etc/redis/redis.conf:
bind 127.0.0.1 192.168.205.12
port 6379
slaveof 192.168.205.10 6379
masterauth password
requirepass password
Start redis-server
service redis-server start
Check wether everything is working:
redis-1:
$ redis-cli
127.0.0.1:6379> auth password
OK
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.205.12,port=6379,state=online,offset=105163101,lag=0
slave1:ip=192.168.205.11,port=6379,state=online,offset=105163242,lag=0
master_repl_offset:105163242
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:104114667
repl_backlog_histlen:1048576
redis-2 and redis-3:
$ redis-cli
127.0.0.1:6379> auth password
OK
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:192.168.205.10
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:105161550
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
Configuring Redis Sentinel
Redis Sentinel provides high availability for redis and will promote a slave to master if needed, among other tasks.
On each node edit /etc/redis/sentinel.conf:
sentinel monitor master 192.168.205.10 6379 2
sentinel auth-pass master password
sentinel down-after-milliseconds master 5000
sentinel failover-timeout master 5000
daemonize yes
Start redis-sentinel service
service redis-sentinel start
Validate:
$ redis-cli -h 192.168.205.10 -p 26379 info
…
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=master,status=ok,address=192.168.205.10:6379,slaves=2,sentinels=3
$ redis-cli -h 192.168.205.10 -p 26379 sentinel sentinels master
1) 1) “name”
2) “192.168.205.12:26379”
2) 1) “name”
2) “192.168.205.11:26379”
$ redis-cli -h 192.168.205.10 -p 26379 sentinel slaves master
1) 1) “name”
2) “192.168.205.11:6379”
2) 1) “name”
2) “192.168.205.12:6379”
Now we have achieved a highly available redis setup with secure communication between nodes.
What about clients?
One possible configuration is adding the clients to the vpn. However the major downside would be reloading the vpn every time a new client is added or removed.
For the sake of completeness, client access will be over tls using stunnel. Stunnel will be listening on port 6380 and redirecting to 127.0.0.1:6379
Install stunnel:
apt-get install stunnel
Edit the file /etc/stunnel/stunnel.conf:
redis-1:
key = /etc/ssl/private/ssl-cert-snakeoil.key
cert = /etc/ssl/certs/ssl-cert-snakeoil.pem
sslVersion = TLSv1.2
options = NO_SSLv2
options = NO_SSLv3
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
[redis]
accept = 192.168.205.10:6380
connect = 127.0.0.1:6379
redis-2:
key = /etc/ssl/private/ssl-cert-snakeoil.key
cert = /etc/ssl/certs/ssl-cert-snakeoil.pem
sslVersion = TLSv1.2
options = NO_SSLv2
options = NO_SSLv3
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
[redis]
accept = 192.168.205.11:6380
connect = 127.0.0.1:6379
redis-3:
key = /etc/ssl/private/ssl-cert-snakeoil.key
cert = /etc/ssl/certs/ssl-cert-snakeoil.pem
sslVersion = TLSv1.2
options = NO_SSLv2
options = NO_SSLv3
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
[redis]
accept = 192.168.205.12:6380
connect = 127.0.0.1:6379
Start stunnel (stunnel4 on debian) service:
service stunnel4 start
Check if we can connect:
$ openssl s_client -connect 192.168.205.10:6380
CONNECTED(00000003)
Obtaining the address of the current master:
$ redis-cli -h 192.168.205.10 -p 26379 SENTINEL get-master-addr-by-name master
1) “192.168.205.10”
2) “6379”
Redis sentinel knows nothing about stunnel and the reported master is running on port 6379, when we were expecting the 6380 (stunnel port) .
Haproxy
Since we can not use redis-sentinel to discover the active master, haproxy will do that job for us.
Haproxy 1.5 or greater is needed.
Edit file /etc/haproxy/haproxy.conf:
Client will need support for secure communications in the redis client or you will need to use stunnel.
Client Configuration
Edit the file /etc/stunnel/stunnel.conf
sslVersion = TLSv1.2
options = NO_SSLv2
options = NO_SSLv3
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
[redis]
client = yes
accept = 127.0.0.1:6379
connect = <haproxy ip>:6379
And finally access the redis server:
$ redis-cli -h 127.0.0.1
127.0.0.1:6379> auth password
OK
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.205.11,port=6379,state=online,offset=575891,lag=1
slave1:ip=192.168.205.12,port=6379,state=online,offset=575891,lag=0
master_repl_offset:575891
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:575890
Final Topology


Conclusion
Although the process described above is functional, it is still non trivial. If in the near future TLS support is included in redis, this might make matters easy.