Files
pikasTech-HWLAB/docs/reference/spec-v02-auth.md
T

328 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v0.2 登录与鉴权规格
本文是 HWLAB `v0.2` 登录、认证和应用层鉴权入口的长期规格。正式用户鉴权只保留两类凭据:浏览器使用 `hwlab_session` Web sessionCLI/API/AgentRun runner 使用用户级 `HWLAB_API_KEY`。Keycloak 只负责 Web 身份认证和注册,HWLAB 在 callback 后发行自己的 session、API key 和 `AuthPrincipal`,避免把 Keycloak token、本地密码、浏览器 cookie 和设备内部系统 key 混用。
基础设施实施跟踪见 [pikasTech/HWLAB#788](https://github.com/pikasTech/HWLAB/issues/788)Keycloak 到 HWLAB 的接入收口见 [pikasTech/HWLAB#814](https://github.com/pikasTech/HWLAB/issues/814)。用户角色、Code Agent session owner、device pod grant 和资源授权矩阵见 [spec-user-access.md](spec-user-access.md);本文只定义“如何登录、如何恢复 actor、如何把请求归一成 actor”。
## 设计目标
- Web 用户通过 Keycloak OIDC 登录或注册,HWLAB 不再把本地账号密码表单作为目标登录体验。
- Web 使用 `hwlab-cloud-api` 发行的 httpOnly `hwlab_session`,每个 session token 最长 24 小时;第一版不做复杂 refresh token 管理,但必须可撤销、可重新登录轮换。
- CLI 是纯 CLI 体验,不打开浏览器、不跳转 Web、不做 device-code flow,也不直接消费 Keycloak access token;标准凭据是环境变量 `HWLAB_API_KEY`
- AgentRun runner 内的 `hwpod` 也必须使用同一类用户 API key 认证,映射到发起 Code Agent session 的 `users.id``HWLAB_DEVICE_POD_API_KEY` 只能作为 cloud-api 到 device-pod 的内部服务凭据或迁移期兼容项,不能作为正式用户鉴权方式。
- 每个用户在首次登录后自动拥有一个默认 API key;用户也可以在 Web 中创建、查看、失效或重新生成 API key。
- API key 长效有效,除非用户或管理员手动 revoke/regenerate;它不跟 Web session 的 24 小时过期绑定。
- Keycloak 只做身份认证和账号注册;HWLAB 的 `users.role``users.status`、device pod grant 和 Code Agent owner 仍是应用层授权 source of truth。
- device-pod 权限只由 `users.role/status`、Code Agent session owner 和 `device_pod_grants` 控制;不存在“对所有正式 device pod 授权”的共享 runner key。
## 系统边界
| 组件 | 职责 |
| --- | --- |
| Keycloak | 独立 `keycloak` namespace 中的身份提供方,提供公网 HTTPS OIDC issuer、登录页、注册页和用户 subject。 |
| Keycloak Postgres | Keycloak 专用数据库,短期可与 Keycloak 同在 `keycloak` namespace;不复用 HWLAB v0.2 业务 Postgres。 |
| `hwlab-cloud-api` | OIDC callback、session token、API key、用户映射、`AuthPrincipal` 和应用层授权收口。 |
| `hwlab-cloud-web` | 浏览器 UI 和同源代理;未登录时通过 `/auth/oidc/login` 引导到 Keycloak,不持有 Keycloak client secret,不直接访问 Postgres。 |
| `hwlab-cli` / AgentRun `hwpod` | 读取 `HWLAB_API_KEY`,向 Cloud Web 同源 API 或 Cloud API 发送 `Authorization: Bearer hwl_live_...`,恢复到同一个用户 actor。 |
| `hwlab-edge-proxy` / FRP | 只转发 cookie/header 和 HTTP 流量,不注入伪 actor,不做业务授权。 |
| HWLAB v0.2 Postgres | 保存 HWLAB 用户映射、session、API key、resource grant 和 runtime durable state。 |
Keycloak 部署不进入 `hwlab-v02` namespace。`hwlab-v02` 只把 Keycloak 当成外部 OIDC issuer 消费,使用 issuer URL、client id 和 client secret SecretRef 完成 callback 校验。
## 公网 HTTPS 与 issuer
Keycloak 和 HWLAB 浏览器入口都必须使用公网 HTTPSOIDC redirect URI、cookie `Secure` policy 和浏览器登录都依赖这一点。短期全自动方案使用主 server Caddy + Let's Encrypt + nip.io 域名:Keycloak 通过 FRP 暴露到 `auth.74-48-78-17.nip.io`HWLAB Cloud Web 通过 Caddy 反向代理到 v0.2 Cloud Web FRP 入口。
目标公网入口:
```text
Keycloak issuer: https://auth.74-48-78-17.nip.io/realms/hwlab
HWLAB Web/API origin: https://hwlab.74-48-78-17.nip.io
```
约束:
- Keycloak admin console、account console、OIDC discovery 和 JWKS 都必须走 HTTPS;不要用裸 HTTP issuer。
- `hwlab-cloud-api` 只记录 issuer、client id、redirect URI 和 SecretRef 名称,不在日志、issue 或 ConfigMap 中输出 client secret。
- Keycloak 的 `hostname`、proxy header 和 frontend URL 必须与公网 issuer 一致,避免 callback 后 token issuer mismatch。
- 浏览器和 CLI 默认使用 `https://hwlab.74-48-78-17.nip.io``http://74.48.78.17:19666/19667` 只作为 legacy/debug 入口,不作为 OIDC callback、cookie 或用户文档默认入口。
- Keycloak OIDC client 的 redirect URI 必须指向 HWLAB callback`https://hwlab.74-48-78-17.nip.io/auth/oidc/callback``https://auth.74-48-78-17.nip.io` 只作为 Keycloak issuer/authorization host,不能配置成 HWLAB callback 目标。
- `https://hwlab.74-48-78-17.nip.io/auth/*` 由 Cloud Web 同源代理到 Cloud APICloud Web 不读取 client secrettoken exchange 只在 Cloud API 内完成。
## Keycloak 当前部署基线
v0.2 的 Keycloak 部署已经具备公网 HTTPS 原生后台管理入口和 `hwlab` realm OIDC issuer。后续 HWLAB 接入工作应把 Keycloak 当成已存在的外部 OIDC provider 消费,不再把 Keycloak 基础部署、Postgres 或 HTTPS 暴露混入 Cloud Web/Cloud API 接入任务。
部署拓扑:
```text
browser
-> https://auth.74-48-78-17.nip.io
-> master Caddy :443
-> master frps remotePort 28443
-> G14 keycloak-frpc
-> keycloak.keycloak.svc.cluster.local:8080
```
运行基线:
- Kubernetes namespace 固定为 `keycloak`,与 `hwlab-v02` 解耦。
- Keycloak Deployment 使用 `quay.io/keycloak/keycloak:25.0`Postgres 使用 `postgres:16-alpine`FRP client 使用 `fatedier/frpc:v0.68.1`
- `keycloak` Deployment、`keycloak-frpc` Deployment 和 `keycloak-postgres` StatefulSet 必须 Ready`keycloak-bootstrap-realm` Job 必须已成功完成。
- 公网管理入口是 `https://auth.74-48-78-17.nip.io/admin/master/console/``/admin/` 可以重定向到该原生 console;不要在 HWLAB Cloud Web 中重做 Keycloak 管理 UI。
- 公网 issuer 是 `https://auth.74-48-78-17.nip.io/realms/hwlab`discovery 与 JWKS 必须全部返回 HTTPS URL。
- Keycloak management 端口 `9000` 不作为公网入口暴露;健康和管理探测留在集群内或受控透传内完成。
- `hwlab` realm 必须 enabled,并在小范围测试阶段允许 self-registration;新注册用户在 HWLAB 应用层只能默认成为普通 `user`,不自动拥有 device pod grant。
- `hwlab-cloud-web` client 的 redirect URI 指向 `https://hwlab.74-48-78-17.nip.io/auth/oidc/callback`web origin 指向 `https://hwlab.74-48-78-17.nip.io`Direct Access Grants 必须关闭,避免把 Keycloak password grant 变成第三种 CLI 鉴权方式。
管理员凭据边界:
- `master` realm 的 `admin` 用户只用于 Keycloak 原生后台和自动化 bootstrap,不是 HWLAB 应用管理员账号。
- Kubernetes Secret `keycloak-admin/password` 是 Keycloak 初始管理员和后续自动化 Job 获取 admin token 的凭据来源;如果人工在 Keycloak UI 里轮换 admin 密码,必须同步更新该 Secret,否则重跑 bootstrap/admin REST Job 会继续使用旧密码并失败。
- 不要把 admin password、client secret、session token 或完整 API key 写入 issue、长期参考、ConfigMap、日志或 trace。临时取密脚本使用后必须删除。
- `hwlab-cloud-web-client` Secret 只保存 Keycloak OIDC client secretCloud Web 不直接读取它,只有 `hwlab-cloud-api` 在 callback/token exchange 中使用。
部署硬化项:
- Keycloak 当前已能通过公网 HTTPS issuer 和 forwarded headers 正常工作;后续 render 可显式固定 `--hostname=https://auth.74-48-78-17.nip.io` 和必要的 `--hostname-admin`,减少代理链路变化导致的 issuer/callback 歧义。
- 小范围测试可先使用 bootstrap 创建的 client secret;进入更大范围公网使用前应轮换为随机 secret,并同时更新 Keycloak client 与 `hwlab-cloud-web-client` Secret。
## 注册策略
短期小范围测试允许 Keycloak 开启自助注册,并且不要求邮箱或手机校验。该策略只有在以下条件同时满足时成立:
- 新注册用户默认只映射为 HWLAB `user` 角色,不自动成为 `admin`
- 新用户没有 device pod grant,也不能看到或使用任何 device pod,直到 `admin` 在 HWLAB 中授权。
- Keycloak 账号状态必须能被管理员禁用;HWLAB `users.status='disabled'` 也必须能独立阻断 session 和 API key。
- 如果公网注册出现垃圾账号或撞库迹象,第一优先级是关闭 Keycloak self-registration 或增加邀请码/管理员审核;不要把防滥用逻辑塞进 device pod 授权。
邮箱和手机字段可以作为 profile 信息保存,但第一版不要求验证。后续如果进入更大范围公网使用,应至少补充邮箱验证、注册限流或邀请制中的一种。
## 身份映射
Keycloak subject 不直接当业务授权 ID。`hwlab-cloud-api` 在 OIDC callback 中按 `issuer + sub` 查找或创建 HWLAB `users` 记录,然后所有业务授权都使用 `users.id`
推荐扩展 `users`
```sql
ALTER TABLE users ADD COLUMN IF NOT EXISTS auth_provider TEXT NOT NULL DEFAULT 'local';
ALTER TABLE users ADD COLUMN IF NOT EXISTS keycloak_issuer TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS keycloak_sub TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_keycloak_subject
ON users(keycloak_issuer, keycloak_sub)
WHERE keycloak_issuer IS NOT NULL AND keycloak_sub IS NOT NULL;
```
映射规则:
- 首次 OIDC 登录:按 `issuer + sub` 不存在则创建 `users`,默认 `role='user'``status='active'`
- 已存在本地 bootstrap admin 时,可以由 admin 在 HWLAB 内手动绑定 Keycloak subject 或提升 Keycloak 用户为 `admin`;Keycloak 注册本身不得自动提权。
- Keycloak profile 更新只同步 `username/display_name/email` 这类展示信息,不覆盖 HWLAB `role/status`
- `disabled` 用户不能创建 Web session,不能通过 API key 鉴权,也不能继续 Code Agent session。
## AuthPrincipal
所有用户态请求最终归一成 `AuthPrincipal`。业务模块只消费 `AuthPrincipal`,不直接判断 Keycloak token、cookie 或 API key 原文。
```ts
type AuthPrincipal = {
userId: string;
username: string;
displayName: string;
role: "admin" | "user";
status: "active" | "disabled";
authMethod: "web-session" | "api-key";
sessionId?: string;
apiKeyId?: string;
keycloakIssuer?: string;
keycloakSub?: string;
scopes: string[];
};
```
正式用户态请求只接受两类凭据:
1. `Authorization: Bearer hwl_live_...``HWLAB_API_KEY` 映射出的 header 是用户 API key,适用于 CLI 和 AgentRun runner 内 `hwpod`
2. `hwlab_session` cookie 是 Web session。
本地 `/auth/login` 只用于空库 bootstrap 和 legacy/debug;它发行的 session 也必须走同一个 `hwlab_session` cookie,不再通过 `Authorization: Bearer <session-token>` 恢复用户。device-pod 内部服务 key 只能在 cloud-api 到 device-pod 的受控链路内使用,不出现在用户 API、CLI、AgentRun runner 或浏览器文档中。
## Web 登录流程
```text
browser -> Cloud Web /
-> no valid hwlab_session
-> GET /auth/oidc/login?returnTo=/
-> 302 Keycloak login/register
-> Keycloak callback with code
-> hwlab-cloud-api exchanges authorization code with confidential client
-> validate signed id_token via Keycloak JWKS, iss/aud/azp/exp/iat/nonce/sub
-> fetch userinfo only as profile enrichment and require userinfo.sub == id_token.sub
-> upsert users by issuer+sub
-> ensure default API key exists for the user
-> create user_sessions token hash, expires_at <= now + 24h
-> Set-Cookie hwlab_session=httpOnly; Secure; SameSite=Lax
-> redirect returnTo
```
Web 页面不要求用户输入或复制 API key。浏览器请求通过 `hwlab_session` 恢复 `AuthPrincipal`;需要 CLI 时,用户在 Web 的 API key 页面生成或重新生成自己的 key。
Session 规则:
- 每次 OIDC 登录或 callback 成功都发行新的 session token,数据库只保存 token hash。
- 单个 session token 最长 24 小时;过期、用户 disabled 或 logout 后必须返回 `401 auth_session_invalid`
- `/auth/logout` 撤销 HWLAB session,并可选择跳转 Keycloak logout;即使 Keycloak 全局会话仍有效,HWLAB 已撤销的 session 也不能继续使用。
- 第一版不实现长期 refresh token 存储,也不把 Keycloak access token 或 refresh token 写入 HWLAB 数据库;用户过期后重新走 Keycloak 登录。
- `returnTo` 只允许同站相对路径,外站 URL、scheme-relative URL、反斜杠或控制字符必须回退到 `/workbench`
## CLI API key 流程
CLI 和 AgentRun `hwpod` 标准认证方式:
```bash
export HWLAB_API_KEY=hwl_live_xxx
bun tools/hwlab-cli/bin/hwlab-cli.ts client auth whoami
```
约束:
- CLI 不跳转浏览器,不依赖 Web cookie,不要求 username/password 交互。
- `hwlab-cli` 默认从 `HWLAB_API_KEY` 读取 key,并发送 `Authorization: Bearer <key>`
- AgentRun runner 的 transient env 只允许注入映射到当前 Code Agent session owner 的 `HWLAB_API_KEY`;可以使用该用户默认 key,也可以使用同一 `api_keys` 表中为该用户创建的 runner 专用 key,但绝不能使用跨用户共享的 device-pod 系统 key 或 Keycloak token。
- `client auth status` 必须显示 endpoint、是否检测到 `HWLAB_API_KEY`、key prefix 和用户摘要;不得输出完整 API key。
- `client auth whoami``client request GET /v1/users/me` 必须能用 API key 返回与 Web 同一用户的 `AuthPrincipal` 摘要。
- `client auth login --username ...` 和本地 cookie profile 属于 legacy 兼容入口;目标体验不再把它作为一等 CLI 登录。
- `hwpod` 在 runner 中必须自动使用 `HWLAB_RUNTIME_API_URL``HWLAB_API_KEY` 直连 `hwlab-cloud-api`,并让 cloud-api 按该 key 对应用户的 role/grant 做授权。
API key 数据结构:
```sql
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT 'Default API key',
key_prefix TEXT NOT NULL,
key_hash TEXT,
display_secret TEXT,
scopes_json TEXT NOT NULL DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
created_at TEXT NOT NULL,
last_used_at TEXT,
revoked_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id, status, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix);
```
目标状态只长期保存 `key_hash``key_prefix`,完整 API key 只在创建或 regenerate 响应中显示一次;用户丢失 key 时通过 regenerate 获取新 key。`display_secret` 只允许作为迁移期开发字段存在,不能作为生产化长期口径;完整 key 不得写入日志、issue、ConfigMap、Kubernetes Secret、trace 或 CLI 默认输出。
API key 行为:
- 首次 OIDC 登录后自动创建默认 key;如果默认 key 被 revoke,用户可在 Web 中重新生成。
- 默认 key、手动创建的 key 和 AgentRun runner 使用的 key 都继承该用户的 `role/status/grant`,不额外绕过权限矩阵。
- revoke 后同一 key 立刻返回 `401 api_key_invalid`,不能等 Web session 过期。
- 每次 API key 成功认证应更新 `last_used_at`,但不得记录完整 key。
## API 接口
| 接口 | 说明 |
| --- | --- |
| `GET /auth/oidc/login?returnTo=...` | 发起 Keycloak OIDC 登录,保存 state/nonce 并 302 到 Keycloak。 |
| `GET /auth/oidc/callback` | 校验 code、issuer、nonce 和 state,映射用户,发行 24 小时 Web session。 |
| `GET /auth/session` | 从 Web cookie 恢复 actorAPI key 用户摘要使用 `/v1/users/me`。 |
| `POST /auth/logout` | revoke 当前 Web session 并清 cookie。 |
| `POST /auth/login` | 本地账号密码 legacy/bootstrap fallback;目标 Web/CLI 登录不依赖该入口。 |
| `GET /v1/users/me` | 返回当前 `AuthPrincipal` 的脱敏摘要。 |
| `GET /v1/api-keys` | 列出当前用户 API key metadata;目标状态不返回完整 key。 |
| `GET /v1/api-keys/default` | 返回或创建默认 API key;目标状态只在新建时返回一次完整 key。 |
| `POST /v1/api-keys` | 创建一个用户 API key。 |
| `POST /v1/api-keys/{id}/regenerate` | 重新生成指定 API key,旧 secret 立即失效。 |
| `DELETE /v1/api-keys/{id}` | revoke 指定 API key。 |
所有 API key 管理接口都要求当前 `AuthPrincipal` 是同一用户本人或 `admin`;普通用户不能查看或生成其他人的 key。常规列表响应只返回 `key_prefix``id``name``status``last_used_at`,完整 key 只在创建或 regenerate 的一次性响应中出现。
## 与资源授权的关系
登录和认证只回答“请求是谁”。资源授权仍由 [spec-user-access.md](spec-user-access.md) 定义:
- `admin` 可以管理用户、device pod profile/grant,并跨用户查看或取消 Code Agent session。
- `user` 只能访问自己的 Code Agent session 和被授权的 device pod。
- Web session、CLI API key 和 AgentRun runner 内 `hwpod` API key 得到同一个 `users.id` 时,应看到相同 device pod grant 和账号 workspace。
- Keycloak realm role、group 或 claim 不直接决定 HWLAB device pod 权限;最多作为创建/绑定用户时的输入线索。
## 测试规格
## T1
阅读 docs/reference/spec-v02-auth.md,然后用 CLI 或 curl 验证 `https://auth.74-48-78-17.nip.io/realms/hwlab/.well-known/openid-configuration` 可访问,issuer 与配置完全一致,JWKS URL 为 HTTPS,响应不包含 admin password、client secret 或数据库连接串。
## T2
阅读 docs/reference/spec-v02-auth.md,然后从浏览器访问 `https://hwlab.74-48-78-17.nip.io/`。未登录时必须进入 Keycloak 登录/注册;注册新用户不要求邮箱或手机验证,callback 后回到 Workbench`GET /auth/session` 返回 active `user` actor 和 24 小时内过期的 session,不返回 session token 原文。
## T3
阅读 docs/reference/spec-v02-auth.md,然后登录 Web 后打开 API key 管理入口,确认可以创建或 regenerate 用户 API key;完整 key 只在创建或 regenerate 响应中显示一次。用 `HWLAB_API_KEY=<key> bun tools/hwlab-cli/bin/hwlab-cli.ts client auth whoami` 验证 CLI 无浏览器跳转即可恢复同一用户。
## T4
阅读 docs/reference/spec-v02-auth.md,然后 revoke 或 regenerate API key,再用旧 `HWLAB_API_KEY` 请求 `/v1/users/me``/v1/device-pods`,必须返回 `401 api_key_invalid`;同一用户重新登录 Web session 不应恢复旧 key。
## T5
阅读 docs/reference/spec-v02-auth.md,然后分别使用 Web session 和同一用户的 CLI API key 访问 `/v1/device-pods`、创建 Code Agent session 和读取自己的 trace/result,确认授权结果一致;另一个普通用户的 API key 不能读取该 sessionadmin 可以跨用户查看。
## T6
阅读 docs/reference/spec-v02-auth.md,然后检查 cloud-api 日志、trace、CLI 默认输出和 issue 复现材料,确认不出现 Keycloak client secret、session token、完整 API key、历史 `HWLAB_DEVICE_POD_API_KEY` 或 Postgres URL 原文;AgentRun runner 中也不得存在跨用户共享的 device-pod 系统 key。
## 规格的实现情况
| 规格项 | 状态 | 说明 |
| --- | --- | --- |
| Keycloak 独立 namespace 与公网 HTTPS issuer | 部署已完成 | `keycloak` namespace、Caddy/FRP HTTPS、`hwlab` issuer、admin console 和 bootstrap Job 已形成部署基线;后续只按本文件继续硬化。 |
| Keycloak 自助注册且不强制邮箱/手机验证 | Keycloak 侧已完成 | 只适合小范围测试;HWLAB 应用层仍必须默认 `user`、无 device pod grant。 |
| Web OIDC login/callback | 待 HWLAB 接入收口 | Keycloak client、Cloud API/Web rollout 和浏览器 callback 验收见 #814redirect URI 必须使用 `https://hwlab.74-48-78-17.nip.io/auth/oidc/callback`。 |
| Web session 24 小时轮换 | 待 HWLAB 接入收口 | 当前目标是 callback 成功后由 `hwlab-cloud-api` 发行 24 小时 `hwlab_session`;验收见 #814。 |
| CLI/AgentRun `HWLAB_API_KEY` 一等登录 | 待 HWLAB 接入收口 | 目标是统一 env API key 映射到用户,无浏览器跳转,无 Keycloak token,无跨用户 device-pod key;验收见 #814。 |
| API key 一次性显示和 revoke/regenerate | 待 HWLAB 接入收口 | 目标状态只在创建或 regenerate 时显示完整 key;列表和已存在默认 key 只返回 metadata。 |
| `AuthPrincipal` 归一 | 待 HWLAB 接入收口 | 后续实现必须把 Web session、CLI API key 和 AgentRun `hwpod` API key 都归一成同一用户 actorlegacy/internal key 不作为正式用户方法。 |
| `admin/user` 与 device pod grant 授权 | 部分实现 | 现有 cloud-api 已有本地用户、session、grant 和 Code Agent owner 绑定;资源授权继续按 spec-user-access 收敛。 |
## Keycloak 部署纪律
G14 v0.2 固定使用 `quay.io/keycloak/keycloak:25.0`(实际启动版本 Keycloak 25.0.6)。该镜像的初始管理员入口是 `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD``KC_BOOTSTRAP_ADMIN_USERNAME/PASSWORD` 属于更新版本文档口径,不作为 v0.2 当前镜像的部署入口。`master` realm 保持 Keycloak 自身初始化结果,不导入或覆盖;业务 realm 使用管理员 token 通过 Admin REST API 创建和更新。
`--import-realm` 只用于导入非 `master` 的预制 realm 文件,且启动导入遇到已存在 realm 会跳过。需要覆盖已存在 realm 时按 Keycloak import/export 文档停服务后运行离线 import,不能在运行中的生产 Deployment 上把 `master` realm 当作可覆盖配置文件反复灌入。
### 禁止直接修改 keycloak 数据库
所有 keycloak admin 操作必须通过 `kc.sh` CLI、Keycloak admin REST API 或 `--import-realm` 启动导入完成。**禁止**对 `keycloak` namespace 里的 Postgres 库直接执行 `INSERT / UPDATE / DELETE` 修改 `user_entity``credential``realm``client``client_scope``client_scope_attributes``keycloak_role``user_role_mapping` 等表。直接改库会导致:
- 破坏 Keycloak 25 的 `pbkdf2-sha256` JSON credential 格式,登录时报 `Unrecognized token 'pbkdf2'` 反序列化失败
- 绕过 Keycloak 启动时缓存的 `client_scope_attributes`(如 `include.in.token.scope`),pod 重启后修改失效
- 跳过 Liquibase changelog,未来 keycloak 升级时迁移冲突
- 在多副本或滚动重启场景下造成各副本行为不一致
### 正确做法
| 场景 | 推荐做法 | 禁止做法 |
| --- | --- | --- |
| 第一次引导管理员 | `KEYCLOAK_ADMIN=admin` + `KEYCLOAK_ADMIN_PASSWORD` 引用 `keycloak-admin` Secret,让 Keycloak 在首次启动时自行创建 `master` 管理员 | `psql` 直插 `user_entity` + `credential`,或导入自制 `master-realm.json` 覆盖 Keycloak 内置 `master` realm |
| 创建 `hwlab` realm + `hwlab-cloud-web` OIDC client | `keycloak-bootstrap-realm` Job 用 `kcadm.sh` / Admin REST API 幂等 create-or-update realm/clientredirect URI 对齐 `hwlab-cloud-api` callback | `psql` 直插,或依赖运行中的 `--import-realm` 覆盖已有 realm |
| 修改 realm/client/user/role | 优先通过 Keycloak Admin REST API 或 `kcadm.sh`;批量离线导入必须停服务后按 import/export 文档执行,并先确认是否会覆盖目标 realm | `psql` UPDATE 后靠重启碰运气 |
| 调试 admin token 没 `realm_access.roles` | `kubectl -n keycloak exec ... -- /opt/keycloak/bin/kc.sh show-config` 查 attribute 实际值;用 `curl /admin/realms/master/client-scopes/<id>` 走 admin REST 验 attribute 状态 | `psql` UPDATE 然后重启 keycloak pod 试运气 |
### 历史教训
#788 收口过程中曾用 `psql` 直插 `user_entity` / `credential` / `user_role_mapping` 临时绕开 Keycloak 初始化问题,结果:
1. 直插的 `pbkdf2-sha256` 字符串 hash 在 Keycloak 25 反序列化时抛 `Unrecognized token 'pbkdf2'`
2. SQL UPDATE `client_scope_attributes.include.in.token.scope = 'true'` 后,access_token 仍然不包含 `realm_access.roles`——Keycloak 在启动时缓存 attributeSQL 修改要重启 pod 才生效
3. `client_scope_client.default_scope = true` 的 SQL 修改同样需要重启才生效
4. master realm admin role 复合 19 个子 role,手工插 `user_role_mapping` 后仍然有缺失
所有这些都是直接改库的副作用。后续所有 Keycloak 部署必须走镜像支持的初始管理员 env、Admin REST API、`kcadm.sh` 或明确停服的离线 import,**禁止 SQL 直改**。