一、为什么要把Memcached的安全当回事

Memcached是个老牌的内存缓存工具,很多网站都在用。它把数据放在内存里,读写极快,能大大缓解数据库的压力。前些年网上闹得沸沸扬扬的反射放大攻击,就是有些人扫描了一大批没有锁门的Memcached端口,然后伪造源IP发个小请求,服务端却返回一大包数据,被用来轰炸目标机器。之所以这么容易中招,是因为Memcached当初的设计就是给内网用的,默认不设防,既不认证,也不限制来源。你把缓存数据搁在这种敞着门的服务里,就像把贵重物品放在没锁的抽屉里,保管是省事了,丢东西也省事了。

生产环境里,Memcached里存的东西常常不只是网页片段的缓存,还有用户登录凭证、验证码、临时订单状态等等。一旦被外部读取,轻则泄露隐私,重则账号被越权操作。如果被攻击者篡改,比如把购物车里的金额改小,那就更麻烦了。所以,访问控制和数据防护是上线前必须做的事。

我们这里用Python来演示各种加固操作。下面的代码中,凡是涉及启动服务、配置规则、读写缓存的,全部使用Python实现,技术栈是Python,配合标准库以及pymemcache客户端。

二、访问控制:别让闲人进你家

访问控制的核心就是回答一个问题:谁可以连接这台机器?这是安全的第一道门,也是最有效的一道。下面我们分几个方面来做。

2.1 绑定监听地址

Memcached默认监听0.0.0.0,也就是机器上的所有网卡都对外开放。在公网环境,这就等于把你的缓存赤裸裸地放在大街上。正确的做法是只监听本机回环地址,或者业务所在的内网网卡。如果你的应用和Memcached在同一台机器上,那么监听127.0.0.1就够了。如果分开部署,就用内网IP,比如10.0.0.8,而不是公网IP。

下面这段Python代码演示了启动Memcached时怎么指定监听地址:

import subprocess

def start_memcached(host='127.0.0.1', port=11211, memory=64, max_conns=1024):
    """
    启动Memcached并绑定指定监听地址。
    host     : 监听IP,生产环境请改成内网IP,不要用0.0.0.0
    port     : 端口号,默认11211
    memory   : 最大内存,单位MB
    max_conns: 最大并发连接数
    """
    # 组装启动命令,注意-l后面直接跟IP
    cmd = [
        'memcached',
        '-l', host,
        '-p', str(port),
        '-m', str(memory),
        '-c', str(max_conns),
    ]
    # 用Popen启动服务,让它后台运行
    subprocess.Popen(cmd)

# 调用示例:只监听本机,外部无法直接连接
start_memcached()

要注意的是,l参数如果设成127.0.0.1,那么其他机器就算能通过内网访问,也没法连接,安全性最高,但如果你有多台Web服务器需要共享一个Memcached,就得改成内网IP,并且通过防火墙限制来源。

2.2 防火墙和安全组

绑定IP只是第一道门槛,内网环境也不一定绝对安全。比如内网里一台机器被挖矿脚本攻破了,它可能会去扫描同一网段的其他服务。所以还要再加一道锁:防火墙或者云安全组。

如果是云服务器,建议直接使用安全组功能,设置一条入方向规则,只允许应用服务器的私有IP访问Memcached端口。这样最省事。要是自建机房,可以用iptables。我们同样用Python来封装iptables的操作,方便你在自动化部署时复用:

import subprocess

def allow_memcached_access(client_ip, port=11211, interface='eth0'):
    """
    用iptables只允许指定客户端IP访问Memcached端口。
    client_ip: 唯一允许访问的应用服务器IP,例如10.0.0.5
    port     : Memcached端口
    interface: 监听外网流量的网卡名,比如eth0
    """
    # 如果规则已经存在,先删除,避免重复添加
    subprocess.run([
        'iptables', '-D', 'INPUT', '-i', interface,
        '-p', 'tcp', '--dport', str(port),
        '-j', 'ACCEPT'
    ], stderr=subprocess.DEVNULL)

    # 第一条规则:放行这个客户端的请求
    subprocess.run([
        'iptables', '-A', 'INPUT', '-i', interface,
        '-s', client_ip,
        '-p', 'tcp', '--dport', str(port),
        '-j', 'ACCEPT'
    ])

    # 第二条规则:拒绝其他所有客户端访问这个端口
    subprocess.run([
        'iptables', '-A', 'INPUT', '-i', interface,
        '-p', 'tcp', '--dport', str(port),
        '-j', 'DROP'
    ])

# 示例:只让内网IP为10.0.0.5的机器访问
allow_memcached_access('10.0.0.5')

这段代码的顺序非常关键,必须先放行再拒绝。如果你把拒绝规则写在前面,那么放行规则就永远不会生效,所有请求都会被丢掉。另外,iptables规则重启后会消失,生产环境记得用iptables-save保存,或者直接使用云安全组。

2.3 启用SASL认证

防火墙和监听地址能挡住大部分网络层的入侵,但万一你的内网确实不太平,或者团队人多手杂,那就需要给Memcached加上用户名密码认证。Memcached支持SASL认证,前提是编译时开启了相关功能。启动的时候加上-S参数,然后给系统创建SASL账号。

