1. 事件背景
今天排查 一个演示项目前端登录超时问题时,发现登录接口一直等待后端响应,最终超时。继续查看后端日志后,核心错误是:
Unknown database 'admin_db'
HikariPool-1 - Connection is not available, request timed out after 30008ms
进入 MySQL 后发现,原本应该存在的 admin_db 数据库已经消失,数据库里多了一个 RECOVER_YOUR_DATA 数据库,里面是一段典型勒索信息,大意是:数据已被备份,要求支付比特币,否则公开或删除数据。
这类攻击并不复杂,通常不是“高级黑客入侵”,而是自动化扫描器批量扫描公网 IP,发现 MySQL、Redis、MongoDB、Elasticsearch、Docker API 等服务暴露在公网后,尝试弱口令登录,成功后执行删除、加密、勒索等操作。
本次事件的直接原因是:
ports:
- "3307:3306"
- "6380:6379"
- "8080:8080"
这类写法会把容器端口绑定到 0.0.0.0,也就是对公网开放。再叠加演示项目里的弱密码。
攻击者只需要扫到端口并尝试常见密码,就可能直接进入数据库。
2. 先记住一个原则:数据库、缓存、内部 API 不应该裸露公网
对公网开放的服务应该尽量少。一般情况下,真正需要公开给用户访问的只有:
- 80:HTTP
- 443:HTTPS
- 必要时的 SSH,但建议限制来源 IP 或修改登录策略
不应该直接暴露公网的服务包括:
- MySQL:3306、3307
- Redis:6379、6380
- PostgreSQL:5432
- MongoDB:27017
- Elasticsearch:9200
- Spring Boot 后端接口:8080
- Docker API:2375
- RabbitMQ 管理后台:15672
- Nacos、Consul、Etcd 等配置中心
Web 项目推荐架构是:
公网用户
↓
Nginx / HTTPS / WAF
↓
前端静态资源 + /api 反向代理
↓
后端服务:只允许内网或本机访问
↓
MySQL / Redis:只允许内网容器访问或本机访问
也就是说,用户访问的是 Nginx,Nginx 再代理到后端,后端再访问数据库。数据库不应该直接面对互联网。
3. Docker Compose 端口绑定的正确写法
很多人以为下面这种写法只是“本机映射端口”:
ports:
- "3307:3306"
实际上它等价于:
ports:
- "0.0.0.0:3307:3306"
也就是所有网卡都监听,公网能访问。
如果只是为了本机调试,可以绑定到 127.0.0.1:
services:
mysql:
image: mysql:8.0
ports:
- "127.0.0.1:3307:3306"
redis:
image: redis:7-alpine
ports:
- "127.0.0.1:6380:6379"
admin-server:
build:
context: ./admin-server
ports:
- "127.0.0.1:8080:8080"
admin-web:
build:
context: ./admin-web
ports:
- "80:80"
这样:
- 外部用户只能访问 80 端口。
- MySQL 只能从服务器本机访问
127.0.0.1:3307。 - Redis 只能从服务器本机访问
127.0.0.1:6380。 - 后端 8080 也只允许本机访问。
- 前端 Nginx 通过 Docker 内部网络访问后端容器。
如果数据库只给容器内部访问,甚至可以完全去掉 ports,改用 expose 或什么都不写:
services:
mysql:
image: mysql:8.0
expose:
- "3306"
expose 只在 Docker 网络内部可见,不会发布到宿主机公网。
4. 服务器防火墙必须开启
即使 Docker Compose 写对了,也建议在宿主机层面再加一层防火墙。常见方案有 ufw、firewalld、云厂商安全组。
Ubuntu / Debian 使用 ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
如果 SSH 只从固定 IP 登录,更推荐:
sudo ufw delete allow 22/tcp
sudo ufw allow from 你的办公公网IP to any port 22 proto tcp
检查当前监听端口:
ss -lntup
检查 Docker 暴露端口:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
看到类似下面这种输出要警惕:
0.0.0.0:3307->3306/tcp
0.0.0.0:6380->6379/tcp
0.0.0.0:8080->8080/tcp
更安全的输出应该是:
127.0.0.1:3307->3306/tcp
127.0.0.1:6380->6379/tcp
127.0.0.1:8080->8080/tcp
0.0.0.0:80->80/tcp
5. 数据库账号和密码策略
演示环境常见密码:
root / root
root / 123456
root / root123
admin / admin
admin / admin123
mysql / mysql
这些密码在公网环境等于没有密码。
建议:
- root 密码至少 20 位,随机生成。
- 业务用户不要用 root。
- 每个项目单独一个数据库用户。
- 业务用户只授权自己的数据库。
- 不允许远程 root 登录,除非确实需要。
生成随机密码:
openssl rand -base64 32
创建业务数据库和用户示例:
CREATE DATABASE admin_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'admin_app'@'%' IDENTIFIED BY '替换成强密码';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON admin_db.* TO 'admin_app'@'%';
FLUSH PRIVILEGES;
如果是生产系统,不建议轻易授予 DROP 权限。很多勒索攻击第一步就是 DROP DATABASE。
6. Redis 必须设置密码或只允许内网访问
Redis 经常被误认为只是缓存,实际上暴露公网后风险很高。攻击者可能利用 Redis 写入 SSH key、写入计划任务、写入 WebShell,甚至进一步拿到服务器权限。
最小安全要求:
- 不暴露公网。
- 设置强密码。
- 禁用危险命令或重命名危险命令。
- 开启 protected-mode。
示例配置:
bind 127.0.0.1
protected-mode yes
requirepass 替换成强密码
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
Docker Compose 中如果 Redis 只给后端容器用,可以不写 ports。
7. 后端接口也不要直接暴露公网
很多 Spring Boot 项目直接把 8080 暴露出去:
ports:
- "8080:8080"
这会带来几个问题:
- 绕过前端 Nginx 的访问控制。
- Swagger、Actuator、错误堆栈等调试端点可能被扫到。
- HTTPS、限流、日志、安全头都不好统一治理。
更推荐:
admin-server:
ports:
- "127.0.0.1:8080:8080"
然后由 Nginx 代理:
location /api/ {
proxy_pass http://admin-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
这样外部用户只看到 /api,看不到内部服务端口。
8. Nginx 也要做基础防护
Nginx 不只是转发,还应该承担入口防护职责。
限制请求体大小
client_max_body_size 20m;
基础安全头
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy no-referrer-when-downgrade always;
简单限流
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://admin-server:8080/;
}
}
}
屏蔽常见扫描路径
location ~* /(\.env|\.git|wp-admin|phpmyadmin|pma|actuator|swagger-ui) {
return 404;
}
注意:屏蔽扫描路径不是根本安全措施,只是减少噪音。真正关键还是不要暴露内部服务。
9. SSH 加固
SSH 是服务器管理入口,也必须重点保护。
建议修改 /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
然后重启 SSH:
sudo systemctl restart ssh
如果暂时不能关闭密码登录,至少安装 fail2ban:
sudo apt update
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
10. 备份:没有备份就没有数据安全
这次 MySQL 被删后,如果没有备份,所谓“恢复”只能靠重新初始化演示数据。生产环境绝不能这样。
推荐备份策略:
- 每天至少一次数据库备份。
- 保留最近 7 天每日备份。
- 每周保留一个周备份。
- 备份文件不要只放在同一台服务器上。
- 定期做恢复演练。
MySQL 备份示例:
#!/usr/bin/env bash
set -euo pipefail
DATE=$(date +%F_%H%M%S)
BACKUP_DIR=/backup/mysql
mkdir -p "$BACKUP_DIR"
docker exec admin-mysql mysqldump \
-uroot \
-p'替换成root强密码' \
--single-transaction \
--routines \
--triggers \
admin_db \
| gzip > "$BACKUP_DIR/admin_db_$DATE.sql.gz"
find "$BACKUP_DIR" -type f -name '*.sql.gz' -mtime +14 -delete
crontab 示例:
0 3 * * * /usr/local/bin/backup-mysql.sh >> /var/log/backup-mysql.log 2>&1
恢复示例:
gunzip -c /backup/mysql/admin_db_2026-08-04_030000.sql.gz \
| docker exec -i admin-mysql mysql -uroot -p'替换成root强密码' admin_db
11. 日志和监控
被攻击后,如果没有日志,很难判断发生了什么。至少保留以下日志:
- Nginx access/error log
- 应用日志
- MySQL 错误日志
- SSH 登录日志
- Docker 容器日志
常用排查命令:
# 查看最近登录
last -a | head -30
# 查看失败登录
sudo grep "Failed password" /var/log/auth.log | tail -50
# 查看当前监听端口
ss -lntup
# 查看容器端口
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
# 查看 Nginx 最近访问
docker logs --tail=100 admin-web
# 查看后端最近日志
docker logs --tail=100 admin-server
推荐增加监控告警:
- 磁盘使用率超过 80% 告警。
- 数据库连接失败告警。
- 关键进程退出告警。
- 备份失败告警。
- 重要端口暴露变化告警。
12. 被删库后的应急处理流程
如果你发现数据库被删、被加密、被勒索,建议按下面步骤处理。
第一步:立即止血
# 临时关闭高风险服务端口
sudo ufw deny 3306/tcp
sudo ufw deny 3307/tcp
sudo ufw deny 6379/tcp
sudo ufw deny 6380/tcp
sudo ufw deny 8080/tcp
或者先把 Docker 服务停掉:
docker compose stop
第二步:保留证据
不要急着清空所有东西。先保存:
mkdir -p incident-logs
docker ps -a > incident-logs/docker-ps.txt
docker logs admin-mysql > incident-logs/admin-mysql.log 2>&1 || true
docker logs admin-server > incident-logs/admin-server.log 2>&1 || true
docker logs admin-web > incident-logs/admin-web.log 2>&1 || true
ss -lntup > incident-logs/ports.txt
last -a > incident-logs/last-login.txt
第三步:判断影响范围
重点检查:
- 是否只有数据库被删。
- 是否服务器账号被登录。
- 是否有异常 crontab。
- 是否有异常 SSH key。
- 是否有异常 Docker 容器。
- 是否有陌生进程或挖矿程序。
命令示例:
crontab -l
sudo ls -la /root/.ssh
ps aux --sort=-%cpu | head
ps aux --sort=-%mem | head
docker ps -a
第四步:恢复服务
如果确认只是数据库被删:
- 先修复端口暴露问题。
- 修改所有弱密码。
- 从备份恢复数据库。
- 重启服务。
- 验证业务链路。
如果怀疑服务器已被拿到 shell,建议直接重装系统,再从可信备份恢复。
13. 一份可以直接照做的加固清单
Docker 层
- MySQL 不绑定
0.0.0.0。 - Redis 不绑定
0.0.0.0。 - 后端 API 不直接暴露公网。
- 只保留 Nginx 的 80/443 对外。
- 删除无用容器和无用镜像。
- 不把
.env、密钥、备份文件打进镜像。
系统层
- 开启防火墙。
- 安全组只开放 22、80、443。
- SSH 禁止 root 密码登录。
- 安装 fail2ban。
- 定期更新系统补丁。
数据库层
- root 使用强密码。
- 业务账号最小权限。
- 生产环境不使用默认密码。
- 开启定时备份。
- 定期验证备份可恢复。
应用层
- 登录接口有失败次数限制。
- 管理后台使用强密码。
- JWT secret 不使用演示值。
- Swagger、Actuator 不对公网开放。
- 上传接口限制文件类型和大小。
运维层
- 每周检查开放端口。
- 每周检查 Docker 暴露端口。
- 每天检查备份结果。
- 关键服务异常有告警。
- 记录变更日志。
14. 本次项目的修复示例
本次演示项目已将内部服务改为仅本机监听:
mysql:
ports:
- "127.0.0.1:3307:3306"
redis:
ports:
- "127.0.0.1:6380:6379"
admin-server:
ports:
- "127.0.0.1:8080:8080"
admin-web:
ports:
- "80:80"
当前容器端口状态类似:
admin-web 0.0.0.0:80->80/tcp
admin-server 127.0.0.1:8080->8080/tcp
admin-mysql 127.0.0.1:3307->3306/tcp
admin-redis 127.0.0.1:6380->6379/tcp
这只是第一步。后续还应该继续完成:
- 更换 MySQL root 和业务用户密码。
- 更换 Redis 密码或取消 Redis 端口映射。
- 更换 JWT secret。
- 配置服务器防火墙或云安全组。
- 增加 MySQL 自动备份。
- 如果上线生产环境,补充 HTTPS。
15. 总结
服务器安全不是买一台云服务器、跑一个 Docker Compose 就结束了。只要服务暴露在公网,就会被自动化扫描器持续探测。
这次 MySQL 被删给出的教训很直接:
- 不要把数据库暴露到公网。
- 不要使用演示密码。
- 不要让后端接口绕过 Nginx 直接暴露。
- 不要没有备份。
- 不要以为“小项目没人攻击”。
小项目更容易被攻击,因为它们往往使用默认密码、默认端口、默认配置,而且缺少监控和备份。
真正可靠的做法是:公网只开放必要入口,内部服务只走内网;账号使用最小权限;每天备份并能恢复;定期检查端口、日志和安全组。
只要把这些基础工作做好,大多数批量扫描和弱口令攻击都能被挡在门外。

9110

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



