你想在 NAS 或服务器上跑个下载面板、刮削面板、某个 Web 管理界面。网上一搜就有现成的 docker run 命令,复制粘贴,服务起来了,用着也顺。直到有一天发现,别人也在用。
现象
用 docker run -p 2345:2345 之类的方式起了个下载面板,后来发现它躺在公网上,任何知道 IP 和端口的人都能打开,而且默认配置里根本没设密码。
能明显看出"被人用过"的痕迹往往有几条:面板里多出陌生的下载任务、连接列表里有外部来源地址、日志里的请求来源 IP 五花八门。这意味着把下载站的账号、甚至更深层的系统权限一起交了出去。类似的错误在不同容器上反复踩过好几次——因为每次都是复制别人的命令,从没认真看过那一行 -p。
验证方法也很直接:拿手机蜂窝网络(不是家里 Wi-Fi)打开 http://<公网地址>:2345,如果转出登录框或者直接进面板,就说明确实暴露了。
根因
两条独立的原因叠在一起。
第一条是 -p 端口:端口 的默认绑定行为。 当你只写端口、不写主机 IP 时,Docker(以及多数容器运行时)会把端口同时绑到 0.0.0.0 和 IPv6 的 [::]。它不会因为你在内网使用就只绑内网。再叠加一个事实:家庭宽带的运营商光猫通常不封 IPv6 入站,于是 IPv6 侧的监听直接就是公网可达的。
docker port 可以一眼验证这一点:
docker port panel
# 2345/tcp -> 0.0.0.0:2345
# 2345/tcp -> [::]:2345
看到 0.0.0.0 和 [::],就等于对所有网卡开放。
第二条是鉴权缺失。 很多自建面板的环境变量里有 USER / PASS 之类的字段,但示例命令里往往留空,或者程序在密码为空时会跳过登录验证。端口一开、鉴权又是空的,就是完全的裸奔。更糟的是,这类面板往往带着"任务执行""文件管理"能力,等于给了一个远程 Shell。
解决
写绑定的时候显式指定内网 IP:
docker run -d \
--name panel \
-p 192.168.0.10:2345:2345 \
-e PANEL_USER=admin \
-e PANEL_PASS='<密码>' \
面板镜像名
关键在 -p 前面的 192.168.0.10——它把发布端口限制在这一个接口上,而不是所有接口。这样即使光猫不封 IPv6 入站,容器也不会在公网网卡上监听。
起来之后立刻确认两件事:
# 确认端口映射绑到了具体 IP,而不是 0.0.0.0
docker port panel
期望输出是 2345/tcp -> 192.168.0.10:2345,而不是 0.0.0.0。以及面板本身有没有设密码、能不能匿名访问(用一个无痕窗口打开,看看是否需要登录)。
最后,用外部节点实测该端口不可达。这一步和配置本身同样重要,因为你可能还漏了别的暴露路径(比如容器还开了别的端口、另有 sidecar 容器):
# 从外部主机测(家庭 Wi-Fi 之外的网络)
curl -m 5 -I http://<公网地址>:2345/
# 期望:连接超时或被拒绝
延伸与预防
建立一条硬性习惯:凡是要起"面板类容器"(带 Web UI 的),动手之前先问两个问题——"它现在绑在哪?有没有密码?" 没答案就先别起。
可以准备一份自己用的 docker run 模板,绑定地址这一栏永远是内网 IP,需要临时对外时再单独想办法(加反向代理、加鉴权层),而不是靠"反正没人知道我的 IP"。
顺带一个容易被忽略的点:docker run 起来之后如果容器重建(docker compose up -d 之类),旧的绑定配置可能被新配置覆盖。所以更稳妥的做法是把 -p 192.168.0.10:2345:2345 写进 compose 文件并纳入版本管理,而不是只在命令行里敲一次。
同样值得记住的是:暴露面是"所有监听端口之和",不是某一个端口。 定期对着容器列表逐个 docker port,比出事之后再补救省事得多。
另一个常见误区是"端口号很冷门就安全"。端口号只是数字,扫描器一秒能试上千个端口,2345、8999 这种"不常见"的端口照扫不误。真正的安全边界是绑定地址和鉴权,而不是端口的隐蔽性。