DevOps · 实践经验

从蓝盾 CI/CD 到本地脚本

部署个人静态网站的踩坑实录——六轮排查,一条 ssh -v 终结

Yeran 2026-05-03 约 10 分钟

上周折腾了一件事:给自己的个人网站搭一套自动部署。听起来很简单对吧?静态站点,rsync 同步到服务器,Nginx 一刷新就完事了。

结果我在 SSH 连接这一个环节上卡了整整一个晚上,排查了 6 轮,走过了从「应该很快搞定」到「要不算了」的完整心路历程。

今天把这个过程完整记录下来。不是教程,更像是一份排查手册——希望能帮到遇到类似问题的你。

📋 背景

项目详情
站点类型纯静态(HTML/CSS/JS,零构建依赖)
代码托管工蜂(公司内部 Git)
服务器腾讯云轻量应用服务器,Ubuntu 24.04,OpenSSH 9.6
CDNEdgeOne 代理 HTTPS,回源到 Nginx 80 端口
目标push 代码后自动部署到服务器

🔧 初始方案:蓝盾 CI/CD 流水线

思路很清晰:Git Push → 蓝盾触发 → SSH 到服务器 rsync 同步 → Nginx reload → 健康检查。

然后噩梦就开始了。

💥 六轮踩坑实录

坑 1:蓝盾变量系统不支持凭证类型

蓝盾有凭证管理功能,我创建了一个 SSH 私钥凭证(ID: deploy_ssh_key)。但在经典模式流水线中,变量类型下拉框里只有字符串、文本框、布尔值——没有「凭证」选项。

解决 用「文本框」变量替代,把 SSH 私钥内容直接粘贴为变量值,勾选「敏感」选项。

💡 经验:不同蓝盾版本的功能差异很大。如果你用的是智研/经典模式,很多高级功能(如凭证引用)可能不可用。提前确认再动手。

坑 2:多行私钥被文本框变量吃掉换行符

第一次运行流水线就报错了:

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
💡 经验:CI/CD 系统中传递多行敏感内容,Base64 编码是最稳的方案。不要相信任何平台的「文本框」能完整保留换行符。

坑 3:服务器遭受 SSH 暴力破解攻击

修复了密钥格式后,还是同一个错误。查看服务器的 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 并发连接限制,导致蓝盾构建机被「挤掉」了。

处理 紧急做了三件事:

措施命令作用
fail2bansudo apt install -y fail2ban3 次错误自动封禁 IP
MaxStartupsMaxStartups 30:50:80并发连接容量提升 3x
禁止密码PasswordAuthentication no暴力破解彻底失效
⚠️ 警告:任何公网服务器的 22 端口都会被扫描。如果你只用密钥登录,第一时间关掉密码认证。这不是可选项,而是必选项。

坑 4:加固后问题依旧——排除了所有服务端原因

做完安全加固后,满怀信心地重新触发流水线……还是一样的错误。

这次进行了更深入的排查,逐一排除了所有服务端因素

排查项结果
TCP 端口可达
当前 SSH 连接数 只有 1 个
密钥格式 PEM RSA,权限 600
hosts.deny / AllowUsers 无限制
Kex 算法 多种算法
authorized_keys 公钥已在

所有服务端因素全部排除。问题一定出在客户端到服务器的网络链路上

坑 5:换到开发容器也一样

放弃蓝盾后,我在 AnyDev 开发容器里写了个部署脚本直接执行——居然也报同一个错!

但换了一个新开的 AnyDev 容器测试,ssh -v 开启详细日志后,SSH 协议握手竟然是成功的。这说明不同容器的网络出口可能不同

🎯 坑 6(真正原因):企业网络的 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

🔑 核心教训:企业内网环境(CI/CD 构建机、云开发容器等)的网络出口通常都有 HTTP 代理,SSH 流量会被拦截。这个限制在服务端完全看不到——因为 SSH 连接压根没到服务器,在代理那一层就被挡了。

✅ 最终方案:本地一键部署

对于纯静态个人网站,最简单可靠的方案就是本地执行部署脚本:

#!/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
💡 关于 EdgeOne:它只代理 HTTP/HTTPS 流量(80/443 端口),SSH 的 22 端口仍然直接暴露在公网。SSH 安全只能靠服务端自身配置。

📝 总结:五条经验

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 脚本比一套流水线更合适。

🗺️ 排查路径全景图

第 1 轮
蓝盾凭证变量不可用 → 改用文本框
第 2 轮
SSH 私钥换行被吃 → Base64 编码
第 3 轮
密钥格式正确仍连不上 → 发现服务器遭暴力破解 → 安全加固
第 4 轮
加固后仍连不上 → 排除所有服务端因素
第 5 轮
换到开发容器也一样 → 怀疑网络链路
第 6 轮
ssh -v 一把定位:HTTP 代理拦截了 SSH 流量
最终
放弃 CI/CD → 本地脚本部署 → 完美运行 ✅

回过头看,这个问题的根因其实就一句话:企业内网的 HTTP 代理拦截了 SSH 协议。但因为错误信息太过笼统(kex_exchange_identification: Connection closed by remote host),很容易被引导到错误的排查方向。

下次遇到 SSH 连接问题,第一件事就是 ssh -v

记住了。