news 2026/10/27 20:05:51

Dockerfile 构建支持 SSH 连接的容器镜像:配置、认证与安全实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dockerfile 构建支持 SSH 连接的容器镜像:配置、认证与安全实践

容器化部署走到一定阶段,几乎都会碰到同一个需求:构建好的镜像跑起来之后,需要能远程登进去看一眼进程状态、翻一下日志、临时跑个诊断脚本。默认的镜像只暴露业务端口,想进去只能靠docker exec从宿主机跳,一旦容器跑在远端节点、或者团队里有人没有宿主机权限,这条路就断了。给镜像内置 SSH 服务,本质上是给容器开一扇受控的运维侧门,让远程登录这件事从"必须摸到宿主机"变成"有账号密码就能进"。

这篇内容围绕"编写 Dockerfile 创建一个支持 SSH 连接的镜像"展开,把镜像构建、sshd 配置、密钥与密码两种认证方式、端口与安全边界、常见启动失败排查这些环节一次讲透。适合已经会用 Docker 打镜像、但没系统整理过容器内 SSH 配置的开发者,也适合刚接手一套容器化环境、需要给运维留入口的同学。下面所有操作都基于公开的通用实践,命令和配置可以直接抄,但每一条为什么这么写我都会交代清楚,避免你照搬之后在别的发行版上翻车。

1. 先想清楚:容器里为什么要跑 sshd

1.1 容器内 SSH 到底解决什么问题

很多人第一反应是"容器不是有docker exec吗,为什么还要 SSH"。这个疑问很合理,但docker exec有三个绕不过去的限制。第一,它依赖 Docker 守护进程的访问权限,能执行docker exec的人基本等同于宿主机 root,权限收不回来;第二,它要求你所在的机器能连到宿主机的 Docker API,跨网络、跨团队协作时这条链路往往不通;第三,docker exec开出来的是非登录 shell,环境变量、工作目录、终端类型都和真实登录有差异,排查某些依赖登录环境的问题时会失真。

内置 sshd 之后,容器对外就是一个标准的 SSH 端点,任何有账号的运维人员用任意 SSH 客户端都能进,权限粒度由 sshd 的账号体系控制,不需要把宿主机 Docker 权限发出去。这在多团队共用一套容器平台、或者需要给外部支持人员临时开通道的场景里,价值非常直接。

不过要提前说清楚一个边界:容器内跑 sshd 不是让你把它当成长期稳定的堡垒机。容器本身是"一次性"的,重启、重建、扩缩容都会让容器内的状态丢失,SSH 进去改的东西不落到镜像或挂载卷里,下次重建就没了。所以它的定位应该是"调试与应急通道",而不是"生产环境的常规运维入口"。想清楚这一点,后面的配置取舍才有依据。

1.2 一个必须先做的取舍:密码登录还是密钥登录

sshd 支持密码认证和公钥认证,容器场景下这两者的取舍和传统服务器不太一样。密码认证配置简单,镜像里设个密码就能用,但密码会写进 Dockerfile 或构建参数,一旦镜像被分发出去,密码就等于公开了,安全性很差。公钥认证需要提前把公钥塞进镜像的authorized_keys,构建时多一步,但私钥留在使用者手里,镜像泄露也不至于被人直接登进来。

我的建议是:调试镜像用密码登录图省事可以,但只要镜像会离开你的本机,就必须切到密钥认证。更稳妥的做法是两者都配好,默认只开密钥,需要临时排查时再通过环境变量或挂载覆盖的方式打开密码。后面的实操我会把两种方式都写出来,你可以按自己的场景裁剪。

还有一个容易被忽略的点:容器里的 sshd 默认不允许 root 登录,也不允许空密码。如果你图省事直接用 root 加密码,大概率会卡在"密码正确但登不进去",这个坑后面第 4 节会专门拆。

2. 基础镜像选型与 Dockerfile 骨架

2.1 为什么推荐从精简发行版起步