先创建SASL用户。我们用Python来调用系统命令,并把密码通过标准输入传进去:

import subprocess

def create_sasl_user(username, password):
    """
    创建Memcached可用的SASL账号。
    username: 要创建的用户名
    password: 明文密码,生产环境请从配置中心或环境变量读取
    """
    # -a 指定应用名,-c 表示创建,-f 指定密码库文件,-p 表示从stdin读密码
    result = subprocess.run(
        [
            'saslpasswd2',
            '-a', 'memcached',
            '-c',
            '-f', '/etc/sasl2/memcached.db',
            '-p',
            username
        ],
        input=password.encode('utf-8'),
        capture_output=True
    )
    if result.returncode != 0:
        raise RuntimeError('创建SASL用户失败: ' + result.stderr.decode())

create_sasl_user('cache_user', 's3cret!password')

接着启动带认证的Memcached。注意要加-S参数:

import subprocess

def start_memcached_with_sasl(host='127.0.0.1', port=11211):
    """
    启动带SASL认证的Memcached。
    """
    subprocess.Popen([
        'memcached',
        '-l', host,
        '-p', str(port),
        '-m', '256',
        '-c', '1024',
        '-S'                     # 开启SASL认证
    ])

客户端连接时,pymemcache支持直接传账号密码。下面这段Python代码演示了如何带着用户名密码读写缓存:

from pymemcache.client.base import Client

# 连接时传入用户名和密码,Memcached会先做认证
client = Client(
    ('127.0.0.1', 11211),
    username='cache_user',
    password='s3cret!password'
)

client.set('theme', 'dark', expire=300)   # 5分钟过期
value = client.get('theme')
print(value.decode())                       # 输出: dark

注意,SASL的密码库文件权限要设置成只有root或Memcached运行用户可读,否则普通用户能直接读走密码库。另外,Memcached的SASL功能不是所有发行版都默认编译,使用之前先检查一下 memcached 的编译选项。

三、数据防护:就算被连上,也拿不到真东西

访问控制做得好,不代表数据完全安全。万一攻击者已经进了内网,或者你的应用层代码有漏洞被利用,缓存的数据还是可能被人拿到。所以数据本身也要穿好防弹衣。

3.1 合理设置过期时间

Memcached是缓存,不是数据库。数据到期自动消失是它的特性,也是它安全的优势。给每个缓存项设置合理的TTL,能有效降低敏感数据泄露后的影响面。比如用户验证码,5分钟就够;商品详情,1小时左右就好。不要图省事把过期时间设成一天甚至更久。

下面这段Python代码演示了带过期的缓存读写:

from pymemcache.client.base import Client

client = Client(('127.0.0.1', 11211))

# 设置短信验证码,300秒后过期
client.set('sms_code_13800138000', '4821', expire=300)

# 设置商品详情,3600秒后过期
client.set('product_detail_10001', '{"title":"手机"}', expire=3600)

# 读取验证码,如果已过期则返回None
code = client.get('sms_code_13800138000')
if code is None:
    print('验证码已过期,请重新发送')
else:
    print('当前验证码是:', code.decode())

代码里注释写得很清楚,没什么高深的地方。关键是养成习惯,所有键都要设置过期时间,别用0代表永久。除非你明确知道某个键必须一直存在,否则不要偷懒。

3.2 敏感字段加密存储

Memcached默认是明文传输,没有SSL/TLS。如果网络链路被监听,用户名、手机号、订单金额这些敏感信息会直接暴露。解决办法有两个方向:一是给网络加隧道,比如Stunnel;二是直接在业务层把数据加密后再写入缓存。后者更简单,不需要额外维护隧道,而且密钥可以控制在应用手里。

我们用Python的cryptography库演示一下AES对称加密。思路是:存之前先用Fernet加密,读出来后再解密。

from cryptography.fernet import Fernet
from pymemcache.client.base import Client

# 生产环境中密钥请从环境变量、KMS或配置中心读取,不要硬编码
key = b'4Im4r9F6kQw1pE2oZlXvYn7c1WzAaE0VxSiXJtL4dG0='
cipher = Fernet(key)

client = Client(('127.0.0.1', 11211))

# 原始敏感数据,例如用户手机号和密码哈希
plain_text = 'phone=13800138000&password_hash=8f9d8a...'

# 加密后存入缓存
encrypted_data = cipher.encrypt(plain_text.encode())
client.set('user_profile_123', encrypted_data, expire=1800)

# 读取时先取密文,再解密
stored_data = client.get('user_profile_123')
if stored_data is not None:
    # 注意verify,如果数据被篡改,Fernet会抛异常
    decrypted = cipher.decrypt(stored_data).decode()
    print('解密后的数据:', decrypted)

这段代码有几个细节要注意。第一,Fernet加密后的数据会包含时间戳和完整性校验,能防止别人篡改密文。第二,每次读缓存都要做一次解密计算,性能开销肯定是有的,所以只对高敏感字段使用,别把普通缓存也全部加密。第三,密钥的保存是安全重点,如果应用服务器被人拿下来,密钥也丢了,那加密就没意义了。

