Wednesday, 14 March 2012

Installing and Hardening Redis

For the recent Security BSides London challenge I wrote, I made use of a Redis database to store user email. I had two motivations for this, firstly I didn't want the mail in the main MySQL database as that was going to be dumpable through SQL Injection, secondly, I wanted to play with Redis. :-)

Redis doesn't have much in the way of security so I knew that anyone who managed to pop the box could theoretically connect to the local Redis instance and mess around. I'll take you through the steps I took to install and harden Redis, on a Debian Squeeze GNU/Linux box.

The first thing to do is get a copy of Redis. This is available as source tarball from their website. In this instance, the latest version is 2.4.8.
$ cd Downloads/
$ wget
--2012-03-13 10:21:06--
Connecting to||:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 618164 (604K) [application/x-gzip]
Saving to: `redis-2.4.8.tar.gz'

100%[======================================>] 618,164 549K/s in 1.1s

2012-03-13 10:21:07 (549 KB/s) - `redis-2.4.8.tar.gz' saved [618164/618164]

Now unpack it of course and run "make".
$ tar xzf redis-2.4.8.tar.gz
$ cd redis-2.4.8/
$ make
cd src && make all
make[1]: Entering directory `/home/marc/Downloads/redis-2.4.8/src'
MAKE hiredis
make[2]: Entering directory `/home/marc/Downloads/redis-2.4.8/deps/hiredis'
cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings -g -ggdb net.c
cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings -g -ggdb hiredis.c
cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings -g -ggdb sds.c
cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings -g -ggdb async.c
ar rcs libhiredis.a net.o hiredis.o sds.o async.o
make[2]: Leaving directory `/home/marc/Downloads/redis-2.4.8/deps/hiredis'

... and so on....

CC sort.o
CC intset.o
CC syncio.o
CC slowlog.o
CC bio.o
LINK redis-server

Hint: To run 'make test' is a good idea ;)

If you want to run make test you can, must admit I don't normally worry. Now you can just run the server from here if you want to but that's not normally what you would do in production. Redis provide an install script which will put everything in /usr/local by default. This is fine by me but the whole thing will be owned by and run as root. This is completely unnecessary so we'll remedy that shortly. First though, install it with its default options.
$ cd utils
$ sudo ./
Welcome to the redis service installer
This script will help you easily set up a running redis server
Please select the redis port for this instance: [6379]
Selecting default: 6379
Please select the redis config file name [/etc/redis/6379.conf]
Selected default - /etc/redis/6379.conf
Please select the redis log file name [/var/log/redis_6379.log]
Selected default - /var/log/redis_6379.log
Please select the data directory for this instance [/var/lib/redis/6379]
Selected default - /var/lib/redis/6379
Please select the redis executable path [/usr/local/bin/redis-server]
Copied /tmp/6379.conf => /etc/init.d/redis_6379
Installing service...
update-rc.d: using dependency based boot sequencing
insserv: warning: script 'redis_6379' missing LSB tags and overrides
Starting Redis server...
Installation successful!

Next we create a system account for Redis to run as. I use redis but you can substitute for whatever you like.
$ sudo useradd -r -s /bin/bash -d /var/lib/redis redis
Next we create a directory under /var/run which redis can write its pid file to and the same under /var/log for logging.
$ sudo mkdir /var/run/redis /var/log/redis
$ sudo chown redis:adm /var/run/redis /var/log/redis
$ sudo chmod 750 /var/log/redis
Then we update the Redis config file to use this.
$ sudo vi /etc/redis/6379.conf
find the line beginning pidfile and change the location to include the /var/run/redis directory like this.
pidfile /var/run/redis/
and repeat for the logfile line:
logfile /var/log/redis/redis_6379.log
The next thing is to sort the owner and permissions on the location where Redis periodically dumps its contents. No-one other than the redis user needs access to here so let's enforce that.
$ sudo chown -R redis:redis /var/lib/redis
$ sudo chmod 700 /var/lib/redis/
Now we turn our attention to the init script at /etc/init.d/redis_6379. We need to make a couple of amendments to specify the correct location of our pidfile and also to start the server as the redis user.
$ sudo vi /etc/init.d/redis_6379
Find the line near the top which defines the PIDFILE variable and change it to:
Now we need to add a new variable declaration. I put it just above REDISPORT but anywhere in the top of the file will technically work.
Now head down the file and find the section which looks like:
echo "Starting Redis server..."
Change it to:
echo "Starting Redis server..."
/bin/su - $REDISUSER -c "$EXEC $CONF"
That's it, we're done getting it up and running. You can test the start up by issuing:
$ sudo /etc/init.d/redis_6379 start
Starting Redis server...
You can check it's running in the normal manner:
$ ps -ef | grep red
redis 4410 1 0 11:11 ? 00:00:00 /usr/local/bin/redis-server /etc/redis/6379.conf
Check that it's running as the redis user which you can see above, it is.


So that's great, but by connecting to the local redis server on port 6379 you'd be able to issue any command including SET and other write actions. I didn't want this obviously. Password authentication can be enabled on Redis but this is as much use as a chocolate teapot when the password is a config file everyone can read. I decided to restrict the command set to only read-only commands using the very helpful rename-command feature.

You can rename any Redis command to whatever you like but if you rename it to "", ie nothing, it disables the commands. Very handy.

There were only two commands I needed for my app, GET and LRANGE so I was free to disable everything else. Redis provide a list of commands on their website at so with a frankly hideous curl | grep | sed | cut | awk | anything else you can think of command line I parsed out all the valid commands and generated the following output which I saved in a file /etc/redis/rename-commands.conf:
rename-command APPEND ""
rename-command AUTH ""
rename-command BGREWRITEAOF ""
rename-command BGSAVE ""
rename-command BLPOP ""
rename-command BRPOP ""
rename-command BRPOPLPUSH ""
rename-command CONFIG ""
rename-command DBSIZE ""
rename-command DEBUG ""
rename-command DECR ""
rename-command DECRBY ""
rename-command DEL ""
rename-command DISCARD ""
rename-command ECHO ""
rename-command EXEC ""
rename-command EXISTS ""
rename-command EXPIRE ""
rename-command EXPIREAT ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command GETBIT ""
rename-command GETRANGE ""
rename-command GETSET ""
rename-command HDEL ""
rename-command HEXISTS ""
rename-command HGET ""
rename-command HGETALL ""
rename-command HINCRBY ""
rename-command HKEYS ""
rename-command HLEN ""
rename-command HMGET ""
rename-command HMSET ""
rename-command HSET ""
rename-command HSETNX ""
rename-command HVALS ""
rename-command INCR ""
rename-command INCRBY ""
rename-command INFO ""
rename-command KEYS ""
rename-command LASTSAVE ""
rename-command LINDEX ""
rename-command LINSERT ""
rename-command LLEN ""
rename-command LPOP ""
rename-command LPUSH ""
rename-command LPUSHX ""
rename-command LREM ""
rename-command LSET ""
rename-command LTRIM ""
rename-command MGET ""
rename-command MONITOR ""
rename-command MOVE ""
rename-command MSET ""
rename-command MSETNX ""
rename-command MULTI ""
rename-command OBJECT ""
rename-command PERSIST ""
rename-command PING ""
rename-command PSUBSCRIBE ""
rename-command PUBLISH ""
rename-command PUNSUBSCRIBE ""
rename-command RANDOMKEY ""
rename-command RENAME ""
rename-command RENAMENX ""
rename-command RPOP ""
rename-command RPOPLPUSH ""
rename-command RPUSH ""
rename-command RPUSHX ""
rename-command SADD ""
rename-command SAVE ""
rename-command SCARD ""
rename-command SDIFF ""
rename-command SDIFFSTORE ""
rename-command SELECT ""
rename-command SET ""
rename-command SETBIT ""
rename-command SETEX ""
rename-command SETNX ""
rename-command SETRANGE ""
rename-command SHUTDOWN ""
rename-command SINTER ""
rename-command SINTERSTORE ""
rename-command SISMEMBER ""
rename-command SLAVEOF ""
rename-command SLOWLOG ""
rename-command SMEMBERS ""
rename-command SMOVE ""
rename-command SORT ""
rename-command SPOP ""
rename-command SRANDMEMBER ""
rename-command SREM ""
rename-command STRLEN ""
rename-command SUBSCRIBE ""
rename-command SUNION ""
rename-command SUNIONSTORE ""
rename-command SYNC ""
rename-command TTL ""
rename-command TYPE ""
rename-command UNSUBSCRIBE ""
rename-command UNWATCH ""
rename-command WATCH ""
rename-command ZADD ""
rename-command ZCARD ""
rename-command ZCOUNT ""
rename-command ZINCRBY ""
rename-command ZINTERSTORE ""
rename-command ZRANGE ""
rename-command ZRANGEBYSCORE ""
rename-command ZRANK ""
rename-command ZREM ""
rename-command ZREMRANGEBYRANK ""
rename-command ZREMRANGEBYSCORE ""
rename-command ZREVRANGE ""
rename-command ZREVRANGEBYSCORE ""
rename-command ZREVRANK ""
rename-command ZSCORE ""
rename-command ZUNIONSTORE ""

The eagle-eyed among you will notice that I've renamed the SHUTDOWN command. If you rename SHUTDOWN the init script doesn't like you much and it doesn't perform a save of the data before it exits. In my case that's actually not too much of an issue as no data is being written but it's something to bear in mind on your app. If you don't rename SHUTDOWN then obviously someone can connect and shutdown your Redis instance. In a deliberately vulnerable app you might expect this, in a normal production server, hopefully not!

Now I've got this file I need to protect it and use it. I decided the redis user should not be able to edit this file and no-one else should be able to read it:
$ sudo chown root:redis /etc/redis/rename-commands.conf
$ chmod 440 /etc/redis/rename-commands.conf

Now we can include it into the main Redis configuration by adding the following line to the bottom of /etc/redis/6379.conf:
include /etc/redis/rename-commands.conf
And that's it, restart your Redis instance and test it out.
$ redis-cli
redis> ECHO foo
(error) ERR unknown command 'ECHO'
There's probably a few more things that could be done to harden it further, I would suggest looking into the authentication too. While it does sit in the config file, the same is true of a traditional database. The difference is you have much more granular control over database objects typically. With redis it's all or nothing, like having your app log in as sa.

For my next trick, I'll be looking into No-SQL Injection.

1 comment:

  1. Thanks for the nice, easy to follow write-up!

    I'm following these instructions to get my own BSides Challenge server up and running and I noticed a small typo.

    When you run the ./ script I had to change the default entry for the log file to point to the redis folder, like so:


    Hope that helps!