安全地使用 Redis(上):端口安全、指令安全和内存使用限制


本来规划开始更新 Redis 底层实现、数据结构和高性能原理了:

-w577

不过考虑到如何安全使用 Redis 也是这个比较基础的东西,新手如果配置不当,很容易造成线上的 Redis 服务处于「裸跑」状态,被黑客恶意攻击,导致 Redis 服务不可用,进而导致依赖 Redis 服务的 Session、缓存、队列、分布式锁等业务功能瘫痪,造成严重的生产事故,所以在深入探索 Redis 底层原理和集群构建之前,学院君准备给大家插播下 Redis 的安全使用。

Redis 的安全隐患

线上运行的 Redis 服务主要有哪些安全隐患呢?

和 MySQL 不同,Redis 默认是没有配置密码认证的,如果为了方便运维,开放了监听的客户端地址限制(默认只监听来自 127.0.0.1 的请求),则相当于把 Redis 服务裸跑在公网服务器上,所有人都可以通过默认的 6379 端口与之建立连接并发送请求,甚至运行 flushdb 之类的命令清空 Redis 数据库的所有内容,是不是细思极恐?

此外,如果应用使用了 Redis 构建缓存系统,通常会在应用发布内容后新增缓存,如果我们没有对用户发布内容做频率限制,或者没有防范恶意用户批量发布的垃圾内容,很可能造成高频的写入操作耗尽 Redis 内存,导致 Redis 服务不可用,如果没有限制 Redis 的内存使用量,甚至导致 Redis 所在服务器内存耗尽而不可用。

最后,Redis 客户端与服务端的通信是明文传输的,并且官方也没有提供类似 HTTPS 这种基于 SSL 的加密技术支持,所以如果通信内容被恶意用户窃取并篡改,也存在安全隐患。

以上这些都是一些最基本最常见的 Redis 安全隐患,要规避这些问题,我们该怎么做呢?

端口安全

首先,我们可以通过配置 Redis 来确保服务端的端口安全,阻止恶意用户建立连接。这里需要关注 Redis 服务端配置文件的两个配置项。

限制客户端 IP

以 Ubuntu 系统为例,使用 apt install redis-server 安装的 Redis 服务配置文件默认位于 /etc/redis/redis.conf,这里面有一个 bind 配置项用于配置 Redis 服务监听的客户端 IP 地址:

bind 127.0.0.1 ::1

只有来自该配置项配置的客户端才能与基于这个配置文件启动的 Redis 服务端建立连接,默认值是 127.0.0.1(::1 表示本地 IP 地址的 IPv6 格式,前者是 IPv4 格式,具体细节可以参考学院君网络协议系列中更新的内容),即只有 Redis 服务所在的服务器才能与之建立连接。如果你的应用部署在和 Redis 服务端不同的机器,可以将其调整为对应机器的内网/公网 IP 地址,如果有多个客户端应用,可以通过空格分隔多个 IP 地址。

配置客户端认证密码

如果你想要开放这个限制(注释掉 bind 配置项即可),比如对于一些小公司,小应用,想要在本地查看 Redis 服务端的键值对信息,则可以通过另一个配置项 requirepass 配置密码对连接进行认证来提高安全性:

# requirepass foobared

默认这个配置项是注释起来的,你可以取消整个注释,然后把 foobar 修改成自己的密码。修改之后,需要重启 Redis 服务使其生效,这样一来,当你再次试图与 Redis 服务端交互,就需要认证了:

-w775

你可以通过 Redis 提供的 auth 指令传递配置的密码进行认证,认证失败会报错,认证成功,则返回 OK:

-w763

认证通过后才能与 Redis 服务端进行正常的交互:

-w752

对于 Laravel 应用而言,要配置 Redis 服务端认证密码,可以在 .env 中配置 REDIS_PASSWORD 选项来完成,非常简单:

REDIS_PASSWORD=password

另外,在 redis.conf 中,你可能还会看到另一个密码相关的配置项 masterauth,该配置项用于在 Redis 从库中配置主库通过 requirepass 设置的认证密码,通过认证后才能在从库进行后续的主从同步操作。

关于端口安全问题造成的线上事故案例,可以阅读学院君之前发布的这篇教程:Laravel 学院今天凌晨四点到上午十点不能访问问题定位及修复细节通报

