某天早上打开 vSphere Client,登录页转两下就报错,连错误提示都很含糊。打开浏览器 F12 看到的是一个很”底层”的错误码:

1
oauth2.client.with.client.id.not.found

顺着这条线挖下去,最后定位到是 vCenter 的 WS1B OAuth2 Client 记录丢失,用 Broadcom KB 311960 的步骤重建后恢复。

这篇记录完整排查链路、取证命令和几个把我带偏的坑。

一、先确认:不是密码错,也不是锁定

登录失败最容易被误导的方向是”密码错了”或”账号被锁”。用 LDAP 直接绑一下,然后去 vmdird 日志里看具体是哪一种

1
2
3
4
5
# LDAP bind 失败
ldapsearch -h localhost -p 389 -D "administrator@vsphere.local" -w '<密码>' -b "" -s base

# 日志
tail -f /var/log/vmware/vmdird/vmdird-syslog.log

日志里的关键区别:

日志事件 含义
VdirPasswordFailEvent + LDAP_INVALID_CREDENTIALS(49) 密码错
lockout 相关事件 账号被锁定

两者在客户端都表现为 “Invalid credentials”,必须靠日志区分。本次是前者——但我清楚地知道密码没错,所以问题不在这条路上。

顺带一个教训:root 的 SSH 密码 ≠ SSO administrator 的密码。这是两套完全独立的体系,排查时不要混用。

二、复现:直接打 authorize 端点

vSphere Client 的登录本质是一次标准 OAuth2 authorization code 流程。直接打那个端点,就能看到裸的、不带前端美化的错误:

1
2
curl -sk -H 'Host: vcenter.example.com' -H 'User-Agent: Mozilla/5.0' \
"https://localhost/acs/t/CUSTOMER/authorize?client_id=<CLIENT_ID>&redirect_uri=https://vcenter.example.com/ui/login/oauth2/authcode&response_type=code&state=test"

返回:

1
2
3
HTTP 400
<p id="error_code">oauth2.client.with.client.id.not.found</p>
<p id="error_description">OAuth2 Client with client id {0} does not exist</p>

注意这里有个大坑:请求必须带正确的 Host 头。vCenter 前面是 envoy 反代,Host 不对的话 Header 校验失败,envoy 直接返回 444 且无响应体——看起来像”服务挂了”,其实只是 Host 写错了。这个假象很容易让人往”vCenter 服务异常”的错误方向跑。

三、日志证据

日志路径 关键内容
/var/log/vmware/vc-ws1a-broker/accesscontrol-service.log OAuth2ClientNotFoundException: oauth2.client.with.client.id.not.found
[getValidatedClient] Client not found
/var/log/vmware/envoy/envoy-access-*.log.gz 登录流量的真实请求:GET /acs/t/CUSTOMER/authorize?client_id=...&redirect_uri=.../ui/login/oauth2/authcode
/var/log/vmware/vmdird/vmdird-syslog.log VdirPasswordFailEvent(用来排除密码问题)

四、去数据库里确认”客户端真的没了”

vCenter 的 OAuth2 client 存在 VCDB 的 vidm_schema.ACS_OAuth2Client 表里。Appliance 自带的 postgres 可以免密登录:

1
psql -h localhost -U postgres -d VCDB

注意:表名和列名全是驼峰,查询时必须加双引号,否则会报列不存在。

1
2
3
4
5
6
7
8
-- 列出所有 OAuth2 client
select "clientId","tenantId","displayName","isInternal",
length("redirectUri") as ru_len, "createdDate"
from vidm_schema."ACS_OAuth2Client" order by "createdDate";

-- 目标 client 是否存在
select count(*) from vidm_schema."ACS_OAuth2Client"
where "clientId" = '<CLIENT_ID>';

本环境查出来的结果很说明问题:表里只有 8 个 2022 年创建的内部 client(crypto / accesscontrol / token / operator / usergroup / federation / tenant_admin × HWS / CUSTOMER / OPERATOR),而 vCenter 登录实际使用的那个 client —— 一条记录都没有

结论明确:不是配置写错,是记录整个丢失了。

ACS_OAuth2Client 的关键列:

  • clientId / tenantId / secret(varchar 4000)
  • grantTypes / scopes
  • redirectUrivarchar 2048,正好对应 KB 里说的长度上限
  • postLogoutRedirectUri(2048)
  • isOIDCClient

五、好消息:原始凭据还在 VMDir 里

client 记录没了,但 provider 的配置保存在 VMDir 的 LMDB 数据库里:

