遇现场景

tx 服务器(ubuntu@124.221.137.194)https://6767.chat/api/login 表单登录对任何密码都返回 401,包括正确密码。用户怀疑服务器被入侵、密码被改。同时 nginx 原生 auth_basic 弹窗(porta / kanna / agy / cc.6767.chat / mihomo.6767.chat)用同一密码却能登入。

发生条件

  • nginx 原生 auth_basic_user_file 与自研 Node 鉴权服务 共用同一个 htpasswd 文件 /etc/nginx/.htpasswd-6767-admin。
  • 该文件里的 hash 是 bcrypt($2y$05$) 格式(由 htpasswd -B 生成)。
  • Node 端 auth-api 的 verifyAdminHtpasswdPassword() 只认 {SSHA} / {SHA} / {PLAIN} 三种前缀,不认 bcrypt。

症状与误判

  • 症状:POST /api/login 无论输什么密码都 HTTP 401,但 GET /api/login(登录页)返回 200,服务进程 PM2 状态 online,端口在监听。nginx Basic 站点用同一密码却 200。
  • 误判 1:误以为"密码被改"或"服务器被入侵"。

    • 反驳:bcrypt hash 用 Python bcrypt.checkpw("Complicate0705{", hash) 实测 匹配,密码 Complicate0705{ 是对的,没被改。
  • 误判 2:误以为 home-api 服务挂了(CLAUDE.md 记 home-api 端口 3003,实测 3003 无监听)。

    • 反驳:home-api 实际监听 3017(ss -tlnp 确认);/api/login 根本不归 home-api,nginx 配置里 location = /api/login → proxy_pass 127.0.0.1:3021/monitor/login,3021 才是 auth-api。CLAUDE.md 端口/职责记录过时。

尝试与排除

  1. ssh tx 连通正常,hostname VM-0-5-ubuntu,排除机器离线。
  2. pm2 list 全 online,排除进程挂掉。
  3. curl 127.0.0.1:3003 → HTTP 000(端口无监听)→ 但这是探错端口,不是服务故障。
  4. curl https://6767.chat/api/login GET → 200(登录页正常);POST 错误密码 → 401;POST 正确密码 Complicate0705{ → 同样 401。任何密码都 401 = 服务端校验逻辑根本认不出存的那串 hash。
  5. 读 nginx 配置确认 /api/login → 3021(auth-api),不是 home-api。
  6. 读 auth-api 源码,定位 verifyAdminHtpasswdPassword() 的前缀分支:只 {SSHA}/{SHA}/{PLAIN},无 bcrypt 分支。

根因

/etc/nginx/.htpasswd-6767-admin 在 2026-08-12 20:50 被重写为 bcrypt($2y$05$):

admin:$2y$05$iISckglMzlWq05HqahfpU.r1HYQZTrg58rrblSCyhQXrGhNdugUS6
  • nginx 原生 auth_basic 支持 bcrypt → Basic 站点正常。
  • auth-api(Node) 不支持 bcrypt → /api/login 对任何密码 return false → 401。

即:两个消费者对同一口令文件的格式支持集不一致,选了交集之外的格式,Node 侧静默失败。 不是密码错,不是被入侵,是一次配置事故。背景概念见 密码哈希的盐与彩虹表 (Salt & Rainbow Table)。

解决步骤

采用最小改动方案:把 hash 改成 两边都认的 {SHA} 格式(nginx 与 auth-api 均原生支持),不装依赖、不改代码、不重启服务。

  1. 备份:sudo cp /etc/nginx/.htpasswd-6767-admin /etc/nginx/.htpasswd-6767-admin.bak.<ts>.bcrypt
  2. 用 openssl 现场算 {SHA}(密码经 stdin/管道喂入,避免进 shell 历史):

    SHA=$(printf "%s" "Complicate0705{" | openssl sha1 -binary | base64)
    echo "admin:{SHA}$SHA" | sudo tee /etc/nginx/.htpasswd-6767-admin > /dev/null
  3. 复权:sudo chown ubuntu:www-data /etc/nginx/.htpasswd-6767-admin; sudo chmod 640
  4. 四层验证(见远端 CLAUDE.md 测试四层结构):

    • POST /api/login 正确密码 → HTTP 302 → /monitor ✅
    • nginx Basic 错误密码 → 401,正确密码 → 200 ✅(cc / porta)
    • 本地 printf ... | openssl sha1 -binary | base64 自校验 hash 一致 ✅
  5. 无需 nginx -s reload、无需 pm2 restart(htpasswd 每请求重读)。

遗留权衡:{SHA} 是无盐 SHA-1,抗彩虹表/穷举弱于 bcrypt。短期可接受(仅挡管理后台、有 HTTPS、有其他防护);长期建议给 auth-api 的 verifyAdminHtpasswdPassword 加 bcrypt 分支或改用 {SSHA}(带盐,代码已支持验签)。概念区别见 密码哈希的盐与彩虹表 (Salt & Rainbow Table) 第 3、5 节。

下次怎么快速识别

特征三联征,命中其一即可怀疑本坑:

  1. 分岔症状:同一 htpasswd 文件,nginx Basic 弹窗能登,但表单/自研 API 登录对任何密码都 401(或反之)。
  2. "任何密码都 401":不是某条密码错,而是校验函数对存的那种 hash 格式根本不认,直接走 return false。
  3. hash 前缀与代码分支对不上:cat .htpasswd 看前缀($2y$/$apr1$/{SHA}/{SSHA}),再 grep 校验函数认哪些前缀,取交集。

诊断命令链(按顺序):

ssh tx 'sudo cat /etc/nginx/.htpasswd-6767-admin'           # 看前缀
ssh tx 'grep -n "startsWith\|verify" .../auth-api/server.js' # 看认哪些前缀
ssh tx 'curl -sk -X POST https://6767.chat/api/login -d "{\"username\":\"admin\",\"password\":\"X\"}" -w "%{http_code}"' # 看是否"任何密码都 401"

相关概念


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web