# 一、简介
对于早期的redis,在安全访问控制方面非常的弱,使用默认配置的话,能访问redis服务的任何人都能连接上服务,进行数据的操作,简直就是在裸奔一样;
在redis2.2之前的版本最多也只能通过requirepass配置一个密码,任何知道密码的人也可以连接上redis进行删库跑路 😆 ,并且redis6之前的传输都是基于TCP明文传输的,直接抓个包就看到密码了,无密码可言;
为了防止一些人对redis操作非常危险的命令,比如删库命令flushall等,在redis2.2.0开始可以通过rename-command配置将危险的命令重命名为众人不知道的命令,减少危险操作;
而到了redis6增加了TLS加密传输,并且引入了ACL访问控制列表,非常细粒度的控制命令执行权限等,这样就不能想干啥就干啥,正如题目那样对于数据是相思而不得见,哈哈哈。

acl访问控制是基于帐号来的,对于不同的帐号可以设置不同的密码,不同的权限。帐号可以被创建、删除、禁用、启用,设置密码,清除密码等。而对于redis访问权限又分为命令的执行权限以及数据key的访问权限。
命令的执行权限细分为:
- 具体某个命令或其子命令的权限
- 某一类命令的权限
数据key的权限细分为:
- Pub/Sub channel
- key
二、ACL 规则简介
2.1 用户级别
2.1.1 启用/禁用用户
启用或者禁用一个user,禁用后,则不能使用此user进行登录,但是已经登录的连接不受影响。
on: 启用某个user
off: 禁用某个user
127.0.0.1:6379> auth user1 111
OK
127.0.0.1:6379> acl setuser user1 off
OK
127.0.0.1:6379> auth user1 111
(error) WRONGPASS invalid username-password pair or user is disabled.
127.0.0.1:6379> acl setuser user1 on
OK
127.0.0.1:6379> auth user1 111
OK
2.1.2 密码配置
2.1.2.1 添加密码
>password
通过>将密码password添加到用户上,而每个用户上都可以有任意多的密码,以链表的形式组织,存储的是密码的hash值。
2.1.2.2 删除密码
<password
从合法的密码列表中删除密码password,如果要删除的密码不存在,则会报错。
2.1.2.3 设置密码的hash
对于acl可以单独配置到acl.conf文件中,而对于密码明文存储的话很危险,所以可以设置密码的hash值进行存储,其中hash使用的256位,只能是小写的16进制,并且必须是64字节。
#hash
2.1.2.4 取消密码的hash
!hash
从有效的密码列表中删除hash的密码。
2.1.2.5 清除所有密码
nopass
清除当前用户的所有密码,并且设置flag,使用任何密码都可以认证通过。
2.1.2.6 重置密码
resetpass
此命令和nopass相比,多了清除flag,后续将没有密码,并且没有flag,不能进行登录认证,必须设置密码或则设置nopass标志。
2.2 命令级别
2.2.1 具体命令
2.2.1.1 授权命令执行
+命令
比如 +GET,此用户将有执行GET命令的权限
+命令|子命令
比如 +config|get将有config的get子命令执行权限,这样可以更细粒度的控制命令的执行,比如只开放某个命令的某个子命令执行权限。
+命令|第一个参数
指定第一个参数为配置的值才能执行此命令,否则此命令不能执行(即无执行权限)。注意这个方式只能使用 + 进行处理,而不能使用 - 来排除某个值。
比如 +SELECT|0。
2.2.1.2 禁用命令执行
-命令
禁止使用某个命令,比如-GET,将没有执行GET命令的权限。
-命令|子命令
禁止某个命令的子命令,比如 -config|set, 对于config命令,不能执行set子命令,即只能查看不能修改。
2.2.2 大类命令
redis中对每个命令都做了一个归类,分为read,write,set,sortedset,list,hash,string,bitmap,hyperloglog,geo,stream,pubsub,admin,fast,slow,blocking,dangerous,connection,transaction,scripting这些大类。可以对用户设置某一类的执行权限,而不用一个一个命令的设置,大大方便了配置等。
2.2.2.1 授权大类命令执行
+@大类名
比如 +@hash, 则有了对于hash类的命令的执行(hset,hsetnx,hget,hmset,hmget等)
可以通过 acl的cat子命令进程查看某个大类中具体有哪些命令
127.0.0.1:6379> acl cat hash
1) "hgetall"
2) "hdel"
3) "hkeys"
4) "hincrbyfloat"
5) "hrandfield"
6) "hset"
7) "hincrby"
8) "hvals"
9) "hmget"
10) "hstrlen"
11) "hscan"
12) "hget"
13) "hsetnx"
14) "hlen"
15) "hexists"
16) "hmset"
2.2.2.2 禁止大类命令执行
-@大类名
禁止某个大类中的所有命令的执行权限,比如 -@set,则对于集合相关的命令都不能执行。
2.2.3 所有命令
2.2.3.1 授权所有命令的执行权限
allcommands 和 +@all, 这两个相同的意思授权所有命令都有执行权限,其中的all这个是一个虚拟的大类,表示所有的命令。
隐式的设置了所有的命令,比如后续通过module增加的命令
2.2.3.2 禁止所有命令的执行权限
nocommands 和 -@all, 禁止所有命令的执行权限, 和allcommands,+@all相对。
2.3 数据级别
2.3.1 key
对于key,都是字符串,所有满足正则条件的key,都授予被访问权限,否则就没有访问权限,可以配置多个 即 ~pattern1 ~pattern2 ~pattern3 …。
2.3.1.1 满足正则条件的key
~pattern
满足正则条件的key可以被访问。比如~userid:*, 表示有权限访问userid:开头的任意key。
其中正则支持格式:
?,任意单个字符
*,零个或多个任意字符
[ab],列表中任意某个字符
[^abc],排除列表中的任意字符
[a-z],多个连续的字符可以简写为一个范围,匹配范围里任意一个字符
\转移字符,对以上的元字符进行转移
2.3.1.2 所有的key
~*
allkeys 是 ~*的别名,表示可以访问所有的key。
2.3.1.3 重置key
resetkeys
删除所有的key相关的配置,即所有的key都不能访问。
2.3.2 Pub/Sub channels
channel也和key类似,也是一个名字,字符串,所以也是通过正则表达式配置,满足正则表达式的名字的channel就有访问的权限,否则没有权限访问这些channel。
2.3.2.1 满足正则条件的channel
&pattern
其中pattern和key类似,但是少了一些。
其中正则支持格式:
?,任意单个字符
*,零个或多个任意字符
[ab],列表中任意某个字符
\转移字符,对以上的元字符进行转移
2.3.3.2 所有的channel
&*
allchannels 是&*的别名。有权访问所有的channel。
2.3.3.3 重置channel
resetchannels
如果某些client使用了没有权限的channel, 这些client将被断开连接。
三、命令简介
3.1 规则解析顺序
从左到右的依次解析,多个规则顺序任意,但又不能任意,如果有重叠部分,后面的规则覆盖前面的。
比如要实现规则排除SET命令的所有命令的执行权限
-SET +@all,这样+@all将覆盖-SET,这样的最终结果是所有命令都有执行权限,所以正确配置为 +@all -SET, 对于这种有包含关系的规则时要特别注意。
3.2 创建用户
acl setuser username acl规则
默认创建的用户是禁用的,需要后续使用on进行启用,并且没有命令执行权限,不能访问任何key,可以访问所有channel(channel是新增加的,为了向下兼容,所以默认有这个权限, 可以通过配置进行设置channel的默认行为)
127.0.0.1:6379> acl setuser zhangsan
OK
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user zhangsan off &* -@all"
也可以一次性将规则都写完整
127.0.0.1:6379> acl setuser lisi +@all -set -@hash -info -save ~pid:* &* on >123456
OK
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user lisi on #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~pid:* &* +@all -@hash -save -info -set"
3) "user zhangsan off &* -@all"
现在就可以使用新的用户进行登录
127.0.0.1:6379> auth lisi 123456
OK
3.3 删除用户
127.0.0.1:6379> acl deluser zhangsan
(integer) 1
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user lisi on #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~pid:* &* +@all -@hash -save -info -set"
127.0.0.1:6379>
default用户不能删除
将某个user删除后,使用此user登录的连接client都将被断开连接。
3.4 保存配置
acl save
此命令将配置的acl规则信息保存到单独的配置文件中,所以redis.conf中必须配置aclfile。
3.5 加载配置
acl load
此命令将从单独的acl.conf(通过配置获取的)文件中加载alc规则,因此必须提前配置aclfile文件。
3.6 列出所有用户
acl users
此命令列出当前redis中所有的用户名,包括禁用的。
127.0.0.1:6379> acl users
1) "default"
2) "zhangsan"
3.7 列出所有用户详细信息
acl list
此命令列出所有的用户以及详细的acl规则。
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user zhangsan off &* -@all"
3.8 获取某个用户信息
alc getusr 用户名
通过指定用户名可以获取对应用户的acl规则详细信息,和acl list类似,但是acl list是列出所有的,而且getuser返回结果是一个列表,list返回一个规则字符串。
127.0.0.1:6379> acl getuser zhangsan
1) "flags"
2) 1) "off"
2) "allchannels"
3) "passwords"
4) (empty array)
5) "commands"
6) "-@all"
7) "keys"
8) (empty array)
9) "channels"
10) 1) "*"
如下是一个简单的对比
| 子命令 | 参数个数 | 返回用户个数 | 返回形式 | 例子 |
|---|---|---|---|---|
| users | 0 | all | 所有用户名的列表 | 1) "default" 2) “zhangsan” |
| getuser1 | 1 | 1 | 拆分为列表的详细acl访问权限 | 1) "flags" 2) 1) "off" 3) "passwords" 4) (empty array) 5) "commands" 6) "-@all" 7) "keys" 8) (empty array) 9) "channels" 10) 1) “*” |
| list | 0 | all | 所有用户的acl规则列表 | 1) "user default on nopass ~* &* +@all" 2) “user zhangsan off &* -@all” |
3.9 获取acl日志
acl log [条数/reset]
此命令获取在redis运行过程中那些因为acl规则没有权限导致执行失败的日志,如果没有指定条数则返回10条的日志,否则返回对应条数(如果有10条,而要返回20条,则只能返回10条);或则设置reset参数,将日志清空。
127.0.0.1:6379> acl log
1) 1) "count"
2) (integer) 2
3) "reason"
4) "command"
5) "context"
6) "toplevel"
7) "object"
8) "acl"
9) "username"
10) "zhangsan"
11) "age-seconds"
12) "58.259999999999998"
13) "client-info"
14) "id=3 addr=127.0.0.1:43246 laddr=127.0.0.1:6379 fd=8 name= age=1956 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=22 qbuf-free=40932 argv-mem=6 obl=0 oll=0 omem=0 tot-mem=61462 events=r cmd=acl user=zhangsan redir=-1"
3.10 获取当前登录用户
acl whoami
此命令获取当前认证的用户是谁。
127.0.0.1:6379> acl whoami
"default"
3.11 生成指定位数的密码
acl genpass [位数]
此命令生成指定位数的密码,位数最大为4096,如果未指定,默认256。
127.0.0.1:6379> acl genpass
"5b2b83056e5fd4253a10351207ee214176b181103734f48ed9d12525983ccb21"
127.0.0.1:6379> acl genpass 10
"528"
3.12 帮助信息
acl help
此命令将详细的列出命令的使用说明。
127.0.0.1:6379> acl help
1) ACL <subcommand> [<arg> [value] [opt] ...]. Subcommands are:
2) CAT [<category>]
3) List all commands that belong to <category>, or all command categories
4) when no category is specified.
5) DELUSER <username> [<username> ...]
6) Delete a list of users.
7) GETUSER <username>
8) Get the user's details.
9) GENPASS [<bits>]
10) Generate a secure 256-bit user password. The optional `bits` argument can
11) be used to specify a different size.
12) LIST
13) Show users details in config file format.
14) LOAD
15) Reload users from the ACL file.
16) LOG [<count> | RESET]
17) Show the ACL log entries.
18) SAVE
19) Save the current config to the ACL file.
20) SETUSER <username> <attribute> [<attribute> ...]
21) Create or modify a user with the specified attributes.
22) USERS
23) List all the registered usernames.
24) WHOAMI
25) Return the current connection username.
26) HELP
27) Prints this help.
127.0.0.1:6379>
四、代码实现
4.1 数据结构描述
4.1.1 用户结构
#define USER_COMMAND_BITS_COUNT 1024
typedef struct {
sds name; //用户名
uint64_t flags; //一些标志,比如启用、禁用等
/*
* 可执行的命令的位图
* 每个命令分配了一个唯一id,id对应的bit位为1的表示有权限执行,否则没权限执行
*/
uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
/*
* 可执行的子命令的指针数组
* 每个命令的id对应数组下标,其子命令为一个链表
*/
sds **allowed_subcommands;
list *passwords; //密码链表,存储的hash
list *patterns; //key的规则链表;当规则为allkeys时,此链表为空,其他时候此链表为空,则没有key的访问权限
list *channels; //channel规则的链表;当规则时allchannels时,此链表为空,其他时候此链表为空,则没有channel的访问权限
} user;
其中位图最大为1024,所以最大命令个数为1024个,超过了则不能处理
4.1.2 client结构
每一个连接到redis服务器上的client中都有一个user指针和某个user信息进行关联。
typedef struct client {
...
user *user; /* User associated with this connection.
If the user is set to NULL the connection can do anything (admin). */
...
};
4.2 整体组织结构
使用了rax结构进行存储,能根据用户名快速的定位对应的用户对象。
其中rax见https://blog.youkuaiyun.com/happytree001/article/details/120165347
比如有Joanne,Kenyon,Diane,Barry,Bridgette这几个用户

