A Redis wrapper to enable "shielded mode" (AKA: "EVALSHA
-only mode")
Usage: redis-shield OPTION... [FILE]...
Apply command renaming to each Lua FILE, apply rename-command directives
to the given Redis server, and load the renamed scripts; return a list of
space separated hash / FILE pairs.
Mandatory options:
-s SERVER-SCRIPT use the given script as the server
-c CONFIG use the given file as base configuration
-e CLI-SCRIPT use the given script as the cli client
Additional options:
-m MAP-FILE write the hash / FILE mapping to the given file
instead of stdout
-n use newlines as terminators for hash / FILE mapping
-z use nulls as terminators for hash / FILE mapping
-x COMMAND exclude COMMAND from renaming (this option may be given
multiple times)
-d COMMAND disable COMMAND, ie. rename to '' (this option may be given
multiple times)
-p PATTERN use PATTERN as the replacement token pattern, the default
pattern is '__%s__'; this must be a printf-friendly
format string with a single '%s' occurence and no other
format specifier; commands are always given in uppercase
-b BITS bit length of the rename tags (will be rounded up to a
multiple of 4), defaults to 1024
-t TIMEOUT time in seconds to wait for server availability (defaults
to 5)
-g try to guess 'redis.call' / 'redis.pcall' appearances
-o don't try to guess 'redis.call' / 'redis.pcall' appearances,
use patterns only
-w enable 'watch-only' mode: this will output the config
directives to be injected and the replaced scripts
-r disable 'watch only' mode, work for real
-- signal the end of command line options
-h display this help and exit
-v output version information and exit
The SERVER-SCRIPT is any script able to start a Redis server being given
a configuration file, to be read from stdin if '-' is specified instead.
The CLI-SCRIPT is any script able to connect to the Redis server,
recognizing the '-p', '-s', and '-a' options for specifying port, socket,
and authentication; additionally, it should support the '-x' switch
instructing the script to read its last argument from stdin. Note that
redis-cli fulfills the requirements.
Exit status:
0 if OK,
1 if command line problems (eg. invalid pattern),
2 if server timeout reached.
Redis install under /opt
:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
*.lua
Only process "template" files:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
*.lua.template
Custom pattern:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-p '::%s::'
*.lua
Exclude inocuous commands from renaming (eg. ping
, echo
, time
):
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-x ping -x echo -x time
*.lua
Disable dangerous commands (eg. keys
, config
, flushall
, flushdb
, shutdown
, debug
):
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-d keys -d config -d flushall -d flushdb -d shutdown -d debug
*.lua
Both of the above:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-x ping -x echo -x time
-d keys -d config -d flushall -d flushdb -d shutdown -d debug
*.lua
Write script map to a file instead of stdout
:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-m /tmp/map.file
*.lua
Write script map to a file instead of stdout
, and terminate lines with a null (\0
) byte:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-m /tmp/map.file
-z
*.lua
Be paranoid about random bits used (viz. use 4096):
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-b 4096
*.lua
Wait for the server for a whole day (ie. 86400 seconds):
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-t 86400
*.lua
Try to guess where replacements should happen:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-g
*.lua
Play voyeur and only watch what would happen:
$ redis-shield -s /opt/redis/redis-server
-c /opt/redis/redis.conf
-e /opt/redis/redis-cli
-w
*.lua
redis-shield
is a Bash script intended to provide EVALSHA
-only mode for Redis. EVALSHA
-only mode means that the only command available to a Redis client is EVALSHA
, this allows for setups where the system administrator or developer sets up a number of Lua scripts for execution on the Redis server providing the only channels by which the application is to access and modify data; think of it as the public interface of the Redis server in this case.
NOTE Starting from version 0.2,
redis-shield
is able to guess some occurrences ofredis.call
andredis.pcall
. Now (given the-g
switch)redis-shield
will look for patterns of the form:W redis S . S P call S ( S Q TOKEN Q S ,
where S stands for arbitrary whitespace, W stands for a word boundary, Q is either a single or double quote (both forms are allowed in Lua,
redis-shield
only looks for matching Q s), and P is an optional "P" / "p" (in order to catchpcall
as well); TOKEN is any Redis command being looked for.This is the most "generous" regex that Lua would swallow without choking, but it may need further tuning.
In order to make this happen, redis-shield
requires you to adhere to a simple convention:
When calling Redis commands inside a Lua script (with
redis.call(...)
), instead of using the command's name (eg. 'SET
'), use a distinctive pattern (eg. '__SET__
', the default one).
Additionally, every Lua script you care to run in the server must be written to an actual file (ie. if you're merely caling EVAL
with a constant string, just place that string in a file and apply the convention above). This makes redis-shield
incompatible with any application that builds its scripts on-the-fly, but I find that to be a questionable practice anyway (that is, in the general case, I'm sure there are very valid and interesting reasons to do that every now and then).
Finally, you'll need to implement an abstraction layer of sorts, mapping script names (ie. file names) to SHA1
hashes and calling that instead of just throwing a command at the Redis connection.
Suppose you have the following Lua script (zset2set.lua
):
--[[
Time complexity: O(N + M) where N is the size of dest (might be 0),
and M the size of source
Space complexity: O(M) where is the size of source
Convert a ZSET into a SET of its members alone (ie. discard scores).
Note that dest and source may be the same keys, this has the effect of
removing scores from a ZSET.
USAGE: ZSET2SET dest source
RETURN: number of elements in dest
]]--
-- initialize counter
local i = 0
-- get the source ZSET
local src = redis.call('zrange', KEYS[2], 0, -1)
-- clean up destination SET
redis.call('del', KEYS[1])
-- look for each element in the source
for _, v in pairs(src) do
-- increment the count
i = i + 1
-- add it
redis.call('sadd', KEYS[1], v)
end
-- return the count
return i
This will convert a ZSET
into the SET
of its members.
Now, lets choose a pattern. We can use whatever we want, but let's stick with the default (ie. __%s__
) one for now. We'll need to rename every appearence of a Redis command to match:
--[[
Time complexity: O(N + M) where N is the size of dest (might be 0),
and M the size of source
Space complexity: O(M) where is the size of source
Convert a ZSET into a SET of its members alone (ie. discard scores).
Note that dest and source may be the same keys, this has the effect of
removing scores from a ZSET.
USAGE: ZSET2SET dest source
RETURN: number of elements in dest
]]--
-- initialize counter
local i = 0
-- get the source ZSET
local src = redis.call('__ZRANGE__', KEYS[2], 0, -1)
-- clean up destination SET
redis.call('__DEL__', KEYS[1])
-- look for each element in the source
for _, v in pairs(src) do
-- increment the count
i = i + 1
-- add it
redis.call('__SADD__', KEYS[1], v)
end
-- return the count
return i
You're done! Now running redis-shield
will launch the Redis server, rename all the commands (except for EVALSHA
), apply the renaming to our file, and load it into the script cache, returning something like:
da39a3ee5e6b4b0d3255bfef95601890afd80709 /path/to/zset2set.lua
(that's the SHA1
of the empty string btw 😉 ).
Now it's up to you to pick that output up and perform a mapping like:
Script | Hash |
---|---|
zset2set |
da39a3ee5e6b4b0d3255bfef95601890afd80709 |
so that your application can make use of it using EVALSHA
.
Simple, right? 😄
Not so simple? No problem! Run redis-shield
with the -g
(ie. guess) option: it will find the occurrences for you most of the time (as with every other tool, blindly relying on its results without checking them is doomed to failure).
Squeamish still? Fret not! Running redis-shield
with the -w
(ie. watch) option will show you the rename-command
directives to be injected in the config and the result of replacing Redis commands inside the scripts passed.
Normally, an application (your application) stands between Redis and the big bad world ®️. But what if all security mechanisms fail and a malicious attacker gains access to the environment your application run on? It is under that attack scenario that redis-shield
was designed: to provide a tool to ensure consistency, even in the face of an attack that strong. Note that is consistency what is "secured", not the actual contents of the Redis dataset (Redis has no way of telling the attacker apart from your application).
In a normal case, your application will surely have an abstraction layer acting as an interface towards Redis, in order to provide for a "domain specific language" of sorts, so that you can say something along the lines of (PHP
code):
$user123 = Users::get(123);
$user123->lastLogin = time();
$user123->store();
instead of (phpredis
syntax):
$redis->hSet('user:123', 'lastLogin', time());
the former being more "high-levely" and, thus, closer to the application domain.
What redis-shield
does is basically provide you with the tools to implement something similar, but directly into Redis: you write the Lua scripts that act as interfaces to your data, and only allow calling them, thus only allowing modification through your (controlled) channels.
Incidentally, if an attacker were to gain access to the Redis pipe (be it a socket or a TCP connection), he would be unable to break the consistency (as imposed by your scripts) of the dataset. Do note though, that he may very well do a plethora of awful things to your data anyway: redis-shield
is just that, a shield, your dataset is shielded from inconsistency, not turned into Sigurd.