3.3 限制连接数和数据大小

这一招更多是为了防资源耗尽。攻击者即使没拿到数据,也可以建立大量连接或者写入超大对象,把Memcached的内存CPU耗尽,造成拒绝服务。启动时通过参数做硬性限制。

看这个Python启动函数:

import subprocess

def start_memcached_with_limits():
    """
    启动一个限制资源使用的Memcached实例。
    -m  最大内存512MB
    -c  最大并发连接数2000
    -I  单条缓存最大4MB
    -u  指定运行用户,避免用root
    """
    subprocess.Popen([
        'memcached',
        '-l', '10.0.0.8',
        '-p', '11211',
        '-m', '512',
        '-c', '2000',
        '-I', '4m',
        '-u', 'memcached'
    ])

start_memcached_with_limits()

这里特别强调一下,绝对不要用root身份运行Memcached。万一被利用,攻击者直接就获得了最高权限。给它一个权限很低的专用用户,就算中招,损失也小很多。限制数据大小也能防止超大payload把你的网络带宽打满。

四、监控与审计:随时知道发生了什么

安全配置不是一劳永逸,需要不断观察运行状态。Memcached提供stats命令,能告诉我们当前连接数、内存占用、命中率等关键信息。用Python写个小脚本,定期把这些数据拉下来做检查,一旦发现异常就告警。

from pymemcache.client.base import Client

def check_memcached_status(host='127.0.0.1', port=11211):
    """
    获取Memcached stats信息,并输出关键指标和告警。
    """
    client = Client((host, port))
    stats = client.stats()  # 返回字典,key和value都是bytes

    # 转换几个常用指标
    curr_connections = int(stats.get(b'curr_connections', b'0').decode())
    total_connections = int(stats.get(b'total_connections', b'0').decode())
    used_bytes = int(stats.get(b'bytes', b'0').decode())
    max_bytes = int(stats.get(b'limit_maxbytes', b'0').decode())

    print('当前连接数:', curr_connections)
    print('历史总连接数:', total_connections)
    print('当前内存使用:', used_bytes, 'bytes')
    print('最大内存:', max_bytes, 'bytes')

    # 简单告警
    if max_bytes > 0 and used_bytes / max_bytes > 0.8:
        print('警告:内存使用率超过80%')
    if curr_connections > 1500:
        print('警告:连接数过高,可能有异常流量')

    client.close()

check_memcached_status()

除了监控,日志也很重要。在排查安全问题时,Memcached可以使用-vv参数输出详细日志。但要注意,生产环境长期开-vv会造成大量日志堆积,所以只在怀疑被攻击时临时打开,或者结合日志系统做采样。

五、应用场景、优缺点与注意事项

5.1 适合用Memcached的场景

Memcached非常适合那些读多写少、允许临时丢失的数据。典型场景有:热点新闻列表、商品详情、用户会话信息、电商秒杀中的计数器。它的优势是性能极高,单机完全可以支撑高并发,而且水平扩展特别简单,加一台机器,改一下客户端配置就行。如果你的业务数据要求持久化,那就不适合用Memcached,应该考虑Redis等支持持久化的缓存组件。

5.2 优点和缺点

从安全角度看,Memcached的优点其实不少。它功能单一,没有那么多复杂命令和功能面,攻击面小;运行稳定,资源占用不大。缺点也很突出:第一,默认没有内置认证,SASL要额外开启且不是默认编译;第二,不支持SSL传输,数据裸奔;第三,数据不持久化,重启就丢,但这一点本身也是缓存该有的特性,不算安全问题,只是不适合做存储。想要安全,就得自己组合网络白名单、防火墙、加密隧道、业务层加密等多层手段。

5.3 注意事项

总结几条容易踩的坑:

第一,千万不能把Memcached的11211端口暴露到公网,也不要在公网环境监听0.0.0.0。第二,升级到最新稳定版本,旧版本可能有已知漏洞,网上能找到利用工具。第三,账号密码不要写在代码里,也不要写进配置仓库,要用环境变量或配置中心。第四,定期用安全扫描器扫描你的端口,看看有没有不该开的口子。第五,如果使用SASL,密码库文件必须做好权限控制。第六,监控和告警要跟上,别等内存被占满、服务被打挂了才后知后觉。第七,你在使用Memcached时,如果遇到启动参数或SASL编译方面的问题,先查看官方文档,确认你的版本真正支持这些能力,别在无效配置上浪费时间。

六、总结

Memcached的安全配置并不复杂,核心就是锁好门、收好货、看好家。锁好门是指控制监听地址、用防火墙或安全组白名单、必要时启用SASL认证。收好货是指给数据设置合理过期时间,对敏感信息做业务层加密,并且限制内存、连接数和数据大小,防止被耗尽。看好家是指用stats监控和日志审计,及时发现问题。每一项单独看都很简单,组合在一起就能构成比较可靠的安全防线。在生产环境里,这些工作一定要上线前就做好,别等出了事故再补救,到那时候可能就晚了。