1
2
3
4
/storage/db/vmware-vmdir/data.mdb

DN: cn=customer,cn=VCIdentityProviders,cn=vsphere.local,
cn=Tenants,cn=IdentityManager,cn=Services,dc=vsphere,dc=local

正常应该用 LDAP 查。但当没有可用凭据、LDAP 查不到时,可以直接从 LMDB 文件里按字符串提取:

1
2
3
4
5
6
import re

data = open('/storage/db/vmware-vmdir/data.mdb', 'rb').read()
i = data.find(b'<CLIENT_ID>')
for m in re.finditer(rb'[\x20-\x7e]{4,}', data[max(0, i - 1500):i + 1500]):
print(m.group().decode())

提取出来的字段(顺序即文件中的顺序):

1
2
3
4
5
6
7
8
auth.example.cn           # 外部 IdP 主机
<IDP_NAME> # IdP 名称
vmwSTSExternalIdp # objectClass
customer # tenant
<CLIENT_ID> # client_id
<CLIENT_SECRET> # client_secret(32 位字母数字)
https://vcenter.example.com/acs/t/CUSTOMER/
CLIENT_SECRET_BASIC # token_endpoint_auth_method

有了 client_id 和 client_secret,就能按 KB 重建了。

六、恢复步骤(KB 311960)

前置条件:需要一个能正常登录的 vsphere.local 管理员账号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
SSO='administrator@vsphere.local'
PW='<SSO_PASSWORD>'

# 1) 获取 vCenter session
curl -sk -X POST -u "$SSO:$PW" \
'http://localhost/rest/com/vmware/cis/session' \
-w '\n[HTTP %{http_code}]\n'

# 2) 取 provider 的 client_id / client_secret(权威来源,优先于 VMDir 提取值)
curl -sk -H "vmware-api-session-id: $TOK" \
'http://localhost/api/vcenter/identity/providers/customer' \
| python3 -m json.tool | grep -iE 'client_id|client_secret'

# 3) 取 broker 的 admin-client token
curl -sk -H "vmware-api-session-id: $TOK" -H 'Accept: application/json' \
'http://localhost/api/vcenter/identity/broker/tenants/customer/admin-client'

# 4) 重建 client
curl -sk -X POST --cacert /var/lib/vmware/vmca/root.cer \
"https://$(hostname)/acs/t/customer/broker/oauth2-clients" \
-H "Authorization: HZN $ADM" \
-H 'Content-Type: application/vnd.vmware.horizon.manager.accesscontrol.broker.oauth2client.with.rule.sets+json' \
--data '{
"client_id": "<CLIENT_ID>",
"secret": "<CLIENT_SECRET>",
"scope": ["openid", "profile", "user", "group"],
"access_token_ttl": 10080,
"refresh_token_ttl": 525600,
"refresh_token_idle_ttl": 525600,
"grant_types": ["refresh_token", "client_credentials", "password", "authorization_code"],
"redirect_uris": ["https://vcenter.example.com/ui/login/oauth2/authcode"],
"post_logout_redirect_uris": ["https://vcenter.example.com/ui/login?logout"]
}'

验收:重新执行第二步那个 authorize 请求,应从 HTTP 400 + client.id.not.found 变成 302(跳转到外部 IdP 登录页)。

