1. 一个【Critical】告警背后的真实含义
上周朋友发来一张告警截图,标题写着【Critical】docker unauthorized 2375。他第一反应是“Docker 连不上了,要不要重装”,我远程上去看了一眼就告诉他:问题不在客户端,在监听地址上。后来一查,daemon.json 里配了 tcp://0.0.0.0:2375,云安全组又把 2375 对全网放行。等于把 Docker 的控制权直接摆在了公网上,任何人不带密码就能刷开这扇门。今天不聊虚的,就围绕 docker、2375、unauthorized 这三个关键词,把端口是怎么被“敲开”的、告警为什么是 Critical、拿到告警之后应该怎么处置,完整讲一遍。这篇内容既适合刚接触 Docker 的新手,也适合正在排查生产环境告警的运维朋友。
1.1 2375 是 Docker 的“裸奔口”,2376 才是加密口
Docker 客户端和守护进程(dockerd)之间不是靠什么魔法通信的,它走的是一套 REST API。默认情况下,这套 API 只监听在本地的一个 unix socket 上,也就是 /var/run/docker.sock。只要你的 Linux 用户有权限访问这个 socket,你就能完整控制这台机器上的 Docker。而 2375 这个端口,是 Docker 官方预留的 TCP“调试”端口,它和 2376 是一对。2375 代表“无 TLS 加密”,2376 代表“启用 TLS 加密”。
很多人看到网上教程写“监听 2375 端口,方便远程管理 Docker”,就照着配了。结果是把整个 Docker 控制面从本机文件系统搬到了公网 TCP 上。用大白话讲,2375 就像你把家里大门的钥匙直接插在锁孔里,然后人出门了。别人根本不需要撬锁,推门就进。2375 默认没有认证、没有加密、没有授权校验,这就是为什么安全产品只要检测到这块暴露,就会直接打上 Critical 级别告警。
1.2 为什么 Docker API 被调用等同于 root 权限被拿走
很多开发同学觉得“就算 Docker 端口暴露了,顶多被别人拉几个容器跑一跑,损失不大”。这个想法非常危险。dockerd 进程本身是以 root 身份运行的,Docker API 的所有能力都继承自 root。通过 API,远端可以调用 POST /containers/create,把宿主机根目录 / 挂载到容器里,然后执行任意命令。说直白一点,攻击者只要发一个请求,就能在你服务器上创建这样一个容器:把整个磁盘读走、写入 SSH 公钥、安装后门脚本。
更讽刺的是,这种“未授权访问”通常不会返回 401 unauthorized。因为 dockerd 没有启用任何认证时,它会直接接受请求并执行。你看到 401,反而说明系统里有别的拦截层。所以当你收到“docker unauthorized 2375”这类告警,千万别以为“有个 401 挡着,入侵失败”,大概率是安全产品检测到了端口暴露,而“unauthorized”是它在描述这种行为属于未授权访问。先按最坏情况做排查,总没错。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先把 unauthorized 拆开:你遇到的到底是哪一类
排查这类告警,最怕的就是把“未授权访问”和“401 unauthorized”混为一谈。这两个词看起来像,解决路径完全不一样。如果方向错了,你能白忙一整天。
2.1 “未授权访问”与“401 unauthorized”是两回事
“未授权访问”描述的是服务器没有验证调用者身份就放行了。这是 Docker 2375 暴露最典型的问题,解决方式是“关闭暴露”或者“启用 TLS 证书认证”。而 HTTP 401 unauthorized 是服务端返回的一个标准状态码,意思是“你请求了,但你没有给我能证明身份的凭证”。它反而说明服务端是有认证逻辑的,只是你的凭证不对。
所以收到告警后第一步,先搞清楚是谁给你返回的 401。是 Docker daemon 本身?是 Nginx 反向代理?是本地的 Docker Registry 镜像仓库?还是公司内部某个统一网关?不同来源的 401,排查方向天差地别。以我看到的经验,很多热搜词里提到的 unexpected status 401 unauthorized: incorrect api key provided,其实是访问第三方 SaaS 服务或镜像仓库接口时 API Key 填错了,和 Docker 2375 端口暴露是两码事。不要因为都带 unauthorized 就混在一个问题上处理。
2.2 四种常见的 401 场景,逐个对号入座
我整理了一下实际运维中最常撞见的四种 401 场景,你可以按症状快速归类。
第一种,客户端用 docker -H tcp://IP:2375 info 连上去,报错 unauthorized: incorrect username or password 或 certificate required。这种情况通常是服务端配置了 TLS,但客户端没有携带证书,或者携带的客户端证书不被信任。症状表现为“curl 一下能通,docker 命令却不行”。
第二种,访问 /v2/ 开头的 Registry API 时报 401。比如 docker login 拉私有仓库,或者 curl http://registry:5000/v2/_catalog。这属于镜像仓库的认证问题,需要检查仓库的用户名密码、token 是否有效,跟 2375 没有直接关系。
第三种,Docker 请求走了一个反代或网关,比如 Nginx 转发到 dockerd,而 Nginx 上配了 Basic Auth。那么你直接访问原始地址可能没问题,一旦走反代就会被 401 挡在外面。这时候要改的不是 Docker,是反代配置。
第四种,你本地配置了 Docker context,把默认 context 指到了某台需要 API Key 的远程环境,然后所有本机 Docker 命令都报 401。很多人排查了半天,最后发现 docker context ls 里面默认 context 早就不是本机了。
2.3 本地先自测三步,快速定位问题层
与其猜,不如直接在服务器上跑几条命令定位。
先看 dockerd 到底监听了哪些地址:
bash复制ss -lntp | grep -E '2375|2376'
如果看到 0.0.0.0:2375 且进程是 dockerd,那就说明 TCP 暴露已经坐实了。接着看 Docker 本机 socket 是否正常:
bash复制curl --unix-socket /var/run/docker.sock http://localhost/version
这条命令能通,说明 Docker daemon 本体健康,问题出在网络层或认证层。最后看 daemon 启动参数和 daemon.json:
bash复制systemctl cat docker | grep -i host
cat /etc/docker/daemon.json
重点关注有没有 -H tcp://0.0.0.0:2375、"hosts": ["tcp://..."] 这类配置。三步跑完,问题出在哪一层基本就清楚了。
3. 拿到 Critical 告警后的五个处置动作
确认确实存在 2375 暴露之后,千万不要一上来就重启 Docker。重启可能会中断现场,也会把正在运行的业务容器带走。下面这套流程是我实际处理过的顺序,每一步都有它的目的。
3.1 第一步:封端口之前,先保存现场
很多人的第一反应是“赶紧把端口关了”。但如果你怀疑已经被未授权访问过,关闭端口前应该先记录现场,否则后面要溯源就无从下手。先把当前容器列表导出来:
bash复制docker ps -a > /tmp/docker_ps_$(date +%Y%m%d%H%M%S).txt
再保存镜像列表和容器挂载信息:
bash复制docker images > /tmp/docker_images_$(date +%Y%m%d%H%M%S).txt
docker inspect $(docker ps -aq) > /tmp/docker_inspect_$(date +%Y%m%d%H%M%S).json
同时用 last、journalctl -u docker --since "7 days ago" 看看有没有异常登录和容器操作记录。这些快照不一定每个都用得上,但真到了需要分析入侵路径的时候,它们比事后回忆可靠得多。记住,保存现场并不影响你下一步封端口,先留证据再关门,两不耽误。
3.2 第二步:检查有没有被拉起的陌生容器
2375 暴露在公网后,最常见的入侵行为就是拉一个挂载宿主根目录的容器,然后写入 SSH key 或者直接执行系统命令。所以第二步是认真看一遍容器列表,重点找两类异常:一类是名字很随机的容器,比如一串乱码;另一类是镜像名称看起来不正常,比如和矿池、代理、挖矿相关的镜像。
看容器还不够,还要看容器的挂载:
bash复制docker inspect <容器名> --format '{{json .Mounts}}'
如果发现某个容器把宿主机 /、/etc、/root/.ssh 之类的目录挂载进去了,那基本上就能断定已经出过事。这时候不要急着删除容器,先把它停止并记录下来。删除容器不等于清理后门,攻击者很可能已经在宿主机上写了定时任务、systemd 服务或者 SSH 公钥。稳妥的做法是:先封端口、保存现场,再逐项排查宿主机上的异常持久化痕迹。
3.3 第三步:停掉 TCP 监听,只保留本地 socket
确认问题后,立即把 TCP 监听关掉。最常用的做法是修改 /etc/docker/daemon.json,只保留 unix socket:
json复制{
"hosts": ["unix:///var/run/docker.sock"]
}
但这里有个非常容易踩的坑:很多发行版的 systemd 单元文件里默认带了一个 -H fd://,如果你在 daemon.json 里再加 "hosts",Docker 启动时会因为同时指定了 hosts 和 -H 而报 conflicting options 直接起不来。正确做法是用 systemd 的 override 配置把 ExecStart 覆盖掉:
bash复制mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock
EOF
systemctl daemon-reload
systemctl restart docker
重启后再执行 ss -lntp | grep 2375,确认没有 TCP 监听。这一步做完,等于把 2375 这扇对外的大门先锁上了。后面再考虑你是不是真的需要远程管理。
3.4 第四步:远程管理请改用 TLS,端口换成 2376
如果你确实需要远程管理 Docker,比如办公电脑连测试服务器,正确姿势是用 TLS 证书双向认证,端口用 2376。这相当于在门上装了一把真正的智能锁,只有拿了你签发的钥匙才能开。
先在工作目录生成 CA、服务端、客户端证书:
bash复制mkdir -p /opt/docker-certs && cd /opt/docker-certs
# 生成 CA 私钥和证书
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# 生成服务端私钥和签名请求
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=your-server-host" -sha256 -new -key server-key.pem -out server.csr
echo subjectAltName = DNS:your-server-host,IP:your-server-ip > extfile.cnf
echo extendedKeyUsage = serverAuth >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf
# 生成客户端私钥和签名请求
openssl genrsa -out client-key.pem 4096
openssl req -subj "/CN=client" -new -key client-key.pem -out client.csr
echo extendedKeyUsage = clientAuth > extfile-client.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile extfile-client.cnf
服务端用 systemd override 启动,监听 2376:
bash复制cat > /etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --tlsverify --tlscacert=/opt/docker-certs/ca.pem --tlscert=/opt/docker-certs/server-cert.pem --tlskey=/opt/docker-certs/server-key.pem -H tcp://0.0.0.0:2376 -H unix:///var/run/docker.sock
EOF
systemctl daemon-reload
systemctl restart docker
客户端连接时带上证书:
bash复制docker --tlsverify \
--tlscacert=ca.pem \
--tlscert=client-cert.pem \
--tlskey=client-key.pem \
-H=tcp://your-server-ip:2376 version
这套方案能同时解决“加密传输”和“身份认证”两个问题。证书只要不泄露,比任何弱口令都安全。
3.5 第五步:安全组和防火墙一起收口
技术配置做完了,最后一步是网络层收口。很多云服务器即使本机防火墙没开,云平台控制台里的安全组照样把端口放进来了。所以两边的规则都要检查一遍。
云安全组里,2375/2376 的入方向来源不要写 0.0.0.0/0,应该只放行你办公网或者跳板机的固定 IP。系统层面,用 ufw 或 firewalld 再拦一道:
bash复制# 如果使用 ufw
ufw allow from 203.0.113.10 to any port 2376 proto tcp
ufw deny 2375/tcp
# 如果使用 firewalld
firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=203.0.113.10 port port=2376 protocol=tcp accept'
firewall-cmd --permanent --add-rich-rule='rule family=ipv4 port port=2375 protocol=tcp reject'
firewall-cmd --reload
安全组和系统防火墙是两层防线,别嫌重复。真出了安全事故,能多挡住一个来源,就少一分风险。
4. 现场翻车记录,每条都是真金白银换来的
这部分我把自己处理过的、以及身边同事踩过的典型坑整理出来。很多问题在官方文档里根本搜不到,只能靠现场经验慢慢磨。
4.1 daemon.json 改完不重启等于白改
有一次我在客户机器上改了 daemon.json,把 hosts 改成了只有 unix socket,然后顺手 systemctl status docker 看了一眼,发现服务还在运行,就直接告诉客户“改完了”。结果第二天客户反馈,从外网还是能连上 2375。我回去一看,原来 docker 服务确实没重启,daemon.json 的修改根本没加载。
坑点是很多发行版默认的 Docker 服务是开机自启、运行中不被 systemd reload 影响的。你改了配置文件,必须执行 systemctl restart docker 才会生效。另外有些环境里 Docker 是容器化部署的,那还要看容器编排系统有没有重新创建容器。改完配置,一定要实际验证端口监听状态和连通性,别只看服务“还在跑”就觉得万事大吉。
4.2 curl 能通,docker 命令却 401
另一个常见现象是:在服务器本机用 curl 访问 2375,返回正常 JSON;但用 docker 客户端去连,却报 unauthorized: certificate required。遇到这种情况,很多人会怀疑端口没开对,其实问题多半出在客户端和服务端对 TLS 的期望不一致。
比如服务端已经配置了 tlsverify,但是你用 curl http://... 不带证书去访问,因为 HTTP 和 HTTPS 的差别以及 curl 的宽松模式,某些情况下它也能返回部分信息,容易造成“服务正常”的错觉。真正用 docker CLI 时,因为 dockerd 要求双向 TLS,没有证书就直接拒绝。所以排查客户端连接问题,不要只依赖 curl,直接跑 docker -H tcp://... --tlsverify info 这样的完整命令,一次性把问题暴露出来。日志方面也可以看 journalctl -u docker --since "10 minutes ago",里面会明确写证书校验失败的客户端地址。
4.3 端口关了,容器还在被反复拉起来
还有一次,我在一台被入侵的机器上关了 2375,也删掉了可疑容器。结果第二天早上又出现了同样的容器,镜像名一模一样。后来发现,攻击者不只是在 Docker 里拉了容器,还在宿主机里留了一个定时任务,每五分钟执行一次 docker 命令,把那个容器重新拉起,并且因为镜像已经缓存在本地,所以拉取速度极快,很难通过“断网”来阻止。
这个教训很关键:当你发现 2375 暴露过,并且已经有可疑容器出现,必须把排查范围从 Docker 扩大到宿主机。检查 /etc/crontab、/var/spool/cron/、systemctl list-timers、/etc/systemd/system/ 下有没有可疑单元文件,以及 /root/.ssh/authorized_keys 有没有多出陌生公钥。Docker 只是入口,攻击者真正想拿的是宿主机。
4.4 问题排查速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 安全告警提示 2375 暴露 | dockerd 监听了 tcp://0.0.0.0:2375 | 关闭 TCP 监听,只保留 unix socket |
客户端报 unauthorized: certificate required |
服务端启用了 TLS 但客户端没带证书 | 配置 TLS 客户端证书,或用 SSH 通道 |
docker login 返回 401 |
镜像仓库凭证错误或 token 过期 | 重新登录,检查 API Key 是否有效 |
| 本机 docker 命令突然全部 401 | Docker context 指向了远程环境 | 执行 docker context ls,切回本机 context |
| daemon.json 改了但没生效 | 服务未重启,或和 -H fd:// 冲突 |
重启 Docker,或使用 systemd override |
| 删除容器后又自动出现 | 宿主机存在定时任务或 systemd 后门 | 排查 crontab、systemd、SSH 公钥 |
5. 不折腾 TLS 也能安全远程的几个方案
不是所有场景都值得配一套完整的 TLS 证书体系,尤其是本地开发或几个人协作的小团队。下面几个替代方案我实测下来更轻量,关键是不碰 2375 这个高危端口。
5.1 SSH 通道是临时方案的 MVP
如果你只是想从笔记本连一下服务器上的 Docker,最省事的办法是 SSH 转发,而不是暴露任何 TCP 端口。在笔记本上执行:
bash复制ssh -L 2375:/var/run/docker.sock user@server -N
这条命令把本地 2375 端口映射到服务器的 Docker socket 上。然后另一终端直接:
bash复制export DOCKER_HOST=tcp://127.0.0.1:2375
docker info
在这个模式下,流量全程走 SSH 加密隧道,服务器不需要监听任何额外端口,Docker 仍然只暴露本地 socket。我自己的开发机长期用这个方式,既没有配证书的负担,也不会被扫描器盯上。唯一要注意的是 SSH 连接断了,隧道就会断开,Docker 客户端也要重连。
5.2 Docker Context 负责管理多台机器
当你有多台机器要管理,而且每台都用 SSH 隧道,命令行会变得很难维护。这时候用 Docker Context 会更顺手。Docker Context 可以保存连接参数,把每次都要敲的 -H 和 --tls 参数固化下来。
基于 SSH 创建 context 的示例:
bash复制docker context create remote \
--docker "host=ssh://user@server"
切换 context:
bash复制docker context use remote
docker ps
切回本机:
bash复制docker context use default
如果服务器用的是上文那套 TLS 证书,也可以把证书路径写进 context:
bash复制docker context create remote-tls \
--docker "host=tcp://your-server-ip:2376,ca=ca.pem,cert=client-cert.pem,key=client-key.pem"
Context 的存在让多环境切换变得干净利落,也避免你因为频繁敲 -H 参数,不小心把命令发到错误环境。
5.3 给 Docker 配置立几条安全底线
最后分享几条我个人每次部署新机器都会遵守的底线,写在这里供你参考。
Docker daemon 永远不要裸奔监听公网 2375。不管你在哪个云厂商、用什么代理,只要 2375 对公网开放,就已经处于高危状态。远程管理优先走 SSH 或者 2376 的 TLS 双向认证。本机 /var/run/docker.sock 的访问权限要控制好,不要随便把普通用户加入 docker 用户组,因为能访问 socket 就等于能控制宿主机。
配置完任何 Docker 服务变更后,必须执行 ss -lntp 确认实际监听状态,不要只看配置文件。同时建议用 docker system events 或者至少开一点审计,记录容器创建和删除事件。安全组和系统防火墙一定要双重配置,别嫌麻烦。服务器上少开放一个端口,就少一类被扫描的可能性。
我在实际处理不少服务器后最大的体会是:2375 这个端口暴露,几乎不会因为“运气好”而没出事。你今天把端口关掉,很可能已经晚了一步。所以建议你现在就去检查一下手上的每一台机器,执行一条 ss -lntp | grep -E '2375|2376',看到结果之后再做下一步判断。这个检查不花五分钟,但它很可能帮你省掉一次完全不必要的“重装系统”。
