代理、证书与 hosts 基线
浏览器能访问,命令行为什么仍然失败
浏览器能打开代码仓库,而 git clone、npm install 或 docker pull 失败,并不矛盾。浏览器可能读取系统代理和系统证书库,Git 可能使用独立 CA bundle,Java 使用 JDK truststore,Docker daemon 又运行在服务或虚拟机上下文中。工程现场不能只问“代理开没开”,而要依次回答:名称解析到了哪里,请求从哪一层出网,握手由哪个信任库裁决。
这三个问题对应三类完全不同的证据。NXDOMAIN、命中旧 IP 或 hosts 静态地址属于名称解析;连接超时、连接拒绝和 HTTP 407 属于传输或代理;unable to get local issuer certificate、证书名称不匹配和过期则属于 TLS。先分类再修改,才能避免用关闭证书校验掩盖 DNS 错误,或者用改 hosts 掩盖代理认证失败。
抓包、HAR 和 TLS 深层取证应在完成这轮分层检查后进行;生产服务器代理、全局证书分发和正式 DNS 变更则进入对应的平台变更流程。开发机的价值是快速证明故障落在哪一层,并交付可复核的证据,而不是越权改动企业网络。
先固定故障现场
修改配置前,准备一个不会泄露业务信息的验证对象。优先选择团队提供的测试仓库、测试制品源或公开 HTTPS 站点;不要把真实 Token、Cookie、代理密码、内网域名和完整企业证书链贴进工单或命令历史。
同时记录以下上下文:
| 对象 | 要记录什么 | 为什么需要 |
|---|---|---|
| 当前账号 | 普通用户或管理员、所属设备策略 | 判断配置作用域和提权来源 |
| 操作系统 | Windows、macOS、Linux、WSL | 不同系统没有统一代理开关 |
| 失败客户端 | 浏览器、Git、npm、pip、JVM、Docker 等 | 确定应检查哪个信任层 |
| 目标 | 脱敏后的 FQDN、端口、访问时间 | 区分解析、代理和证书变化 |
| 错误证据 | 状态码、退出码、证书错误类别 | 避免用“网络不通”概括所有失败 |
先确认终端、Git、Node、Python、Java、Maven、Gradle 与 Docker 实际来自哪里。若同一工具在 Windows、WSL 和 IDE 内各有一份,必须把它们视为三个客户端,不能假设一次配置全局生效。
代理和证书基线不是安装一个软件,而是按客户端链路启用正确入口。推荐按下面顺序处理,上一层验证通过后再进入下一层。
系统网络入口:确认设备是否由 VPN、系统代理、PAC 或安全客户端接管,不自行覆盖组织策略。企业 CA 入口:只从安全团队或受信分发系统取得证书,先核对用途、Subject、Issuer、有效期和 SHA-256 指纹,再导入指定信任库。命令行客户端入口:分别确认 Git、npm、pip、Maven、Gradle、Node.js 和 Java 是否继承系统设置,还是要求自身配置。
容器入口:区分 Docker Desktop、daemon、构建阶段和容器运行时;宿主能访问不代表镜像内能访问。名称解析入口:仅在 DNS 尚未具备测试记录且有明确到期时间时使用 hosts;它不是常规服务发现方案。
Windows 可先查看 WinHTTP 当前状态。Microsoft 的 netsh winhttp 参考明确把 WinHTTP 代理作为独立配置对象,因此浏览器成功不能替代这一步:
netsh winhttp show proxy
Get-ChildItem Env:HTTP_PROXY,Env:HTTPS_PROXY,Env:NO_PROXY -ErrorAction SilentlyContinuemacOS 可分别配置 PAC、HTTP、HTTPS 与 SOCKS 等入口,具体入口见 Apple 的代理设置说明。macOS 和 Linux 除系统界面外,还应检查当前 shell 环境:
env | grep -iE '^(http|https|no)_proxy='不要看到空环境变量就断言“没有代理”。PAC、VPN、安全客户端、IDE、Docker Desktop 和系统服务可能仍在独立接管流量。
先画清请求分层
同一个 HTTPS 请求至少可能经过以下链路:
代理决定请求怎样到达目标;DNS 或 hosts 决定名称对应哪个地址;信任库决定客户端是否接受对端证书。修改其中一层,不应期待自动修复另外两层。
系统与环境变量
仅在团队政策允许时,为需要读取环境变量的 CLI 配置代理。示例不包含认证信息:
export HTTP_PROXY="http://proxy.example.test:8080"
export HTTPS_PROXY="http://proxy.example.test:8080"
export NO_PROXY="localhost,127.0.0.1,::1,.example.test"NO_PROXY 没有完全统一的跨工具语法。是否支持 CIDR、端口、前导点和大小写必须逐客户端验证。代理认证信息不应直接写入仓库脚本、Dockerfile、终端 profile 或截图;优先使用企业认证代理、操作系统凭据能力或组织批准的凭证注入方案。
Git
Git 的 http.proxy 与 http.sslCAInfo可以覆盖通用环境,因此先查看配置来源,再决定是否修改:
git config --show-origin --get-regexp '^http\.'
git config --global --get http.proxy
git config --global --get http.sslCAInfo如 Git 未继承系统代理,可在用户作用域设置经过批准的代理;企业 CA 可通过独立 PEM 文件配置:
git config --global http.proxy http://proxy.example.test:8080
git config --global http.sslCAInfo /path/to/company-ca-bundle.pem删除配置应精确撤销,而不是覆盖成空字符串:
git config --global --unset http.proxy
git config --global --unset http.sslCAInfo禁止把 http.sslVerify=false 作为修复。它会让真实的伪造证书、过期证书和名称不匹配一起被忽略。
npm 与 Node.js
npm 的 proxy、https-proxy、cafile 与 strict-ssl属于 npm 配置,Node.js 进程的 CA 选择属于运行时配置,两层不能互相代替:
npm config get proxy
npm config get https-proxy
npm config get cafile
npm config get strict-ssl在需要独立 CA bundle 时,使用不含私钥的 PEM 文件:
npm config set cafile /path/to/company-ca-bundle.pem
npm config set strict-ssl trueNode.js 应先判断当前版本是否支持并允许使用系统 CA。需要追加 CA 时,可在启动进程前设置 NODE_EXTRA_CA_CERTS。根据 Node.js CLI 参考,该文件在进程启动时读取,且应用显式传入 ca 时会形成另一条信任路径:
export NODE_EXTRA_CA_CERTS=/path/to/company-ca-bundle.pem
node ./scripts/verify-https.mjs修改该变量后必须启动新进程。不要把它误认为 npm registry 配置,也不要把企业 CA 文件打包进公开制品。
pip 与 Python
先记录配置来源:
python -m pip config debug
python -m pip config list -v若企业基线要求指定 CA bundle,可通过组织批准的 pip 配置或进程环境传入。pip 的 HTTPS 证书说明指出系统证书行为与 Python、pip 组合有关,因此升级运行时后要重跑握手验证。禁止长期使用 --trusted-host 跳过 HTTPS 身份校验;它不能证明目标服务可信。
Maven、Gradle 与 Java
Maven 的代理配置放在用户级 ~/.m2/settings.xml,仓库镜像放在 mirrors,仓库凭证放在 servers;Maven settings 参考把三者定义为不同对象,不能混成一段通用配置。示意代理如下,认证字段应通过组织批准的凭证方案管理:
<settings>
<proxies>
<proxy>
<id>team-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy.example.test</host>
<port>8080</port>
<nonProxyHosts>localhost|127.*|*.example.test</nonProxyHosts>
</proxy>
</proxies>
</settings>Gradle 通常按网络配置说明读取 JVM systemProp.* 属性。用户级代理可放在 GRADLE_USER_HOME/gradle.properties,但含凭证的文件不得提交:
systemProp.http.proxyHost=proxy.example.test
systemProp.http.proxyPort=8080
systemProp.https.proxyHost=proxy.example.test
systemProp.https.proxyPort=8080
systemProp.http.nonProxyHosts=localhost|127.*|*.example.testJava 应确认实际运行 JDK、实际 cacerts 或显式 javax.net.ssl.trustStore,再用 keytool核对证书:
java -XshowSettings:properties -version 2>&1
keytool -list -cacerts -alias company-root
keytool -printcert -file company-root-ca.pem导入动作会改变信任边界,必须按团队流程执行并保留原始指纹、目标 JDK、别名和撤销方法。不要把 CA 私钥放进开发机或 Java truststore;开发机只需要公开证书。
Docker
Docker CLI 代理可以向 build 与容器注入环境变量,Docker CA 指南又分别处理宿主和镜像信任。配置前必须分清四个位置:
| 层次 | 管什么 | 典型误判 |
|---|---|---|
| Docker Desktop | Desktop 自身与受管环境的出网 | 修改 daemon.json 后以为 Desktop 代理已改变 |
| Docker Engine daemon | 拉取镜像、访问 registry | shell 有代理但 daemon 服务没有 |
~/.docker/config.json | CLI 给 build/container 注入代理 | 把带密码代理写进镜像历史或容器环境 |
| 镜像文件系统 | 容器内应用的 CA 信任 | 宿主信任 CA 就以为容器也信任 |
镜像内需要企业 CA 时,应在 Dockerfile 构建阶段从受控构建上下文导入公开证书并更新系统信任库,使结果可重建。不要进入运行中容器手工修补,也不要用不安全 registry 代替 TLS 治理。
hosts 与 DNS
hosts 只把一个名称映射到一个地址。它不会配置代理,不会修改端口,也不会让证书自动匹配新地址。Microsoft 的 DNS 客户端排障路径也要求分别核查本机缓存、hosts 和 DNS 响应。若证书的 SAN 不包含请求 FQDN,即使 TCP 已连通,TLS 仍应失败。
临时条目至少要有:用途、owner、创建日期、到期日期和删除验证。不要用 IP 直接替换 HTTPS URL 来“绕过 DNS”,否则 SNI、Host header 和证书名称都可能改变。
最小验证必须让同一个目标依次通过不同客户端,而不是只运行一次 curl。推荐建立如下证据矩阵:
| 客户端 | 最小动作 | 成功标准 | 失败时先看 |
|---|---|---|---|
| 浏览器 | 打开测试 HTTPS 地址 | 无证书警告,目标域名正确 | 浏览器证书链、系统/PAC |
| Git | git ls-remote <测试仓库> | 返回 refs,退出码为 0 | GIT_TRACE_CURL=1 的脱敏输出、Git 配置来源 |
| npm | npm ping --registry <测试源> | registry 应答成功 | npm config、CA、代理状态码 |
| pip | 查询一个允许的测试包 | 能完成 TLS 与索引访问 | pip config debug、Python/pip 证书行为 |
| Maven | 最小项目解析一个依赖 | 依赖可解析,来源正确 | effective settings、JDK truststore |
| Gradle | 最小项目执行依赖解析 | 任务成功,仓库来源正确 | gradle.properties、daemon 使用的 JDK |
| Docker | docker pull 允许的测试镜像 | daemon 完成拉取 | Desktop/daemon 代理和 registry CA |
| Node.js | 发起一个最小 HTTPS 请求 | 状态码和证书链符合预期 | 进程启动环境、系统/额外 CA |
| Java | 最小 HTTPS 客户端请求 | TLS 握手和主机名校验成功 | 实际 JDK、truststore、代理系统属性 |
名称解析要分别检查系统结果和应用结果。Windows 可使用:
Resolve-DnsName example.test
ipconfig /displaydnsmacOS/Linux 可使用当前系统提供的解析工具,例如:
getent hosts example.test 2>/dev/null || true每次验证保留时间、客户端版本与路径、目标 FQDN、退出码和错误类别。输出中出现 Authorization、Cookie、Token、代理认证头、内网地址或用户名时必须先脱敏。
成功不是“命令有输出”,而是:目标名称正确、证书链和主机名校验通过、请求确实经过预期出口、未关闭 TLS、重开终端或重启相关服务后仍然成立。
正向实验:保持域名、替换测试地址
当团队提供了测试 IP 与匹配该域名的证书时,可用 curl --resolve 临时替换单次请求的解析结果。它保留 URL 中的 FQDN,因此 SNI、HTTP Host 和证书名称校验仍然有效,也不会污染系统 hosts:
curl --fail-with-body --show-error --verbose \
--resolve api.example.test:443:192.0.2.10 \
https://api.example.test/health预期证据是连接地址为指定测试 IP,请求主机仍为 api.example.test,证书 SAN 匹配该名称,健康检查返回团队定义的成功状态。若 TCP 已连接但证书名称或签发链失败,问题在 TLS,而不是 DNS。
反向实验:让代理层稳定失败
在不需要代理的公开测试目标上,可在子 shell 中指向一个确定没有监听的本地端口,验证该客户端是否真的读取代理环境。子 shell 退出后不会污染当前会话:
(
export HTTPS_PROXY=http://127.0.0.1:9
curl --connect-timeout 3 --show-error https://example.com/
)预期失败应是连接 127.0.0.1:9 被拒绝或超时,而不是证书错误。恢复后同一请求成功,才能证明环境变量确实改变了当前客户端的出口。若结果完全不变,客户端可能使用 PAC、独立配置或忽略该变量,应回到配置来源继续定位。
反向与恢复实验:证明 CA 属于当前客户端
对企业提供的 TLS 测试站点,先保持默认信任执行一次,再显式传入经过指纹核验的 CA bundle:
curl --fail-with-body --show-error https://tls-test.example.test/health
curl --fail-with-body --show-error \
--cacert /path/to/company-ca-bundle.pem \
https://tls-test.example.test/health第一条出现“无法找到签发者”,第二条成功,才能支持“curl 的默认信任库缺少企业 CA”这一判断。两条都失败时要比较错误类别;两条都成功则说明默认信任已具备,不应重复导入。反例绝不能改成 -k、--insecure 或关闭 strict-ssl,因为那只会删除证据。
项目仓库只应声明可共享且不敏感的网络依赖,不应携带个人或企业凭证。推荐保留:
docs/development/network-prerequisites.md
config/proxy.example
scripts/verify-development-network.*其中应写清:
必须访问的服务类别与 FQDN 模式,不记录真实凭证;哪些地址必须走代理、哪些必须绕过代理;使用哪个公开测试对象验证 Git、包管理器和容器;
企业 CA 从哪里申请、由谁核验、安装到哪些客户端;项目允许读取哪些环境变量,禁止提交哪些本机配置;撤销配置、删除临时 hosts 条目和清理测试对象的方法。
CI 不应依赖开发者个人 profile、个人 truststore 或本机 hosts。CI runner 需要独立的镜像、CA 分发、代理和短期凭证策略;项目脚本只负责验证所需能力,不负责偷偷修改系统信任库。
盘点配置来源
netsh winhttp show proxy
Get-ChildItem Env:HTTP_PROXY,Env:HTTPS_PROXY,Env:NO_PROXY -ErrorAction SilentlyContinue
git config --show-origin --get-regexp '^(http|https)\.'
npm config list -l
python -m pip config debug输出可能包含内部地址,应在共享前脱敏。JVM 项目还应记录 Maven/Gradle 实际使用的 Java 路径,而不是只看交互终端的 java。
核对证书而不导入
keytool -printcert -file company-root-ca.pem先将显示的 Subject、Issuer、有效期和 SHA-256 指纹与安全团队发布值比对。指纹不一致时停止,不要因为文件名相同就导入。
撤销临时配置
撤销应针对原始作用域:删除 Git/npm 用户配置、清除当前会话环境变量、恢复代理设置、删除到期 hosts 条目,并重新执行最小验证。若导入过 CA,还要按照受控流程从准确的信任库和别名撤销,不能删除不明证书。
| 现象 | 先取什么证据 | 主要判断 | 修复与再验证 |
|---|---|---|---|
| 浏览器成功,Git/npm 失败 | 客户端路径、代理配置来源、证书错误 | CLI 未继承系统代理或信任库不同 | 配置正确客户端,重新执行同一目标验证 |
返回 407 Proxy Authentication Required | 状态码、代理入口、账号作用域 | 已到达代理但认证缺失或策略拒绝 | 使用批准的认证入口,不把密码写进 URL |
unable to get local issuer 或类似错误 | 客户端证书链、实际 truststore | 当前客户端缺少中间或根 CA | 核验并导入正确公开证书,保持 TLS 校验开启 |
| 证书名称不匹配 | 请求 FQDN、SAN、hosts 与 DNS 结果 | 名称被错误映射或使用 IP 访问 HTTPS | 恢复正确 FQDN,删除错误 hosts 条目 |
| Maven 成功,Gradle 失败 | 两者使用的 JDK、daemon、代理属性 | Gradle daemon 使用另一 JDK 或配置层 | 停止旧 daemon,以正确 JDK 重新验证 |
宿主成功,docker pull 失败 | Desktop/daemon 日志、registry 证书 | daemon 没有代理或 registry CA | 配置对应 daemon/Desktop 层后重试 |
docker pull 成功,容器内失败 | 镜像 CA bundle、容器代理环境 | 镜像运行时没有 CA 或代理 | 在镜像构建中可复现地导入 CA |
| 修改 CA 后 Node.js 仍失败 | 进程启动时间与环境 | NODE_EXTRA_CA_CERTS 修改后未重启 | 启动新进程并记录实际环境 |
| 清除 DNS 缓存仍命中旧地址 | hosts、应用缓存、VPN DNS | 本地静态映射或应用自带解析缓存 | 删除到期映射,重启相应客户端再验证 |
超时、丢包、TLS 握手包、VPN 路由、IPv6 选择和安全软件拦截无法由上述证据定位时,转入 13 家族做网络与抓包取证,不在开发机基线里继续猜测。
代理配置常与账号密码、PAC 地址、内网域名和企业 CA 同时出现,必须把“能连接”与“可以公开”分开。
日常验证使用普通开发账号;修改系统信任库、系统代理或 hosts 时才按流程提权。代理密码、PAT、Cookie 和私钥不得进入仓库、Dockerfile、镜像层、命令输出、截图或 AI 上下文。CA 私钥永远不应分发到开发机。开发机只安装经过核验的公开根证书或中间证书。
NO_PROXY 可能暴露内部域名结构,公开日志前同样需要脱敏。不允许用关闭 TLS、忽略主机名校验、启用不安全 registry 或全局信任未知根证书换取临时可用。临时 hosts 条目和例外代理必须有 owner、到期时间、撤销记录和再验证证据。
如果代理要求长期明文密码配置,问题已经超出个人配置技巧,应由安全和平台团队提供受管认证方式,而不是让每个开发者复制一份高风险配置。
团队基线应交付一份“分层支持矩阵”,而不是一份万能代理脚本:
| 治理对象 | 最低要求 |
|---|---|
| 支持入口 | OS、WSL、IDE、CLI、Docker 分别支持哪种代理模式 |
| CA 来源 | 发布 owner、指纹核验入口、有效期、撤销通道 |
| 客户端矩阵 | Git、npm、pip、JDK、Docker 的配置和验证责任人 |
| 例外规则 | NO_PROXY、临时 hosts、离线源的审批和到期时间 |
| 验收证据 | 脱敏命令、退出码、目标、时间、客户端路径 |
| 升级策略 | 运行时或代理产品升级后的重新验证范围 |
新机验收要逐客户端通过,不能由浏览器成功代替。企业 CA 更新、代理切换、JDK/Node/Docker 升级后,应重新跑分层验证矩阵。离职或设备退出时,撤销个人代理凭证、删除本地例外、清理私有 CA 配置,并保留不含敏感信息的完成记录。
系统成功不代表应用成功
最常见的错误决策是“浏览器已经能开,所以网络没问题”。浏览器可能使用系统证书库和 PAC,Git 可能使用独立 CA bundle,Java 使用 JDK truststore,Docker daemon 又运行在独立服务上下文。架构判断应以客户端矩阵为单位,不能以整台机器为单位宣布成功。
企业 HTTPS 检查扩大了信任面
把企业根 CA 加入信任库后,持有对应私钥的基础设施可以为任意域名签发客户端接受的证书。因此必须明确 CA 的发布 owner、适用设备、指纹、有效期和撤销路径。为了让一个包管理器工作而把根 CA 散落到多个未知 JDK、镜像和用户目录,会形成无法审计的长期信任债务。
代理配置可能进入制品
Docker CLI 能把代理配置注入 build/container;Dockerfile 的 ENV、构建参数、镜像历史和 CI 日志都可能留下代理 URL。若 URL 含认证信息,制品扫描通过也不代表没有泄露。判断标准是最终镜像配置、历史与构建日志均不含凭证,而不是仓库里没看到明文。
NO_PROXY 是一致性风险
不同客户端对域名后缀、端口、CIDR 与大小写的解释可能不同。同一条规则在 Git、JVM 和容器里不一定等价。团队应使用实际目标逐客户端验证“走代理”和“绕过代理”两条路径,不依赖复制一串未经测试的值。
hosts 会制造局部真相
hosts 能让单机快速联调,也能让这台机器长期偏离真实 DNS。服务迁移后,开发者可能继续访问旧地址;证书、Cookie Domain、SNI 和负载均衡也可能被绕开。临时映射只有在明确 owner、到期时间、删除动作和 DNS 恢复验证都存在时才可接受。
动态版本改变信任行为
Node.js、pip、Docker Desktop、JDK 和操作系统会调整系统 CA 集成、默认 TLS 行为和配置入口。团队不能把一次成功截图当成永久基线。升级验收应至少覆盖一个系统信任客户端、一个独立信任客户端、一个 JVM 客户端和一个容器客户端。
已区分系统代理、应用代理、DNS/hosts 和证书信任四个问题域。已记录失败客户端的版本、路径、目标 FQDN、时间、退出码和错误类别。企业 CA 来自受信渠道,并核对 Subject、Issuer、有效期和 SHA-256 指纹。
浏览器、Git、npm、pip、Maven/Gradle、Docker、Node.js/Java 按实际使用范围分别验证。没有关闭 TLS 校验、主机名校验或启用不安全 registry。代理密码、Token、Cookie、私钥、内网地址未进入仓库、日志、截图或镜像。
Docker Desktop、daemon、build 和容器运行时的代理/CA 边界已分清。Java 项目已确认实际 JDK 与 truststore,Node.js 修改 CA 后已用新进程验证。NO_PROXY 已用真实客户端验证,而不是只检查字符串。
临时 hosts 条目有 owner、到期时间、删除动作和 DNS 恢复验证。超出开发机基线的抓包和生产网络问题已转交对应家族或部署运维。目标环境尚未验证的配置先进入隔离环境,补齐成功证据、失败现象和回滚动作后再推广。
