从一次 MySQL 数据库被删说起:服务器防攻击实战指南

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 写对了,也建议在宿主机层面再加一层防火墙。常见方案有 ufwfirewalld、云厂商安全组。

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

这些密码在公网环境等于没有密码。

建议:

  1. root 密码至少 20 位,随机生成。
  2. 业务用户不要用 root。
  3. 每个项目单独一个数据库用户。
  4. 业务用户只授权自己的数据库。
  5. 不允许远程 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

第四步:恢复服务

如果确认只是数据库被删:

  1. 先修复端口暴露问题。
  2. 修改所有弱密码。
  3. 从备份恢复数据库。
  4. 重启服务。
  5. 验证业务链路。

如果怀疑服务器已被拿到 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

这只是第一步。后续还应该继续完成:

  1. 更换 MySQL root 和业务用户密码。
  2. 更换 Redis 密码或取消 Redis 端口映射。
  3. 更换 JWT secret。
  4. 配置服务器防火墙或云安全组。
  5. 增加 MySQL 自动备份。
  6. 如果上线生产环境,补充 HTTPS。

15. 总结

服务器安全不是买一台云服务器、跑一个 Docker Compose 就结束了。只要服务暴露在公网,就会被自动化扫描器持续探测。

这次 MySQL 被删给出的教训很直接:

  • 不要把数据库暴露到公网。
  • 不要使用演示密码。
  • 不要让后端接口绕过 Nginx 直接暴露。
  • 不要没有备份。
  • 不要以为“小项目没人攻击”。

小项目更容易被攻击,因为它们往往使用默认密码、默认端口、默认配置,而且缺少监控和备份。

真正可靠的做法是:公网只开放必要入口,内部服务只走内网;账号使用最小权限;每天备份并能恢复;定期检查端口、日志和安全组。

只要把这些基础工作做好,大多数批量扫描和弱口令攻击都能被挡在门外。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值