实际结果:

  • POST 重建 → HTTP 201,返回新记录 uuid
  • DB 检查 → ACS_OAuth2Client 出现该行(tenant=CUSTOMER,isInternal=false,redirectUri 正确)
  • POST /acs/t/customer/token(Basic + client_credentials)→ 200 + access_token(说明 secret 正确)
  • authorize 端点 → 从 400 变 302(location 指向 /federation/t/CUSTOMER/auth/login?...

KB 文档本身有个笔误:文档示例里 post_logout_redirect_uris 最后一条写成了 .../ui/login/oauth2/authcode,与前面几条的 .../ui/login?logout 格式不一致,按 ?logout 写。

顺带验证了一下 KB 提到的”ELM 节点多 + 主机名长导致 redirectUri 超过 2048”这个成因是否成立:

1
2
3
4
5
python /usr/lib/vmware-lookupsvc/tools/lstool.py list \
--url $(/usr/lib/vmware-vmafd/bin/vmafd-cli get-ls-location --server-name localhost) \
--product 'com.vmware.trustmanagement' --type 'trustmanagement' \
--ep-proto 'vapi.json.https' --ep-type 'com.vmware.trustmanagement.vapi' --as-spec 2>/dev/null \
| grep 'endpoint[0-9]*\.url'

只有一个节点 → 本环境的成因不是”超长”,就是记录单纯丢失

七、踩过的坑(重点)

1. 差点改错关联表——动手前先反查 uuid

ACS_RuleSetAssociation 表里有两行 subjectType=client 的记录。我一度认为它们是”被删 client 留下的悬空引用”,准备 UPDATE 成新 client 的 uuid。

结果发现搞错了:那个 uuid 其实是 tenant_admin_client(CUSTOMER)的 uuid,对应 “Super Admin” 规则集,是正常的原有配置

好在动手前有备份,发现后逐行还原并比对通过。

教训:修改任何关联关系之前,先把 uuid 反查清楚它属于哪条记录。

1
select "clientId","tenantId","displayName" from vidm_schema."ACS_OAuth2Client";

全表扫一遍,花不了 10 秒。

2. 改表前必须备份整表

1
2
copy (select * from vidm_schema."ACS_OAuth2Client")
to '/tmp/oauth2client_backup.csv' with csv header;

这次能顺利完成回滚验证,全靠这份备份。

3. 别用响应长度判断 session 是否成功

401 的错误 JSON 也有 400+ 字符,肉眼扫一眼很容易误判成”拿到 token 了”。

**必须看 [HTTP %{http_code}]**,或者判断响应体里是否包含 "value":"

4. psql -f 不能和 -c 混用

1
2
psql -tAc -f file   # ❌ 会把 -f 当成 -c 的参数值
psql -tA -f file # ✅

5. 想查”某个 uuid 被哪些表引用”?别逐表二分

information_schema 生成 union all 查询一次性扫完,效率高得多:

1
2
3
4
select string_agg(
format('select %L as loc where exists (select 1 from %I.%I where %I::text = %L)',
...), ' union all ')
from information_schema.columns where ...;

6. 内部服务端口清单(别指望绕过鉴权)

accesscontrol=10114token=10113usergroup=10118federation=10115 —— 都是内部 HTTP,直接访未授权请求只会返回 444/401。

7. /etc/krb5.keytab 可能是 0 字节

没有 default realm 的情况下,GSSAPI 机器账号绑定完全不可用,别指望走这条路读 VMDir

八、遗留问题

client 修好后,第二段链路继续跟下去会撞到另一个错:

1
2
3
4
Workspace ONE Access encountered an error. Message: Invalid access policy
RuleAdviceEvaluator - No advice configured for the rule, request denied
RulesetResolver - Failed to resolve ruleset with tenantId CUSTOMER, conditions ...
LoginExceptionHandler - InvalidRuleConfigurationException access.policy.is.invalid

排查记录(留档给接手的人):

  • 换完整 Chrome UA、带 cookie jar 跟重定向、直连该端点 —— 都是 500,不是单纯的 UA 问题
  • ACS_RuleSet 里 CUSTOMER 与 OPERATOR 的 default_access_policy_set(tag=APP_ACCESS)规则体完全一致,没法通过”对比租户”判断哪边缺配置
  • 日志里有真实浏览器请求(9/14 16:04、16:06)也撞到同一个 Deny access based on ruleset resolution result这个策略拒绝很可能早于本次 client 丢失就存在,属独立问题
  • vCenter 在 9/14 23:21 / 23:26 / 00:00 / 00:14 被连续重启 4 次,说明当晚已经有人在排查;日志只保留到 9/14 15:32 之后,没法对比更早的”登录成功”记录
  • 下一步方向:确认 client 是否应绑定 app access policy(rule_set_names)。本次创建时该字段为空数组(KB 的 content-type 名叫 ...with.rule.sets+json,暗示本应带规则集),但该版本上 API 的 GET/PUT 走 /acs/oauth2clients/<uuid> 返回 404、PUT 返回 405,没能通过 API 补写

九、小结

  1. 症状含糊时,直接打底层端点。vSphere Client 前端只给一句”登录失败”,而 curl 打 authorize 端点能拿到精确到 client_id 的裸错误。
  2. 注意 Host 头。envoy 在 Host 不对时返回 444 无响应体,极易误判成”服务挂了”。
  3. **先确认”是记录没了”还是”是配置错了”**。查 DB 全表,比对着文档瞎猜快得多。
  4. 凭据可能还在别处。client 记录没了,但 VMDir 的 LMDB 里还躺着 client_id / secret。
  5. 动关联表之前,先反查 uuid 属于谁;动表之前,先备份整表。 这两条让我避免了一次真实的误操作。
⬆︎TOP