你有没有在半夜接过这样的告警:云监控弹出一条“CPU使用率超过90%”,你的第一反应是业务高峰,可登录服务器后看到的是一个叫kthreadds的陌生进程,藏着/tmp/.X11/目录下,连着一串境外矿池地址。不用怀疑,服务器中挖矿木马了。
这篇文章是我处理一次真实挖矿入侵的完整复盘。从发现异常、定位木马、切断持久化,到把Docker重构成Rootless模式做权限隔离,再到日常加固方案,都会一步步展开。内容面向运维、后端开发,以及所有自己维护服务器的独立开发者。如果你正用默认配置的Docker跑业务,或者根本不清楚自己的Socket暴露范围,这篇文章至少能帮你少踩几个坑。
1. 事发当晚:挖矿木马是怎么进来的
1.1 现场第一次响应
我习惯每天睡前瞄一眼监控,那天看到八核CPU全部跑满,第一反应是定时任务或者日志处理脚本出了问题。登录服务器先执行top,排前面的进程里有个kthreadds,名字看起来很像系统内核线程,但CPU占用高达380%。经验告诉我,真内核线程的名字不会这么刻意,更不会单独占一个进程组。
接着我做了三件事:
bash复制ps -ef | grep kthreadds
ls -l /proc/$(pgrep kthreadds)/exe
cat /proc/$(pgrep kthreadds)/cmdline | tr '\0' ' '
输出很快暴露了问题。可执行文件路径在/tmp/.X11/config.sh,进程命令行里带着-a cryptonight这类参数,再配合外联IP段,基本可以确定是挖矿木马。这类木马通常还会释放免密公钥到authorized_keys,方便攻击者随时回来取货。
1.2 攻击者到底走了哪条路
清理前我强迫自己先回答一个问题:它是怎么进来的?
我检查了这几个常见入口:
- SSH暴力破解:看
/var/log/auth.log里有没有大量Failed password记录,确认是否弱口令账号被爆破。 - Docker API未授权:默认Docker Daemon只会监听
/var/run/docker.sock,但以前有同事为了远程管理,直接在daemon.json里加了"hosts": ["tcp://0.0.0.0:2375"],等于把Docker的控制权放到了公网。 - 暴露的Web服务漏洞:服务器上跑着某个旧版本管理后台,反代日志里有一些
/cgi-bin/路径探测记录。
排查后发现主要入口是SSH弱口令加Docker Socket权限过大。攻击者先登录服务器,发现当前用户在docker组里,直接利用Docker Socket容器挂载宿主目录,把挖矿程序带进了宿主机。这类攻击路径现在很常见:不需要内核漏洞,一个属于docker组的账号就能提权到宿主root,因为docker run -v /:/host本身就是一种最直接的提权方式。
所以这次事故真正让我紧张的,不只是CPU被打满,而是攻击者已经具备通过Docker Socket控制整台服务器的能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主机排查实战:把入侵痕迹挖干净
2.1 识别伪装进程
挖矿木马很喜欢伪装成系统进程名。除了kthreadds,我还见过ksoftirqds、systemd-sudo、networkd这类命名。它们会利用系统管理员疲劳,匆匆扫一眼top就放过。
判断进程真伪需要看三条信息:
bash复制readlink /proc/PID/exe
cat /proc/PID/environ | tr '\0' '\n' | head -50
ls -l /proc/PID/fd | grep -E 'deleted|/tmp'
正常内核线程的exe链路指向/usr/bin/或者内核目录,不会从/tmp/下执行。很多木马为了防查杀,会把自己写成一个脚本,动态编译二进制,再立刻删除源文件。这样你看到进程虽然活着,文件已经没了。别慌,/proc/PID/exe在进程运行期间依然可以读,先复制出来:
bash复制cp /proc/PID/exe /root/malware-sample.bin
后续可以丢给杀毒引擎或在线沙箱做特征确认,也能判断挖矿变种家族。
2.2 持久化藏匿点
挖矿木马最核心的目标不是跑一次,而是每次重启后都能活过来。我梳理了服务器上所有可能的持久化位置,下面这张表可以作为排查模板:
| 位置 | 检查命令 | 备注 |
|---|---|---|
| 用户定时任务 | crontab -l |
注意每个账号都看 |
| 系统定时任务 | cat /etc/crontab、ls /etc/cron.d/ |
文件名往往是无规则字符串 |
| systemd服务 | systemctl list-unit-files |
检查异常Unit和动态生成的服务 |
| SSH公钥 | cat ~/.ssh/authorized_keys |
防止后门登录 |
| Shell启动文件 | cat ~/.bashrc ~/.profile /etc/profile |
检查是否有curl下载逻辑 |
| Docker容器与镜像 | docker ps -a、docker image ls |
排查陌生镜像和不断重启的容器 |
我在这台机器上找到了两个恶意cron任务,一个写在root的crontab里,另一个藏在/etc/cron.d/rotate里,文件名模拟日志轮转。两者的内容都是外网下载+执行Shell脚本。如果把注意力只放在crontab -l上,第二个很难被发现。
另外还发现系统多了一个systemd-helper.service,启动命令指向/var/tmp/.cache/update。这类在/var/tmp下的伪系统服务,在排查过程中必须完整记录后再清理。
2.3 观察网络连接定位矿池
挖矿程序总要有网络连接。执行:
bash复制ss -antp | sort -k3 -n
重点看长时间活跃且连接的远端地址不在白名单中的TCP连接。此时恶意进程会持续连接矿池地址,有时走443端口伪装HTTPS流量,有时用3333、4444、5555等非标准端口。如果服务器有DNS日志,可以通过频繁解析的陌生域名反向定位。
我通过ss看到攻击者的挖矿程序连接一个欧洲IP的4444端口。同时发现同进程还定期外连一个C2地址,说明它不只是挖矿,还接受远程指令。
2.4 Docker残留痕迹也要一并排查
记住:不光要查宿主机进程,还要查Docker容器。
bash复制docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
docker inspect $(docker ps -aq) | jq '.[] | {Name: .Name, Image: .Config.Image, Mounts: .Mounts, Privileged: .HostConfig.Privileged}'
我当时发现有一个本地目录不知道什么时候被挂载进了某个长期运行的容器,且容器启动参数里带了--privileged。这个容器本来只应该处理业务数据,但攻击者利用Docker API在这个容器里创建了一个新容器,挂载宿主根目录,向宿主机写入了挖矿程序。
所以检查时不要只看主机进程,Docker容器的启动参数和挂载关系同样重要。
3. 止血修复:从清理矿机到封堵入口
3.1 先隔离,再动手
发现入侵后,第一反应是立刻kill -9矿机进程,但这是很多人会犯的错误。木马往往有守护进程,你杀掉挖矿进程,它几分钟后就能拉起来一个新的。更危险的是,在没有记录攻击路径的情况下贸然操作,会打草惊蛇,攻击者可能立刻清掉痕迹然后断开连接。
我的处置顺序是:
- 先在云控制台创建服务器快照,保留证据,防止清理过程中破坏关键痕迹。
- 修改安全组策略,只允许我的管理机IP访问SSH,其余端口暂时按最小必要开放。
- 用
iptables快速DROP掉异常外联IP段,切断C2和矿池通信。 - 再回服务器逐项清理。
安全组和iptables的作用类似门禁卡,在排查时关上大门,比一边查木马一边担心它继续外传数据要高效得多。
3.2 完整清掉进程和文件
清理时我按“进程→文件→定时任务→系统服务→SSH后门”的顺序操作。
先记录进程PID、关联文件、启动参数,再执行:
bash复制kill -9 PID
# 杀掉后立刻检查是否重生
ps -ef | grep 恶意进程名
如果进程立刻复活,说明有守护机制,不要硬杀,应该先把cron和systemd服务停掉。
停掉相关服务后,我把可疑目录整个保留到一个隔离目录,而不是直接删除:
bash复制mv /tmp/.X11 /root/quarantine/tmp_X11.bak
mv /var/tmp/.cache /root/quarantine/var_tmp_cache.bak
chattr +i /root/quarantine/*
然后把所有cron任务恢复成干净状态,处理异常systemd服务:
bash复制systemctl disable --now systemd-helper.service
rm /etc/systemd/system/systemd-helper.service
systemctl daemon-reload
检查SSH免密公钥时,发现root和deploy用户的authorized_keys都被追加了一段公钥。这种情况单纯删公钥不够,还要排查是否存在可疑用户账号:
bash复制awk -F: '$3==0 || $3>=1000 {print $1, $3, $7}' /etc/passwd
我甚至发现攻击者新建了一个sysadmin用户,UID 0,也就是root权账号。看到这里基本确认:这台机器的控制权已经被完全拿走过,光清理木马文件不够,必须重做权限模型。
3.3 封堵入口
清理完现场后,我强制自己换掉所有账号密码,关闭SSH密码登录,只保留密钥登录:
bash复制# /etc/ssh/sshd_config 关键配置
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers deploy
# 修改后重启服务
systemctl restart sshd
同时把Docker Daemon还原为只监听本机Socket,不暴露2375端口:
json复制// /etc/docker/daemon.json
{
"hosts": ["unix:///var/run/docker.sock"],
"iptables": true
}
这一步算是把最开始的洞口堵住了。之后才敢安心做重构。
3.4 全盘扫描确认
清理完成后,我跑了几个检测工具确认没有漏网之鱼:
bash复制apt install chkrootkit rkhunter -y
chkrootkit
rkhunter --check
另外用clamav对/tmp、/var/tmp、用户目录做了一次全盘扫描。虽然Linux上的扫描引擎误报率偏高,但结合手动排查结果,基本能放心。
4. Docker Rootless 加固:从根上拿掉容器逃逸特权
4.1 为什么要Rootless
默认Docker Daemon以root运行,所有容器进程由root用户fork出来。虽然容器有namespace隔离,但Docker Socket暴露或某个容器内漏洞被利用,攻击者可能直接调用Docker API创建高权限容器,最终获得宿主机的root权限。
Docker Rootless模式的核心思路是:把Docker Daemon和容器进程放到一个普通用户的用户命名空间里,让它们自身没有宿主root能力。即使容器被攻破,攻击者拿到也只是这个受限用户,而不是整个宿主操作系统。
类比一下:默认Docker有一个总管理员钥匙,进入系统内部后能开所有房间;Rootless模式则把钥匙换成普通员工卡,能进办公区,但进不了控制室,更不可能修改大楼的安保系统。
4.2 开启Rootless前的环境检查
Rootless模式不是所有内核版本都开箱即用,先检查:
bash复制uname -r
sysctl kernel.unprivileged_userns_clone
cat /proc/sys/user/max_user_namespaces
Ubuntu/Debian上,如果kernel.unprivileged_userns_clone是0,需要先放开:
bash复制sysctl kernel.unprivileged_userns_clone=1
echo 'kernel.unprivileged_userns_clone=1' > /etc/sysctl.d/90-rootless.conf
sysctl --system
还需要安装基础依赖包:
bash复制apt install -y uidmap dbus-user-session slirp4netns fuse-overlayfs
uidmap负责用户ID映射,slirp4netns用来提供用户态网络栈,fuse-overlayfs用于镜像层存储。这些都是Rootless运行时的三块基石。
4.3 用专用用户安装Rootless Docker
我的原则是:永远用独立低权限用户跑服务。创建一个专用账号:
bash复制useradd -m -s /bin/bash dockerless
passwd -l dockerless
su - dockerless
然后在这个账号下执行官方Rootless安装脚本:
bash复制curl -fsSL https://get.docker.com/rootless | sh
脚本执行完后,把环境变量写进~/.bashrc:
bash复制export PATH=/home/dockerless/bin:$PATH
export DOCKER_HOST=unix:///run/user/1003/docker.sock
这里1003是dockerless用户的UID,需要根据实际情况替换。关键点在于DOCKER_HOST环境变量,它决定了docker命令连接的是哪个Daemon。
4.4 开机自启与Docker Context切换
Rootless Docker依赖systemd用户服务,为了让重启后能自动启动,要开启linger:
bash复制loginctl enable-linger dockerless
然后确认服务状态:
bash复制systemctl --user start docker
systemctl --user enable docker
systemctl --user status docker
之后在普通客户端,通过Docker Context切换到Rootless实例上:
bash复制docker context create rootless \
--docker "host=unix:///run/user/1003/docker.sock"
docker context use rootless
docker info
这里有一个容易被忽略的点:如果服务器上原来已经运行过旧版Docker服务,并且旧服务占用了docker0网桥或相关iptables规则,Rootless实例可能起不来。最省事的方式是先停掉、禁用系统级Docker服务,清理残留的网络规则,再启动Rootless实例。
4.5 Rootless后的体验差异
迁移到Rootless后,需要注意两个实际变化:
- 无法直接监听低于1024的端口。例如
docker run -p 80:80这种映射会报权限不足,需要把宿主机端口换成8080,再由Nginx或防火墙转发到80。 - 挂载目录的权限表现不同。因为用了用户命名空间,容器内看到的UID已经过映射,宿主目录如果属主不是
dockerless用户,容器内会出现写权限问题。最简单的做法是单独建立数据目录并chown给dockerless。
我的业务容器迁移后跑了一周,CPU和内存都平稳,docker inspect查看每个容器的进程归属,确认都落在普通用户层级。
5. 额外的防线:我把攻击面又压了一遍
5.1 容器运行参数最小化
Rootless解决的是权限边界问题,但容器内应用本身的漏洞依旧存在。实际部署时我会给容器附加一组安全参数:
bash复制docker run -d --name app \
--restart=unless-stopped \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
--pids-limit 512 \
--memory 512m \
--read-only \
--tmpfs /tmp \
myapp:latest
解释几个关键参数:
--cap-drop ALL:丢掉Linux默认赋予容器的全部Capability,只额外授权NET_BIND_SERVICE,允许监听低端口。--security-opt no-new-privileges:禁止容器内进程通过setuid等方式提升权限。--pids-limit:限制最大进程数,防止fork炸弹拖垮机器。--read-only:根文件系统只读,恶意程序难以在容器内写入可执行文件。
如果应用必须写数据,用Volume挂载指定目录,不要对整层文件系统开放写权限。
5.2 镜像供应链管理
这次事故里的攻击者是通过Docker Socket直接写宿主的,但更常见的风险是镜像投毒。很多团队的习惯是写FROM ubuntu或者FROM nginx:latest,完全不关心基础镜像现在被谁维护。
我现在的做法是:
- 项目镜像固定用Digest拉取,不追latest。
- 每次上线前用Trivy扫描漏洞:
bash复制trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:latest
- 私有仓库存储编译产物,运行时镜像不随便从公网拉取。
实际扫描过几次之后会发现自己觉得没问题的镜像漏洞一抓一大把,很多基础镜像里藏着老版本OpenSSL或libc,这比挖矿木马更隐蔽,危害也一点不小。
5.3 网络层收敛
容器逃逸木马再强,也得有外联通道。我之前的教训是服务器安全组开得太宽,出方向允许所有流量。修复后就做了收紧:
- 在生产服务器上用UFW只放行22、80、443端口。
- 容器出口流量通过代理或NAT控制到业务必需的目标段。
- 数据库端口绝不暴露到公网,只允许内网和应用所在网段访问。
如果还需要远程管理Docker,可以用SSH隧道方式访问Socket,不要直接暴露2375。更稳妥的方案是给Docker API配置TLS客户端证书,只允许持有证书的客户端连接,但所有配置成本都比被入侵后的恢复成本低得多。
5.4 监控告警做兜底
我在这台服务器上加了最简单的CPU和进程监控告警:
- CPU连续5分钟超过80%就告警。
- 新增系统用户时通知。
- crontab文件hash变化时通知。
这些任务不需要额外复杂平台,一个跑在宿主机外的监控脚本就能实现。关键不在于告警形式,而在于十分钟内发现异常,就能在挖矿程序进入矿池开始大量消耗CPU之前介入。
6. 复盘工具箱:排查/加固常用命令与避坑记录
6.1 入侵排查命令速查
下面这些命令是我每次处理疑似入侵都会成套执行的:
| 目的 | 命令 |
|---|---|
| 查看最耗CPU的进程 | `top -b -n1 |
| 打印全部进程名 | ps aux --sort=-%cpu |
| 查看进程真实路径 | ls -l /proc/PID/exe |
| 查看进程外联 | ss -antp |
| 查看所有计划任务 | cat /etc/crontab; ls /etc/cron.*; |
| 查看异常系统服务 | systemctl list-unit-files --state=enabled |
| 查最近登录 | last -20 |
| 查认证失败记录 | grep Failed /var/log/auth.log |
| 检视Docker容器 | docker ps -a --no-trunc |
| 扫描主机Rootkit | chkrootkit; rkhunter --check |
这套流程跑下来,常见挖矿木马很难藏住。
6.2 Rootless实践中的高频率问题
这一节记录我迁移Rootless过程中踩过的坑,也有一些是群友复现时遇到的典型问题。
| 问题 | 原因与解法 |
|---|---|
docker: permission denied while trying to connect to the Docker daemon socket |
没切Context,确认DOCKER_HOST写没写对,执行docker context use rootless |
| Rootless启动但容器无法映射80端口 | 取消80映射改用8080高端口,再通过宿主机Nginx转发 |
| 挂载宿主目录后容器内无法写入 | 目录属主改为运行Docker的普通用户,或用chown -R 1000:1000 data |
| 系统重启后容器没自动起 | 未开启linger,执行loginctl enable-linger dockerless |
| 镜像下载很慢或超时 | 配置镜像源,写进Rootless的~/.config/docker/daemon.json,注意不是系统级/etc/docker/daemon.json |
| 容器日志无限增长撑爆磁盘 | 设置/etc/docker/daemon.json的log-opts为max-size=10m、max-file=3 |
第六个问题很容易被忽略。即使做了Rootless,如果容器日志不轮转,磁盘写满后业务照样挂。加固不是在某一刻做完就结束了,每一步遗漏都在给下一次事故做铺垫。
6.3 本地Docker与生产环境别搞混
在这个复盘发布前,我收到不少朋友问:本地Windows上装了Docker Desktop怎么也有这类权限问题?还有一些人遇到了Docker Desktop启动时报virtualization support was not detected,这种情况跟生产环境的Rootless迁移完全不是一回事。前者是本地开发环境虚拟化没有开启或者Hyper-V/WSL2组件没配好,想办法在BIOS里打开虚拟化支持、安装WSL2就能解决。
如果做技术架构,可以直接这样区分:
- 本地开发机的重点:把Docker Desktop跑起来、挂载目录顺畅、容器启动不卡。
- 生产服务器的重点:Socket权限、Rootless隔离、系统服务特权、日志与CPU告警。
把两者混为一谈,很容易在一台电脑上装了Docker Desktop之后以为就在用安全加固版的Docker,实际上它们的运行模型和权限边界完全不同。
6.4 从这次事故里沉淀的例行检查清单
我最后把这套流程做成了每月一次的巡检清单:
last- 检查所有root用户和可用Shell账号
- 检查
authorized_keys - 检查
/etc/crontab和/etc/cron.d - 检查Docker运行容器数是否与既定部署一致
- 检查安全组是否仍然只有最小开放端口
- 检查CPU基线:连续CPU跑满,且对应进程不是本应用,立刻按第2节流程排查
这个清单不用花太久,二十分钟能跑完。遇到真问题,能省下来的却可能是几天恢复时间。
6.5 几句个人体会
这次事故给我最大的触动是:大多数时候安全缺口不是某个高深漏洞,而是最基础的权限暴露。Docker给了我们非常方便的容器管理能力,也同时把高风险的操作通道放在手边,只要被任何一个入口撬开,后果就是整机沦陷。Rootless模式不能解决所有问题,但它把Docker的权限天花板从「root」降到了「普通用户」,每次运行容器、每个挂载点、每一条新规则,都少了一分失控的可能。修复之后我再部署新服务,默认先问自己:这个服务需要宿主机root吗?需要完整的Linux Capability吗?需要通往外网的权限吗?三个问题答不上来,就该继续收敛权限。这也是我想通过这次复盘分享的核心态度:别等一次挖矿木马把你叫醒,才想起来看服务器日志。
