虽然是定番,但还是要强调:不该暴露的服务不暴露,不该做出的假设不做出。先把安全做好,再上业务逻辑。
2025-05-17 22 时左右,我们的团队发现游戏后台在报数据库写入失败的错误。我看了之后发现一个恐怖的事情:
无法写入数据库
prod: 数据库不存在
当我登上 MySQL 控制台查看的时候:
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| RECOVER_YOUR_DATA |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
我们的数据库被骇客勒索了
这是一台新的裸金服务器,我们才刚将生产环境迁移过来不到十小时,许多安全加固都还没有实装。没想到这么快就吃了开门红。
从攻击手法上,不难看出这是自动化脚本攻击。互联网每时每刻都有闲着蛋疼不断地扫各种漏洞、爆破弱口令的脚本在跑。如果配置错误,那么你离中招也许只有五分钟的距离。
有意思的是:这个傻逼居然开口要价 0.0082 BTC, 折合超过十万美刀,这也太看得起我们了。俺们这种为爱发电的是真掏不起。
虽然我们配置了异地冷备,但冷备毕竟是冷备——它每 24 小时产生一次。所以我们丢失了今天产生的数据。得,那只能认栽。该恢复恢复,该给玩家赔偿赔偿。
根本原因:数据库弱口令
最开始这个服务器配置的时候,开发人员为了省事,不仅使用了 root 敢死队,还配置了一个弱口令。就是 6 个数字的那种弱口令。后来由于使用数据库的程序越来越多,等我开始执行安全加固的时候已经积重难返,所以敢死队和弱口令就一直延续地用了下去。
这一遭之后,我们换了强口令,然后给各个程序重新配置了数据库访问凭证。人教人教不会,事教人一遍通,大概就是这么个道理。
直接原因:防火墙未到位
这里有三个层面的防火墙均未到位。三层防火墙中如果有任何一个到位,那么它就可以抵御这样的防御。
第一层防火墙是安全组。在之前,我们的服务器是在一层 NAT 之后的。如果要放行端口,则必须通过厂商的控制面板配置映射来放行。这实际上是一层防火墙——虽然难用了一点,但它可以说是抵御绝大部分自动攻击的最外围的防线。切到裸金服务器之后,我们实际上就没有了厂商安全组设置了。
第二层防火墙是系统防火墙。新系统并没有默认启用防火墙,而我也没有去主动配置防火墙。这就导致了当自动攻击脚本来扫我们的服务器的时候,服务器处在一个大门敞开的状态。
第三层防火墙是应用级访问控制。root 账号本来应该是只能通过本地访问的,但是由于我们的程序运行在主机内,但数据库用了 Docker, 所以 root 账号的默认策略变成了允许任何 IP 地址登录(毕竟从容器里来看,从主机中的登录就是来自于一个外部网络)。这使得自动攻击脚本得以对我们的数据库执行爆破攻击。
除此之外,我们还可以配置的:
- fail2ban 检查数据库登录行为,封禁失败过多的 IP
- TCP 并发连接检查,封禁短时间内尝试建立过多连接的 IP
- ……
总之,这个锅确实在我。
ufw 和 Docker
即使配置了防火墙,也万不可掉以轻心。尤其是如果你的系统使用的是 ufw 的情况下,错误的 Docker 配置仍然会将你的服务暴露于风险之中。
考虑这样一个配置:
services:
db:
image: postgres:16-alpine
ports:
- 5432:5432
# 余下从略
如果你使用 ufw, 那么这么写会直接将端口 5432 暴露于公网访问之中,即使你从未主动开放过这个端口:
# ufw status
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere
80/tcp (v6) ALLOW Anywhere
# _
正确的写法是:
services:
db:
image: postgres:16-alpine
ports:
- "127.0.0.1:5432:5432"
# 余下从略
这样可以保证它只接受来自本地的连接。
我实在是懒得上 ufw-docker 之类的东西了。最理想的情况是 Docker 完全不需要暴露端口,而是将所有的程序都塞到 Docker 里,然后直接通过 Docker 的内部网络实现通联:
# in db/docker-compose.yml
services:
db:
image: postgres:16-alpine
networks:
- db
# 余下从略
networks:
db:
name: db
external: true
# in app/docker-compose.yml
services:
app:
image: prod.image.my.company.arpa/game-group/the-game:1
ports:
- 23333:23333 # 这个是刻意暴露给外部的端口
networks:
- app
- db
# 余下从略
networks:
db:
name: db
external: true
app:
name: app
external: true
这样即使我家大门常打开,那最多就是进一个空无一物的院子里看看。各个房间的门是直接焊死的。
正在加载评论……