指令安全

前面我们介绍 Redis 安全隐患的时候提到恶意用户建立连接后可能执行 flushdb 之类的指令清空 Redis 内存数据库,现在,我们已经通过配置 bind 或者 requirepass 配置项阻断了恶意用户与线上 Redis 服务建立连接的可能,但是如果是自己不小心在线上 Redis 中执行了 flushdb 指令呢,这个是有可能出现的,就像 MySQL 中的误删除一样。

为了规避这个问题,我们可以在 Redis 服务端配置文件中通过 rename-command 配置项对这种危险指令进行重命名:

rename flushdb flush_this_db
rename flushall flush_all_dbs

其中 flushdb 用于清空当前数据库,flushall 用于清空所有数据库。

这样一来相当于在清空前做了一个确认操作,避免「一失手成千古恨」:

-w858

当然了,如果你觉得这些命令实在不安全,还可以通过如下方式将其彻底禁用:

rename flushdb ""
rename flushall ""

内存使用与写入频率限制

接下来,我们来看如何避免大量高频的写入导致 Redis 内存耗尽的问题。

内存限制和淘汰策略

作为兜底,我们先在 Redis 服务端配置文件 redis.conf 中通过 maxmemory 配置项配置 Redis 可用的服务器内存上限:

# maxmemory <bytes>

这个配置项默认是注释起来的,说明没有做限制,随着 Redis 占用内存的飙升最终会导致服务器内存耗尽,进而导致部署在这台机器的其他服务,比如数据库、Web 应用等不可用。一般而言,如果这台机器只部署了 Redis 服务,可将其配置为系统内存总量的 80%,否则的话根据其他服务占用内存的情况酌情进行分配即可。

配置了 maxmemory 后,一般还要配置 maxmemory-policy 配置项,用于设置 Redis 内存使用量到达上限后的内存清理策略,目前支持以下策略选项:

  • noeviction:当 Redis 可用内存不足以存放新写入数据时,新写入操作会报错;
  • allkeys-lru:当 Redis 可用内存不足以存放新写入数据时,移除最近最少访问的 key;
  • allkeys-lfu:当 Redis 可用内存不足以存放新写入数据时,移除最近访问频次最少的 key;
  • allkeys-random:当 Redis 可用内存不足以存放新写入数据时,随机移除某个 key;
  • volatile-lru:当 Redis 可用内存不足以存放新写入数据时,在设置了过期时间的 key 中,移除最近最少访问的 key;
  • volatile-lfu:当 Redis 可用内存不足以存放新写入数据时,在设置了过期时间的 key 中,移除最近访问频次最少的 key;
  • volatile-random:当 Redis 可用内存不足以存放新写入数据时,在设置了过期时间的 key 中,随机移除某个 key;
  • volatile-ttl:当 Redis 可用内存不足以存放新写入数据时,在设置了过期时间的 key 中,优先移除即将要过期的 key。

通常,我们会选择基于 LRU 淘汰算法 LFU 模式的内存清理策略,比如 volatile-lfu,关于 Redis LRU 淘汰算法,后面在 Redis 底层原理篇我会详细介绍。

写入频率限制

以上是 Redis 底层的内存兜底策略,主要是为了避免服务器内存耗尽导致所有服务不可用,在上层业务代码中,我们也要合理设置对 Redis 的写入频率,尽可能避免内存使用飙升的情况出现,尤其是恶意用户通过机器人发布垃圾信息这种非常规操作。你可以通过对用户的合法性进行校验,以及对发布内容进行数量限制和频率限制,来有效规避这种情况出现,具体细节,不属于 Redis 系列的讨论范畴,这里就不详细展开了。

关于恶意用户批量发布内容导致 Redis 内存耗尽的案例,可以阅读学院君之前发布的这篇教程:Laravel 学院今天下午两点半到三点半期间不能访问问题定位及修复细节报告

最后,我们来看如何对 Redis 客户端与服务端的通信内容进行加密,避免数据被窃取和篡改,限于篇幅,学院君将在下篇教程给大家详细演示。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 基于 Redis 实现 Laravel 分布式 Session 存取及底层源码探究

>> 下一篇: 安全地使用 Redis(下):基于 Spiped 代理对通信进行加密