技术指南
SSH 与 SFTP 文件交付:验证身份、校验内容与安全上线
把 SSH/SFTP 文件传输拆成可信主机、受限暂存、内容校验与可回滚上线,并验证拒绝路径。
SSH 解决的是受信主机上的远程会话,SFTP 解决的是同一信任链上的文件传输;两者都不会自动证明文件完整、目标路径正确或新版本可上线。可靠交付应把主机身份、客户端身份、暂存目录、内容摘要、发布切换和回滚证据分开,并让每一步都能失败停止。
来源覆盖 SSH 入门,本文收敛为文件交付契约
归档来源完整介绍客户端、会话、SFTP、SCP、密钥、配置与排障。本文保留协议分层、密钥优先、host key 变化需核对、第二会话和配置校验;不沿用客户端排行、root/密码优先、全局压缩与连接复用、固定保活值、修改端口即加固、通用 UseDNS/GSSAPI 关闭、危险删除与强制终止速查、攻击后先备份可疑主机或 SSH 会自动重连等表述。
先写清一次交付的四个身份和两个边界
| 对象 | 需要固定的事实 | 验收证据 |
|---|---|---|
| 目标主机 | host、port、服务器 host key 指纹 | 独立渠道指纹与严格 known_hosts 命中 |
| 操作者 | 本次允许的用户、客户端 key、来源与期限 | 只提供预期 identity,错误 key 被拒绝 |
| 候选物 | release ID、文件清单、字节数与 SHA-256 | 本地与服务器暂存区摘要一致 |
| 目标路径 | 暂存、版本目录、live 指针与文件系统 | 候选不能直接覆盖 live,切换前后目标可读 |
| 权限边界 | 谁能上传、验证、切换与回滚 | 上传身份没有 shell/转发或 live 写权限 |
| 停止条件 | 身份、空间、校验、验证或回滚任一失败 | 非零退出并保持旧版本不变 |
第一次连接先证明服务器身份
OpenSSH 当前手册说明 host key 会记录在 known_hosts;目标身份变化时客户端会警告,并限制可能被中间人利用的认证。首次连接的指纹应从供应商控制台、配置管理或另一条已认证渠道取得。`ssh-keyscan` 只能收集网络对端给出的 key,若在同一未验证路径上使用,不能独立证明对端是谁。
# On a trusted console or configuration channel:
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
# On the client, inspect the exact stored host entry:
ssh-keygen -F '[203.0.113.10]:22' -f ~/.ssh/known_hosts.delivery
ssh -o StrictHostKeyChecking=yes \
-o UserKnownHostsFile=~/.ssh/known_hosts.delivery \
[email protected] true- 记录指纹算法、完整 SHA256 值、来源、取得人和 UTC 时间。
- 重装、迁移或轮换 host key 后,从独立渠道重新取得新指纹和变更原因。
- 只删除精确 host:port 的旧记录;保留事件记录,不能清空整个 known_hosts。
- 使用主机名时同时核对 DNS、最终地址和 SSH 配置展开结果,避免命中意外 Host 块。
给每个目标限定用户、key 和转发能力
Host app-delivery
HostName 203.0.113.10
Port 22
User upload
IdentityFile ~/.ssh/app-delivery
IdentitiesOnly yes
StrictHostKeyChecking yes
UserKnownHostsFile ~/.ssh/known_hosts.delivery
ForwardAgent no
ForwardX11 no`IdentityFile` 选择 key,`IdentitiesOnly yes` 避免客户端继续提供 agent 中的其他身份。OpenSSH 默认不转发 agent,并明确提醒远端高权限用户可借转发的 agent 执行签名认证;普通文件交付不需要 `ForwardAgent yes`。用 `ssh -G app-delivery` 查看最终展开配置,但输出可能含内部主机名和路径。
SFTP、scp 和 shell 是不同接口
| 接口 | 当前行为 | 适合场景 |
|---|---|---|
| sftp | 通过 SSH 传输,支持交互与可失败停止的 batch | 明确暂存路径、批量上传、rename 与自动化 |
| scp | 当前 OpenSSH 默认使用 SFTP;`-O` 才强制旧 SCP 协议 | 简单单次复制且已理解路径展开 |
| ssh command | 在远端执行 shell 命令 | 由部署身份做校验、解包、切换和健康检查 |
| GUI SFTP | 仍依赖客户端保存的主机与凭据配置 | 人工少量传输,但要能展示并核对指纹与目标路径 |
传输前先审阅候选物而不是整个工作目录
- 生成不可变 release ID,记录构建提交、构建环境和输出文件,不上传 `.git`、`.env`、私钥、缓存或本机日志。
- 枚举普通文件、目录、符号链接、设备节点和硬链接;归档解包前检查绝对路径与 `..` 穿越。
- 明确应保留的 mode、owner/group 与执行位;不要用递归 777 处理权限未知。
- 确认目标暂存空间、inode、配额和同文件系统切换条件;空间不足时停止而不是清理未知文件。
- 为每个传输文件计算摘要,并把清单与候选一起作为交付单元。
RELEASE='20260818-0052'
ARTIFACT='app.tar'
umask 077
find build -xdev -printf '%y %m %u:%g %p\n' | sort > FILES.txt
tar -C build -cf "$ARTIFACT" .
sha256sum "$ARTIFACT" > SHA256SUMS
wc -c "$ARTIFACT" FILES.txt SHA256SUMS上传到唯一暂存名,并让 batch 在错误处停止
SFTP batch 会在 `put`、`rename`、`mkdir` 等命令失败时中止,除非命令被 `-` 前缀显式忽略。批处理应使用新的 release 路径和 `.part` 名称,上传完成后再 rename;不要给关键命令加忽略失败前缀,也不要把凭据写进 batch 文件。
mkdir incoming/20260818-0052
put app.tar incoming/20260818-0052/app.tar.part
put SHA256SUMS incoming/20260818-0052/SHA256SUMS.part
rename incoming/20260818-0052/app.tar.part incoming/20260818-0052/app.tar
rename incoming/20260818-0052/SHA256SUMS.part incoming/20260818-0052/SHA256SUMS
ls -l incoming/20260818-0052网络中断可能留下 `.part` 或完整但未 rename 的文件。清理只能针对本次 release ID,并在确认没有活动传输、没有审计/恢复需求后执行;共享目录中不要使用通配符删除。
在目标端重新计算内容摘要
RELEASE='20260818-0052'
STAGE="/srv/upload/incoming/$RELEASE"
cd "$STAGE"
sha256sum --check SHA256SUMS
tar -tf app.tar > archive-files.txt
test -s archive-files.txt只传文件的账号不应顺带获得 shell 和隧道
Match User upload
ForceCommand internal-sftp
DisableForwarding yes
PermitTTY no
X11Forwarding noOpenSSH 的 `ForceCommand internal-sftp` 可强制内置 SFTP,但手册明确指出它本身不限制 TCP、agent、socket 或 X11 转发,因此还需要 `DisableForwarding yes`。目录隔离仍依赖文件系统权限或经过验证的 ChrootDirectory;本段配置没有自动阻止 upload 用户读取所有世界可读文件。
校验后由独立部署身份切换版本
上传身份只写 incoming;部署身份把已校验候选复制或解包到新的只读版本目录,运行语法/配置/应用检查,再切换 live 指针。以下 Linux 演示要求 `releases`、`current.next` 和 `current` 位于同一文件系统,并记录当前目标;跨文件系统移动、共享存储和 Windows 语义必须另行验证。
ROOT='/srv/app'
RELEASE='20260818-0052'
PREVIOUS=$(readlink "$ROOT/current")
install -d -m 0755 "$ROOT/releases/$RELEASE"
tar -C "$ROOT/releases/$RELEASE" -xf "/srv/upload/incoming/$RELEASE/app.tar"
"$ROOT/releases/$RELEASE/bin/validate"
ln -s "releases/$RELEASE" "$ROOT/current.next"
mv -T "$ROOT/current.next" "$ROOT/current"
# Roll back by recreating current.next for the recorded PREVIOUS target.- 切换前确认旧 current 目标、摘要、owner/group/mode 和回滚命令。
- 候选从新版本目录启动或读取,不在 live 目录原地覆盖。
- 用真实 Host/TLS/认证路径做正向请求,并对不允许的路径做拒绝检查。
- 失败时恢复精确旧指针与相关配置,重复同一健康检查;不是只把进程重启。
- 保留旧版本到恢复窗口结束,再按明确 release ID 回收。
交互会话不是持久任务管理器
tmux 或 screen 能让交互会话在网络断开后继续,但不能自动重连 SSH,也不提供服务依赖、重启策略、资源限制和系统启动集成。一次性人工排查可以使用会话复用;长期服务、定时任务与发布进程应交给 systemd、队列或实际编排器,并从新连接验证状态。
| 症状 | 先证明 | 不要直接做 |
|---|---|---|
| host key changed | 独立渠道的新指纹与变更记录 | 删除全部 known_hosts 或关闭检查 |
| Permission denied | 最终 user/host/port、提供的 key 与 authorized key | 开启 root 密码或递归 chown home |
| batch 非零 | 第一条失败命令、暂存残留和空间/权限 | 忽略退出码继续切换 |
| 摘要不符 | 本地清单、远端字节数、是否错误续传 | 重新生成远端摘要使其“通过” |
| 上线检查失败 | 候选日志、真实入口与旧版本状态 | 在 live 上继续手改直到能访问 |
隔离演练验证了身份、批处理与回滚门禁
VPScope 在 mode-0700 临时目录和独立网络 namespace 中启动 OpenSSH 9.6p1,使用预先固定的服务器 host key、专用客户端 key 与 `IdentitiesOnly yes`。正确身份完成 SFTP batch 上传和 rename;错误 host key 与错误客户端 key 均以 255 失败。包含一次不存在文件 rename 的 batch 非零退出,后续 marker 没有上传,证明关键错误会停止序列。
候选文件在远端暂存区通过 SHA-256,故意损坏的副本校验失败且 live 指针保持基线。随后在同一文件系统创建新版本并用 `mv -T` 切换符号链接,检查候选内容后恢复旧指针;基线内容与目标均精确返回。另一个配置验证了 internal-sftp、DisableForwarding、PermitTTY no 和 X11Forwarding no,且缺少 DisableForwarding 的候选被门禁拒绝。演练未使用真实 VPS、ChrootDirectory、GUI 客户端、外部网络、签名供应链、共享存储或生产服务。
SSH/SFTP 文件交付的可复查清单
- 目标 host、port 与 host key 指纹来自独立受信渠道,StrictHostKeyChecking 保持开启。
- 专用客户端 key、用户和期限明确;只提供预期 identity,agent/X11 forwarding 未无意开启。
- 候选 release ID、文件清单、大小、权限和摘要由受控构建产生。
- SFTP batch 上传到唯一暂存路径,关键命令失败会非零停止。
- 部分文件和续传结果不会直接进入 live;目标端重新计算完整摘要。
- 上传、验证、部署和回滚身份分离;SFTP-only 配置同时限制 shell 与 forwarding。
- 归档路径、符号链接、空间、inode 和跨文件系统边界在高权限解包前检查。
- 新版本在独立目录完成配置/应用验证,live 不被原地覆盖。
- 切换前记录旧目标;候选失败时恢复精确指针并重复真实入口检查。
- 日志和工单不含私钥、口令、agent socket、完整内部路径或敏感文件清单。
- 暂存残留与旧版本按精确 release ID 和保留期回收,不使用共享目录通配删除。
- 交互任务与长期服务分开管理;会话工具不被误当成重启、恢复或自动重连机制。