4.3 redis启动时规则加载
4.3.1 创建default用户
int main(int argc, char **argv) {
...
ACLInit();
...
}
//全局变量
rax *Users;
user *DefaultUser;
list *UsersToLoad;
list *ACLLog;
/* Initialization of the ACL subsystem. */
void ACLInit(void) {
Users = raxNew();
UsersToLoad = listCreate();
ACLLog = listCreate();
ACLInitDefaultUser();
}
/* Initialize the default user, that will always exist for all the process
* lifetime. */
void ACLInitDefaultUser(void) {
DefaultUser = ACLCreateUser("default",7);
ACLSetUser(DefaultUser,"+@all",-1);
ACLSetUser(DefaultUser,"~*",-1);
ACLSetUser(DefaultUser,"&*",-1);
ACLSetUser(DefaultUser,"on",-1);
ACLSetUser(DefaultUser,"nopass",-1);
}
4.3.2 解析配置文件(redis.conf)
因为规则可以通过单独的acl.conf文件进行存储,也可以通过redis.conf文件中直接存储,但是这两种方式不能共存。如果redis.conf中有acl规则,则在进行合法性校验后,将合法的规则都放入到UsersToLoad链表中。
redis.conf中的规则和单独存放acl.conf文件内容规则都是一样的。
4.3.2.1 将rule加入到UsersToLoad链表
# redis.conf
...
user worker +@list +@connection ~jobs:* on >ffa9203c493aa99
...
int main(int argc, char **argv) {
...
loadServerConfig(server.configfile, config_from_stdin, options);
...
}
void loadServerConfig(char *filename, char config_from_stdin, char *options) {
...
loadServerConfigFromString(config);
...
}
void loadServerConfigFromString(char *config) {
...
for (i = 0; i < totlines; i++) {
...
else if (!strcasecmp(argv[0],"user") && argc >= 2) {
int argc_err;
if (ACLAppendUserForLoading(argv,argc,&argc_err) == C_ERR) {
...
goto loaderr;
}
}
...
}
...
}
int ACLAppendUserForLoading(sds *argv, int argc, int *argc_err) {
if (argc < 2 || strcasecmp(argv[0],"user")) {
if (argc_err) *argc_err = 0;
return C_ERR;
}
/* Try to apply the user rules in a fake user to see if they
* are actually valid. */
user *fakeuser = ACLCreateUnlinkedUser();
for (int j = 2; j < argc; j++) {
if (ACLSetUser(fakeuser,argv[j],sdslen(argv[j])) == C_ERR) {
if (errno != ENOENT) {
ACLFreeUser(fakeuser);
if (argc_err) *argc_err = j;
return C_ERR;
}
}
}
/* Rules look valid, let's append the user to the list. */
sds *copy = zmalloc(sizeof(sds)*argc);
for (int j = 1; j < argc; j++) copy[j-1] = sdsdup(argv[j]);
copy[argc-1] = NULL;
listAddNodeTail(UsersToLoad,copy);
ACLFreeUser(fakeuser);
return C_OK;
}
4.3.3 从链表中解析规则
第一步首先会判断链表和acl文件是否同时存在,同时存在则出错,启动失败。
int main(int argc, char **argv) {
...
ACLLoadUsersAtStartup();
...
}
首先判断单独的acl文件和redis.conf单独的acl规则是否同时存在,是则出错,启动失败,进程退出。
void ACLLoadUsersAtStartup(void) {
if (server.acl_filename[0] != '\0' && listLength(UsersToLoad) != 0) {
serverLog(LL_WARNING,
"Configuring Redis with users defined in redis.conf and at "
"the same setting an ACL file path is invalid. This setup "
"is very likely to lead to configuration errors and security "
"holes, please define either an ACL file or declare users "
"directly in your redis.conf, but not both.");
exit(1);
}
....
}
尝试从redis.conf文件中加载acl规则。
int ACLLoadConfiguredUsers(void) {
listIter li;
listNode *ln;
listRewind(UsersToLoad,&li);
while ((ln = listNext(&li)) != NULL) {
sds *aclrules = listNodeValue(ln);
sds username = aclrules[0];
if (ACLStringHasSpaces(aclrules[0],sdslen(aclrules[0]))) {
serverLog(LL_WARNING,"Spaces not allowed in ACL usernames");
return C_ERR;
}
user *u = ACLCreateUser(username,sdslen(username));
if (!u) {
u = ACLGetUserByName(username,sdslen(username));
serverAssert(u != NULL);
ACLSetUser(u,"reset",-1);
}
/* Load every rule defined for this user. */
for (int j = 1; aclrules[j]; j++) {
if (ACLSetUser(u,aclrules[j],sdslen(aclrules[j])) != C_OK) {
const char *errmsg = ACLSetUserStringError();
serverLog(LL_WARNING,"Error loading ACL rule '%s' for "
"the user named '%s': %s",
aclrules[j],aclrules[0],errmsg);
return C_ERR;
}
}
/* Having a disabled user in the configuration may be an error,
* warn about it without returning any error to the caller. */
if (u->flags & USER_FLAG_DISABLED) {
serverLog(LL_NOTICE, "The user '%s' is disabled (there is no "
"'on' modifier in the user description). Make "
"sure this is not a configuration error.",
aclrules[0]);
}
}
return C_OK;
}
4.3.4 从acl.conf单独文件中加载
如果是配置了aclfile指定单独的acl文件,并且redis.conf文件中没有单独的acl规则。则从单独的acl文件中进行解析加载。具体的加载过程在后续的load命令解析时再讲解。
if (server.acl_filename[0] != '\0') {
sds errors = ACLLoadFromFile(server.acl_filename);
if (errors) {
serverLog(LL_WARNING,
"Aborting Redis startup because of ACL errors: %s", errors);
sdsfree(errors);
exit(1);
}
}
4.4 命令解析
4.4.1 setuser
void aclCommand(client *c) {
char *sub = c->argv[1]->ptr;
if (!strcasecmp(sub,"setuser") && c->argc >= 3) {
...
}
...
}
4.4.1.1 检查用户名合法性
对于用户名来说,不能有空格和null字符。
sds username = c->argv[2]->ptr;
/* Check username validity. */
if (ACLStringHasSpaces(username,sdslen(username))) {
addReplyErrorFormat(c,
"Usernames can't contain spaces or null characters");
return;
}
//检查字符串中是否包含空格或则null字符
int ACLStringHasSpaces(const char *s, size_t len) {
for (size_t i = 0; i < len; i++) {
if (isspace(s[i]) || s[i] == 0) return 1;
}
return 0;
}
4.4.1.2 创建临时用户
这里创建临时用户,后续的解析修改都是在这个临时对象上进行,不影响现有正常数据,如果其中某个参数解析错误等,直接释放临时对象,然后返回即可,并且因为redis没有回滚操作,这样也保证了每条命令执行的原子性。
如果是已经存在的对象,则将对象的数据复制到这个临时对象中,后续修改都在这个临时用户上,等完全修改成功后,再将临时对象的数据复制回原有对象。
...
user *tempu = ACLCreateUnlinkedUser();
user *u = ACLGetUserByName(username,sdslen(username));
if (u) ACLCopyUser(tempu, u);
...
创建一个临时用户,此函数是一个死循环,直到创建成功,而和普通的创建用户对象的不同之处只是将对象从rax树上删除,即没有和任何结构进行关联。
user *ACLCreateUnlinkedUser(void) {
char username[64];
for (int j = 0; ; j++) {
snprintf(username,sizeof(username),"__fakeuser:%d__",j);
user *fakeuser = ACLCreateUser(username,strlen(username));
if (fakeuser == NULL) continue;
int retval = ra

本文概述了Redis从早期的安全漏洞,到2.2版本引入的密码保护、危险命令重命名,再到Redis 6的TLS加密和ACL访问控制,展示了如何从无防护到实现精细数据权限控制的过程。
最低0.47元/天 解锁文章
137

被折叠的 条评论
为什么被折叠?



