Backstage 外部服务认证(BEP-0007):基于 backend.auth.externalAccess 的 REST API 外部访问控制指南
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
导读
本文围绕 Backstage 官方 BEP(Backstage Enhancement Proposal)0007:Authentication of External Services(见 beps/0007-auth-external-services/README.md),系统讲解如何让外部服务安全地访问 Backstage 后端插件暴露的 REST API。你将掌握backend.auth.externalAccess配置区的完整语法、static/legacy/jwks三种外部访问类型的用法与差异、访问限制(access restrictions)的精细控制方式,以及这些机制在@backstage/backend-defaults中的底层实现原理,最终能够为 CI/CD 流水线、外部异步服务、本地开发脚本等场景配置一套安全可控的 API 调用凭据。
背景:为什么需要外部服务认证
在 Backstage 的新后端系统中,插件之间的通信通过coreServices.auth与coreServices.httpAuth两个核心认证服务完成:插件默认会生成自签名令牌并自动验证,无需任何配置。然而这套插件间认证机制仅限 Backstage 生态内部使用——外部调用者无法参与其中,导致以下问题:
- 外部异步服务(如需要向用户发送通知的后台任务)无法方便地调用 Backstage 插件 API;
- 外部集成系统需要与软件目录(catalog)等插件交互时缺乏清晰的认证路径;
- 本地开发阶段希望用临时令牌直接
curl调用 API,却没有轻量方案。
在旧的 service-to-service-auth--old.md 教程中虽然提供了共享密钥签名令牌的方案,但教程对如何正确构造调用缺乏清晰说明,使用体验笨重。BEP-0007 正是在新认证服务(coreServices.auth/coreServices.httpAuth)语境下,对旧方案的演进与替代——其前置基础是 BEP-0003: Auth Architecture Evolution。
目标(Goals)
- 让异构外部服务以最低复杂度访问 Backstage REST API;
- 迁移到新认证方案完全可选,旧的共享密钥签名令牌继续可用;
- 通过配置指定外部实体可以访问哪些 API 端点;
- 通过限制 API 上下文、为不同外部方授予独立访问权限来降低安全风险——即使某个外部服务被攻破,也只能访问受限的 API 子集;
- 提供更清晰、更新的示例,让方案易于理解和使用。
非目标(Non-Goals)
- 不替换现有的 Backstage 服务到服务认证或令牌机制;
- 不提供让插件绕过现有服务到服务认证的出口——即使技术上可行,也被视为反模式。
核心方案:backend.auth.externalAccess 配置区
BEP-0007 引入了一个新的应用配置区段backend.auth.externalAccess,作为声明外部服务访问方式的推荐途径。同时,旧的backend.auth.keys配置区段仍然受支持,但被视为**遗留(legacy)**形式——新认证代码读取它仅出于向后兼容目的,使用它会触发一条日志警告,提示迁移到新格式。
关键设计:
externalAccess是一个数组,每个元素带有一个type字段以支持未来扩展。使用数组意味着配置系统不会在不同配置文件之间对访问方法做合并(merge),从而避免因意外合并而造成的安全漏洞。
BEP 文档给出的最小配置示例:
backend: auth: externalAccess: - type: static options: token: ${SERVICE_API_TOKEN} scope: plugins: catalog其中:
type:任意字符串,框架内置处理若干类型(初始为固定集合,未来可能做成可扩展);options:通用对象,具体字段因类型而异(上例中static类型只有token,从环境变量读取);scope(可选):控制该访问方法可执行的操作范围,超出范围的调用会被 403 拒绝;作用域可包含插件 ID 和/或权限(详细说明见下文"作用域与访问限制"一节)。
需要说明的是:BEP 提案阶段将限制字段命名为scope,而当前仓库实现中该字段已演进为accessRestrictions,本文后续章节会按实现现状详细展开。
三种外部访问类型详解
BEP-0007 明确覆盖legacy与static两种类型;而jwks类型在 BEP 中仅作为未来方向的示意,在当前的仓库实现中已经落地(见 packages/backend-defaults/config.d.ts 与 jwks.ts)。当前实现内置的三种类型如下。
1. static:静态令牌(API Key)
static类型允许你指定任意静态字符串作为 API Key,调用方将其原样放入请求头:
Authorization: Bearer <token>配置示例:
backend: auth: externalAccess: - type: static options: token: ${SERVICE_API_TOKEN} subject: cicd-system-completion-events # accessRestrictions: ...实现要点(来自 static.ts):
token可以是任意不含空白字符的字符串,但出于安全考虑应足够长、难以暴力猜测;initialize阶段会强制校验:令牌必须匹配^\S+$(非空白字符),且长度至少为 8 个字符,否则直接抛出配置错误;subject(主题)同样必须是非空白字符串,用于标识每个调用方,并成为接收方插件拿到的 credentials 对象的一部分;- 推荐在命令行生成令牌:
node -p 'require("crypto").randomBytes(24).toString("base64")'; - 由于令牌可以是任意字符串,你还可以为其添加辨识前缀(例如
freben-local-dev-)方便调试追踪。
2. legacy:旧共享密钥签名令牌
legacy类型与旧的共享密钥签名方法完全对应,任何在此输入的密钥都会与backend.auth.keys中指定的密钥合并使用。
配置示例:
backend: auth: externalAccess: - type: legacy options: secret: ${EXTERNAL_ACCESS_SIGNATURE_SECRET} subject: my-external-service # accessRestrictions: ...实现要点(来自 legacy.ts):
secret是 base64 编码的随机字节,同时用于签名与验证(对称密钥),必须足够长以防暴力猜测;配置校验要求其为合法 base64 字符串;- 从源码可推断其验证逻辑:调用方需用 HS256 算法、以 base64url 解码后的密钥签署 JWT,JWT 负载要求
sub为backstage-server、不带aud声明,令牌通过Authorization: Bearer <jwt>传递; - 验证流程会先做"鸭子类型"预检(检查
alg是否为 HS256、sub/aud是否符合预期),再执行jwtVerify,只有签名验证失败(ERR_JWS_SIGNATURE_VERIFICATION_FAILED)才返回未匹配,其他错误会继续抛出; - 当通过旧的
backend.auth.keys配置加载时,subject 会被固定为external:backstage-plugin。
3. jwks:基于 JWKS 的 JWT 验证
jwks类型允许通过配置的 JSON Web Key Set(JWKS)端点验证外部调用者的 JWT 令牌,适合使用第三方身份提供方(如 Auth0)签发的令牌做认证的外部调用方。BEP-0007 中将其列为未来方向示例,当前仓库已实现:
backend: auth: externalAccess: - type: jwks options: url: https://other-service.acme.org/.well-known/jwks.json issuer: https://example.com algorithm: RS256 audience: example, other-example subjectPrefix: custom-prefix # accessRestrictions: ...各选项含义(见 packages/backend-defaults/config.d.ts):
url(必填):JWKS 端点完整 URL,必须指向一个无需认证即可返回 JWKS 的端点;algorithm(可选):用于验证 JWT 的算法(可多个),传入的 JWT 必须使用其中之一签名;issuer(可选):JWT 的签发者,传入的 JWT 的iss声明必须匹配其中之一;audience(可选):JWT 的目标受众,传入的 JWT 的aud声明必须匹配其中之一,或完全没有 audience;subjectPrefix(可选):主体前缀。所有验证通过后的 subject 都会带external:前缀,若配置了subjectPrefix,则拼接为external:<subjectPrefix>:<sub>形式(例如external:custom-prefix:sub)。
BEP 文档中还以示意形式给出了未来可能出现的更多类型(不属于本 BEP 范围,仅用于说明扩展方向):
backend: auth: externalAccess: - type: certificate options: publicCert: $file: ./service-cert.pem作用域与访问限制:精细控制外部调用权限
BEP 提案中的 scope 概念
BEP-0007 提出使用可选的scope字段控制访问方法的操作范围:不指定任何 scope 时,该访问方法拥有无限作用域,可执行所有类型的操作;一旦指定,超出范围的请求将返回403拒绝。
提案阶段支持三种限制方式(scope下可给字符串或字符串列表,任一规则匹配即允许):
按目标插件 ID 限制:
scope: plugin: catalog按请求的权限类型限制:
scope: permission: catalog.entity.read按权限属性限制:
scope: permissionAttributes: { action: read }需要特别提醒:如果设置了插件规则,那么再为该插件添加权限规则将不会生效——因为插件规则已经匹配,直接放行。
实现现状:accessRestrictions
在最终实现中,BEP 的scope概念被落地为accessRestrictions字段(数组形式,见 docs/auth/service-to-service-auth.md 与 helpers.ts)。每个externalAccess条目可携带可选的accessRestrictions,数组中的每项包含:
plugin(必填):插件 ID 字符串,例如'catalog'。允许访问该插件;可用下面的字段进一步细化;permission(可选):权限名称集合(逗号/空格分隔的字符串或字符串数组)。给定后,该访问方法在对应插件中仅能执行这些具名权限;permissionAttribute(可选):权限属性键值对象,每个值同样是集合。常用于限制action属性,取值限定为'create'、'read'、'update'、'delete'(readAccessRestrictionsFromConfig会校验非法值,见 helpers.ts)。
注意:
permission与permissionAttribute仅对启用了权限系统检查的端点生效;未受权限系统保护的端点不受这些设置影响。
完整示例:
backend: auth: externalAccess: - type: static options: token: ${CICD_TOKEN} subject: cicd-system-completion-events accessRestrictions: - plugin: events - type: static options: token: ${ADMIN_CURL_TOKEN} subject: admin-curl-access上例中,使用CICD_TOKEN的调用者只能访问events后端插件,访问其他插件会被拒绝;而ADMIN_CURL_TOKEN未加限制,拥有全部插件、全部功能的无限制访问权——官方文档建议尽可能显式声明访问限制以降低风险。
helpers.ts中的解析逻辑还包含若干防御性校验:accessRestrictions中只允许plugin、permission、permissionAttribute三个键;同一个插件 ID 不允许声明两次;permissionAttribute下只允许action键。任何违规都会在启动阶段抛出配置错误,而非运行期才暴露。
源码级实现原理
令牌验证入口:ExternalAuthTokenHandler
外部令牌的验证完全落在coreServices.auth服务实现中,核心是 ExternalAuthTokenHandler.ts:
- 配置读取:
ExternalAuthTokenHandler.create通过config.getOptionalConfigArray('backend.auth.externalAccess')读取新配置,用config.getOptionalConfigArray('backend.auth.keys')读取旧配置; - 类型注册:默认处理器表
defaultHandlers内置static、legacy、jwks三种(defaultHandlers,见同文件 ExternalAuthTokenHandler.ts);未知的type会在启动时直接抛错并列出合法取值; - 旧配置兼容:一旦检测到
backend.auth.keys存在,就会输出DEPRECATION WARNING日志,提示该配置已被backend.auth.externalAccess取代,并将旧密钥按legacy处理器逐一并入上下文; - 验证流程:
verifyToken(token)依次尝试所有上下文(context),每个上下文由一个type处理器与其对应的allAccessRestrictions组成;首个成功返回结果的处理器即命中;若存在访问限制且当前插件 ID 不在允许列表中,则抛出NotAllowedError(403 语义):This token's access is restricted to plugin(s) ...。
可扩展的处理器接口
处理器遵循统一接口(见 types.ts):
export interface ExternalTokenHandler<TContext> { type: string; initialize(ctx: { options: Config }): TContext; verifyToken( token: string, ctx: TContext, ): Promise<{ subject: string } | undefined>; }同时框架提供了createExternalTokenHandler辅助函数(见 helpers.ts),以及externalTokenHandlersServiceRef服务引用(multiton 类型),允许开发者注册自定义外部令牌处理器——这正是 BEP 中"未来可能把访问类型做成可扩展"的方向:通过在 ExternalAuthTokenHandler.ts 中声明的core.auth.externalTokenHandlers服务引用注入自定义 handler,即可支持新的type(各 handler 的type必须唯一,重复会抛错)。
访问限制如何生效
- 配置中的
accessRestrictions会被解析为AccessRestrictionsMap(Map<pluginId, BackstagePrincipalAccessRestrictions>); - 验证通过后,若存在限制映射,会按当前请求目标插件 ID 取出对应的限制条件,随验证结果一并返回;
- 服务主体(service principal)类型上会带有可选的访问限制字段(自配置携带而来),
ServerPermissionClient据此与允许的操作列表比对;同时 auth 服务可基于插件 ID 规则做早期拒绝。
整体而言,外部令牌验证的职责全部收敛在coreServices.auth的authenticate方法内部(无需新增 API),返回的是带 service principal 的常规 credentials——接收方插件无需任何改动即可正常处理外部调用者的身份。
实战:外部调用方如何使用
引入新的外部调用者及其专属密钥,需要更新 app-config 文件并重启后端——该机制面向选定服务的集成,因此暂不提供运行期动态添加调用者的能力。
配置完成后,外部调用方通过Authorization: Bearer请求头发送令牌即可:
# 使用 static 令牌 curl -H "Authorization: Bearer ${SERVICE_API_TOKEN}" \ https://backstage.example.com/api/catalog/entities # 使用 legacy 共享密钥签名的 JWT curl -H "Authorization: Bearer eyJhbGciOiJIUzI..." \ https://backstage.example.com/api/events典型使用场景包括:
- 需要向用户发送通知的外部异步服务(配合 notifications 插件);
- 与软件目录(catalog)交互的外部集成服务;
- 本地开发中临时可
curl的令牌——用static类型配置一个带freben-local-dev-前缀的短期令牌即可。
发布计划与演进
legacy与static类型的初始试点实现及对应配置项已合并进当前仓库;- 访问限制(scope/accessRestrictions)的校验能力仍在持续完善,但由于默认作用域为"全部",后续增量添加限制不会造成破坏性变更;
- 新增更多访问类型(如
jwks已在实现中落地)未来可能需要框架层面的服务扩展点(service extension points),目前尚未作为优先级。
备选方案对比
BEP-0007 在设计过程中评估了以下备选方案,各自的取舍如下:
| 方案 | 思路 | 权衡 |
|---|---|---|
| 数据库中动态持久化共享密钥 | 密钥存库,增删无需改app-config.yaml、无需重启应用 | 灵活性强,但复杂度高,且未必需要这种弹性 |
| 维持现状 | 调用方自行组装适用于现有实现的 JWT | 外部调用方类型与运行环境差异大,组装难度高 |
| 按插件逐个做访问控制 | 保留各插件独立的访问控制(参考 PR #23441) | 用例高度重复,建立通用机制是更优选择 |
| 共享令牌申请器(shared token requester) | 引入统一令牌申请服务(参考 PR #23465) | 可简化令牌管理与提升可访问性,但需深入考虑其对现有系统的集成影响 |
总结
BEP-0007 为 Backstage 的外部服务认证建立了一套配置驱动、默认安全、可精细限权的机制:以backend.auth.externalAccess数组取代旧的backend.auth.keys,通过type区分static/legacy/jwks三种(且未来可扩展)访问方式,配合accessRestrictions实现按插件、按权限、按权限属性的精细化访问控制。其实现完全内聚于coreServices.auth服务(见 packages/backend-defaults/src/entrypoints/auth),对接收方插件透明无侵入。在实际部署中,官方建议即便配置了外部访问,也应尽量将 Backstage 实例屏蔽在公网之外,仅在确有需要时开放访问,并将外部令牌的访问限制声明到最小必要范围。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考