Backstage Kubernetes 后端代理端点(Proxy Endpoint)完全指南:从 REST API 透传到权限管控
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
导读
Backstage 的 Kubernetes 插件默认提供了 Pod、Service、Deployment 等资源的展示能力,但当你需要基于 Kubernetes 数据构建更丰富的开发者门户体验时(例如读取并操作 Custom Resources、调用任意 Kubernetes REST API),就需要借助 Kubernetes 后端插件的Proxy 端点。本文将基于docs/features/kubernetes/proxy.md文档并结合仓库源码,系统讲解代理端点的工作原理、认证机制、如何通过权限框架禁用该端点,以及已知的局限性与规避思路。读完本文,你将掌握如何在前端插件中通过KubernetesBackendClient向目标集群发起任意 REST 请求,并能用 PermissionPolicy 精确管控该端点的访问。
一、Proxy 端点是什么:为插件提供任意 Kubernetes API 访问
Backstage 的 Kubernetes 插件(plugins/kubernetes)面向 Catalog 实体提供了开箱即用的资源浏览能力。但贡献者(Contributors)如果希望基于 Kubernetes 数据创建自定义的开发者门户体验——典型场景是与默认 Kubernetes 插件行为之外的 Custom Resources 进行交互——就可以利用 Kubernetes 后端插件的 Proxy 端点,向 Kubernetes REST API 发起任意请求。
简单来说,Proxy 端点充当了一个"中转站":前端插件把请求发给 Backstage 后端,后端根据请求头解析出目标集群,再把请求转发到真实的 Kubernetes API Server,并把响应原样返回给前端。这样做的核心价值在于:
- 前端无需直接暴露或持有集群凭据,所有认证逻辑收敛在 Backstage 后端;
- 插件开发者可以访问 Kubernetes 的完整 REST API 表面(包括自定义资源),而不局限于插件内置的
kubernetes资源列表; - 后端可以在转发前统一执行权限校验(见第四节)。
最小可运行示例:获取命名空间列表
文档给出了一段使用KubernetesBackendClient获取命名空间的示例,这是使用 Proxy 端点最直接的入口:
import { useApi } from '@backstage/core-plugin-api'; import { kubernetesApiRef } from '@backstage/plugin-kubernetes'; const CLUSTER_NAME = ''; // use a known cluster name const kubernetesApi = useApi(kubernetesApiRef); await kubernetesApi.proxy(CLUSTER_NAME, '/api/v1/namespaces');其中:
CLUSTER_NAME需要替换为你在集群配置中定义的实际集群name字段(关于集群命名见 configuration.md);- 第二个参数
/api/v1/namespaces是 Kubernetes REST API 的路径,会被拼接到 Proxy 端点之后转发给目标集群。
前端客户端的底层实现
kubernetesApiRef对应的实现是KubernetesBackendClient,其proxy方法在 KubernetesBackendClient.ts 中定义。从源码可以看到它做了三件事:
- 根据集群名解析出该集群的
authProvider与oidcTokenProvider; - 通过
getCredentials获取该认证提供方的凭据; - 把请求发往
${discoveryApi.getBaseUrl('kubernetes')}/proxy${options.path},并附上两个关键请求头:
return { ...options.init?.headers, [`Backstage-Kubernetes-Cluster`]: options.clusterName, ...(k8sToken && { [kubernetesAuthHeader]: k8sToken, }), };注意这里的认证头并非固定名:getKubernetesAuthHeaderByAuthProvider会根据authProvider动态拼接,例如authProvider为google时生成Backstage-Kubernetes-Authorization-google,若还配置了oidcTokenProvider则进一步拼为Backstage-Kubernetes-Authorization-google-<provider>。这个细节说明:Proxy 端点在请求头层面就支持多认证提供方的区分。
二、工作原理:从请求头到集群转发的完整链路
2.1 集群解析:Backstage-Kubernetes-Cluster头
Proxy 会把请求中的Backstage-Kubernetes-Cluster请求头解释为目标集群的名称。这个名称会被逐一与所有已配置的 cluster locators 返回的集群进行比对——第一个name字段与请求头值匹配的集群即为转发目标。
在后端实现中,这个逻辑位于 KubernetesProxy.ts 的getClusterForRequest方法:
const clusterName = req.headers[HEADER_KUBERNETES_CLUSTER.toLowerCase()]; const clusters = await this.clusterSupplier.getClusters({ ... }); if (hasClusterNameHeader) { cluster = clusters.find(c => c.name === clusterName); } else if (clusters.length === 1) { cluster = clusters.at(0); } if (!cluster) { throw new NotFoundError(`Cluster '${clusterName}' not found`); }从源码还可以看到两个值得注意的行为:
- 当没有提供集群头时,如果 locator 恰好只返回一个集群,则默认使用该唯一集群(
clusters.at(0)); - 没有任何已配置集群时,直接抛出
NotFoundError(No Clusters configured); - 请求头的常量定义在 KubernetesProxy.ts 中:
HEADER_KUBERNETES_CLUSTER = 'Backstage-Kubernetes-Cluster'。
2.2 请求转发:仅有的两处修改
集群确定后,请求会被转发到目标集群。整体上,代理对每个请求只做两处修改:
- 剥离端点的基础 URL 前缀:即把
/proxy前缀从请求路径中移除,剩下的路径(如/api/v1/namespaces)才是转发到 API Server 的路径。实现上,prepareProxyTarget使用正则把req.baseUrl替换为集群的url.pathname,见 KubernetesProxy.ts; - 认证头改写:请求中的
Backstage-Kubernetes-Authorization头会变成转发请求时使用的Authorization头(详见第三节)。
2.3 中间件与 WebSocket 支持
从实现细节看,转发基于http-proxy-middleware的createProxyMiddleware完成(KubernetesProxy.ts),并且有几个值得插件作者了解的工程细节:
- 每个集群一个中间件实例:代理为每个远端集群创建并缓存一个中间件,因为
secure(是否跳过 TLS 校验)无法在单个实例上按请求动态决定;条目会在 TTL 之后或集群详情变化时刷新; - 支持 WebSocket:
dispatchToProxy会检测Connection: upgrade与Upgrade: websocket头,走middleware.upgrade路径,这意味着 Proxy 端点同样可用于kubectl式的 WebSocket 交互场景; - 中间件缓存可配置:在 KubernetesRouter.ts 中,代理会读取
kubernetes.proxy.middlewareCache配置(ttl.milliseconds与maxSize)来控制缓存行为; - 审计事件:代理通过
ProxyAuditSession记录每次代理请求的审计事件(包含集群名、方法、路径等),失败时会把错误序列化进响应体,并在开发环境下附带堆栈(NODE_ENV === 'development')。
三、认证机制:Bearer Token 与不支持的 mTLS
3.1 当前实现期望什么
文档明确指出:Proxy 没有任何 mTLS 支持,因此它不能用于连接使用 x509 Client Certs 认证策略的集群。当前的/proxy实现期望调用方通过Backstage-Kubernetes-Authorization头提供一个Bearer token,这个 token 在转发请求时会被用作Authorization头的值。
这一逻辑对应 KubernetesProxy.ts 中的处理:
const authHeader = req.headers[HEADER_KUBERNETES_AUTH.toLocaleLowerCase('en-US')]; if (typeof authHeader === 'string') { req.headers.authorization = authHeader; } else { // 通过 authStrategy 获取凭据 const credential = await this.authStrategy.getCredential(cluster, authObj); if (credential.type === 'bearer token') { req.headers.authorization = `Bearer ${credential.token}`; } else if (credential.type === 'x509 client certificate') { target.key = credential.key; target.cert = credential.cert; } }有趣的是,虽然文档说明 x509 客户端证书不被代理支持,但源码中AuthenticationStrategy的凭据类型定义(types.ts)仍然包含x509 client certificate分支。从源码结构看,这是为AuthenticationStrategy接口预留的能力,而 Proxy 的对外契约依然是Bearer token——因此在文档语境下,连接 mTLS/x509 集群的请求不应走/proxy端点。
3.2 默认认证装饰:KubernetesAuthTranslator 的作用
文档进一步说明:Proxy 期望提供一个KubernetesAuthTranslator,用于默认给所有请求装饰认证信息。它的做法是:根据clusterDetails中定义的authProvider,向clusterDetails填充一个serviceAccountToken字段。
对应到源码:
AuthenticationStrategy接口(types.ts)定义了getCredential、validateCluster、presentAuthMetadata三个方法;- 具体的策略实现在
plugins/kubernetes-backend/src/auth/目录下,例如ServiceAccountStrategy会读取clusterDetails.authMetadata.serviceAccountToken(ServiceAccountStrategy.ts); - 如果请求头中没有提供认证信息,
prepareProxyTarget会调用authStrategy.getCredential(cluster, authObj)获取凭据并注入Authorization头。这也与文档"The proxy expects a KubernetesAuthTranslator to be provided that is used to decorate all requests with Auth by default"的表述一致。
换句话说,Proxy 的认证策略是"调用方显式提供 token 优先,否则回退到集群配置中的默认认证方式"。对应的测试用例在 KubernetesProxy.test.ts 中均有覆盖,例如"应在未提供backstage-kubernetes-auth字段时默认使用策略提供的 bearer token 作为授权头"、"应把Backstage-Kubernetes-Auth字段追加到请求的授权头"等(见 L932、L1034 附近)。
四、通过 PermissionPolicy 禁用 Proxy 端点
Kubernetes 插件可以借助权限框架禁用proxy端点的使用。这种集成允许管理员使用明确定义的 PermissionPolicy 来整体限制该端点的访问。关键点在于:即使请求携带了集群会授权的有效 ID token,proxy端点也可以返回 403 错误——这给了集成方信心:Backstage 不会在未授权的场景下替不受欢迎的一方访问 Kubernetes 集群。
4.1 前置条件
该功能假设你的 Backstage 实例已经启用了 权限框架。权限框架是 Backstage 提供的细粒度访问控制体系,PermissionPolicy在其中负责裁决每个权限请求。
4.2 权限点的定义
Proxy 端点对应的权限点在 permissions.ts 中定义:
export const kubernetesProxyPermission = createPermission({ name: 'kubernetes.proxy', attributes: {}, });它与另外两个权限一起被导出:
kubernetes.resources.read(/resources与/services/:serviceId端点)kubernetes.clusters.read(/clusters端点)
4.3 示例策略:拒绝一切 proxy 请求
文档给出了一个可直接落地的示例策略,在handle中匹配kubernetes.proxy权限名并返回DENY:
import { AuthorizeResult, PolicyDecision, } from '@backstage/plugin-permission-common'; import { PermissionPolicy, PolicyQuery, PolicyQueryUser, } from '@backstage/plugin-permission-node'; class KubernetesDenyAllProxyEndpointPolicy implements PermissionPolicy { async handle( request: PolicyQuery, user?: PolicyQueryUser, ): Promise<PolicyDecision> { if (request.permission.name === 'kubernetes.proxy') { return { result: AuthorizeResult.DENY, }; } return { result: AuthorizeResult.ALLOW }; } }4.4 后端如何强制执行
从后端路由源码(KubernetesRouter.ts)可以看到,/proxy路由挂载时就把permissionApi传给了代理:
router.use('/proxy', proxy.createRequestHandler({ permissionApi }));在 KubernetesProxy.ts 的authorizeAndDispatch中,代理会在解析集群、转发请求之前先执行权限校验:
const authorizeResponse = await permissionApi.authorize( [{ permission: kubernetesProxyPermission }], { credentials: await this.httpAuth.credentials(req) }, ); if (authorizeResponse[0].result === AuthorizeResult.DENY) { ... res.status(403).json({ error: serializeError(new NotAllowedError('Unauthorized')), }); return false; }因此,即使请求附带了集群会授权的有效 ID token,只要权限策略拒绝,代理也会直接返回 403。文档明确给出了被拒绝时的响应体示例:
{ "error": { "name": "NotAllowedError" } }这与源码中serializeError(new NotAllowedError('Unauthorized'))的序列化结果一致(NotAllowedError来自@backstage/errors)。
4.5 为什么先鉴权再转发很重要
从调用顺序看,权限校验发生在prepareProxyTarget(集群解析与凭据注入)之前。这意味着被拒绝的请求根本不会触发集群 locator 查询与认证凭据的获取,既避免了不必要的后端开销,也杜绝了"凭据先被解析、后被发现无权访问"的潜在信息泄露窗口。这也正是文档所说的"allowing integrators the confidence that Backstage is not accessing kubernetes clusters on behalf of undesired parties"。
五、其他已知限制
文档同时披露了 Proxy 端点的一个已知缺陷。该代理随 Backstage 1.9 发布,存在以下已知 bug:
- 无法可靠地定位与其他已定位集群共享相同名称的集群(对应上游 issue #15901)。
换句话说,如果两个不同集群配置了相同的name字段,getClusterForRequest中clusters.find(c => c.name === clusterName)只会命中第一个匹配项,代理可能把请求转发到错误的集群。规避建议:在生产环境中为每个集群配置全局唯一的name(参见 configuration.md 中clusters.*.name的说明),避免同名冲突。
六、实战小结与最佳实践
基于本文的文档说明与源码分析,使用 Proxy 端点时的最佳实践可以归纳为:
- 确认目标集群已配置:通过 cluster locator(
config、gke、eks等)注册集群,并保证每个集群的name全局唯一; - 在前端使用
kubernetesApiRef:优先复用KubernetesBackendClient.proxy(),它会自动处理集群解析与认证头拼接,无需手写请求头; - 认证选型:Proxy 端点面向 Bearer token 场景,mTLS/x509 客户端证书不在支持范围内,这类集群应走其他认证通道;
- 权限管控:如需收紧访问,使用
PermissionPolicy对kubernetes.proxy权限进行 DENY/ALLOW 裁决——即使集群侧会放行,Backstage 侧的 403 依然生效; - 关注已知 bug:避免同名集群配置,规避 issue #15901 的误转发风险;
- 善用审计能力:代理每次转发都会产生审计事件(包含解析后的目标集群名),可结合 audit-events.md 对代理流量进行追踪与合规审计。
参考资源
- 文档原文:docs/features/kubernetes/proxy.md
- 后端代理实现:plugins/kubernetes-backend/src/service/KubernetesProxy.ts
- 后端路由挂载:plugins/kubernetes-backend/src/service/KubernetesRouter.ts
- 前端客户端实现:plugins/kubernetes-react/src/api/KubernetesBackendClient.ts
- 权限点定义:plugins/kubernetes-common/src/permissions.ts
- 认证策略接口:plugins/kubernetes-node/src/types/types.ts
- 集群配置说明:docs/features/kubernetes/configuration.md
- 相关测试:plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts、plugins/kubernetes-backend/src/service/KubernetesRouter.test.ts
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考