同事的项目在 CI 上跑了两年都没问题,上周突然一连挂了好几次,日志里只有一行红字:Host key verification failed. 一开始大家以为是 GitHub 服务抽风,重跑了几次照样崩,最后我帮他查了一圈才发现,问题根本不在网络,也不在 token,而是那台新换的构建机上 known_hosts 里躺着一份过期指纹。
这种经历应该不少人都有过。本地 npm install 本来是最日常不过的操作,突然有一天依赖装到一半,终端蹦出一句 Host key verification failed,后面还跟着 fatal: Could not read from remote repository. 很多人第一反应就是删 node_modules、清缓存、重新 clone,结果一顿操作猛如虎,错误原封不动。其实这个报错和 npm、和 node_modules 都没关系,真正在报错的是底层 git 调用,而 git 背后又是 SSH 在拒绝连接。文章会把这个链路彻底讲透,分别覆盖本地开发机、CI/Docker 自动化环境、以及依赖源配置三种最常见场景,最后给出一套完整的定位排查过程。无论你是普通前端项目装不上依赖,还是执行 npm install -g @openai/codex 这类全局工具时被卡住,应该都能从这里找到答案。
1. 先看报错长什么样:本地、容器、陈旧指纹三种现场
Host key verification failed 只是一句结果,不是原因。要处理它,第一步应该是看报错出现的上下文,因为不同环境下它的“长相”不一样,处理路径也完全不一样。
1.1 本地开发机最常见的完整错误段
本地终端里执行 npm install 时,如果某个依赖需要走 SSH,通常不会只报一行,而是一整段:
text复制npm ERR! Error while executing:
npm ERR! /usr/bin/git ls-remote git+ssh://git@github.com/company/private-repo.git HEAD
npm ERR!
npm ERR! Host key verification failed.
npm ERR! fatal: Could not read from remote repository.
npm ERR!
npm ERR! Please make sure you have the correct access rights
npm ERR! and the repository exists.
注意这里有个容易被忽视的细节:npm 真正执行的是 git ls-remote,不是直接 git clone。npm 在解析版本依赖时,需要先知道远端仓库有哪些 tag、哪个分支是默认分支,所以它会调用 git 去远端“看一眼”。这一眼就是 SSH 连接,也是报错开始的地方。
很多人这时候会下意识地执行 rm -rf node_modules && npm install,或者干脆把整个仓库重新 clone 一遍。但这不是依赖损坏,node_modules 里没有任何东西能修复 SSH 连接问题。只要 package-lock 还锁着同一个 git 依赖源,重装一百次也会在同一个位置报同样的错。
1.2 CI 与全新容器里的另一种形态
在 CI 或 Docker 构建环境里,报错会更“干瘪”。因为没有交互式终端,SSH 客户端无法弹出确认指纹的提示,所以你不会看到 Are you sure you want to continue connecting 这类询问,而是直接失败:
text复制npm ERR! code 128
npm ERR! command failed
npm ERR! command git ls-remote git+ssh://git@github.com/company/private-repo.git HEAD
npm ERR! Host key verification failed.
如果是在 GitHub Actions、GitLab CI、Jenkins Pipeline 这类平台里,错误日志往往被折叠在某个 step 中,只显示最后几行。这时候判断依据就不是“有没有弹窗”,而是“known_hosts 文件到底存不存在、有没有对应的 host 记录”。
1.3 容易被当成网络问题的陈旧指纹
第三种现场最隐蔽:本地之前一切正常,某天开始报错,而且不是首次连接。这种场景通常出在远端服务器重置过、更换过 SSH host key,或你手动清理过 ~/.ssh/known_hosts 后残留了旧记录。SSH 客户端发现 known_hosts 里存的主机指纹和服务器当前返回的指纹对不上,就会拒绝连接。
它的报错文本和第一种几乎一样,但处理方式完全不同:不能简单地把新指纹追加进 known_hosts,因为旧记录还在,SSH 会认为存在“中间人攻击”风险,仍然拒绝连接。必须先把旧指纹删掉,再写入新指纹。
判断到底是哪种现场,有个很实用的口诀:看报错之前有没有 REMOTE HOST IDENTIFICATION HAS CHANGED 警告,有就是指纹变更;没有就是首次连接或 known_hosts 缺失。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 真正的原因:npm install 是怎么被 SSH 的 host key 挡下的
这一节把机制讲清楚。理解了机制之后,你会发现网上大量“重新生成 SSH key”“把公钥加到 Git 平台”之类的回答,其实都没打在点上。
2.1 触发链路:从 package.json 到 git ls-remote
先看一个典型的会触发 SSH 的依赖写法:
json复制{
"dependencies": {
"internal-tool": "git+ssh://git@github.com/company/internal-tool.git#v1.2.0"
}
}
git+ssh:// 开头的 URL 已经说明一切:npm 要拿到这个包,必须先通过 SSH 连上 GitHub。然而 npm 自己并不处理 SSH 协议,它只是一个包管理器,真正干活的是 git。
当 npm 开始解析依赖树时,遇到这种 git 依赖,它会:
- 调用
git ls-remote <repository-url>获取远端引用; - 根据返回的 commit/tag 确定该安装哪个版本;
- 把这个 commit 对应的代码 clone 到 npm 的缓存目录;
- 再通过缓存目录把包内容链路到
node_modules。
其中第 1 步就需要建立 SSH 连接。如果连接在“验证服务器身份”这一步就被拒绝,后面所有步骤都不会执行。
所以 Host key verification failed 的本质是:npm 在执行 git 命令,git 在执行 ssh,ssh 在确认“我连的到底是不是我以为的那台服务器”时被卡住了。
2.2 host key 验证的五秒钟原理
SSH 协议和 HTTPS 有一个关键区别:HTTPS 的证书信任体系依赖于系统预置的 CA 证书;而 SSH 默认使用 TOFU(Trust On First Use,首次使用即信任)策略。
类比一下:第一次去一栋新办公楼,门禁系统里没有你的信息,保安会拦下你,问你认不认识楼里的某个人,确认后才放你进去。SSH 的 known_hosts 文件就是这栋楼的“访客登记簿”。第一次连接某台服务器时,SSH 会把服务器返回的主机公钥指纹记录到 ~/.ssh/known_hosts;下次再连同一台服务器,它会拿登记簿上的指纹和服务器实际返回的指纹做比对,一致才放行。
这个行为由 OpenSSH 的 StrictHostKeyChecking 参数控制,常见取值有三个:
| 取值 | 行为 | 适用场景 |
|---|---|---|
ask |
首次连接时询问是否信任,默认值;非交互环境无法询问时直接失败 | 本地开发机 |
yes |
严格比对,known_hosts 里没有记录就直接拒绝 | 对安全性要求高的自动化环境 |
no |
不校验,自动把新指纹写入 known_hosts | 临时排错、一次性测试环境 |
Host key verification failed 实际上发生在 SSH 客户端确认服务器身份的阶段,比用户名密码或密钥认证更早。也就是说,哪怕你的 SSH key 已经正确加到 GitHub,还报这个错,也说明 SSH 客户端根本还没走到“校验你的 key”那一步,就被 host key 拦在了门外。很多人给 GitHub 反复重传 SSH key 却毫无作用,原因就在这里。
2.3 known_hosts 的“脏数据”问题
known_hosts 本身也是一个普通文件,默认在 ~/.ssh/known_hosts。它的常见问题有两类:
第一类是文件里压根没有目标 host 的记录。这在全新容器、新克隆的虚拟机里非常常见。Docker 镜像默认情况下的 ~/.ssh 目录甚至是空的。
第二类是记录和目标 host 不匹配。Git 平台偶尔会轮换 SSH host key,或者你更换了 Git 托管服务商,但 known_hosts 里还保留着旧平台的记录。更常见的是,本地曾经手动连过某个 IP 或域名,但后来该服务器的 SSH host key 重新生成过。
SSH 客户端在处理“记录不匹配”时的逻辑很严格:它不会默默更新,而是直接拒绝连接并提示 host key verification failed。这种设计是有意为之,目的是防止中间人攻击。理解这一点,你就知道为什么“直接把新指纹追加进去”是无效的。
3. 本地开发机的修复路径:从录入指纹到清理脏数据
本地开发机的处理原则是:能交互就尽量交互,能让 SSH 自己把指纹写进去就不要手动改文件。下面按场景一步步来。
3.1 处理之前先锁定报错来自哪个主机
不管三七二十一先 ssh-keyscan github.com 是很多人的第一反应,但这样做有个问题:如果依赖实际来自你们公司自建的 GitLab,或者某个内网 IP,你把 GitHub 的指纹写进去毫无意义。
判断方法很简单:看 npm 报错里 git ls-remote 后面跟的 URL。比如:
text复制git ls-remote git+ssh://git@gitlab.example.com/team/foo.git HEAD
说明目标主机是 gitlab.example.com;如果 URL 是 git@github.com:company/foo.git,则目标主机是 github.com。这一步不能省,尤其在公司同时使用 GitHub 和自建 GitLab 的场景下。
确认主机名之后,可以先用一条命令单独测试 SSH 连通性:
bash复制ssh -T git@github.com
如果输出类似 Hi username! You've successfully authenticated,说明 SSH key 和 host key 都没问题,问题可能出在 npm 侧用了不同的 URL 或环境变量。如果这里直接报 Host key verification failed,那就按下面 3.2 或 3.3 处理。
3.2 首次连接:把主机指纹登记到 known_hosts
如果 ssh 命令提示无法确认主机真实性,说明 known_hosts 里没有该主机的记录。最简单的方式就是让 SSH 客户端在交互过程中自行询问:
bash复制ssh -T git@github.com
正常终端下,SSH 会显示 fingerprint 并询问是否继续连接,输入 yes 即可。这一步会自动把指纹写入 ~/.ssh/known_hosts。
如果终端确实无法交互,或者你想在脚本里完成,可以用 ssh-keyscan 手动录入:
bash复制ssh-keyscan github.com >> ~/.ssh/known_hosts
然后验证:
bash复制ssh -T git@github.com
如果主机端口不是默认的 22,比如某些平台要求走指定端口,则 ssh-keyscan 也要带上端口参数:
bash复制ssh-keyscan -p 2222 git.example.com >> ~/.ssh/known_hosts
3.3 报错提示 host key 变了:先删除旧指纹再重新登记
如果 ssh 命令给出的提示不是首次连接,而是类似:
text复制@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
这说明 known_hosts 里的旧指纹和服务器当前指纹不匹配。此时最干净的办法是删除该主机的旧记录,然后重新录入。
删除命令用 ssh-keygen -R:
bash复制ssh-keygen -R github.com
如果当初连接的是带端口的主机,删除时也要带上端口:
bash复制ssh-keygen -R '[git.example.com]:2222'
删除后重新录入指纹:
bash复制ssh-keyscan github.com >> ~/.ssh/known_hosts
再验证一次:
bash复制ssh -T git@github.com
这一步做完,原来的 npm install 大概率就能顺利通过了。个人经验里,很多“昨天还好好的,今天突然 Host key verification failed”的情况,都是这个原因。
3.4 StrictHostKeyChecking=no:临时排错可以,别当常备方案
有时项目非常紧急,你只是想快速确认到底是不是 host key 问题,可以用环境变量临时让 SSH 跳过校验:
bash复制GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" npm install
GIT_SSH_COMMAND 是 git 用来替代默认 ssh 命令的环境变量,Git 2.3 及以上版本都支持。设了之后,git 发起的 SSH 连接会带上 -o StrictHostKeyChecking=no 参数,不会再因为 host key 问题中断。
但这个方案只能用来“验证问题定位”,绝不能长期使用。它会让你无法察觉服务器身份变化,等于关掉了 SSH 的防中间人机制。尤其不要把它写进 shell 配置文件或 Docker 镜像的全局环境变量里,否则以后所有 git 操作都不校验主机身份,安全性大打折扣。
如果执行后仍然报错,那说明问题不在 host key 校验,而是 SSH key 认证阶段,需要去查 ~/.ssh 下的私钥是否被正确加载、权限是否正常。
4. CI/Docker 环境没有交互确认,host key 得靠预置
自动化环境是 Host key verification failed 的重灾区,主要是因为全新容器或临时 runner 里没有 ~/.ssh/known_hosts 文件。SSH 客户端想校验却找不到记录,而又没有终端可以弹提示,只能失败。
4.1 为什么全新容器最容易触发
Docker 镜像的构建过程本身是非交互式的。如果你在 Dockerfile 里执行:
dockerfile复制RUN npm ci
而这个项目依赖里有 git+ssh 形式的包,npm 就会在构建阶段触发 SSH 连接。但基础镜像(比如 node:20)默认不会预置任何 Git 平台的 host key。你就算把 SSH 私钥正确放进了镜像,SSH 客户端仍然会在 host key 校验这一步失败。
这里要反复强调那个容易混淆的先后顺序:SSH 连接时,先验证“服务器是不是我以为的那台”,再验证“客户端的 key 是否合法”。known_hosts 针对的是前者;你配的 SSH key 针对的是后者。前置没通过,后面配置得再对也没用。
所以 CI/Docker 的修复思路不是“把 key 塞进镜像”,而是“先把 known_hosts 准备到位”。
4.2 Dockerfile 预置 known_hosts 的正确姿势
最简单的做法是在 Dockerfile 里直接通过 ssh-keyscan 生成 known_hosts:
dockerfile复制FROM node:20
RUN mkdir -p -m 700 ~/.ssh \
&& ssh-keyscan github.com >> ~/.ssh/known_hosts \
&& chmod 600 ~/.ssh/known_hosts
# 之后在构建过程中使用 SSH 访问私有依赖
RUN npm ci
这段命令要注意三点:
- 必须保证执行用户的
$HOME是预期的。如果镜像后续切到了USER node,那RUN npm ci会以 node 用户执行,而~/.ssh就要放在 node 用户的 home 下。建议先明确最终执行用户,再准备 known_hosts。 ~/.ssh目录权限必须是 700,known_hosts文件权限建议 600,太宽松的话部分 SSH 版本会直接拒绝读取。- 如果项目同时依赖 GitHub 和企业内部的 GitLab,要把所有相关 host 都 keyscan 进去,不能只处理一个。
如果你使用的是 BuildKit,构建过程中需要 SSH 私钥时,不要图省事把私钥 COPY 进镜像,这会留在镜像层里,泄露风险极大。正确方式是通过 SSH mount:
dockerfile复制# syntax=docker/dockerfile:1
FROM node:20
RUN mkdir -p -m 700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN --mount=type=ssh npm ci
构建命令大致是:
bash复制docker build --ssh default=$HOME/.ssh/id_ed25519 -t my-app .
host key 预置和私钥注入是两件事,两者都需要做,缺一个都会导致 npm ci 失败。
4.3 CI 流水线里注入 host key 的方法
在 GitHub Actions、GitLab CI 这类平台里,做法类似:在 npm install 之前的 step 里把 host key 写入 known_hosts。
GitHub Actions 的示例:
yaml复制- name: Add GitHub host key
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
chmod 700 ~/.ssh
chmod 600 ~/.ssh/known_hosts
如果还需要私钥去拉私有仓库,不要直接在 run 里 echo 私钥,应先把私钥放到平台的 Secret 中,再用 ssh-agent 加载。GitHub Actions 里可以用现成的 ssh-agent action,也可以手写:
yaml复制- name: Setup SSH agent
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-add ~/.ssh/id_ed25519
GitLab CI 的思路一样,只是变量引用方式变为 $SSH_PRIVATE_KEY。核心原则就两条:known_hosts 在 npm install 前准备好;私钥通过平台 secret 机制注入,不要写死在仓库或流水线配置中。
4.4 进阶:安全要求高时不要依赖 ssh-keyscan
ssh-keyscan 本质上也是“首次使用即信任”,它直接从目标服务器实时抓取指纹,不验证指纹的真伪。如果攻击者能在网络层面劫持这台服务器,你第一次 keyscan 到的就已经是假指纹。对一般 npm 包安装来说,这种风险可以接受,但在金融、政企等高安全环境,最好使用平台官方公布的 host key 固定指纹。
做法是:把已知的 host public key 直接写成 known_hosts 的内容,而不是运行时去 keyscan。GitHub 就官方提供了 ed25519 和 RSA 的 host key,你可以从官方文档获取,内容长这样:
text复制github.com ssh-ed25519 AAAA...
github.com ssh-rsa AAAA...
把这段内容放到 Dockerfile 或 CI 的预置步骤中,构建过程就不再依赖临时网络请求获取指纹,安全性更高。这里要提醒一句:不同平台的 host key 各不相同,不要拿 GitHub 的指纹去校验 GitLab 的域名,那只会越弄越乱。
5. 更省心的做法:从依赖 URL 层面让 SSH 退出战斗
处理 Host key verification failed 最彻底的方式不是反复去填 known_hosts,而是让依赖根本不要走 SSH。公开的 npm 包通常应该走 HTTPS 或 registry tarball,没必要给 SSH 留机会。
5.1 顺藤摸瓜:从 lockfile 里找 SSH 依赖
先检查你的 package.json 和 package-lock.json 里有多少依赖来自 SSH。
在项目根目录执行:
bash复制grep -n "git+ssh\|git@github.com" package.json package-lock.json
如果输出中包含类似:
text复制"internal-tool": "git+ssh://git@github.com/company/internal-tool.git#v1.2.0"
或者 lockfile 的 resolved 字段长这样:
json复制"resolved": "git+ssh://git@github.com/company/internal-tool.git#abc123..."
说明这个依赖就是罪魁祸首。项目里所有这种形式的依赖,都会在安装时触发 SSH 连接。
注意 package.json 里 github: 这种简写也要留意。它具体走 SSH 还是 HTTPS,取决于 npm 版本和依赖 URL 写法,不能靠猜,直接看 npm verbose 日志最准确。排查时可以这样:
bash复制npm install --verbose 2>&1 | grep -E "git\+ssh|ls-remote|Host key"
日志会清楚地告诉你 npm 正在对哪个 URL 发起 git 操作。
5.2 公开依赖尽量改成 HTTPS URL
如果依赖仓库是公开的,最简单的方法是把 git+ssh://git@github.com/company/internal-tool.git 这种 URL 改成 HTTPS 形式:
json复制{
"dependencies": {
"internal-tool": "git+https://github.com/company/internal-tool.git#v1.2.0"
}
}
改成 HTTPS 之后,npm 会通过 git+https 协议访问仓库,走的是系统 CA 证书信任链,不会触发 SSH host key 校验。对于 GitHub 上的公开仓库,这种方式不需要任何额外凭据,安装非常顺滑。
涉及多个历史依赖时,可以借助 git 的 URL 改写能力,不用逐个手改 package.json。在命令行执行:
bash复制git config --global url."https://github.com/".insteadOf "git@github.com:"
这个配置的意思是:当 git 遇到 git@github.com: 开头的地址时,自动替换成 https://github.com/。这样即使 package.json 里写的是 SSH 地址,实际执行 git 操作时也会走 HTTPS。
这个方案相当有效,但有一个副作用:它不只影响 fetch/clone,也会影响 push。原先用 SSH 方式 push 的仓库,配置后会被改写成 HTTPS 形式,如果你的 HTTPS 凭据没有配置好,push 反而会失败。所以这个配置要么在项目级作用域下使用,要么确认你本机 HTTPS 访问 GitHub 的凭据已经配好。
5.3 私有仓库和私有包的正确处理方式
对于私有仓库,事情要复杂一些。因为改成 HTTPS 后,git 拉取私有仓库需要凭据。GitHub 已经不支持密码,使用 HTTPS 访问私有仓库只能用 Personal Access Token 或通过凭据管理器。你可以把 token 拼进 URL,但千万不要提交到公共仓库:
bash复制git clone https://$TOKEN@github.com/company/private-repo.git
更推荐的做法是使用内部 npm registry。把私有依赖发布到 Nexus、Verdaccio、GitHub Package Registry 这类 npm registry 服务上,然后在 package.json 中通过普通依赖名引用,npm 直接从 registry 拉 tarball,完全不经过 git。这样既不需要 SSH,也不需要锁 host key,统一走 npm 自身的鉴权体系,CI 里也更好配置。
如果你的团队规模小,暂时不想维护私有 registry,那 SSH 方式依然是可接受的方案。但至少要做到:known_hosts 的预置纳入 CI 配置模板,SSH 私钥只通过 secret 注入,且不要让开发者在本地手动改 known_hosts 去迁就某个依赖。
6. 一次完整的定位复盘:从 npm verbose 到 ssh -vvv
最后用一个模拟案例把整个排查链路串起来。假设你在 CI 上执行 npm ci 报错,日志只显示:
text复制npm ERR! code 128
npm ERR! Host key verification failed.
这时候不要急着搜索怎么关 StrictHostKeyChecking,按照下面的顺序一步步定位,通常五分钟内能锁定根因。
6.1 第一步:锁定是哪个依赖在触发 git 操作
npm ci 的默认日志输出太少,先加 verbose:
bash复制npm ci --verbose 2>&1 | tee npm-ci.log
然后看日志中 Host key verification failed 之前的内容,你会看到一段类似:
text复制npm verb git ls-remote git+ssh://git@github.com/company/private-repo.git HEAD
这一行信息量很大:目标仓库 URL 和协议一目了然。如果日志里找不到,可以全量搜索:
bash复制grep -n "ls-remote\|Host key\|git@github\|git+ssh" npm-ci.log
这一步能确认到底是某个依赖走了 SSH,并且具体是哪一台主机。不要跳过,因为后面所有操作都依赖这个主机名。
6.2 第二步:放大 SSH 日志,看它卡在哪一步
定位到主机名后,单独用 SSH 测试。最常用的命令是 ssh -T:
bash复制ssh -T git@github.com
如果也报 Host key verification failed,接着用 -vvv 看详细握手过程:
bash复制ssh -vvv -T git@github.com
输出里会出现类似:
text复制debug1: Host 'github.com' is known and matches the ED25519 host key.
如果 known_hosts 里没有记录,则会出现:
text复制debug1: Host 'github.com' is unknown.
注意:如果日志显示 Host 'github.com' is known and matches,但 npm ci 依然报 Host key verification failed,那就要怀疑 npm 执行 git 时使用的环境和你手动测试时不一致。常见原因包括:CI 中 ssh-agent 没有启动、$HOME 被修改、或在 Docker 里以不同用户执行导致 known_hosts 路径不同。这时开启 GIT_SSH_COMMAND 环境变量最有效:
bash复制GIT_SSH_COMMAND="ssh -vvv" npm ci --verbose 2>&1 | tee debug.log
这样每次 git 调用 ssh 时的详细日志都会输出到文件里,可以清楚看到 SSH 是用哪个用户身份执行、去哪个路径找 known_hosts、加载了哪个私钥文件。很多“本地 ssh 正常但 npm 不行”的奇怪问题,都能在这个日志里找到答案。
6.3 第三步:分情况修复并验证
确认根因后,按表格中的方案处理:
| 现象 | 根因 | 修复 |
|---|---|---|
| ssh -T 提示 unknown host | known_hosts 无记录 | ssh-keyscan github.com >> ~/.ssh/known_hosts |
| ssh -T 提示 HOST IDENTIFICATION HAS CHANGED | know_hosts 有旧指纹 | ssh-keygen -R github.com 后重新 keyscan |
| ssh -T 正常,npm ci 仍报错 | npm 调用环境不同 | 用 GIT_SSH_COMMAND 调试,检查 HOME 和用户身份 |
| 所有 SSH 都正常但 git ls-remote 失败 | URL 被改写或协议异常 | 用 git config -l 检查 insteadOf 配置 |
修复后不要只跑一次 npm ci 就算完事。建议在干净环境里再验证一次,因为本地修复和 CI 修复的路径往往不一致。比如本机手动 keyscan 可以解决开发环境,但 CI 那个全新 runner 每次启动还是没有 known_hosts,必须把预置步骤写进流水线。
这里还想特别提一个容易反复的坑:如果你在 Dockerfile 里通过 ssh-keyscan 生成了 known_hosts,但之后又把镜像推送到了远程仓库,镜像层里的 known_hosts 内容可能会在后续构建缓存中被复用。一旦某个 Git 平台轮换了 host key,旧镜像层里的缓存会让新的构建继续失败。遇到这种情况,需要强制 Docker 不缓存那一层,或者重新 pull 基础镜像,不要只改依赖版本。
最后说说我自己的习惯。遇到 npm install 或 npm ci 报 Host key verification failed,我现在不会先去翻 GitHub 文档,而是先跑一句 ssh -T git@github.com,判断是 known_hosts 缺失还是指纹变更。缺失就 keyscan,变更就 keygen -R 后重录。如果项目里有大量 git+ssh 依赖,我会顺手检查下 package-lock,能改成 HTTPS 的一律改掉,不能改的就确保 CI 模板里固定了 known_hosts 预置步骤。这套流程跑顺之后,这个报错基本不会再浪费我超过十分钟的时间。
