简介:面向需要在离线环境下为 Ubuntu 20.04 桌面版部署 SSH 服务的用户,这是一份可直接使用的 openssh-server 离线安装包。资源内已整理好安装所需的核心组件与一键辅助脚本,解决无网络环境下手动收集依赖、逐个安装的麻烦,适合系统管理员、运维人员及在内网或隔离网络中搭建远程管理通道的开发者。压缩包仅 1.05MB,共 4 个文件,包含 3 个 amd64 架构的 deb 软件包(覆盖 ssh 客户端、ssh 服务端及 sftp 服务模块)和 1 个安装引导脚本,结构简单,便于快速部署。目前已有 1253 人学习下载。通过该资源,用户可在离线 Ubuntu 20.04 上按步骤完成 SSH 服务的安装与启动,同时获得端口修改、公钥认证等安全加固思路,适合作为离线环境运维工具包直接留存复用。
1. Ubuntu 20.04 sshd 离线安装包:内网机器装 OpenSSH 服务端的最短路径
刚拿到一台 Ubuntu 20.04 的内网服务器,开机发现连 sshd 都没有。管理员不让出外网,apt update 全部超时,systemctl start ssh直接报 Unit not found。这种情况我在维护隔离网络时碰到不止一次,最稳的解决办法就是提前准备好一套 sshd 离线安装包,拷进去一次装齐。这份资源是在同版本 Ubuntu 20.04 上拉取的 OpenSSH 服务端及全部依赖包,覆盖 openssh-server、openssh-client 和底层 openssl、zlib、PAM 相关依赖,解决内网机器装 SSH 服务端的最后一公里。适用人群是维护内网机房、做批量装机、或者想彻底搞懂 sshd 依赖链的人。下面我用实际安装过的方式,把这套离线包的选型、生成、安装和排坑讲透。
2. 为什么离线装一台 sshd 这么麻烦:依赖链、架构判别与包选型
2.1 sshd 不是一个文件:客户端、服务端与系统库的关系
Ubuntu 20.04 默认只装了 OpenSSH 客户端,sshd 服务端需要单独安装。这里的门道在于 sshd 不是一个孤立的二进制,/usr/sbin/sshd启动时动态链接 libcrypto、libssl、zlib、libpam、libsystemd、libselinux 等一系列库,这些库在最小化安装的服务器上未必都在。很多第一次做离线包的人只下载一个 openssh-server 的 .deb 就以为完事了,拷过去dpkg -i直接报依赖缺失,就是这个原因。
离线安装不能靠拷一个/usr/sbin/sshd文件解决。二进制文件拷过去,动态库路径、配置目录、systemd 服务单元全都没有。正确的做法是准备一整组 .deb 包,交给 dpkg 去安装:它会负责把/etc/ssh/sshd_config放到正确位置、生成主机密钥、注册 ssh.service 单元。这个选型理由也是这套资源的核心思路——用官方 .deb 而不是手工拷贝,才能让 sshd 变成被系统管理的服务,而不是一个裸文件。说句实在话,能用包管理器解决的事就不要自己去搬二进制,后者在升级和排障时会变成一个黑匣子。
2.2 动手前先查三样东西:系统版本、CPU 架构、包架构
在联网机器上拉包之前,先确认三件事:目标机的系统版本、内核架构、dpkg 架构名。lsb_release -a看系统版本,uname -m看内核架构,dpkg --print-architecture看包管理器使用的架构代码。这三项里任何一项对不上,离线包装上去都可能报版本或架构不匹配。
lsb_release -a uname -m dpkg --print-architecturelsb_release -a的输出会带上 Distributor ID、Description、Release、Codename,这里要看 Release 是不是 20.04,Codename 是不是 focal,两者决定了源里包版本的选择区间。uname -m打印的是 CPU 架构的内核表示,x86_64 对应 x86_64 架构,aarch64 对应 ARM64。dpkg --print-architecture是 dpkg 自己用的架构名,离线包文件名里通常带的就是这个名字。注意它和uname -m不一定完全一致:uname -m返回 x86_64 时,dpkg 一般写 amd64;返回 aarch64 时,dpkg 一般写 arm64。
| uname -m 输出 | dpkg 架构名 | 常见场景 |
|---|---|---|
| x86_64 | amd64 | 绝大多数服务器与桌面 |
| aarch64 | arm64 | 常见的 ARM 服务器 |
| armv7l | armhf | 老款 32 位 ARM 板子 |
这个对应关系在下载时用得上:你在联网机器上看到 .deb 文件名里的 amd64,就能确定它不是 arm64 版。额外提醒一句,判断系统版本时不要只看uname -a,它只显示内核版本和架构,不会告诉你这个系统是 20.04 还是 22.04。内核版本相同的两个系统也可能因为发行版源不同而存在 OpenSSL 版本差异,这点在 2.3 会再展开。
2.3 用 apt-cache 和 apt-rdepends 把依赖链摊开看
在联网机器上确认目标环境信息后,先看一眼源里能解析出什么依赖,再决定下载清单。apt-cache depends openssh-server执行后,输出里以 Depends 开头的行就是安装它必须满足的包,openssh-client、libssl1.1、libpam-modules、zlib1g、libsystemd0 这些都会冒出来。只要任一缺失,dpkg 就会在安装阶段报错。
apt-cache depends openssh-server apt-cache show openssh-server | grep -E "^(Package|Version|Depends)"apt-cache depends适合快速看一层,apt-cache show则给出包的详细元信息,从中可以看到 Depends 字段的完整约束,比如某个版本下限。但如果想一次性把依赖的依赖也找齐,靠眼睛一层层翻会漏。常见做法是先装 apt-rdepends 这个工具,它对指定包做递归依赖分析,一行命令就把整棵依赖树展开:
sudo apt-get install -y apt-rdepends apt-rdepends openssh-serverapt-get install -y apt-rdepends中的-y跳过确认,sudo 是因为安装工具要写系统目录。apt-rdepends openssh-server的输出是缩进式的依赖树,左边不带缩进的包是被依赖的父级,每往下一层多一重缩进。把不带缩进的包名全部收拢,就是离线环境需要的最小集合。如果网络环境不太干净,也可以把 Recommends 行一并纳入,按场景取舍。
这里有个参数细节:apt-cache 本身也支持递归,不装 apt-rdepends 也可以用下面这行命令拿到类似效果:
apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances \ openssh-server | grep -E "^[a-zA-Z0-9]" | sort -u--recurse打开递归,--no-recommends和--no-suggests只保留 Depends 强制依赖,--no-conflicts --no-breaks --no-replaces --no-enhances这四项把冲突、破坏、替代、增强四类关系全部排除,只留真正决定能否装得上的包。grep -E "^[a-zA-Z0-9]"过滤掉带缩进的依赖行,sort -u去重。多数情况下手边没有 apt-rdepends 时,这条命令就是全包替代方案。
如果输出里混进了gcc-9-base、libc6-dev这类只在编译期使用、运行时根本用不到的工具,说明依赖树里带了 build-dep 级的包,离线安装时不需要它们进 dpkg 列表,可以手工删掉这些包名。更保守的做法是:把列表里所有包名交给 apt-get download,然后在目标机上按依赖顺序装,只要 Depends 行能全部满足即可。这里没有黑匣子,依赖树长什么样是可以每次跑一遍看一遍的。
3. 一条命令拉齐离线依赖:生成、拷贝、安装与验证四连
3.1 在联网机器上把依赖包全部拽下来
确定依赖清单后,用 apt-get download 把这一批包下载到本地目录。注意这里不能用 apt-get install,因为 install 会把包安装进当前系统,破坏你正在用的联网机器;download 只是下载 .deb 到当前目录,不改变系统状态。这是离线包制作和日常安装的本质区别。
mkdir -p ~/sshd-offline cd ~/sshd-offline apt-get download $(apt-rdepends openssh-server | grep -v "^ " | sort -u)mkdir -p创建目录时不会因为目录已存在而报错,cd进入目录,shell 的$()把 apt-rdepends 的输出转成参数列表喂给 apt-get download。grep -v "^ "去掉带前导空格的行,这些行是依赖树里的被依赖描述行,只提取真正的包名。sort -u去重,避免同一个包反复下载。如果你把命令换成 2.3 的 apt-cache 递归版本,把grep -v "^ "换成grep -E "^[a-zA-Z0-9]",效果一样。
拉完包后建议立刻检查目录,这一步只花五秒钟,却能避免后面的整场返工:
ls -lh *.deb dpkg-deb -I openssh-server*.deb | grep -E "^(Package|Version|Architecture)"ls -lh看每个 .deb 的体积,dpkg-deb -I读取 deb 的控制信息,grep -E "^(Package|Version|Architecture)"确认主包的三个关键字段。Package 是不是 openssh-server,Version 是不是和源机器一致,Architecture 是不是 amd64。如果这里发现架构写的是 arm64 而目标机是 amd64,直接回到 2.2 重新核对。
3.2 把包带到目标机器:三种现实可行的传输方式
传输方式取决于目标机器当前状态。如果目标机器已经能 ssh,比如你只是要补装或升级 sshd,直接用 scp 推过去就行。如果目标机器是全新的、连 sshd 都没有,scp 推不进去,只能用 U 盘、文件服务器或通过内网 Web 服务下载。
scp -r ~/sshd-offline/ user@192.168.x.x:/tmp/scp -r递归拷贝整个目录,地址里的 user 换成目标机用户名,IP 换成目标机实际内网地址,这里只是示例。如果没有 sshd,把整个目录拷进 U 盘,再到目标机上执行cp -r /media/usb/sshd-offline /tmp/。文件名和扩展名不要动,dpkg 处理 deb 时不看文件名,但保持原样能让排查时更快找到对应包。
拷完后第一件事不是安装,而是先在目标机上看一遍包清单是不是完整。离线安装最怕的就是少一个依赖包还看不出来,等到 dpkg 报错再回头找,来回一折腾就是半天。我在实际分发时通常会把整个目录压缩成 tar 包再传输,一方面减少文件数量,另一方面tar tf能快速校验文件有没有中途损坏。
3.3 dpkg 安装与依赖修复:先后顺序决定成败
进入目录后可以直接尝试dpkg -i *.deb,dpkg 内部会做依赖排序,多数情况下一次过。但离线环境里如果之前残留过半安装的包,或者依赖列表里有遗漏,一次-i会中途停下。此时不要盲目再跑一遍,先把包解包到系统、再统一配置:
cd /tmp/sshd-offline sudo dpkg -i *.debsudo dpkg -i *.deb中星号由 bash 展开成所有 deb 文件,dpkg 逐个安装。如果看到类似 dependency problems prevent configuration of openssh-server 的报错,执行下面这段补救流程:
sudo dpkg --unpack *.deb sudo dpkg --configure -a sudo apt-get -f install -y --no-install-recommendsdpkg --unpack只把每个包的文件释放到系统目录并更新 dpkg 数据库,不做配置;dpkg --configure -a对所有处于 unpacked 状态的包执行配置脚本;apt-get -f install -y尝试修复依赖关系。在离线环境里 apt 源不可用,-f不会真的去网上下载,它只是检查本地已解包的状态并尝试补齐依赖记录。如果这一步输出没有报错,说明离线包已经全部就位。
这里有个常见误用:拿到离线包就急着dpkg --force-all -i强装。大多数情况下没必要,force 参数会跳过配置脚本,反而让 sshd 的 systemd 单元不生成,后面服务起不来。优先用 unpack 加 configure 的组合,force 只留给 4.2 那种库文件被误删的恢复场景。
3.4 安装后的三重验证:语法、服务、端口
装完的瞬间不要以为就结束了。离线环境里没有 apt 能帮你验证,一切只认三条:配置语法对不对、服务有没有起来、端口有没有在监听。
sudo sshd -t sudo systemctl start ssh sudo systemctl enable ssh systemctl status ssh --no-pagersshd -t只检查/etc/ssh/sshd_config语法,不启动服务,没有任何输出或者打印 OpenSSH 版本信息都算通过。如果它报错,说明配置里有非法参数,先别启动服务。接下来systemctl start ssh启动,systemctl enable ssh设置开机自启,systemctl status ssh --no-pager查看运行状态,Active 行出现 active (running) 就说明服务进程活着。
服务活着不代表端口就在监听,再补一条:
ss -tlnp | grep ":22"ss是 iproute2 自带的网络状态工具,-t只看 TCP,-l只看监听,-n不做域名解析,-p显示进程名,grep ":22"过滤 22 端口。如果输出里有LISTEN 0 128 0.0.0.0:22或[::]:22,说明 sshd 已经就绪。没有输出就把grep :22换成grep sshd,看是不是端口被改过或监听地址不是默认。
4. 避坑指南:sshd 离线包常见的五种翻车现场
4.1 依赖缺失:dpkg 提示 openssh-server 依赖的包没装
现象:sudo dpkg -i *.deb执行到中间停住,终端打印类似 openssh-server depends on openssh-client; however: Package openssh-client is not installed 的提示,openssh-server 卡在 configured 状态。
原因:离线包生成时依赖清单没拉全。常见做法是只下载了 openssh-server 一个 deb,或者用apt-get install --download-only但过滤掉了 Recommends 和 Suggested。另一个原因是目标机器是最小化安装,连 openssh-client 这种被当作基础包的软件都没装。
解决:回到联网机器,重新执行 2.3 里的apt-rdepends openssh-server全量依赖命令,把输出里所有包名下载齐全。如果时间紧、暂时不想重新拉包,可以在目标机上用最小清单手工分层安装:
sudo dpkg -i libssl1.1*.deb libpam*.deb zlib1g*.deb libsystemd0*.deb sudo dpkg -i openssh-client*.deb sudo dpkg -i openssh-server*.deb手工指定顺序时,基础库在最前面,客户端在服务端之前,依赖方向只能由被依赖方指向依赖方。dpkg 自己可以排序,但手工分层能把报错定位缩小到一个具体的阶段。
4.2 libcrypto.so.1.1 找不到:OpenSSL 库文件被外力改动
现象:openssh-server 装好了,systemctl start ssh后服务秒退,journalctl -u ssh或/var/log/auth.log里出现 error while loading shared libraries: libcrypto.so.1.1: cannot open shared object file 的报错。
原因:Ubuntu 20.04 的 OpenSSL 版本是 1.1 系列,sshd 二进制链接的是 libcrypto.so.1.1。如果目标机器上之前有人编译安装过新版本 OpenSSL,或手动删过老库,系统里只剩 .so.3 而没有 .so.1.1,sshd 就会在启动时找不到库。这是离线环境里最典型的看起来装完了、其实没法用的场景。
解决:离线包里保留 libssl1.1 对应的 deb,用它把库文件恢复到系统目录:
sudo dpkg -i --force-depends libssl1.1*.deb sudo ldconfig ldconfig -p | grep libcrypto--force-depends会跳过依赖检查,只把库文件放回系统;ldconfig重建动态链接缓存;最后ldconfig -p | grep libcrypto确认缓存里出现 libcrypto.so.1.1 与 libcrypto.so.3 都属于正常。这里用 force 是可接受的,因为本质不是缺依赖,而是库文件丢失。
4.3 服务名对不上:Unit ssh.service not found
现象:systemctl start ssh报 Failed to start ssh.service: Unit ssh.service not found,但dpkg -l | grep openssh明明显示 openssh-server 已经 install。
原因:不同版本的 openssh-server 生成的 systemd 单元文件名不完全一致。Ubuntu 官方包通常生成 ssh.service,而某些第三方维护的包生成的是 sshd.service。如果包是从其它源或者旧版本那边拉来的,服务名就可能对不上。另外还有一类情况:openssh-server 实际装好了,但安装时配置脚本没执行,系统里根本没有生成 systemd 单元。
解决:先看系统里实际有哪些单元文件:
systemctl list-unit-files | grep -E "ssh|sshd"如果输出里有 ssh.service,就用 ssh 启动;如果只有 sshd.service,就用 sshd 启动。两个都在时优先用 openssh-server 包自带的那个。都没有,说明安装的配置阶段失败了,执行sudo dpkg --configure openssh-server重新跑一遍配置脚本。
4.4 防火墙和端口双重限制:本机能连,跨网段连不上
现象:在服务器本机用ssh localhost能连上,同一局域网里其它机器也能连,但从外部网段访问超时,或者ssh -v user@host卡在 connect 阶段。
原因:常见是两层问题叠加。一层是 ufw 默认策略拦了 22 端口,另一层是/etc/ssh/sshd_config里既写了 Port 22 又写了 ListenAddress 指向内网地址,导致外网接口上根本没有监听。
解决:先放行端口,再看 sshd 实际的监听地址:
sudo ufw allow 22/tcp sudo ufw status sudo grep -E "^(Port|ListenAddress|PermitRootLogin)" /etc/ssh/sshd_configufw allow 22/tcp与ufw allow OpenSSH等价,写 22/tcp 更明确。grep 的^锚定行首,只显示没有被注释掉的配置行。如果 ListenAddress 写死了某个内网 IP,把这行注释掉或改为0.0.0.0,然后执行sudo sshd -t && sudo systemctl reload ssh重载配置。这里注意,改配置之前先把当前 ssh 连接保持住,避免自己把自己关在门外。
4.5 两台同型号机器装不上:离线包环境与目标环境版本漂移
现象:在一台机器上把 sshd 离线包装成功了,把同一个目录拷到另一台同型号、同系统版本的机器上,dpkg 报版本冲突,比如 libssl1.1 已经存在且版本比离线包里的更高。
原因:离线包是从源机器的 apt 状态生成的,它和目标机器的软件快照并不一定一致。如果目标机器之前升级过 openssl、zlib 或 systemd,离线包里带的是旧版本,dpkg 会拒绝降级安装。这是批量装机最容易踩的坑,同一批硬件、同一张镜像,不代表两台机器的软件版本完全一样。
解决:先看目标机器现有包的版本,再决定是降级还是另找源:
apt-cache policy libssl1.1 openssh-server dpkg -l | grep -E "openssh|libssl"apt-cache policy同时显示已安装版本与源里可用版本,dpkg -l列出两个包的实际安装状态。如果目标机的库版本比离线包新,就不要强行覆盖;重新在另一台与目标机软件快照一致的机器上生成离线包,或者直接修改 sources.list 指向内网 apt 镜像,让 apt 自己解析版本。血泪经验是:离线包要跟着目标机器当前软件清单走,不要跟着生成机器走。
5. 安装完只是起点:sshd 配置加固与回环自测
5.1 回环自测:ssh -v localhost 是最快的体检
离线环境没有复杂的监控工具,验证 sshd 是否真能用,一条回环命令就够了:
ssh -v localhost-v让客户端打印完整调试过程。连接成功后日志末尾会输出 Authenticated to localhost。如果停在 Permission denied,是认证配置的问题;如果停在 Connection refused,说明 sshd 没在监听。想看得更细,用ssh -vvv user@host并过滤关键阶段:
ssh -vvv user@host 2>&1 | grep -E "kex|auth|debug"2>&1把错误输出合并到标准输出,grep -E "kex|auth|debug"滤出密钥交换、认证、调试三段信息。kex 阶段看加密算法协商是否一致,auth 阶段看公钥还是密码被接受。这一步能直接区分服务没起来和认证不过两种截然不同的故障。
5.2 最小暴露面配置与密钥登录
sshd 能用之后,建议尽快把默认配置收紧。默认的 Ubuntu 20.04 允许密码登录且允许 root 直接登录,这对离线内网也许不致命,但如果这台机器将来要接入其它网络,风险会放大。我一般会在/etc/ssh/sshd_config里把下面几项改成显式配置:
| 配置项 | 建议值 | 作用 |
|---|---|---|
| PermitRootLogin | prohibit-password | 禁止 root 密码登录,保留密钥登录 |
| PasswordAuthentication | no | 关闭密码认证,只接受公钥 |
| PubkeyAuthentication | yes | 显式开启公钥认证 |
| Port | 2222 | 改默认端口,降低扫描面 |
改完配置不要直接重启服务,先用sudo sshd -t确认语法没有把服务搞挂。之后在客户端生成密钥并投递公钥:
ssh-keygen -t ed25519 -a 100 -C "offline-host" ssh-copy-id -p 2222 user@hostssh-keygen -t ed25519生成 ed25519 密钥,-a 100把 KDF 迭代次数调到 100 轮,-C加备注方便区分钥匙。ssh-copy-id会自动把本机公钥追加到目标机的~/.ssh/authorized_keys并设置权限。没有 ssh-copy-id 时,手工追加也一样:
cat ~/.ssh/id_ed25519.pub | ssh -p 2222 user@host \ 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'管道把公钥内容传进 ssh 命令,远程那边先建目录、收紧目录权限,再把公钥追加进 authorized_keys,最后把文件权限收紧到 600。sshd 会拒绝权限过宽的 authorized_keys,这一步是很多人连不上公钥的玄学原因,其实只是权限位不对。
5.3 平滑重载与权限检查
配置全部到位后,正式生效走 reload 而不是 restart,这样不会踢掉现有连接:
sudo sshd -t sudo systemctl reload ssh systemctl status ssh --no-pagersshd -t在 reload 之前再跑一遍是最低成本的后悔药,语法错了连接全部断掉,离线环境里你只能跑去机房。reload 是 SIGHUP 信号,sshd 会重新读取配置但保留现有会话。systemctl status ssh --no-pager最后确认状态,Active 行显示 active (running) 就收工。
以前我在一次远程变更里,改完配置文件直接 reload,结果少了个参数,所有连接全部断掉,最后只能跑一趟机房。从那以后我每次离线环境装完 sshd,都强制把sshd -t、回环自测、权限检查、reload 这条链完整走一遍再收工。这套流程看着琐碎,但每走一步,都在把装得上变成一定能用。希望这篇笔记能帮到正在内网机房折腾的你。
本文还有配套的精品资源,点击获取