local-ssl-proxy 本地 HTTPS 包装工具手册
OAuth 回调在 HTTP 本地地址前停住了
一个常见开发现场是:应用已经在 http://localhost:9000 正常运行,OAuth/OIDC 提供方却要求回调地址使用 HTTPS;或者浏览器只有在安全上下文中才开放 Secure Cookie、Service Worker、WebAuthn 等能力。为了验证回调链路,开发者需要一个 https://localhost:9001/callback,但不想先搭完整网关。
local-ssl-proxy 可以在 9001 接收 TLS,再把请求以 HTTP 转发到 9000:
Browser -- HTTPS :9001 --> local-ssl-proxy -- HTTP :9000 --> application这条链路解决的是本地 HTTPS 包装,不会把后半段变成加密连接,也不会自动带来认证、限流、审计、证书轮换或生产级超时治理。先确认应用本身能直接响应,避免把后端错误误判成 TLS 问题:
curl -i http://localhost:9000/health预期返回应用定义的 200 和健康响应。连接拒绝说明应用未监听或端口错误;这里没有成功时,启动 HTTPS 包装只会得到转发失败。
把工具锁进项目,而不是装进每台机器
先确认项目 Node.js 基线。当前项目锁定 local-ssl-proxy 2.0.5,版本信息、依赖与维护状态可以从 npm 包页面 和 GitHub 仓库 交叉确认。升级时重新检查 npm 发布物、Node.js 兼容性、依赖变化和仓库维护状态,再审查 lockfile 差异并完整复验 HTTPS、WebSocket 与错误传播。
npm view local-ssl-proxy version engines dependencies --json
npm install --save-dev --save-exact local-ssl-proxy@2.0.5
npm ls local-ssl-proxynpm ls 应显示项目实际锁定的唯一版本;package.json 与 lockfile 都应发生可审查的变更。若安装因代理或私有仓库失败,先检查 npm config get registry、企业 CA 和代理配置,不要临时关闭 TLS 校验。
全局安装会让不同开发机得到不同版本,也不容易从项目变更中看出升级。一次性的 npx local-ssl-proxy 如果不指定版本,还可能解析到不同发布物。团队脚本应调用 devDependencies 中的本地二进制。
用独立开发 CA 签发实际访问名
包内自带的默认 localhost 证书和私钥是公开发布物,任何人都能取得,不能作为可信主路径。每台开发机应使用自己的开发 CA 签发证书,并显式传入 --key 与 --cert。
mkcert 会为当前设备建立本地开发 CA。安装工具后执行:
mkcert -install
mkcert -CAROOT
mkdir -p .local-certs
mkcert -cert-file .local-certs/localhost.pem \
-key-file .local-certs/localhost-key.pem \
localhost 127.0.0.1 ::1mkcert -CAROOT 输出的是本机 CA 目录。生成结果应包含项目专用证书和私钥,证书 SAN 覆盖实际使用的 localhost、IPv4 与 IPv6 loopback。若 OAuth 提供方登记的是另一个开发域名,必须把该名称加入签发命令,并确保名称解析到开发机。
检查证书内容而不是只看文件存在:
openssl x509 -in .local-certs/localhost.pem -noout -subject -issuer -dates -ext subjectAltName输出中的有效期应覆盖开发周期,SAN 必须包含浏览器地址栏使用的名称。CN=localhost 不能代替 SAN。私钥、项目证书和本地 CA 私钥都不进入 Git;仓库可以保留 .local-certs/README.md,说明生成与撤销方式。
.local-certs/*
!.local-certs/README.mdmkcert -install 修改的是设备信任库。企业设备策略不允许自行安装 CA 时,应使用组织批准的开发证书或统一开发入口,不能通过忽略浏览器警告绕过策略。
显式启动 HTTPS 包装
在 package.json 中固定所有关键参数:
{
"scripts": {
"https:dev": "local-ssl-proxy --hostname localhost --key ./.local-certs/localhost-key.pem --cert ./.local-certs/localhost.pem --source 9001 --target 9000"
},
"devDependencies": {
"local-ssl-proxy": "2.0.5"
}
}先启动应用,再在另一个终端运行:
npm run https:dev终端会输出类似 https://localhost:9001 -> http://localhost:9000 的启动信息,但这行文本在调用 .listen(source) 后立即打印,并没有等待 Node.js 的 listening 事件。端口冲突等异步错误可能随后出现,多入口配置还可能只成功启动部分 listener,因此启动日志不能作为成功判据。
保持进程运行,在另一个终端确认每个 source 端口的监听地址和进程归属,再发送一次普通 HTTPS 请求。Windows PowerShell 使用:
Get-NetTCPConnection -LocalPort 9001 -State Listen |
Select-Object LocalAddress, LocalPort, OwningProcess
Get-Process -Id (Get-NetTCPConnection -LocalPort 9001 -State Listen).OwningProcess
curl.exe -i https://localhost:9001/healthmacOS/Linux 使用:
lsof -nP -iTCP:9001 -sTCP:LISTEN
curl -i https://localhost:9001/health端口必须由刚启动的 Node.js 进程持有,HTTPS 请求必须到达目标应用。配置多个入口时对每个 source 端口逐一执行,任何一个缺失都算启动失败,不能接受部分可用状态。
--hostname localhost 有一个容易误解的语义:它是代理连接上游时使用的主机,并参与启动提示,不是 source listener 的绑定地址。官方实现把它传给 http-proxy 的 target,并直接调用 .listen(source);命令行没有 --listen-host 或 --bind-address 参数。源码可在 src/main.ts 与 src/lib.ts 中核对。
因此,下列命令中的 --hostname 127.0.0.1 只会把上游改为 127.0.0.1:9000,不能证明 9001 仅监听 loopback:
local-ssl-proxy --hostname 127.0.0.1 --source 9001 --target 9000如果安全基线要求进程必须显式绑定 127.0.0.1,这个工具不满足要求,应直接改用支持监听地址配置的 Caddy、Nginx 或应用自身 HTTPS。主机防火墙或隔离容器可以降低风险,但不能把缺失的应用级绑定能力描述成已经具备。
用三层证据验证回调链路
第一层验证 TLS 握手和 SAN。不要用 -k 作为最终结果:
openssl s_client -connect localhost:9001 -servername localhost \
-CAfile "$(mkcert -CAROOT)/rootCA.pem" -verify_return_error </dev/null
curl -i https://localhost:9001/health预期握手验证成功,curl 不带跳过校验参数也能得到与后端一致的 200。若 curl -k 成功而普通 curl 失败,转发链路可能正常,但 CA 信任或 SAN 仍然错误。Windows PowerShell 需要明确使用 curl.exe,避免与旧版别名行为混淆:
$ca = Join-Path (mkcert -CAROOT) 'rootCA.pem'
curl.exe --cacert $ca -i https://localhost:9001/health第二层验证代理确实转发到了目标应用。让应用回显并检查:
请求路径与查询参数没有丢失;Host 是否符合应用路由预期;X-Forwarded-For、X-Forwarded-Port、X-Forwarded-Proto 是否被应用正确处理;
WebSocket 如果是业务必需能力,完成真实握手、双向消息和断线重连。
该工具基于 http-proxy,实现开启了 xfwd 和 WebSocket。应用如果无条件信任 forwarded headers,必须确保请求只能来自受控代理;否则外部客户端可以伪造来源与协议事实。
第三层回到真实浏览器执行 OAuth:
在身份提供方登记精确回调 https://localhost:9001/callback。从应用发起登录,确认浏览器地址栏证书正常,没有继续访问警告。核对 state、PKCE verifier/challenge 和回调参数由应用按协议验证。
确认 Secure Cookie 被写入且不会通过 HTTP 地址发送。完成回调后检查页面是否存在 Mixed Content、错误的绝对 http:// URL 或重定向循环。
TLS 包装不会替代 OAuth 的 state、PKCE、nonce 和回调白名单。浏览器出现安全锁也不代表认证协议已经正确。
多入口配置仍然是轻量转发
一个项目需要两个本地 HTTPS 入口时,可以使用官方 README 支持的配置文件:
{
"frontend": {
"source": 9001,
"target": 9000,
"hostname": "localhost",
"key": "./.local-certs/localhost-key.pem",
"cert": "./.local-certs/localhost.pem"
},
"callback-fixture": {
"source": 9101,
"target": 9100,
"hostname": "localhost",
"key": "./.local-certs/localhost-key.pem",
"cert": "./.local-certs/localhost.pem"
}
}npx local-ssl-proxy --config local-ssl-proxy.json每个对象都会创建独立 listener,但依然没有路由树、认证、访问日志、细粒度超时或证书自动轮换。入口数量增长通常意味着需求已经从“包装一个本地端口”变成“维护本地网关”,此时继续堆配置文件只会掩盖迁移信号。
进程启动、停止与清理必须成对出现
开发结束时在前台终端按 Ctrl+C,然后确认 source 端口已经释放。Windows 可执行:
Get-NetTCPConnection -LocalPort 9001 -ErrorAction SilentlyContinue |
Select-Object LocalAddress, LocalPort, State, OwningProcessmacOS/Linux 可执行:
lsof -nP -iTCP:9001 -sTCP:LISTEN没有输出才表示 listener 已退出。仍有进程时,根据 PID 确认归属后正常终止,不要盲目杀掉占用同端口的其他服务。
同时检查实际监听地址。看到 0.0.0.0、:: 或局域网地址说明它不只接受 loopback 连接。不要通过路由器端口映射、动态 DNS、隧道或共享 Wi-Fi 把它暴露出去;需要跨设备回调时,换成有认证、审计和访问控制的受管入口。
项目结束可以删除 .local-certs/ 中的叶子证书和私钥。设备级 CA 可能被多个本地项目共享,先确认依赖再撤销:
mkcert -CAROOT
mkcert -uninstallmkcert -uninstall 从系统信任库撤销该 CA;CA 目录是否删除要遵循团队设备治理流程。离职、设备移交或 CA 私钥疑似泄露时,不能只删除项目证书,必须撤销信任并重新签发。
失败时沿链路从后向前排查
source 启动失败时,先查 9001 是否被占用,再查证书路径和私钥读取权限。私钥格式错误通常在进程启动阶段报错,不会等到浏览器请求才出现。
HTTPS 可以握手但响应 502 或连接断开,说明 TLS listener 已工作,问题更可能位于 hostname:target。先重新执行:
curl -i http://localhost:9000/health容器中的 localhost 指向代理容器自身,不是宿主机。需要跨容器时应使用同一网络中的服务名;这时后半段仍是明文 HTTP,网络必须受控。
浏览器不信任而 curl -k 能访问,检查 SAN、实际访问名和 CA 是否进入该浏览器使用的信任库。重新签发后要完全重启仍缓存证书状态的客户端,最终复验不能保留 -k。
页面出现 Mixed Content,说明应用仍生成 http:// 绝对 URL,或没有正确解释 X-Forwarded-Proto: https。应修应用的 public/base URL 和可信代理配置,不能靠浏览器关闭安全策略。
OAuth 回调进入重定向循环时,同时核对登记回调、应用外部 URL、Cookie 的 Secure/SameSite 属性和 forwarded proto 信任。代理只负责传输,不会修复身份协议配置。
生产误用的红线与迁移触发器
local-ssl-proxy 的实现没有认证、授权、访问日志、限流、请求体限制、可配置监听地址、生产 TLS 策略、自动证书轮换或优雅排空。官方仓库也把它定位为本地开发工具。即使某个请求“跑得通”,也不能把它变成临时生产入口。
出现以下任意一项,停止扩展该脚本并迁移:
source 必须强制绑定 loopback 或指定网卡;请求需要穿过局域网、公网、隧道、共享开发环境或不可信容器网络;Envoy/Nginx/Caddy 后面的 upstream 也必须使用 HTTPS 或 mTLS;
需要多个域名、按 path/host 路由、header 改写或流量灰度;需要认证授权、IP 策略、限流、请求体上限、安全头或 WAF;需要访问审计、结构化日志、指标、追踪和告警;
需要统一证书签发、轮换、吊销、密钥托管和到期监控;需要可配置超时、重试、健康检查、故障隔离和优雅重载;该入口由多人长期依赖,已经需要值班、SLA 或变更审批。
单机开发且仅有一个 HTTPS 包装需求时,项目锁版、独立证书、显式 key/cert、真实信任验证和进程清理可以让它保持简单。只要需求开始触及网络边界或长期治理,完整网关或受管开发入口的成本反而更低。
团队可重复使用的完成标准
一个可复制的本地 HTTPS 回调环境应留下这些事实:
Node.js、local-ssl-proxy 和 lockfile 版本明确,升级有人负责;每台设备使用独立开发 CA,项目证书覆盖真实访问 SAN,私钥没有进入仓库;启动脚本始终显式传入 hostname、source、target、key 和 cert;
团队知道 hostname 指向上游,不具备 listener 绑定能力;普通 curl、openssl 和真实浏览器都完成信任验证,没有用 -k 或忽略警告充当成功;OAuth 的 state、PKCE、Cookie、回调白名单与 Mixed Content 分别完成业务验证;
进程可停止、端口可复查、证书与设备 CA 有清理和撤销路径;迁移触发器写进团队开发约定,轻量包装不会演变成无人维护的入口基础设施。
