尧图精选

Docker容器root密码修改全指南:可写层、PAM、sshd与安全边界

🕒 发布时间:2026/10/2 3:30:52 📁 来源:尧图网络
你有没有遇到过这种情况测试环境里随手跑了一个docker run后面忘了容器里的 root 密码或者拉了一个第三方镜像镜像里 root 密码只有作者知道更常见的是容器重启后在容器里辛辛苦苦改好的密码一夜回到解放前。Docker 容器里修改 root 密码这件事看起来就是passwd一条命令实际上牵涉到容器层、镜像层、PAM、sshd 乃至数据库账号踩坑点非常多。这篇文章就把正确方法、救急方法和那些错误示范一次说清适合用 Docker 做开发、测试和轻量运维的读者。1. 先搞清楚容器里的 root 到底是哪个 root1.1 容器 root 与宿主机 root 不是同一个账号很多新手第一次在容器里执行passwd root改完觉得很正常直到某天在宿主机上用sudo passwd root时才发现自己可能动了不该动的东西。其实不用慌Docker 默认利用 Linux 的命名空间做隔离容器内的 UID 0 和宿主机的 UID 0 是两套账号体系。你在容器里改 root 密码改的是容器命名空间内/etc/shadow里的条目不会同步到宿主机也不会影响宿主机上其他容器的账号。反过来也一样宿主机上改 root 密码不会改变任何容器里的密码。但这里有一个关键细节如果启动容器时把宿主机的文件挂载进容器比如-v /etc/shadow:/etc/shadow那就等于把两个世界强行打通。此时容器里改 root 密码改的很可能就是宿主机上的账号文件这种操作极其危险后面我会专门讲为什么不能这么干。容器内默认用户也未必是 root。官方 nginx、redis 镜像会在 Dockerfile 里指定USER nginx或USER redis所以直接docker exec进去经常是普通用户身份。这时如果执行passwd root大概率会得到passwd: Only root may specify the user name这类报错。正确做法是docker exec -u 0或docker exec -u root以 root 身份进入再改密码。1.2 密码到底存在哪/etc/shadow 与容器可写层继续深挖一层容器里的/etc/shadow是从镜像里来的严格说是镜像层中的一份文件。当你修改容器内 root 密码时Docker 会在当前容器的可写层生成一个新的/etc/shadow副本原始镜像层里的文件不会被改动。这也是为什么容器一删改过的密码就没了——可写层跟着容器一起销毁了。只要理解了这一点后面所有“改了又丢”“重启就失效”的疑问都能解释通。容器 root 密码修改的不是镜像而是“当前这个容器实例”的运行状态。如果想让下次docker run也生效必须把改动固化到镜像或者通过 Dockerfile、挂载方式持久化。这个思路贯穿全文建议先记在脑子里。2. 运行中的容器三种改密码的正确姿势2.1 最稳妥的方式docker exec 进容器再用 passwd既然容器还在运行第一选择永远是进容器执行系统自带的工具不需要任何额外依赖docker exec -it 容器名或ID bash passwd root如果没有 bash很多精简镜像只有 sh那就用docker exec -it 容器名或ID sh passwd root如果默认用户不是 root先指定用户再操作docker exec -u root -it 容器名或ID passwd root执行后根据提示输入两次新密码没有任何错误提示就是成功了。这里有三个容易踩的坑必须带-tpasswd 需要从终端读取密码只带-i不带-t可能会报passwd: Have exhausted maximum number of retries。容器里可能没有 passwd 二进制。极简镜像如 scratch 或 distroless 没有 shell 也没有 passwd这种方法不适用需要换后面介绍的临时容器方案。容器内如果启用了 PAM且 root 账号原来是被锁定的状态shadow 文件中 root 行的密码哈希前面会带!标记例如!$6$...。passwd 修改后通常会正常解锁但改完最好用chage -l root确认一下状态。提示docker exec -it 容器 passwd root可以一条命令完成不用先进入 shell。前提是 passwd 在镜像的 PATH 中并且你有可交互的终端。2.2 脚本场景用 chpasswd 做非交互修改手动输两遍密码适合临时改但如果你在写部署脚本、批量初始化容器或者密码要由配置中心下发passwd 的交互式体验反而碍事。这时推荐 chpasswddocker exec -i 容器名或ID chpasswd root:你的新密码或者更直白echo root:你的新密码 | docker exec -i 容器名或ID chpasswdchpasswd 从标准输入读取“用户名:密码”的格式一条命令可以同时改多个账号docker exec -i 容器名 chpasswd root:NewPass! docker exec -i 容器名 chpasswd app:AppPass!注意管道左侧的 echo 在宿主机执行右侧 chpasswd 在容器内执行中间靠 docker exec 的 stdin 传递。因此-i必须保留-t有没有都无所谓。这种方式很适合 CI 自动化但密码会出现在 shell history 和进程列表中临时调试没问题生产环境要结合密钥管理系统这点后面会再说。2.3 容器已经进不去用 nsenter 从宿主机绕进去容器没完全死只是 sshd 挂了或者 shell 坏了docker exec可能进不去。这时候可以从宿主机用 nsenter 直接卡进容器的命名空间。原理不复杂Docker 容器本质上就是宿主机上的一组进程找到容器对应进程的 PIDnsenter 就能进入它的 mount、PID、网络等命名空间执行命令PID$(docker inspect --format {{.State.Pid}} 容器名或ID) nsenter -t $PID -m -u -i -n -p passwd root如果容器里连 passwd 都没有或者想连续执行多条命令可以先进入 shellnsenter -t $PID -m -u -i -n -p bash这里有个前提宿主机需要安装 util-linux且当前用户有权限访问/proc/PID/ns。普通 Linux 发行版自带 nsenterDocker Desktop 这类虚拟机环境不一定好用因为它需要直接访问内核命名空间。生产 Linux 服务器上 nsenter 是真正能救命的工具我在现场遇到过容器内 bash 崩溃、sshd 拉不起进程的情况靠它把现场抢救回来过。还有一种“伪进入”方案直接用docker run启动一个挂载了原容器文件系统的临时容器。但要注意这只是覆盖文件不是真正进入原容器的运行状态。要改 root 密码最直接的方法还是 nsenter。3. 停止、删除和重建改了又丢的根源和对策3.1 容器已停止先 docker start实在不行再导出容器处于停止状态时docker exec会直接报错。最常规的救法是先把容器启动起来然后再改密码docker start 容器名或ID docker exec -it 容器名或ID passwd root有些容器启动后会立刻退出因为它的命令本来就不是常驻进程这时候可以临时覆盖 entrypointdocker start 容器名或ID docker exec -it 容器名或ID sh -c echo root:新密码 | chpasswd如果容器已经彻底起不来或者业务进程一启动就崩可以把当前容器的文件系统保存成一个新镜像再通过新容器进去改docker commit 容器名或ID rescue-image docker run --rm -it --entrypoint bash rescue-image passwd root docker commit 临时容器ID final-image这个方法会把容器当时的状态全部保留下来root 密码也会被固化到 final-image。但要注意docker commit会保留一些冗余的容器层数据而且生成的镜像没有可读的 Dockerfile 记录时间长了没人知道镜像里到底改过什么所以只适合一次性的救急操作。更底层的办法是docker export导出文件系统解包后用 chroot 修改/etc/shadow再docker import回来。这种方案看起来很硬核但会丢失镜像的历史、CMD、ENV 等元数据导入后的镜像几乎等于一张白纸配置全要重来。非特殊场景我不推荐反而是上面 commit 路线更实用。3.2 为什么密码总丢容器层不是持久化层很多人在容器里改完密码后用docker stop再docker start密码还在但一旦docker rm删掉容器再用同一个镜像docker run密码就没了。原因还是第一节说的改密码写在了容器可写层没有写进镜像层。镜像可以被多个容器共用默认是只读的。容器启动时才在最上层加一个可写层所有文件变更都发生在这一层。删掉容器等于把可写层丢掉所以任何临时修改都会随容器消失。如果你希望改完的密码在容器重建后依然生效必须把修改固化到镜像或者用外部存储挂载覆盖/etc/shadow后者的风险我稍后会讲。这也是为什么我不建议长期依赖“手动进容器改密码”。临时改一次没问题但所有运维操作都应该能自动化、可重建。容器世界里不可重建的服务器状态都是技术债。3.3 想让密码稳定存在用 Dockerfile 而不是 docker commit如果你希望镜像一开始就带着一个明确 root 密码最干净的做法是重新构建镜像而不是改来改去再 commitFROM your-image RUN echo root:新密码 | chpasswd构建命令docker build -t your-image-fixed .后面用这个新镜像启动容器密码就是确定的了。但要注意RUN echo root:密码会留在镜像构建历史里任何能docker history的人都能看到明文。如果镜像要推送到公共仓库千万别直接写密码。更安全一点的做法是用 BuildKit 的 secret 功能构建时把密码文件挂给 RUN 指令使用最终镜像层不会保留密码内容# syntaxdocker/dockerfile:1 FROM your-image RUN --mounttypesecret,idroot_pw \ chpasswd /run/secrets/root_pw构建命令docker build --secret idroot_pw,srcroot_pw.txt -t your-image-fixed .root_pw.txt 内容就是root:新密码。构建完成后文件不会写进任何镜像层。这套方案适合需要把镜像分发给多台服务器但又不希望密码直接裸奔的场景。4. 高频坑密码过期、sshd 和数据库 root4.1 一登录就提示密码过期用 chage 解决容器里的 root 密码明明刚改完SSH 登录时却提示Your password has expired这不是密码不对而是系统的密码老化策略在起作用。很多基础镜像继承自长期运行的系统模板/etc/shadow中 root 行的过期时间可能早就过了。排查命令chage -l root如果看到Password expires: never以外的结果说明有强制过期策略。直接设置成永不过期chage -M 99999 root chage -l root这里-M 99999表示密码最长使用天数为 99999 天等效于不过期。更精确的临时方案是设置一个合理的到期时间比如chage -M 90 -W 7 root密码 90 天后过期过期前 7 天开始提醒。容器毕竟不是长期运维的物理机大多数场景下我会直接设成永不过期避免半夜被密码过期报警叫醒。还要检查/etc/login.defs里的默认值比如PASS_MAX_DAYS。如果容器里跑的是老系统chage 改完本次生效但某些发行版的服务重启后可能重新读取 login.defs最好顺手确认一下。4.2 容器内 sshd 登录失败多数时候不是密码的锅“容器内开着 sshd密码是对的但 ssh 登录还是失败”这类问题我在 CentOS 容器里遇到过太多次。比如centos:7.9这种镜像默认没有生成 SSH host keysshd 起来也会报错。解决办法ssh-keygen -A另外还要保证/run/sshd目录存在否则 sshd 可能直接启动失败mkdir -p /run/sshd /usr/sbin/sshd如果 sshd 起来了但登录仍被拒第一件事不是怀疑密码而是检查/etc/ssh/sshd_config里的PermitRootLogin。容器里默认禁 root 直接登录的情况很常见改成yes后重启 sshdsed -i s/#PermitRootLogin yes/PermitRootLogin yes/ /etc/ssh/sshd_config sed -i s/#PermitRootLogin prohibit-password/PermitRootLogin yes/ /etc/ssh/sshd_config还要提醒一个容易混淆的点很多容器里根本没有 systemdsystemctl restart sshd会失败。这时候直接 kill 掉 sshd 进程再启动或者用nohup /usr/sbin/sshd手动拉起。4.3 别把 MySQL 的 root 当成 Linux root 来改另一个高频混淆是在 MySQL 容器里执行ALTER USER rootlocalhost IDENTIFIED BY 新密码改的是数据库账号不是 Linux 系统 root。两者完全无关甚至 MySQL 容器的 Linux root 密码完全可以保持锁定状态。遇到 MySQL 报ERROR 1045 (28000): Access denied for user rootlocalhost时正确入口是docker exec -it mysql mysql -uroot -p然后执行ALTER USER rootlocalhost IDENTIFIED BY 新密码; FLUSH PRIVILEGES;如果 MySQL 是用环境变量MYSQL_ROOT_PASSWORD初始化的注意这个环境变量只在数据目录初始化的第一次启动时生效。数据目录已经存在后你再改环境变量并不会覆盖数据库里已经设置的 root 密码必须用 SQL 语句改。Redis 里的requirepass也是同理。CONFIG SET requirepass 密码只是临时生效重启就没了想持久化要写进配置文件或者设置appendonly yes后执行CONFIG REWRITE。这些数据库账号密码和容器 Linux root 密码是完全不同的体系排查问题前先分清自己改的是哪一层。5. 生产环境更该注意的权限与安全边界5.1 千万别把宿主机的 /etc/shadow 挂进容器网上有些教程为了“一劳永逸”会把宿主机的 passwd、shadow 文件挂载到容器里让容器和宿主机共享账号。这确实能统一密码但代价是灾难性的容器内一旦执行passwd root改的是宿主机 root 密码如果容器被入侵攻击者等于直接拿到了宿主机的账号文件。类似的危险挂载还包括-v /etc/shadow:/etc/shadow-v /etc/passwd:/etc/passwd-v /:/host或者把宿主机根目录挂进容器容器安全的第一原则是宿主机敏感文件永远不要以可写方式暴露给容器。即使只读挂载也可能被用来读取密码哈希做离线破解。合理地使用--read-only限制容器写操作比在容器里费劲改密码更值得。还要注意--privileged参数。加了特权模式的容器 root 基本等价于宿主机 root能访问宿主机所有设备这时候任何密码策略都形同虚设。能用默认 capability 解决的事不要随便开特权容器。5.2 密码初始化、环境变量与 Secrets 的取舍很多镜像支持用环境变量设置 root 密码比如MYSQL_ROOT_PASSWORD。开发环境图省事可以这么干但生产环境要清楚环境变量的风险任何有权限执行docker inspect的人都能看到明文容器日志、编排平台的状态页面也可能泄露。更稳的做法是使用 Docker secrets 或挂载只读 secret 文件。以 MySQL 为例docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORD_FILE/run/secrets/mysql_root_password \ --secret mysql_root_password \ mysql:8.0或者用 Docker Compose 的 secrets 配置。密码文件只挂给需要的容器且挂在/run/secrets下不以环境变量形式出现在进程参数和镜像配置里。这样既能自动化又能减少泄露面。另外在容器里改完 root 密码后如果发现密码被写进了 shell history 或者 docker inspect 的 env 字段记得清理一下。你可以用history -c清掉当前 shell 历史但已经在容器层落地的文件如果担心泄露最干净的办法是删掉容器重建。5.3 容器 root 不等于拥有一切还要看 capabilities即使容器里以 UID 0 运行Linux kernel 仍然会通过 capability 机制限制它的权限。默认情况下容器 root 只是拥有 Docker 推荐的一组 cap并不具备完整的CAP_SYS_ADMIN所以它不能随意 mount、加载内核模块或操作宿主机设备。这带来的实际影响是即使你拿到了容器 root 密码也无法直接做所有“root 能做的事”。反过来如果你把--cap-addALL加上那就等于给了容器完整的内核权限密码反而成了最后一道门。安全实践是尽量去掉不需要的权限比如docker run --cap-dropALL --cap-addCHOWN --cap-addSETUID ...这里SETUID和SETGID是改密码时需要的 capability因为passwd或chpasswd要更新/etc/shadow。如果你把这两个 cap 也 drop 掉容器里改密码就会失败。这也提醒我们改 root 密码不只是会敲命令就行还要理解命令依赖的内核权限边界。最后分享一个我自己的习惯新建容器后第一件事不是改密码而是先确认这个容器到底要不要开 SSH。能用docker exec和日志解决的场景尽量不要在容器里跑 sshd如果确实需要 SSH也要给容器设置一个独立、随机、短周期的密码配合白名单 IP 使用。容器本来就是可重建的把密码玩成“永久资产”只会增加维护负担真正的正确方法永远是让密码可自动化、可回收、可重建。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →