20 KiB
SDD-007:内置飞书 OIDC 发行者
| 字段 | 内容 |
|---|---|
| 级别 | S3(公网身份入口、签名密钥、生产网关变更) |
| 状态 | 实施中,尚未验证真实飞书登录 |
| 日期 | 2026-10-22 |
1. 现状与目标
当前 OpenVPN OIDC 模式调用 openvpn-auth-oauth2,要求外部标准 issuer;/api/tunnels/openvpn/oidc-readiness 仅检测已安装插件与 HTTPS 管理入口。Nginx 只允许管理页面/API,其他路径返回 444;现有 LDAP 与密码专线均应保留。
增加单租户、单机部署的 Go /id 身份模块:飞书网页 OAuth 授权后,向插件提供受限的 OIDC authorization code 流程。仅已启用的管理员配置可提供发现文档、JWKS 与发码;不把登录成功视为自动获得 VPN 权限。沿用现有 HTTPS 域名及数据库,不引入外部身份服务器。
2. 功能与约束
- FR-01:管理员配置飞书 App ID/Secret(密钥不回显)、允许的 tenant key;系统生成并仅在服务端使用 VPN OIDC Client Secret,管理页面只显示 Client ID;复用现有 TLS 域名。
- FR-02:提供 discovery、JWKS、authorize、飞书回调、token、userinfo;只接收登记的一个 HTTPS 回调地址,发放一次性短时 code;签发 RS256 ID Token。飞书返回的 open_id 与 tenant_key 组合成稳定 subject。
- FR-03:OpenVPN 插件通过机密客户端交换授权码;客户端凭据、回调、nonce、state 绑定,存在 code_challenge 时校验 S256;不接受外部指定的上游 URL。
- FR-04:前端展示 issuer、飞书回调及 VPN 回调、准备状态,允许选择内置 issuer 填写专线配置,但不覆盖既有配置。
- FR-05:Nginx 仅允许同域
/id/的固定身份端点转发到管理进程;/oauth2/转发到单条已启动的认证组件(仅就绪后启用);其他未知路径保持 444。 - NFR-01:签名密钥 0600、密钥材料不可回显,令牌与随机状态只保存哈希或受限数据;严格 TLS、过期时间、状态原子消费、请求限流及日志脱敏。
- NFR-02:未启用或配置不完整时不发布可用 issuer,不破坏现有 OpenVPN/LDAP/管理入口。
3. 方案与契约
发行者建议 https://<现有域名>/id;只按 issuer 文档发布实际 URL,不假设根目录 discovery。飞书上游回调 https://<现有域名>/id/feishu/callback;插件回调 https://<现有域名>/oauth2/callback。飞书 App ID/Secret 只用于上游;插件使用系统生成的独立 VPN 凭据。数据加密利用现有 secret.key;RSA 签名私钥独立文件,备份必须同时包含数据库和密钥。
独立 internal/identityprovider 包承载协议逻辑和存储;api 管理路由只对管理员开放;Nginx 模板明确路由优先级。JWT 仅签发 openid 授权范围,不在令牌里包含飞书访问令牌。用户授权初期先采用飞书应用可用范围 + 明确租户约束;后续如需逐用户允许名单再增加独立配置。
接口参考 /Users/logan/devops/demo/id/Feishu-OIDC-Workers 的飞书官方端点定义与 JSON 换码格式;授权状态和客户端凭据隔离参考 /Users/logan/devops/demo/id/dex 的协议边界。没有复制其实现;Workers 示例把原始飞书 code 转交客户端的做法不用于本项目。
4. 风险与验收
| 风险 | 防护 / 恢复 |
|---|---|
| 伪造授权回调 / 重放 code | 随机 state 与 code、短有效期、数据库原子消费、精确 redirect URI 和客户端绑定 |
| 泄露私钥或飞书密钥 | 0600 与 AES-GCM、API 不回显、部署时不传入对话中曾公开的旧密钥 |
多专线共用 /oauth2/ |
网关只选择唯一运行中的同域 OIDC 专线,冲突时不启用转发 |
| 网关修改影响管理入口 | 先预览、备份现有站点、nginx -t 后 reload;失败恢复旧站点 |
验收:禁用与错误配置拒绝授权;模拟飞书服务测试完整码流、重放、错误租户、错误回调/nonce/PKCE、JWT 验签;真实飞书应用授权和 OpenVPN WebAuth 需单独联调。发布顺序:完成本地测试 → 部署但默认禁用 → 管理员轮换并保存飞书凭据 → 预览并启用网关 → 测试专线正/负向登录 → 再决定生产切换。恢复:禁用 issuer、恢复 Nginx 旧站点和旧二进制;不删除现有专线或密钥。
5. 专线与身份配置分工(编辑页迭代)
现有专线编辑页仍把外部 OIDC 的 issuer 与 VPN Client Secret 输入框作为主流程,容易让管理员误把飞书 App ID/Secret 填入专线。管理员仅在「设置 → 飞书身份」维护全局飞书 App ID、App Secret 和 tenant_key;专线的内置模式自动引用当前 issuer、独立 VPN Client ID 和公网入口。编辑既有外部 OIDC 专线时保持原始设置,只有明确切换来源才更改字段。外部 OIDC 仍允许手工配置,不把飞书上游凭据放入专线。
验收:专线编辑和新建都能直达身份设置;内置模式不展示飞书密钥或可编辑的 VPN 密钥,提示飞书回调与 VPN 回调的区别;外部模式保留原先的输入能力;服务端对内置标记校验已启用的发行者、Client ID 和当前域名,不因提交旧密钥而回退至外部模式。该迭代不自动切换、启用或重启已有专线;上线前构建前端并跑相关 Go 测试,上线后检查管理入口与禁用的身份入口,失败恢复上一版二进制。
6. 公网白名单与页面故障修复
线上新增 /api/system/identity 后托管 Nginx 仍使用旧白名单,浏览器看到 ERR_EMPTY_RESPONSE(Nginx 444);本机管理进程可正常返回 401。已有 /api/system/tls/domain 路由在无凭据时返回 401,不能把用户同时报告的域名保存失败归结于同一缺失路由。Vue 侧栏在 route 创建前注册 watch,导致初始化时引用尚未声明的变量;WireGuard SVG 含截断的路径数据。
每次启动且确认当前是有效 Nginx HTTPS 托管站点时,按已注册 Gin 路由重建站点白名单;文件内容相同则不重载,冲突或 nginx -t 失败时保留旧站点并记录日志,不阻止本机服务启动。公网只同步已注册 API、固定身份端点与当前单条运行的同域认证组件,未知路径仍 444。验收:新身份管理 API 不再返回空连接,未经认证返回 401;既有域名 PUT 也仍返回 401,授权后的保存需单独实测;设置和编辑页无布局初始化异常;SVG 路径有效;Nginx 测试通过。恢复时使用 /tmp 的上一版二进制和原 Nginx 站点快照重新加载,不修改专线或飞书凭据。
7. 自动读取飞书企业标识(2026-10-22,S3)
当前管理员必须手填 tenant_key 才能启用身份服务;用户提供的飞书 SDK tenant.v2.tenant.query 可通过应用凭据获取企业信息。新增仅管理员可调用的 POST /api/system/identity/tenant:沿用 HTTPS 管理入口限制,使用表单中 App ID/Secret 或同一 App ID 已保存的加密 Secret,向飞书换取 tenant access token 后查询企业名称和 tenant_key。仅返回这两个字段,不保存临时输入的凭据、令牌或查询结果;响应禁止缓存。页面提供「自动获取」并回填可编辑字段;保存启用配置且企业标识为空时也尝试自动获取。更换 App ID 时绝不复用原 App Secret 或旧企业标识;查询失败时不启用或改变已保存的配置。
接口限定固定飞书域名、短超时、受限响应体和无重定向;错误不回显密钥或 token。飞书授权需要应用具备企业信息读取权限(按飞书后台实际授权提示配置)。管理员仍可手填企业标识以兼容既有配置;真实飞书 API 联调在获得轮换后的凭据时进行。
| 验收 | 操作与预期 | 验证 |
|---|---|---|
| AC-07-01 | HTTPS 管理员查询正确凭据,返回企业标识与名称;未保存凭据不落库 | 模拟上游请求/响应 |
| AC-07-02 | 同一 App ID 可复用加密 Secret;换 App ID 不复用旧密钥或旧租户 | 管理接口测试 |
| AC-07-03 | 飞书拒绝授权、响应异常、无标识或 HTTP 管理请求时,安全失败且不泄露凭据 | 负向测试 |
| AC-07-04 | 启用时企业标识留空可自动获取;查询失败不保存;手工输入兼容 | 管理接口测试与前端构建 |
发布前完成 Go 测试与前端构建;部署后检查新路由被托管 Nginx 白名单接受、未授权返回 401,并在轮换飞书 Secret 后进行真实查询。恢复可回退二进制与前端资源;不需要数据迁移,也不修改已运行专线。
8. 飞书应用引导与查询错误诊断(S3)
当前 tenant/v2/tenant/query 失败时统一显示“请确认应用已开通企业信息读取权限”,掩盖接口路径、调用方式、应用发布状态、权限和网关拒绝等不同故障。页面需要针对企业自建应用给出最小配置步骤:凭据页 App ID/Secret;权限管理查找企业信息读取(tenant:tenant:readonly,以控制台实际名称为准)并发布版本;安全设置登记系统显示的精确 HTTPS 飞书回调;网页授权所需权限和应用可用范围。最初页面错误地将 user_profile 称为飞书范围,第 10 节修正为飞书用户基础信息权限。自动读取企业标识失败允许手填,但不能把未核实的企业标识视为认证成功。
后端只回显飞书返回的数字错误码与 HTTP 状态,不回显第三方原始 msg、访问令牌或凭据;页面保留错误提示以便管理员根据飞书错误码排查,不再把所有错误归因于权限。验收:模拟 code != 0、非 2xx、无响应码,区分令牌与企业查询阶段且不泄露上游原始信息;前端引导能看到精确回调、自动查询权限与发布要求;构建和身份模块测试通过。发布先构建和测试,再按现有 /tmp 双备份流程部署并检查 HTTPS、401 和 Nginx;恢复为上一版二进制,保留数据库与身份配置。
9. OpenVPN 浏览器入口随专线状态同步(S3)
线上 OpenVPN OIDC 专线运行且认证组件监听 127.0.0.1:19090,但当专线在管理服务启动后才启动时,受管 Nginx 未同步 /oauth2/,浏览器访问 /oauth2/start 被兜底 444 关闭。现有 RefreshManagedGatewayOnStart 仅启动时运行;设置页允许手动同步,容易遗漏。
专线启动、停止、自动启动、运行中更新、删除及批量状态变更后,依据现有唯一运行中且公网域名匹配的 OIDC 专线刷新受管站点;串行执行刷新,避免并发 Nginx 写入。仅允许现有严格的回环端口代理,不改变其他路径的 444。刷新失败时专线实际状态仍以数据库为准,向操作方明确报告“专线状态已改变、路由同步失败”,不要谎称 VPN 启动失败或放行宽泛路径。
验收:启动后自动出现 /oauth2/ 转发,停止或删除最后一条专线后移除,重复操作不改变配置;公网无 state 的 /oauth2/start 由空响应变为认证组件的 400,Nginx -t 成功;模拟冲突/同步失败时有可见警告。发布先备份二进制、构建和测试,再部署并检查运行中专线不被管理进程重启干扰;恢复为先前二进制并通过设置页手动同步路由,不修改数据库或证书。
10. 飞书授权范围修复与连接超时排查(S3)
生产中 OpenVPN 客户端已接入 TCP 1194,认证组件发送 WebAuth 并等待 60 秒;此前 /oauth2/ 入口的 444 已修复。隔离浏览器对内置发行者的合成请求跳到飞书错误页 20043:user_profile 有误,实际返回 invalid_scope。代码在 handlers.go 将 user_profile 写入飞书授权链接,设置页也错误地指导管理员启用它。参考项目 /Users/logan/devops/demo/id/Feishu-OIDC-Workers 使用飞书权限 contact:user.base:readonly 获取用户基础资料,替换无效范围,并在页面清楚区分企业信息查询权限与浏览器登录授权权限。OpenVPN OIDC 模式通过飞书 tenant_key 和 open_id 识别用户,不自动读取 VPN 拨号用户目录;如需逐用户允许名单须另行设计,不在本次导入飞书用户。
验收:单元测试验证生成的飞书授权链接包含新的权限范围、不再包含 user_profile;现有令牌/JWKS/用户信息测试、前端构建通过;上线后授权链接跳转路径和无 state 入口验证通过。真实飞书授权仍需应用批准该权限、发布并由用户本人完成登录;未经本人授权不得宣称已跑通完整 VPN 连接。发布前备份旧二进制、发布后检查健康与 Nginx;可回退到旧二进制但将恢复无效范围,须停止测试专线或等待修复。
11. 权限检测与浏览器结果页(S3)
真实飞书授权及 OpenVPN 建链现已在实例日志中验证。配置页输入 App ID/Secret 后延迟调用已有 HTTPS 管理员查询接口:成功读取企业信息才显示 tenant:tenant:readonly 已验证,并自动填入 tenant_key;失败仅展示脱敏错误并允许重试。contact:user.base:readonly 是用户授权范围,不能仅靠应用凭据证明;首次成功换取用户资料并校验租户后记录无用户标识的验证时间,换应用凭据或租户时清除,设置页只展示是否曾通过。服务端启动或页面刷新不重复发起 OAuth,也不把手填 tenant_key 当作验证通过。密钥只经 HTTPS 管理接口传输,页面异步请求需忽略被新输入覆盖的旧响应。
认证组件通过原生 http.template 加载每实例的 0600 HTML 页面;页面只提示返回客户端查看连接状态,不在可能复用的终态模板上错误宣称 VPN 已连接。不额外放宽 Nginx 路径。验收:凭据变更后状态清零,查询正确/错误与旧响应均不误报;首次真实登录后状态可见;插件能正常加载模板并保持浏览器认证、OpenVPN 建链。发布时先构建测试,再替换二进制并重启受影响的 OIDC 实例以应用新模板;若插件不兼容模板,回退之前的配置与二进制、重启实例,旧认证仍可使用。数据库新列由 AutoMigrate 添加,回退版本忽略该列。
12. 在线会话的未知身份(S1)
OpenVPN status-version 3 的第 1 列 Common Name 与第 9 列 Username,在无客户端证书的 WebAuth 首次连接时都可能为 UNDEF。目前会话同步把占位字符串当作拨号用户名,误写入在线会话及审计。优先使用有效 Username,其次使用有效证书 CN;两列均为 UNDEF 时留空,并在在线会话及连接审计页面明确显示“身份未识别”。保持原流量快照键以避免部署后的首次轮询重复计数;不根据公网 IP、姓名或认证时间猜测身份,更不把“LG”这样的显示名自动绑定 VPNUser。已有历史日志保留原值,仅页面标记其身份未识别。
验收:密码与证书连接仍展示其用户名/CN;无身份的 WebAuth 状态行不会记录 UNDEF 为 VPNUser,也不会误关联飞书人员;在线会话保留连接地址与流量。若同一连接随后获得可信 Username,沿用此前 UNDEF 流量快照计算增量,不把旧流量重复累计;单元测试覆盖未知身份、重复轮询和后续获得稳定 subject 的过程。真正按人展示还需验证 openvpn-auth-oauth2 是否能向 OpenVPN 提供带稳定 subject 的 Username;如插件不传递,须在认证阶段增加可信映射,不能用会话 IP 推断。此修复不改变认证权限或 Nginx;发布前运行 OpenVPN 单元测试及前端构建,发布后检查真实状态文件第 1/9 列与在线会话,失败则恢复旧管理二进制,保留数据库和专线进程。
13. 授权身份传递与回调结果页(S2)
现有 fetchFeishuUser 从飞书 /authen/v1/user_info 读取 open_id、tenant_key、name,/id/userinfo 输出稳定 sub 与 name;但 ID Token 仅包含 sub。浏览器 /oauth2/callback 是认证组件消费授权码的端点,地址栏的 code 不能代表用户资料。将已获取的非空姓名作为标准 OIDC name claim 加入 ID Token;sub 继续由 tenant_key:open_id 组成,不能以可变姓名替代。结果模板改为简洁蓝白灰页面,继续保持结果中性,不能输出令牌、姓名或根据地址栏宣称 VPN 已连通。
FR-13-01:模拟飞书换码与用户信息返回时,服务端传递稳定标识与姓名,签发的 ID Token 在非空时包含 name,/id/userinfo 保留原字段;空姓名不写入空 claim。FR-13-02:同一 0600 模板通过 http.template 用于回调终态,无论认证结果如何都只提示切回客户端检查状态。
AC-13-01:模拟上游含 open_id、tenant_key、name 的响应,验签并检查 ID Token 的 sub/name,验证名称为空时不增加 claim;运行身份模块测试。AC-13-02:读取生成的模板,核对页面标题、移动端视口及不出现回调参数/用户资料;运行 OpenVPN 模块测试。真实账号授权和 OpenVPN 状态第 1/9 列另需在线联调,单元模拟不等同于线上已显示真人姓名。
风险:姓名属于个人信息,只放入面向已认证机密客户端的短时 ID Token 与现有 UserInfo,不写浏览器结果页或日志;上游未提供姓名时维持稳定 sub。兼容性:已有依赖 sub 的客户端行为不变;不增公网路由或数据库列。发布先构建并验证,再部署管理二进制、重启受影响的 OIDC 专线以生成新模板,检查 HTTPS 认证流程;失败回退二进制并重启该专线,不删除身份数据或签名密钥。
14. WebAuth 身份进入 OpenVPN 会话(S3,待组件核实)
现状证据:飞书回调读取 tenant_key、open_id、name,受限 OIDC Token 返回稳定 sub;实例配置 openvpn.auth-token-user: true,但服务端 status-version 3 的 Username 和 CN 仍可能都是 UNDEF。auth-token-user 是推送给客户端以后重新认证使用的用户名,不能证明服务端首次连接状态已绑定 OIDC subject。会话同步只信任 OpenVPN 状态文件,不应按公网 IP、浏览器回调时间或显示名匹配人。旧版代码把 UNDEF 直接写入审计,本地 S1 修正正在避免新增这类伪用户;历史审计保持原样。
目标链路:飞书应用确认用户和租户 → OIDC 使用 tenant_key:open_id 作为稳定 subject、姓名只用于显示 → 认证组件验证令牌与 pending CID/KID 的绑定 → 在同一条管理接口 client-auth 的客户端配置中设置 override-username <稳定 subject> → OpenVPN 状态文件 Username 出现该 subject → 平台在线会话与审计使用这个 subject。OpenVPN 本地源码文档 doc/man-sections/server-options.rst 明确 override-username 会被状态输出采用;但须先核实当前 openvpn-auth-oauth2 版本是否支持下发此配置,若不支持,应修改/封装认证组件并在集成测试验证,不能在 server.conf 中静态设置一个共用用户名。将来展示飞书姓名需按 subject 保存受控身份资料或提供受限查询,不得将姓名当成授权键或自动关联同名 VPNUser。
FR-14-01:每个成功认证的 OpenVPN CID/KID 必须绑定经验证的非空稳定 subject;缺失身份则拒绝授权,不得以 UNDEF、IP 或可变姓名补全。FR-14-02:状态文件的 Username 必须与该 subject 一致,平台在线列表和连接/断开审计使用该值;密码和证书专线保留原行为。FR-14-03:部署不得扩大 Nginx、管理 socket 或飞书资料接口的权限;失败时能回退认证组件与配置。
AC-14-01:模拟两个并发 WebAuth 请求及其不同 CID/KID,证明不能交叉赋值,令牌验证失败、租户不符和空 subject 均拒绝;检查管理命令仅含安全编码的 subject。AC-14-02:测试专线真实授权后读取 openvpn-status.log 的 CN/Username、在线会话和连接/断开审计,三处能追溯同一稳定 subject;重复认证和重连无误绑定。AC-14-03:测试旧密码/证书连接、认证失败、组件崩溃恢复,并检查失败时无匿名会话被错误归到已认证用户。
待决:当前运行的 openvpn-auth-oauth2 对 client-auth 配置和令牌 sub 的实际处理,以及线上状态文件原始列值;在没有组件源码/运行输出验证前,不声明此节已实现。发布顺序为先验证并编译组件、测试专线联调,再更新管理服务及组件,最后观察线上状态与审计;回退为恢复先前组件及配置并重启测试专线,保留密钥、身份数据和历史审计。