Envoy Injected Credentials 深入指南:向代理请求注入 Basic Auth、Bearer Token 与 OAuth2 访问令牌
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
导读
Envoy 的 Injected Credentials(注入凭证)扩展体系为代理转发路径提供了一套标准化的"凭据注入"能力:它允许你通过 HTTP 过滤器,把从 SDS(Secret Discovery Service)读取的静态凭证(如 HTTP Basic Auth、Bearer Token 或任意自定义令牌),或从授权服务器动态获取的 OAuth2 access token,自动注入到发往上游的 HTTP 请求头中。本文以 docs/root/api-v3/config/injected_credentials/injected_credentials.rst 为索引主线,结合api/envoy/extensions/http/injected_credentials/下的 proto 定义与source/extensions/http/injected_credentials/下的 C++ 实现,完整讲解 Generic 与 OAuth2 两类凭证注入器的配置字段、工作流程、错误处理与统计指标,并给出可直接落地的配置示例。读完本文,你将掌握如何把 Envoy 侧车(sidecar)配置成"自动携带工作负载身份凭证"的代理,而无需修改业务应用的请求代码。
一、Injected Credentials 是什么:定位与适用场景
Injected Credentials 并不是一个独立的过滤器,而是一组可被其他 HTTP 过滤器按需装配的凭证注入扩展,注册在envoy.http.injected_credentials扩展类别下。当前仓库中该类别包含两个实现:
envoy.http.injected_credentials.generic:注入任意静态凭证(Basic Auth、Bearer Token 等),定义见 generic.proto;envoy.http.injected_credentials.oauth2:通过 OAuth2 Client Credentials Grant 流程动态获取 access token 并注入,定义见 oauth2.proto。
两者的 proto 均标记为package_version_status = ACTIVE(其中 oauth2.proto 在 xds 状态注解中额外标有work_in_progress = true,从 oauth2.proto 可见),说明该能力已进入正式 API 版本管理。
承载注入动作的是 HTTP 过滤器envoy.filters.http.credential_injector,其配置定义在 credential_injector.proto 中。proto 文档明确了两点重要定位:
- 面向工作负载认证(workload authentication):被注入凭证所代表的身份,被视为 Envoy 代理后面那个工作负载的身份(典型部署形态是 Envoy 作为 sidecar 与业务进程同舱运行);
- 不处理终端用户认证(end user authentication):该过滤器的唯一目的是认证工作负载本身,而不是登录态的最终用户。
相关文档索引:作为 API 导航页,injected_credentials.rst 通过
toctree通配符../../extensions/http/injected_credentials/*/v3/*将两类凭证的 proto 文档聚合到同一目录树,即上文两个 proto 文件对应的 API 参考文档。
二、整体架构与调用链
从源码结构看,凭证注入能力由三层构成:
HTTP Filter(credential_injector) │ 在 decodeHeaders 阶段调用 ▼ CredentialInjector 抽象接口(Common::CredentialInjector) │ 按 TypedExtensionConfig 选择实现 ├── GenericCredentialInjector(generic) └── OAuth2ClientCredentialTokenInjector(oauth2) └── TokenProvider(异步拉取/缓存 token)- 抽象接口:
source/extensions/http/injected_credentials/common/credential.h定义了CredentialInjector::inject(RequestHeaderMap& headers, bool overwrite),返回absl::Status表示注入是否成功; - 凭证读取:
source/extensions/http/injected_credentials/common/secret_reader.h中的SDSSecretReader基于ThreadLocalGenericSecretProvider从 SDS 读取 Generic Secret,并把凭证缓存在线程本地槽位中; - 过滤器入口:
source/extensions/filters/http/credential_injector/credential_injector_filter.cc在decodeHeaders阶段调用注入器;注入失败时通过sendLocalReply返回401 Unauthorized(响应体代码为failed_to_inject_credential),否则返回Continue继续转发。
工厂类方面,generic 与 oauth2 的config.cc均实现了NamedCredentialInjectorConfigFactory,并在REGISTER_FACTORY中静态注册(见 generic/config.cc、oauth2/config.cc),因此可以在任意TypedExtensionConfig中通过@type直接引用。
三、Generic 凭证注入:任意凭证的头注入
3.1 配置字段
generic.proto 中Generic消息仅含三个字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
credential | SdsSecretConfig | 是 | 要注入的凭证,必须是 Generic Secret(generic_secret类型),可走 SDS 动态下发,也可引用静态 secret |
header | string | 否 | 注入的目标请求头名,留空时默认Authorization,取值须符合 HTTP header name 校验规则(HTTP_HEADER_NAME,允许为空) |
header_value_prefix | string | 否 | 注入前的值前缀,用于拼上 scheme(如Bearer、Basic);不设置则注入原始凭证值 |
header_value_prefix的语义在 proto 注释中有明确示例:若凭证值为xyz123、前缀为Bearer,最终头值为Bearer xyz123。
3.2 配置示例(Basic Auth / Bearer Token)
credential_injector.proto 内嵌了完整可运行的示例。过滤器配置:
overwrite: true credential: name: generic_credential typed_config: "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic credential: name: credential sds_config: path_config_source: path: credential.yaml header: Authorization配套的 SDS 文件credential.yaml(Basic Auth 场景):
resources: - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" name: credential generic_secret: secret: inline_string: "Basic base64EncodedUsernamePassword"Bearer Token 场景只需把inline_string换成"Bearer myToken"即可(此时无需配置header_value_prefix,因为前缀已写在凭证值里)。
注意:
header_value_prefix与"把 scheme 写进凭证值"是两种等价做法。前者让凭证与 scheme 分离管理(凭证文件只存令牌本身),更利于凭证轮换时不动 scheme。
3.3 底层实现要点
从 generic_impl.cc 可以看到GenericCredentialInjector::inject的三个关键行为:
- 覆盖策略:
overwrite=false且目标头已存在时,返回AlreadyExistsError,不覆盖已有值(由过滤器层决定放行还是拒绝); - 尾随换行剥离:凭证文件末尾常见的
\n/\r不是合法的 HTTP 头值字符,实现会循环剥离尾部 CR/LF 后再注入; - 空凭证兜底:剥离后凭证为空(SDS 尚未就绪或 secret 缺失)时返回
NotFoundError,由过滤器按配置决定是否放行。
在 generic/config.cc 中,工厂根据SdsSecretConfig是否携带sds_config走两条路径:有则findOrCreateGenericSecretProvider(SDS 动态下发),无则findStaticGenericSecretProvider(静态 secret);header为空时在工厂内兜底为Authorization。
四、OAuth2 凭证注入:动态获取并注入 Access Token
4.1 支持的流程与认证方式
oauth2.proto 中的OAuth2消息目前仅支持 Client Credentials Grant(flow_typeoneof 中只有client_credentials分支,且为validate.required),即授权服务器直接为客户端签发令牌、不涉及用户授权码的机器对机器场景。令牌以 Bearer 形式注入Authorization头。
AuthType枚举控制客户端向授权服务器出示client_id/client_secret的方式:
BASIC_AUTH = 0:使用 HTTP Basic 认证 scheme 发送(默认推荐);URL_ENCODED_BODY = 1:将凭据放在 URL 编码的请求体中发送,仅当授权服务器不支持 Basic 认证时才应使用。
4.2 配置字段总览
| 字段 | 类型 | 必填/默认 | 说明 |
|---|---|---|---|
token_endpoint | HttpUri | 是 | 授权服务器上获取 access token 的端点;HttpUri内含cluster(走哪个上游集群)与timeout(请求超时),见 oauth_client.cc 的setTimeout用法 |
scopes | repeated string | 否 | 请求的 OAuth scope;不填时按默认空 scope 处理(见下) |
client_credentials.client_id | string | 是(min_len 1) | Client ID |
client_credentials.client_secret | SdsSecretConfig | 是 | Client Secret,同样必须是 Generic Secret |
client_credentials.auth_type | AuthType | 否(默认 BASIC_AUTH) | 凭据发送方式 |
token_fetch_retry_interval | Duration | 否(默认 2s) | 两次 token 拉取失败重试的间隔,校验规则为不小于 1 秒(gte {seconds: 1}) |
endpoint_params | repeated EndpointParameter | 否 | 附加到 token 请求体中的自定义参数(URL 编码后追加) |
关于scopes的默认行为,token_provider.cc 中的oauthScopesList显示:scopes 为空时也会注入一个默认空 scope 字符串(DEFAULT_AUTH_SCOPE = ""),最终以空格连接后放入请求体。
4.3 Token 请求与响应处理
oauth_client.cc 展示了 token 请求的完整构造过程:
- 请求体格式为
grant_type=client_credentials&client_id={0}&client_secret={1},有 scope 时追加&scope={2}(L25-L28); client_id、client_secret、scope及自定义endpoint_params均通过PercentEncoding::encode(..., ":/=&?")做 URL 编码,防止特殊字符破坏请求体;- 请求通过
clusterManager().getThreadLocalCluster(uri_.cluster())定位集群并用httpAsyncClient().send(...)异步发送; - 成功回调
onSuccess要求响应码必须为200,响应体必须是包含access_token与expires_in的 JSON(如{"access_token":"...","expires_in":3600}),解析失败或字段缺失一律按失败处理; - 若集群不存在或请求流被重置,分别对应
NotDispatchedClusterNotFound与StreamReset失败路径。
4.4 Token 缓存、过期与重试机制
token_provider.cc 与 token_provider.h 共同实现了完整的令牌生命周期管理:
- 启动即拉取:
TokenProvider构造时立即调用asyncGetAccessToken(); - 线程本地缓存:成功获取的 token 以
"Bearer " + access_token形式写入ThreadLocalOauth2ClientCredentialsToken,所有工作线程通过 TLS 槽位读取,避免每次请求都触发网络调用; - 定时刷新:
onGetAccessTokenSuccess记录token_expiry_time_,并在expires_in / 2时间后通过dispatcher_->createTimer触发预刷新,确保 token 在过期前就被替换; - 失败重试:
onGetAccessTokenFailure按失败原因分桶统计;BadToken(响应解析失败)不重试,其余原因按token_fetch_retry_interval(默认 2s)重试;token 已过期时还会清空缓存的过期 token,避免上游收到陈旧凭证; - 空 secret 保护:
asyncGetAccessToken开头检测到 client secret 为空(SDS 未就绪)时不发请求,直接等待重试间隔。
对应的统计指标(见 token_provider.h):token_requested、token_fetched、token_fetch_failed_on_client_secret、token_fetch_failed_on_cluster_not_found、token_fetch_failed_on_stream_reset、token_fetch_failed_on_bad_token、token_fetch_failed_on_bad_response_code,可用于监控令牌拉取的成败与原因分布。
4.5 注入行为
client_credentials_impl.cc 中的OAuth2ClientCredentialTokenInjector::inject逻辑与 Generic 类似:overwrite=false且Authorization已存在时返回AlreadyExistsError;token 为空(尚未获取成功或已过期被清空)时返回NotFoundError;成功则以setReferenceKey写入Authorization头(值为缓存好的Bearer <token>)。
五、CredentialInjector HTTP 过滤器:装配与行为控制
5.1 过滤器配置字段
credential_injector.proto 中CredentialInjector消息:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
overwrite | bool | false | 目标头已存在时是否覆盖。为false且头已存在时,注入器返回AlreadyExists,过滤器统计already_exists并继续转发(不覆盖原值) |
allow_request_without_credential | bool | false | 凭证缺失或注入失败时是否仍把请求发给上游。默认情况下返回401 Unauthorized;为true时不带凭证直接放行 |
credential | TypedExtensionConfig | 必填 | 凭证注入器本体,@type指向envoy.http.injected_credentials.generic或envoy.http.injected_credentials.oauth2之一 |
5.2 过滤器行为细节
credential_injector_filter.cc 的实现要点:
- 过滤器继承
PassThroughDecoderFilter,只重写decodeHeaders,对请求体与响应路径零侵入; FilterConfig::injectCredential按absl::Status分类处理:AlreadyExists→ 记already_exists并放行;其他失败 → 记failed并按allow_request_without_credential决定放行与否;成功 → 记injected;- 失败且不允许无凭证转发时,
sendLocalReply(401 Unauthorized, "Failed to inject credential.", ..., "failed_to_inject_credential")并StopIteration,请求被本地拒绝; - 过滤器级统计指标共三个:
injected、failed、already_exists(定义见 credential_injector_filter.h)。
5.3 完整装配示例(Basic Auth 场景)
http_filters: - name: envoy.filters.http.credential_injector typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.credential_injector.v3.CredentialInjector overwrite: true allow_request_without_credential: false credential: name: generic_credential typed_config: "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.generic.v3.Generic credential: name: credential sds_config: path_config_source: path: /etc/envoy/credential.yaml header: Authorization在过滤器链中,应把credential_injector放在需要携带凭证的转发路径之前(如router过滤器之前),并在 HTTP 连接管理器中引用该过滤器链。
六、OAuth2 场景的完整配置示例
结合 oauth2.proto 的字段,一个使用 Client Credentials Grant 的典型配置如下:
http_filters: - name: envoy.filters.http.credential_injector typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.credential_injector.v3.CredentialInjector overwrite: true allow_request_without_credential: false credential: name: oauth2_credential typed_config: "@type": type.googleapis.com/envoy.extensions.http.injected_credentials.oauth2.v3.OAuth2 token_endpoint: cluster: auth_server_cluster uri: https://auth.example.com/oauth2/token timeout: 3s scopes: - "read:data" - "write:data" client_credentials: client_id: my-workload client_secret: name: oauth2_client_secret sds_config: path_config_source: path: /etc/envoy/client_secret.yaml auth_type: BASIC_AUTH token_fetch_retry_interval: 2s endpoint_params: - name: audience value: internal-api配套的client_secret.yaml(Generic Secret 形式):
resources: - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" name: oauth2_client_secret generic_secret: secret: inline_string: "s3cr3t-value"该配置下,Envoy 会在启动时立即向auth_server_cluster对应的授权服务器发起 Client Credentials Grant 请求,成功后把Bearer <access_token>缓存在线程本地,并在每个请求的Authorization头中注入;token 过期前一半时间自动预刷新,拉取失败则按 2 秒间隔重试(BadToken类解析错误除外)。
七、测试与验证
仓库为两个注入器提供了完整的单元测试与集成测试,可作为行为契约参考:
- Generic 注入器集成测试:credential_injector_integration_test.cc、credential_injector_upstream_integration_test.cc;
- OAuth2 相关测试:config_test.cc(配置解析与工厂装配)、token_provider_test.cc(令牌拉取/缓存/重试逻辑)、credential_injector_oauth_integration_test.cc(端到端令牌注入);
- 过滤器行为测试:filter_test.cc 覆盖了 overwrite、401 拒绝、无凭证放行等分支。
这些测试同时印证了本文所述的关键行为:凭证已存在且不覆盖时放行、注入失败返回 401、SDS 未就绪时进入重试等待等。
八、注意事项与使用限制
- 仅 Client Credentials Grant:OAuth2 注入器当前不支持授权码、隐式等流程,只面向机器身份(service-to-service)场景;
- 工作负载身份而非用户身份:被注入凭证标识的是代理背后的工作负载,不要把该过滤器当作终端用户认证方案(credential_injector.proto 中有明确声明);
- 凭证必须是 Generic Secret:
SdsSecretConfig引用的 secret 类型必须是generic_secret,且建议优先走 SDS 动态下发以支持无重启轮换;静态 secret 仅按名称引用; - 凭证值合法性:从文件读取的凭证末尾换行会被自动剥离,但凭证值本身不应包含其他非法头字符(换行等);空凭证会被视为注入失败;
- 401 语义:默认情况下,SDS 未就绪或令牌获取失败都会导致请求直接以 401 失败,请结合
allow_request_without_credential谨慎决定生产环境的降级策略,并利用injected/failed/token_fetch_failed_*等指标监控凭证链路的健康度; - 超时与集群依赖:OAuth2 的
token_endpoint.cluster必须指向可用的上游集群,HttpUri.timeout决定 token 请求超时;令牌刷新完全在 Envoy 内部异步进行,不影响业务请求的数据面性能。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考