选基础镜像时,很多人习惯性写FROM ubuntu:latest,能跑,但镜像体积会到 70MB 以上,而且latest标签意味着每次构建拉到的版本可能不同,构建结果不可复现。容器内跑 sshd 这种轻量服务,完全没必要背一个完整发行版的包袱。

我一般用debian:bookworm-slim或alpine起步。debian slim 体积在 30MB 左右,包管理是熟悉的 apt,sshd 的配置路径和常规 Linux 一致,排查问题时资料多;alpine 更小,5MB 出头,但用的是 musl libc 和 apk,某些依赖 glibc 的二进制跑不起来,sshd 本身没问题,可你后续要往镜像里塞别的工具时容易踩坑。如果你不确定后续会不会加别的组件,选 debian slim 更稳;如果镜像体积是硬指标、且只跑 sshd,alpine 更合适。

这里有个实操细节:slim镜像默认没有openssh-server,也没有passwd、sudo这些常用工具,需要显式安装。别指望它自带,构建时报"command not found"多半就是这个原因。

2.2 Dockerfile 的整体结构

一个支持 SSH 的镜像,Dockerfile 大致分四段:装包、生成主机密钥、写 sshd 配置、声明启动方式。下面先给一个 debian slim 的完整骨架,后面几节逐段拆解。

FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends \ openssh-server \ passwd \ sudo \ && rm -rf /var/lib/apt/lists/* RUN mkdir -p /var/run/sshd \ && ssh-keygen -A COPY sshd_config /etc/ssh/sshd_config RUN useradd -m -s /bin/bash devuser \ && echo 'devuser:ChangeMe_2024' | chpasswd EXPOSE 22 CMD ["/usr/sbin/sshd", "-D", "-e"]

这段能跑,但里面每一行都有讲究。DEBIAN_FRONTEND=noninteractive是为了让 apt 安装过程不弹交互式配置界面,否则构建会卡住;--no-install-recommends避免拉进一堆用不上的推荐包,控制体积;ssh-keygen -A生成所有类型的主机密钥,缺了这步 sshd 启动会报"no hostkeys available";CMD里的-D让 sshd 前台运行(容器主进程必须前台,否则容器立刻退出),-e把日志打到 stderr,方便docker logs查看。

提示:CMD用-D而不是后台启动,是容器化 sshd 最容易搞错的地方。写成service ssh start或sshd不带-D,容器启动后主进程退出,容器直接 Exited,日志里什么都看不到。

2.3 主机密钥要不要在构建时生成

ssh-keygen -A放在构建阶段,意味着所有用这个镜像启动的容器共享同一套主机密钥。这在安全上是个隐患:主机密钥的作用是让客户端确认"我连的还是上次那台机器",如果所有容器密钥相同,中间人攻击的检测就失效了。

更严谨的做法是把生成主机密钥放到容器启动时,用一个 entrypoint 脚本先ssh-keygen -A再exec sshd -D。这样每个容器实例的主机密钥都不同。代价是每次启动多花几十毫秒,完全可以接受。如果你的镜像只在本机调试用,构建时生成无所谓;如果会分发到多节点,强烈建议改成启动时生成。这个取舍我在第 5 节会给出完整的 entrypoint 写法。

3. sshd_config 的关键项逐条拆解

3.1 一份可直接用的最小配置

sshd 的默认配置在/etc/ssh/sshd_config,直接改它容易和发行版默认值纠缠,我习惯单独写一份精简配置覆盖进去。下面这份是容器场景下验证过能用的最小集:

Port 22 ListenAddress 0.0.0.0 PermitRootLogin no PasswordAuthentication yes PubkeyAuthentication yes PermitEmptyPasswords no UsePAM yes X11Forwarding no AllowTcpForwarding no ClientAliveInterval 300 ClientAliveCountMax 2 Subsystem sftp /usr/lib/openssh/sftp-server

逐条说重点。PermitRootLogin no是安全底线,容器里用 root 登录风险太高,后面会建普通用户。PasswordAuthentication和PubkeyAuthentication按你的认证方式开关,两个都开也行,但生产镜像建议只留公钥。UsePAM yes在 debian 系上必须开,否则密码认证会失败——这是很多人配完发现"密码对却登不进"的头号原因。AllowTcpForwarding no关掉端口转发,防止有人把容器当跳板,容器场景下这个功能基本用不上,关掉更安全。

ClientAliveInterval 300和ClientAliveCountMax 2是一对,意思是服务端每 300 秒发一次心跳,连续 2 次没响应就断开。容器网络偶尔抖动,没有这个配置会出现"连接挂着但实际已死"的僵尸会话,加上之后能自动清理。

3.2 为什么 UsePAM 是密码登录的隐形开关

单独把UsePAM拎出来讲,是因为它坑过太多人。PAM 是 Linux 的认证框架,sshd 的密码认证在 debian/ubuntu 系上默认走 PAM 校验,如果UsePAM no,sshd 会用自己的内部密码校验逻辑,而容器里用户密码是通过chpasswd写进/etc/shadow的,两套逻辑对不上,结果就是密码明明正确却提示Permission denied。

判断方法很简单:登录失败时看docker logs里 sshd 的输出,如果出现Failed password for xxx,说明密码校验走了但没通过;如果连这条都没有,多半是 PAM 或认证方式没开。在 debian 系镜像里,UsePAM yes基本是密码登录的必要条件,别省这一行。

另外 alpine 镜像的 PAM 支持和 debian 不同,alpine 默认不带完整 PAM,配密码登录时反而可能要关掉UsePAM走内部校验。这就是为什么我一直强调"换发行版要重新验证",同一份配置跨发行版不一定通用。

3.3 端口与监听地址的边界

Port 22是容器内端口,和宿主机端口是两回事。容器内保持 22 最省事,映射到宿主机时用-p 2222:22这类方式避开宿主机自己的 SSH 端口。ListenAddress 0.0.0.0让 sshd 监听所有网卡,容器网络里这是必须的,写成127.0.0.1就只有容器内部能连,外部映射进来也连不上。

这里有个容易混淆的点:EXPOSE 22只是声明意图,不实际开放端口,真正对外要靠docker run -p或编排文件里的端口映射。很多人EXPOSE写了却没映射,然后疑惑"为什么连不上",其实是把声明当成了实际开放。

注意:宿主机如果本身跑着 sshd 占用了 22,映射时务必换端口,比如-p 2222:22。直接-p 22:22会和宿主机 sshd 抢端口,启动直接报错。

4. 用户、密码与密钥的落地配置

4.1 建普通用户而不是直接用 root

前面配置里关了 root 登录,所以必须建一个普通用户。Dockerfile 里用useradd -m -s /bin/bash devuser建用户,-m创建家目录(authorized_keys要放这里),-s指定登录 shell。不指定 shell 的话默认是/bin/sh,某些交互体验会差一些,指定 bash 更符合习惯。

密码用echo 'devuser:密码' | chpasswd写入。这里要提醒:密码明文写在 Dockerfile 里,构建出的镜像任何拿到的人都能docker history看到。调试镜像无所谓,正式镜像必须换成构建参数(ARG)传入,或者干脆只用密钥认证、不设密码。

如果确实需要 sudo 权限,把用户加进 sudo 组:usermod -aG sudo devuser。但容器里给 sudo 要谨慎,等于把容器内 root 权限又放出去了,安全边界会模糊。我的做法是调试镜像给 sudo,正式镜像不给,需要提权的操作走docker exec -u root。

4.2 公钥认证的完整落地步骤

公钥认证分三步:生成密钥对、把公钥塞进镜像、确认权限。生成密钥对在使用者本机做,不是镜像里做:

ssh-keygen -t ed25519 -C "container-debug" -f ~/.ssh/container_key

-t ed25519比默认的 rsa 更现代、密钥更短,现在主流 SSH 客户端都支持。生成后会得到container_key(私钥,留在本机)和container_key.pub(公钥,进镜像)。

把公钥放进镜像有两种方式。方式一是构建时 COPY 进去:

COPY container_key.pub /home/devuser/.ssh/authorized_keys RUN chown -R devuser:devuser /home/devuser/.ssh \ && chmod 700 /home/devuser/.ssh \ && chmod 600 /home/devuser/.ssh/authorized_keys

权限这三行不能省。sshd 对authorized_keys的权限检查很严格,目录必须是 700、文件必须是 600、属主必须是登录用户,任何一项不对都会拒绝密钥登录,而且日志里只给一句模糊的Authentication refused,排查起来很费劲。

方式二是运行时挂载,把公钥通过 volume 挂进容器,镜像本身不含密钥,更灵活:

docker run -d -p 2222:22 \ -v ~/.ssh/container_key.pub:/home/devuser/.ssh/authorized_keys:ro \ my-ssh-image

挂载方式适合"镜像通用、密钥因人而异"的场景,但要注意挂载文件的属主和权限,宿主机上的文件权限会带进容器,权限过宽同样会被 sshd 拒绝。

4.3 密码与密钥并存时的优先级

两种认证都开时,sshd 会先尝试公钥,公钥失败再回落到密码(具体顺序还受客户端PreferredAuthentications影响)。这个行为本身没问题,但有个细节:如果authorized_keys存在但权限不对,sshd 可能直接判定公钥认证失败并不再尝试密码,表现为"密钥没配好,密码也登不进"。遇到这种"两种方式都失灵"的情况,先检查authorized_keys的权限,再考虑其他原因。

排查时可以在客户端加-v看详细握手过程:

ssh -v -p 2222 devuser@127.0.0.1

输出里会明确显示服务端接受了哪些认证方式(Authentications that can continue: publickey,password),以及每种方式失败的原因,比盲猜高效得多。

5. 启动脚本与容器生命周期的配合

5.1 用 entrypoint 处理启动时初始化

前面提到主机密钥最好在启动时生成,这需要一个 entrypoint 脚本。脚本要做的事:确保/var/run/sshd存在(容器重启后这个目录可能没了)、按需生成主机密钥、最后exec启动 sshd。

#!/bin/bash set -e mkdir -p /var/run/sshd if [ ! -f /etc/ssh/ssh_host_ed25519_key ]; then ssh-keygen -A fi exec /usr/sbin/sshd -D -e

set -e让脚本任何一步失败就退出,避免带着半初始化状态启动。exec很关键,它用 sshd 替换掉脚本进程,让 sshd 成为容器的主进程(PID 1),这样docker stop发的信号能直接到 sshd,容器能优雅退出。不写exec的话,脚本是 PID 1,sshd 是子进程,停止容器时信号到不了 sshd,只能等超时强杀。

Dockerfile 里对应改成:

COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]

注意这里用ENTRYPOINT而不是CMD,entrypoint 更适合做"固定初始化 + 启动主进程"这种不可省略的逻辑。

5.2 容器重启后 SSH 状态为什么会丢

容器重启(不是重建)时,容器内文件系统的改动是保留的,但/var/run这类 tmpfs 挂载的目录内容会清空。/var/run/sshd是 sshd 存放运行时文件的目录(比如 privilege separation 用的 chroot 目录),它没了 sshd 启动会报错。这就是 entrypoint 里第一行mkdir -p /var/run/sshd的原因。

而容器重建(docker rm后重新run)时,所有容器内改动全部丢失,回到镜像初始状态。所以通过 SSH 进去改的配置、装的工具,重建后都没了。想让改动持久,要么改 Dockerfile 重新构建,要么把需要持久化的目录挂载出来。理解"重启保留、重建丢失"这个区别,能省掉很多"我明明改了啊"的困惑。

5.3 前台运行与日志可见性

sshd -D -e这个组合再强调一次:-D前台运行是容器主进程的硬要求,-e把日志输出到 stderr 让docker logs能看到。默认 sshd 把日志写 syslog,容器里没有 syslog 服务,日志就丢了,出问题时两眼一抹黑。

如果日志量太大想调级别,可以在 sshd_config 里加LogLevel VERBOSE,会记录每次登录尝试的详细信息,排查认证问题时很有用。但 VERBOSE 日志量大,长期开着会刷屏,建议只在排查阶段临时开。

6. 构建、运行与连通性验证

6.1 完整构建与启动命令

把前面的片段拼起来,构建和启动的命令如下:

docker build -t my-ssh-image:1.0 . docker run -d --name ssh-test \ -p 2222:22 \ my-ssh-image:1.0 docker logs ssh-test

启动后先看日志,正常应该看到Server listening on 0.0.0.0 port 22。如果看到no hostkeys available,说明主机密钥没生成;看到Missing privilege separation directory: /var/run/sshd,说明 entrypoint 里没建目录;看到容器直接 Exited,多半是 CMD 没带-D。这三类错误覆盖了启动失败的大多数情况。

6.2 分步验证连通性

验证不要一上来就ssh连,按层次来更快定位问题。第一步确认端口映射生效:

docker port ssh-test

应该输出22/tcp -> 0.0.0.0:2222。第二步确认容器内 sshd 在监听:

docker exec ssh-test ss -tlnp | grep 22

第三步才是从宿主机连:

ssh -p 2222 devuser@127.0.0.1

如果第三步失败但前两步正常,问题就在认证配置上,回到第 3、4 节检查UsePAM、authorized_keys权限、密码是否正确。这种分层验证的思路比反复试ssh高效得多,因为每一层都能明确排除一类原因。

6.3 常见失败现象与对应原因

现象最可能原因处理方向
容器启动即 ExitedCMD 未用-D前台运行改 CMD 为sshd -D -e
日志报 no hostkeys未执行ssh-keygen -A构建或 entrypoint 中补上
日志报 Missing privilege separation directory/var/run/sshd不存在entrypoint 里mkdir -p
密码正确但 Permission deniedUsePAM no或用户密码未写入开UsePAM yes,确认chpasswd执行
密钥登录被拒authorized_keys权限或属主不对目录 700、文件 600、属主为登录用户
宿主机连不上但容器内正常端口未映射或映射到错误端口检查docker port输出
连接建立后很快断开缺心跳配置或网络抖动加ClientAliveInterval

这张表基本覆盖了从构建到连接的全链路高频问题,遇到报错先对号入座,能省掉大量翻日志的时间。

7. 安全边界与生产化改造建议

7.1 容器内 SSH 的安全风险清单

给容器开 SSH 等于多了一个攻击面,几个风险必须心里有数。第一,镜像里的密码或私钥会随镜像分发泄露,这是最直接的风险,正式镜像绝不能把密码写死在 Dockerfile 里。第二,sshd 版本漏洞,容器里的 openssh 版本如果长期不更新,会积累已知问题,构建时apt-get update能拿到较新版本,但镜像本身要定期重建。第三,端口暴露范围,-p 2222:22默认绑定所有网卡,如果宿主机在公网,这个端口就对全网开放了,应该绑定到内网地址,比如-p 127.0.0.1:2222:22,只允许本机访问,再通过其他受控方式转发。

第四,账号权限过大,容器内用户如果带 sudo 且无密码,等于登进来就是 root。调试镜像可以接受,正式镜像要去掉。第五,AllowTcpForwarding 开着会让容器变成流量跳板,前面配置里已经关掉,别为了图方便又打开。

7.2 从调试镜像到生产镜像的改造路径

如果这套东西要从"本机调试"升级到"团队共用",需要做几件事。认证方式切到纯公钥,去掉密码;主机密钥改成启动时生成;端口绑定收窄到内网或本机;加AllowUsers devuser白名单,只允许特定用户登录;日志级别调到 INFO 并接入统一的日志收集;镜像定期重建以更新 openssh 版本。

还有一个容易被忽略的点:生产环境其实很少直接在业务容器里跑 sshd。更常见的做法是业务容器保持精简,需要调试时用docker exec或专门的调试边车容器,sshd 只存在于专门的"运维跳板镜像"里。所以这套配置更适合做成一个独立的调试镜像,而不是塞进每个业务镜像。想清楚这个定位,能避免把调试通道变成生产环境的常驻风险点。

7.3 几个实测下来值得记住的细节

最后分享几个踩过之后才记住的点。第一,chpasswd在某些精简镜像里需要先装passwd包,否则命令不存在,Dockerfile 里我特意装了它。第二,ssh-keygen -A在 alpine 上生成的密钥路径和 debian 一致,但 alpine 的 sshd 默认配置里UsePAM相关项不同,跨发行版务必重新验证认证流程。第三,docker logs看不到 sshd 日志时,先确认-e参数在,再确认LogLevel没被设成QUIET。第四,用ssh -v排查认证问题时,输出里的Offering public key和Server accepts key两行能直接告诉你公钥有没有被服务端接受,比看服务端日志还快。

这套镜像我在本机和内网环境都跑过,debian slim 加 entrypoint 启动时生成密钥的组合最稳,容器重启、重建、换节点都没出过认证相关的问题。真正需要小心的是权限和认证方式这两块,配置本身不复杂,复杂的是各种"看起来配了其实没生效"的细节。把第 6 节那张排查表存下来,下次遇到连不上先对一遍,基本能自己解决。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 20:05:42

SAP Fiori Launchpad导航机制与Target Mapping配置详解

1. 理解Fiori Launchpad中的导航机制在SAP Fiori架构中,Launchpad作为统一入口门户,其核心功能之一就是实现应用间的智能导航。这种导航不是简单的URL跳转,而是基于语义对象(Semantic Object)和操作(Action…

作者头像 李华
网站建设 2026/10/27 20:05:42

西门子PLC通讯类库开发:C#实现高效自动化控制

1. 项目背景与核心价值在工业自动化领域,西门子PLC作为市场占有率最高的控制器之一,其通讯对接一直是开发者的高频需求。传统做法往往需要针对不同PLC型号编写重复代码,处理繁琐的通讯协议细节,这不仅效率低下,还容易引…

作者头像 李华
网站建设 2026/10/27 20:02:59

运维转型网络安全:核心技能与实战路径

1. 运维与网络安全的核心技能重叠度分析作为在IT行业摸爬滚打十多年的老运维,我可以负责任地告诉你:运维人员转网络安全绝不是从零开始。当你拆解网络安全岗位的实际工作内容时,会发现大量技能树与运维工作高度重合。以最常见的Linux系统运维…

作者头像 李华
网站建设 2026/10/27 20:00:39

TCP打洞与NAT穿透实战:信令、端口映射与中继兜底

1. 先把问题拆开:TCP打洞到底在解决什么,以及它解决不了什么 第一次做内网设备互联时,我对"TCP打洞"这四个字有过一段相当天真的理解——以为只要两端互相知道对方的公网地址,直接 connect 过去就完事了。真跑起来才发…

作者头像 李华
网站建设 2026/10/27 19:59:46

华为云ModelArts从模型训练到在线部署全流程实战

最近花了几天时间,把华为云ModelArts从模型训练到在线部署这条链路完整走了一遍。整个过程踩了不少坑,也把官方文档里写得不清楚的地方做了不少实测验证。这篇笔记是我自己的学习记录,不是官方教程,尽量把实际操作里会遇到的问题和…

作者头像 李华
网站建设 2026/10/27 19:59:11

VB6+SQL Server 2000图书管理系统:三角色权限与借阅状态机实战

简介:本资源是一份面向软件工程初学者与课程设计实践者的《图书管理系统》完整开发文档,聚焦需求分析、系统设计与实现全过程,适用于高校软件工程课程设计、毕业设计参考及小型信息管理系统学习。文档以Word格式(.doc)…

作者头像 李华