部署个人静态网站的踩坑实录——六轮排查,一条 ssh -v 终结
上周折腾了一件事:给自己的个人网站搭一套自动部署。听起来很简单对吧?静态站点,rsync 同步到服务器,Nginx 一刷新就完事了。
结果我在 SSH 连接这一个环节上卡了整整一个晚上,排查了 6 轮,走过了从「应该很快搞定」到「要不算了」的完整心路历程。
今天把这个过程完整记录下来。不是教程,更像是一份排查手册——希望能帮到遇到类似问题的你。
| 项目 | 详情 |
|---|---|
| 站点类型 | 纯静态(HTML/CSS/JS,零构建依赖) |
| 代码托管 | 工蜂(公司内部 Git) |
| 服务器 | 腾讯云轻量应用服务器,Ubuntu 24.04,OpenSSH 9.6 |
| CDN | EdgeOne 代理 HTTPS,回源到 Nginx 80 端口 |
| 目标 | push 代码后自动部署到服务器 |
思路很清晰:Git Push → 蓝盾触发 → SSH 到服务器 rsync 同步 → Nginx reload → 健康检查。
然后噩梦就开始了。
蓝盾有凭证管理功能,我创建了一个 SSH 私钥凭证(ID: deploy_ssh_key)。但在经典模式流水线中,变量类型下拉框里只有字符串、文本框、布尔值——没有「凭证」选项。
解决 用「文本框」变量替代,把 SSH 私钥内容直接粘贴为变量值,勾选「敏感」选项。
第一次运行流水线就报错了:
kex_exchange_identification: Connection closed by remote host
rsync error: unexplained error (code 255)
排查发现,蓝盾的文本框变量在注入到 Bash 脚本时,多行内容的换行符被破坏了。SSH 私钥是严格的 PEM 格式,每 64 字符必须换行,格式一破坏就完全无法使用。
解决 把私钥做 Base64 编码后存入变量(变成单行),脚本中用 base64 -d 解码还原:
# Mac 上编码私钥为单行
base64 < key.pem | tr -d '\n' | pbcopy
# 脚本中解码还原
echo "${deploy_ssh_key}" | base64 -d > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
修复了密钥格式后,还是同一个错误。查看服务器的 auth.log:
Failed password for invalid user admin from 204.76.203.233
Failed password for root from 200.89.69.247
Connection reset by 113.250.184.183 [preauth]
Connection closed by 200.196.50.91 [preauth]
... 平均每分钟都有陌生 IP 在尝试登录
服务器 22 端口裸露在公网上,当时初步怀疑是攻击流量占满了 SSH 的 MaxStartups 并发连接限制,导致蓝盾构建机被「挤掉」了。
处理 紧急做了三件事:
| 措施 | 命令 | 作用 |
|---|---|---|
| fail2ban | sudo apt install -y fail2ban | 3 次错误自动封禁 IP |
| MaxStartups | MaxStartups 30:50:80 | 并发连接容量提升 3x |
| 禁止密码 | PasswordAuthentication no | 暴力破解彻底失效 |
做完安全加固后,满怀信心地重新触发流水线……还是一样的错误。
这次进行了更深入的排查,逐一排除了所有服务端因素:
| 排查项 | 结果 |
|---|---|
| TCP 端口可达 | ✅ |
| 当前 SSH 连接数 | ✅ 只有 1 个 |
| 密钥格式 | ✅ PEM RSA,权限 600 |
| hosts.deny / AllowUsers | ✅ 无限制 |
| Kex 算法 | ✅ 多种算法 |
| authorized_keys | ✅ 公钥已在 |
所有服务端因素全部排除。问题一定出在客户端到服务器的网络链路上。
放弃蓝盾后,我在 AnyDev 开发容器里写了个部署脚本直接执行——居然也报同一个错!
但换了一个新开的 AnyDev 容器测试,ssh -v 开启详细日志后,SSH 协议握手竟然是成功的。这说明不同容器的网络出口可能不同。
在报错的容器里用 ssh -v,真相终于浮出水面:
debug1: Connecting to 139.199.72.245 port 36000.
debug1: Connection established.
debug1: kex_exchange_identification: banner line 0: HTTP/1.1 502 Server UnReachable
debug1: kex_exchange_identification: banner line 1: proxy-agent: VM-129-13-centos
两个致命问题同时暴露:
问题一:默认端口不是 22。容器的 SSH 客户端配置(/etc/ssh/ssh_config.d/)把默认端口改成了 36000——企业内部的 SSH 跳板端口。
问题二:HTTP 代理拦截了 SSH 流量。容器的所有外网 SSH 连接都经过一个 HTTP 代理。这个代理只能理解 HTTP 协议,收到 SSH 握手包后直接返回了 502 Server UnReachable。
这就是 kex_exchange_identification 错误的真正原因——SSH 客户端期望收到 SSH 版本字符串(如 SSH-2.0-OpenSSH_9.6),结果收到的是 HTTP/1.1 502。
对于纯静态个人网站,最简单可靠的方案就是本地执行部署脚本:
#!/bin/bash
set -e
SERVER="ubuntu@139.199.72.245"
KEY="~/.ssh/light_server.pem"
# 1. rsync 增量同步
rsync -avz --delete \
--exclude='.git' --exclude='deploy.sh' \
-e "ssh -i $KEY" ./ $SERVER:/var/www/my-space/
# 2. 重载 Nginx
ssh -i $KEY $SERVER 'sudo nginx -t && sudo systemctl reload nginx'
# 3. 健康检查
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://yeranyang.com)
echo "✅ 部署完成 (HTTP $HTTP_CODE)"
配合一个 shell alias:
alias deploy="git push && ./deploy.sh"
一条命令,push + 部署一步到位。
虽然蓝盾方案没跑通,但排查过程中顺便把服务器安全加固了一遍。这些对任何公网服务器都是必备的:
# 1. 安装 fail2ban
sudo apt install -y fail2ban
# 2. 配置规则:3 次失败封禁 1 小时
sudo tee /etc/fail2ban/jail.local > /dev/null << 'EOF'
[sshd]
enabled = true
maxretry = 3
bantime = 3600
findtime = 600
EOF
sudo systemctl restart fail2ban
# 3. 禁止密码登录
sudo sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshd
# 4. 提升并发连接上限
sudo sed -i 's/^#MaxStartups.*/MaxStartups 30:50:80/' /etc/ssh/sshd_config
sudo systemctl restart sshd
1. ssh -v 是排查一切 SSH 问题的第一步
别猜。一个晚上的排查工作,一条 ssh -v 就能定位。它会告诉你连的是哪个端口、走没走代理、在哪一步断开的。
2. 企业内网出口有代理,SSH 可能被拦截
CI/CD 构建机、云开发容器等企业环境的外网流量通常经过 HTTP 代理。SSH 协议无法通过 HTTP 代理,会被直接拦截。服务端日志看到的只是 "Connection closed [preauth]",完全看不出是代理拦截。
3. CI/CD 传递多行密文用 Base64 编码
SSH 私钥、证书等多行内容,在 CI/CD 变量中传递时几乎必然会丢失换行。Base64 编码转成单行是最稳妥的做法。
4. 公网服务器的 SSH 安全是 Day 1 的事
服务器上线后几乎立刻就会被扫描。关密码登录 + 装 fail2ban,应该写进服务器初始化脚本里。
5. 简单场景用简单方案
纯静态站点 + 个人项目,一个 deploy.sh 就够了。CI/CD 是好东西,但如果你的场景不需要多人协作、不需要构建流程、不需要自动化测试,一个 shell 脚本比一套流水线更合适。
ssh -v 一把定位:HTTP 代理拦截了 SSH 流量回过头看,这个问题的根因其实就一句话:企业内网的 HTTP 代理拦截了 SSH 协议。但因为错误信息太过笼统(kex_exchange_identification: Connection closed by remote host),很容易被引导到错误的排查方向。
下次遇到 SSH 连接问题,第一件事就是 ssh -v。
记住了。