最近Higress社区上提出了一个issue#2154,因为当前实现的 provider 只支持 ak/sk 这种鉴权方式,严格来说不是很完备,有些情况下用户更希望通过 role 鉴权方式(无 AK ),是否可以支持 litellm 这样支持更灵活的鉴权方式.以下是自己的一些调研报告
Litellm的无role鉴权架构
AWS Bedrock Role鉴权时序图
sequenceDiagram
participant Client as 客户端应用
participant LiteLLM as LiteLLM SDK
participant SecretMgr as 密钥管理器
participant STS as AWS STS
participant Bedrock as AWS Bedrock
Note over Client,Bedrock: Web Identity Token方式的Role鉴权
Client->>LiteLLM: completion(model="bedrock/...", aws_web_identity_token, aws_role_name, aws_session_name)
LiteLLM->>LiteLLM: 检查参数完整性
Note right of LiteLLM: 验证aws_web_identity_token、aws_role_name、aws_session_name存在
LiteLLM->>SecretMgr: get_secret(aws_web_identity_token)
SecretMgr-->>LiteLLM: 返回OIDC Token
alt OIDC Token获取失败
LiteLLM-->>Client: 抛出BedrockError(401)
end
LiteLLM->>STS: boto3.client("sts")
LiteLLM->>STS: assume_role_with_web_identity(RoleArn, RoleSessionName, WebIdentityToken)
STS-->>LiteLLM: 返回临时凭证(AccessKeyId, SecretAccessKey, SessionToken)
LiteLLM->>Bedrock: 使用临时凭证创建bedrock-runtime客户端
LiteLLM->>Bedrock: 发送API请求
Bedrock-->>LiteLLM: 返回响应
LiteLLM-->>Client: 返回标准化响应
AWS Bedrock Role鉴权实现
- Web Identity Token方式的STS AssumeRole
在AWS Bedrock的客户端初始化中,实现了基于OIDC token的角色假设机制: common_utils.py:190-223
这段代码的核心流程是:
- 检查参数完整性:验证
aws_web_identity_token、aws_role_name和aws_session_name都存在 - 获取OIDC token:通过
get_secret()从密钥管理器获取Web Identity Token - 调用STS服务:使用
assume_role_with_web_identityAPI获取临时凭证 - 创建Bedrock客户端:使用临时凭证(AccessKeyId、SecretAccessKey、SessionToken)创建bedrock-runtime客户端
- 标准AssumeRole方式
当只提供角色名称和会话名称时,使用标准的AssumeRole机制: common_utils.py:224-245
这种方式需要现有的AWS凭证来假设目标角色,适用于跨账户访问场景。
- Pass-through端点的自动签名
在Bedrock的pass-through端点中,系统自动处理AWS签名认证: llm_passthrough_endpoints.py:421-433
这里使用了AWS SigV4签名算法,自动为请求添加认证头。
Vertex AI Role鉴权时序图
sequenceDiagram
participant Client as 客户端应用
participant LiteLLM as LiteLLM SDK
participant SecretMgr as 密钥管理器
participant GCPAuth as Google Cloud Auth
participant VertexAI as Vertex AI API
Note over Client,VertexAI: Service Account方式的Role鉴权
Client->>LiteLLM: completion(model="vertex_ai/...", vertex_credentials)
LiteLLM->>LiteLLM: 凭证优先级检查
Note right of LiteLLM: 1. vertex_credentials参数<br/>2. vertex_ai_credentials参数<br/>3. VERTEXAI_CREDENTIALS环境变量
alt 使用Service Account JSON
LiteLLM->>LiteLLM: 加载JSON文件或字符串
LiteLLM->>GCPAuth: 使用Service Account凭证
else 使用环境变量
LiteLLM->>SecretMgr: get_secret("VERTEXAI_CREDENTIALS")
SecretMgr-->>LiteLLM: 返回凭证
LiteLLM->>GCPAuth: 使用环境变量凭证
end
LiteLLM->>LiteLLM: _ensure_access_token_async()
LiteLLM->>GCPAuth: 获取访问令牌
GCPAuth-->>LiteLLM: 返回Bearer Token
LiteLLM->>LiteLLM: _get_token_and_url()
Note right of LiteLLM: 构建API URL和认证头
LiteLLM->>VertexAI: 发送API请求(带Bearer Token)
VertexAI-->>LiteLLM: 返回响应
LiteLLM-->>Client: 返回标准化响应
Vertex AI Role鉴权实现
- 凭证获取的优先级机制
在主要的completion函数中,Vertex AI的凭证处理遵循优先级顺序: main.py:2393-2397
优先级顺序为:
- 函数参数中的
vertex_credentials - 函数参数中的
vertex_ai_credentials(别名) - 环境变量
VERTEXAI_CREDENTIALS - Service Account JSON文件的使用示例
文档中展示了如何使用Service Account JSON文件: vertex.md:37-51
这种方式通过加载JSON文件并转换为字符串传递给completion函数。
- 环境变量配置方式
支持通过环境变量进行配置: vertex.md:741-761
主要环境变量包括:
GOOGLE_APPLICATION_CREDENTIALS:Service Account文件路径VERTEXAI_LOCATION:Vertex AI部署区域VERTEXAI_PROJECT:项目ID(可选)
- 统一认证参数映射机制
litellm实现了统一的认证参数映射,确保不同云提供商的认证参数正确映射: test_completion.py:3959-3964
这个机制通过各自的Config类获取特殊认证参数映射
Service Account JSON格式
Service Account JSON文件是Google Cloud Platform用于身份认证的标准格式。测试代码:test_amazing_vertex_completion.py:95-101
以下是完整的Service Account JSON结构:
{
"type": "service_account",
"// 账户类型,固定值为service_account,表示这是一个服务账户凭证文件": "",
"project_id": "your-gcp-project-id",
"// Google Cloud项目ID,用于标识该服务账户所属的GCP项目": "",
"private_key_id": "a1b2c3d4e5f6...",
"// 私钥ID,用于标识特定的私钥版本,通常是一个十六进制字符串": "",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...\n-----END PRIVATE KEY-----\n",
"// RSA私钥,用于JWT签名认证,包含完整的PEM格式私钥内容": "",
"client_email": "service-account-name@your-project-id.iam.gserviceaccount.com",
"// 服务账户的邮箱地址,格式为{account-name}@{project-id}.iam.gserviceaccount.com": "",
"client_id": "123456789012345678901",
"// 客户端ID,是服务账户的唯一数字标识符": "",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"// OAuth2认证URI,用于获取授权码的端点地址": "",
"token_uri": "https://oauth2.googleapis.com/token",
"// 令牌获取URI,用于交换访问令牌的端点地址": "",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"// 认证提供商的X.509证书URL,用于验证JWT签名": "",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account-name%40your-project-id.iam.gserviceaccount.com",
"// 客户端X.509证书URL,包含该服务账户的公钥证书": "",
"universe_domain": "googleapis.com"
"// 宇宙域,指定API调用的目标域,通常为googleapis.com": ""
}
当前Higress鉴权架构分析
Higress目前已经具备了多种的鉴权能力:
- 外部认证服务集成能力
ext-auth插件提供了完整的外部认证服务集成框架 ,支持两种endpoint模式:envoy模式和forward_auth模式,可以灵活地将认证请求转发给外部认证服务处理
- 基于Consumer的角色控制
jwt-auth插件已经实现了基于consumer的细粒度角色控制机制 ,支持在路由或域名级别配置允许访问的consumer列表,这为role模式鉴权提供了基础架构。
- OIDC标准认证流程
oidc插件实现了完整的OpenID Connect认证流程 ,支持多种OIDC提供商,并具备黑白名单匹配规则。
Role模式鉴权(无ak)实现建议
方案一:基于ext-auth插件扩展
利用现有的ext-auth插件架构,实现类似AWS Bedrock的STS AssumeRole机制:
- Web Identity Token方式:在外部认证服务中实现OIDC token验证和角色映射逻辑,类似AWS的
assume_role_with_web_identity - 临时凭证生成:认证服务验证身份后,生成带有角色信息的临时token
- 请求头传递:利用ext-auth的
authorization_response.allowed_upstream_headers配置 ,将角色信息传递给后端服务
方案二:扩展jwt-auth插件
参考Vertex AI的Service Account认证模式,扩展现有jwt-auth插件:
- 角色与Consumer映射:将云服务的Service Account概念映射到higress的consumer机制
- 动态角色验证:扩展JWT验证逻辑,支持从外部身份提供商获取角色信息
- 细粒度权限控制:利用现有的
allow配置 ,实现基于角色的访问控制
方案三:新建role-auth插件
结合AWS和Vertex AI的优势,创建专门的role-auth插件:
- 多种认证源支持:支持Web Identity Token、Service Account JSON、环境变量等多种认证方式
- 角色假设流程:实现完整的角色假设和临时凭证管理机制
- 黑白名单模式:参考ext-auth的匹配规则机制 ,支持灵活的访问控制策略
实现关键点
- 认证优先级机制
参考Vertex AI的凭证获取优先级,需要支持功能如下:
- 插件配置中的角色凭证
- 请求头中的角色token
- 环境变量配置的默认角色
- 失败处理模式
借鉴ext-auth的failure_mode_allow机制 ,为role认证提供降级处理能力。
- 请求上下文传递
利用higress的context机制 ,在认证成功后将角色信息传递给后续处理流程。
实现方案(方案三)
sequenceDiagram
participant Client as "客户端"
participant Gateway as "Higress网关"
participant RoleAuth as "role-auth插件"
participant STS as "STS服务"
participant OIDC as "OIDC验证器"
participant RoleValidator as "角色验证器"
participant Upstream as "上游服务"
Note over Client,Upstream: Web Identity Token认证流程
Client->>Gateway: 发送请求 (带Web Identity Token)
Note right of Client: Headers:<br/>x-web-identity-token: <token><br/>x-role-arn: arn:aws:iam::xxx:role/xxx<br/>x-session-name: session-name
Gateway->>RoleAuth: 拦截请求进行认证
Note right of RoleAuth: onHttpRequestHeaders()
RoleAuth->>RoleAuth: 检查黑白名单规则
Note right of RoleAuth: config.MatchRules.IsAllowedByMode()
alt 请求在白名单中
RoleAuth->>Gateway: 跳过认证,继续处理
Gateway->>Upstream: 转发请求
else 需要进行认证
RoleAuth->>RoleAuth: 提取认证信息
Note right of RoleAuth: extractAuthInfo()<br/>优先级:<br/>1. Web Identity Token<br/>2. Role Token<br/>3. Authorization Bearer<br/>4. 默认角色
RoleAuth->>RoleAuth: 识别为Web Identity Token类型
Note right of RoleAuth: AuthType: WebIdentityTokenType
RoleAuth->>STS: 调用AssumeRoleWithWebIdentity
Note right of RoleAuth: handleWebIdentityToken()<br/>POST /sts with:<br/>- WebIdentityToken<br/>- RoleArn<br/>- RoleSessionName
STS->>OIDC: 验证Web Identity Token
Note right of STS: oidcVerifier.VerifyToken()
OIDC->>OIDC: 获取OIDC配置
Note right of OIDC: GET /.well-known/openid_configuration
OIDC->>OIDC: 获取JWKS公钥
Note right of OIDC: GET /jwks_uri
OIDC->>OIDC: 验证JWT签名和Claims
Note right of OIDC: 验证签名、过期时间、issuer等
alt Token验证失败
OIDC-->>STS: 返回验证失败
STS-->>RoleAuth: 返回401错误
RoleAuth->>RoleAuth: 处理认证失败
Note right of RoleAuth: handleAuthFailure()
alt failure_mode_allow=true
RoleAuth->>Gateway: 添加失败标记继续处理
Gateway->>Upstream: 转发请求 (带failure header)
else failure_mode_allow=false
RoleAuth-->>Client: 返回401未授权
end
else Token验证成功
OIDC-->>STS: 返回OIDCClaims (subject, issuer等)
STS->>RoleValidator: 验证角色权限
Note right of STS: roleValidator.ValidateRole()
RoleValidator->>RoleValidator: 检查subject是否可假设该角色
Note right of RoleValidator: 检查roleMapping[subject]
alt 角色验证失败
RoleValidator-->>STS: 返回权限不足错误
STS-->>RoleAuth: 返回403错误
RoleAuth-->>Client: 返回403禁止访问
else 角色验证成功
RoleValidator-->>STS: 验证通过
STS->>STS: 生成临时凭证
Note right of STS: generateTemporaryCredentials()<br/>生成:<br/>- AccessKeyId<br/>- SecretAccessKey<br/>- SessionToken (JWT)<br/>- Expiration
STS-->>RoleAuth: 返回临时凭证
Note left of STS: AssumeRoleResponse:<br/>- Credentials<br/>- AssumedRoleUser
RoleAuth->>RoleAuth: 设置角色信息到请求头
Note right of RoleAuth: setRoleHeaders()<br/>添加到上游请求:<br/>- x-user-role<br/>- x-access-key-id<br/>- x-session-token<br/>- x-role-expiry
RoleAuth->>Gateway: 继续请求处理
Gateway->>Upstream: 转发带角色信息的请求
Upstream-->>Gateway: 返回响应
Gateway-->>Client: 返回最终响应
end
end
end
Note over Client,Upstream: Role Token认证流程(简化)
Client->>Gateway: 发送请求 (带Role Token)
Note right of Client: Headers:<br/>x-role-token: <token>
Gateway->>RoleAuth: 拦截请求
RoleAuth->>RoleAuth: 识别为Role Token类型
RoleAuth->>RoleAuth: 调用角色Token验证服务
Note right of RoleAuth: handleRoleToken()<br/>验证token并获取角色信息
alt Token有效且角色被允许
RoleAuth->>RoleAuth: 设置角色信息到请求头
RoleAuth->>Gateway: 继续处理
Gateway->>Upstream: 转发请求
else Token无效或角色不被允许
RoleAuth-->>Client: 返回认证失败
end
Note over Client,Upstream: Service Account认证流程(简化)
Client->>Gateway: 发送请求 (带Service Account Token)
Note right of Client: Authorization: Bearer <sa-token>
Gateway->>RoleAuth: 拦截请求
RoleAuth->>RoleAuth: 识别为Service Account类型
RoleAuth->>RoleAuth: 调用Service Account验证服务
Note right of RoleAuth: handleServiceAccount()<br/>验证Google Service Account
alt 验证成功
RoleAuth->>RoleAuth: 设置Service Account信息到请求头
RoleAuth->>Gateway: 继续处理
Gateway->>Upstream: 转发请求
else 验证失败
RoleAuth-->>Client: 返回认证失败
end
关键实现要点
-
认证优先级机制:插件按照Web Identity Token → Role Token → Service Account Token → 默认角色的优先级顺序提取认证信息
-
黑白名单匹配:参考现有
ext-auth插件的匹配规则机制,支持域名、路径和方法的灵活匹配 -
失败处理模式:借鉴
ext-auth插件的failure_mode_allow机制,提供降级处理能力 -
STS服务集成:当前Higress已在配置中预留STS服务端口配置,通过设置非零端口值启用STS服务
创建一个新的role-auth插件,结合AWS Bedrock和Vertex AI的优势,实现无AK的角色鉴权机制。
- 插件主文件
// plugins/wasm-go/extensions/role-auth/main.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"role-auth/config"
"role-auth/util"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
)
func main() {
wrapper.SetCtx(
"role-auth",
wrapper.ParseConfigBy(config.ParseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
const (
HeaderAuthorization = "authorization"
HeaderRoleToken = "x-role-token"
HeaderWebIdentityToken = "x-web-identity-token"
HeaderRoleArn = "x-role-arn"
HeaderSessionName = "x-session-name"
HeaderUserRole = "x-user-role"
HeaderRoleExpiry = "x-role-expiry"
HeaderFailureModeAllow = "x-role-auth-failure-mode-allowed"
)
func onHttpRequestHeaders(ctx wrapper.HttpContext, config config.RoleAuthConfig, log wrapper.Log) types.Action {
// 检查黑白名单规则,跳过不需要认证的请求
if config.MatchRules.IsAllowedByMode(ctx.Host(), ctx.Method(), wrapper.GetRequestPathWithoutQuery()) {
ctx.DontReadRequestBody()
return types.ActionContinue
}
ctx.DisableReroute()
ctx.DontReadRequestBody()
return checkRoleAuth(ctx, config, log)
}
func checkRoleAuth(ctx wrapper.HttpContext, config config.RoleAuthConfig, log wrapper.Log) types.Action {
// 按优先级获取认证信息
authInfo := extractAuthInfo(ctx, config, log)
if authInfo == nil {
log.Errorf("failed to extract authentication information")
return handleAuthFailure(ctx, config, http.StatusUnauthorized, "Missing authentication information")
}
// 根据认证类型处理
switch authInfo.AuthType {
case config.WebIdentityTokenType:
return handleWebIdentityToken(ctx, config, authInfo, log)
case config.ServiceAccountType:
return handleServiceAccount(ctx, config, authInfo, log)
case config.RoleTokenType:
return handleRoleToken(ctx, config, authInfo, log)
default:
log.Errorf("unsupported authentication type: %s", authInfo.AuthType)
return handleAuthFailure(ctx, config, http.StatusBadRequest, "Unsupported authentication type")
}
}
type AuthInfo struct {
AuthType string
Token string
RoleArn string
SessionName string
ServiceAccount string
ProjectID string
}
func extractAuthInfo(ctx wrapper.HttpContext, config config.RoleAuthConfig, log wrapper.Log) *AuthInfo {
// 优先级1: 请求头中的Web Identity Token
if webToken, _ := proxywasm.GetHttpRequestHeader(HeaderWebIdentityToken); webToken != "" {
if roleArn, _ := proxywasm.GetHttpRequestHeader(HeaderRoleArn); roleArn != "" {
sessionName, _ := proxywasm.GetHttpRequestHeader(HeaderSessionName)
if sessionName == "" {
sessionName = fmt.Sprintf("higress-session-%d", time.Now().Unix())
}
return &AuthInfo{
AuthType: config.WebIdentityTokenType,
Token: webToken,
RoleArn: roleArn,
SessionName: sessionName,
}
}
}
// 优先级2: 请求头中的Role Token
if roleToken, _ := proxywasm.GetHttpRequestHeader(HeaderRoleToken); roleToken != "" {
return &AuthInfo{
AuthType: config.RoleTokenType,
Token: roleToken,
}
}
// 优先级3: Authorization头中的Bearer token
if authHeader, _ := proxywasm.GetHttpRequestHeader(HeaderAuthorization); authHeader != "" {
if strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
// 检查是否为Service Account token格式
if isServiceAccountToken(token) {
return &AuthInfo{
AuthType: config.ServiceAccountType, Token: token, } }
// 默认作为Role Token处理
return &AuthInfo{
AuthType: config.RoleTokenType,
Token: token,
}
}
}
// 优先级4: 配置中的默认角色
if config.DefaultRole != "" {
return &AuthInfo{
AuthType: config.RoleTokenType,
Token: config.DefaultRole,
}
}
return nil
}
func handleWebIdentityToken(ctx wrapper.HttpContext, config config.RoleAuthConfig, authInfo *AuthInfo, log wrapper.Log) types.Action {
// 构建STS AssumeRoleWithWebIdentity请求
stsRequest := util.STSAssumeRoleRequest{
WebIdentityToken: authInfo.Token,
RoleArn: authInfo.RoleArn,
RoleSessionName: authInfo.SessionName,
DurationSeconds: config.TokenDuration,
}
// 调用外部STS服务
callback := func(numHeaders, bodySize, numTrailers int) {
responseBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
if err != nil {
log.Errorf("failed to get STS response body: %v", err)
handleAuthFailure(ctx, config, http.StatusInternalServerError, "STS service error")
return
}
var stsResponse util.STSAssumeRoleResponse
if err := json.Unmarshal(responseBody, &stsResponse); err != nil {
log.Errorf("failed to parse STS response: %v", err)
handleAuthFailure(ctx, config, http.StatusInternalServerError, "Invalid STS response")
return
}
// 设置角色信息到请求头
setRoleHeaders(ctx, &stsResponse, config)
proxywasm.ResumeHttpRequest()
}
// 发送HTTP请求到STS服务
if err := util.CallSTSService(config.STSEndpoint, &stsRequest, callback); err != nil {
log.Errorf("failed to call STS service: %v", err)
return handleAuthFailure(ctx, config, http.StatusInternalServerError, "STS service unavailable")
}
return types.ActionPause
}
func handleServiceAccount(ctx wrapper.HttpContext, config config.RoleAuthConfig, authInfo *AuthInfo, log wrapper.Log) types.Action {
// 验证Service Account token
saRequest := util.ServiceAccountRequest{
Token: authInfo.Token,
ProjectID: authInfo.ProjectID,
}
callback := func(numHeaders, bodySize, numTrailers int) {
responseBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
if err != nil {
log.Errorf("failed to get service account response: %v", err)
handleAuthFailure(ctx, config, http.StatusInternalServerError, "Service account verification failed")
return
}
var saResponse util.ServiceAccountResponse
if err := json.Unmarshal(responseBody, &saResponse); err != nil {
log.Errorf("failed to parse service account response: %v", err)
handleAuthFailure(ctx, config, http.StatusInternalServerError, "Invalid service account response")
return
}
// 设置角色信息
setServiceAccountHeaders(ctx, &saResponse, config)
proxywasm.ResumeHttpRequest()
}
if err := util.CallServiceAccountService(config.ServiceAccountEndpoint, &saRequest, callback); err != nil {
log.Errorf("failed to call service account service: %v", err)
return handleAuthFailure(ctx, config, http.StatusInternalServerError, "Service account service unavailable")
}
return types.ActionPause
}
func handleRoleToken(ctx wrapper.HttpContext, config config.RoleAuthConfig, authInfo *AuthInfo, log wrapper.Log) types.Action {
// 验证Role Token
roleRequest := util.RoleTokenRequest{
Token: authInfo.Token,
}
callback := func(numHeaders, bodySize, numTrailers int) {
responseBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
if err != nil {
log.Errorf("failed to get role token response: %v", err)
handleAuthFailure(ctx, config, http.StatusInternalServerError, "Role token verification failed")
return
}
var roleResponse util.RoleTokenResponse
if err := json.Unmarshal(responseBody, &roleResponse); err != nil {
log.Errorf("failed to parse role token response: %v", err)
handleAuthFailure(ctx, config, http.StatusInternalServerError, "Invalid role token response")
return
}
// 检查角色权限
if !isRoleAllowed(roleResponse.Role, config.AllowedRoles) {
log.Errorf("role %s is not allowed", roleResponse.Role)
handleAuthFailure(ctx, config, http.StatusForbidden, "Role not authorized")
return
}
// 设置角色信息
setRoleTokenHeaders(ctx, &roleResponse, config)
proxywasm.ResumeHttpRequest()
}
if err := util.CallRoleTokenService(config.RoleTokenEndpoint, &roleRequest, callback); err != nil {
log.Errorf("failed to call role token service: %v", err)
return handleAuthFailure(ctx, config, http.StatusInternalServerError, "Role token service unavailable")
}
return types.ActionPause
}
func setRoleHeaders(ctx wrapper.HttpContext, response *util.STSAssumeRoleResponse, config config.RoleAuthConfig) {
// 设置用户角色信息到上游请求头
for _, header := range config.UpstreamHeaders {
switch header {
case "x-user-role":
proxywasm.AddHttpRequestHeader("x-user-role", response.AssumedRoleUser.Arn)
case "x-access-key-id":
proxywasm.AddHttpRequestHeader("x-access-key-id", response.Credentials.AccessKeyId)
case "x-session-token":
proxywasm.AddHttpRequestHeader("x-session-token", response.Credentials.SessionToken)
case "x-role-expiry":
proxywasm.AddHttpRequestHeader("x-role-expiry", response.Credentials.Expiration)
}
}
}
func setServiceAccountHeaders(ctx wrapper.HttpContext, response *util.ServiceAccountResponse, config config.RoleAuthConfig) {
for _, header := range config.UpstreamHeaders {
switch header {
case "x-user-role":
proxywasm.AddHttpRequestHeader("x-user-role", response.ServiceAccount)
case "x-project-id":
proxywasm.AddHttpRequestHeader("x-project-id", response.ProjectID)
case "x-role-expiry":
proxywasm.AddHttpRequestHeader("x-role-expiry", response.TokenExpiry)
}
}
}
func setRoleTokenHeaders(ctx wrapper.HttpContext, response *util.RoleTokenResponse, config config.RoleAuthConfig) {
for _, header := range config.UpstreamHeaders {
switch header {
case "x-user-role":
proxywasm.AddHttpRequestHeader("x-user-role", response.Role)
case "x-user-id":
proxywasm.AddHttpRequestHeader("x-user-id", response.UserID)
case "x-role-expiry":
proxywasm.AddHttpRequestHeader("x-role-expiry", response.Expiry)
}
}
}
func handleAuthFailure(ctx wrapper.HttpContext, config config.RoleAuthConfig, statusCode int, message string) types.Action {
if config.FailureModeAllow {
if config.FailureModeAllowHeaderAdd {
proxywasm.AddHttpRequestHeader(HeaderFailureModeAllow, "true")
}
return types.ActionContinue
}
proxywasm.SendHttpResponse(uint32(statusCode), nil, []byte(message), -1)
return types.ActionPause
}
func isServiceAccountToken(token string) bool {
// 简单检查Service Account token格式
return strings.Contains(token, "serviceAccount") || len(token) > 500
}
func isRoleAllowed(role string, allowedRoles []string) bool {
if len(allowedRoles) == 0 {
return true // 如果没有限制,允许所有角色
}
for _, allowedRole := range allowedRoles {
if role == allowedRole {
return true
} }
return false
}
- 配置结构
// plugins/wasm-go/extensions/role-auth/config/config.go
package config
import (
"encoding/json"
"errors"
"fmt"
"github.com/alibaba/higress/plugins/wasm-go/extensions/ext-auth/expr"
"github.com/tidwall/gjson"
)
type RoleAuthConfig struct {
// 认证类型常量
WebIdentityTokenType string
ServiceAccountType string
RoleTokenType string
// 服务端点配置
STSEndpoint string `json:"sts_endpoint"`
ServiceAccountEndpoint string `json:"service_account_endpoint"`
RoleTokenEndpoint string `json:"role_token_endpoint"`
// 认证配置
TokenDuration int `json:"token_duration"` // token有效期(秒)
DefaultRole string `json:"default_role"` // 默认角色
AllowedRoles []string `json:"allowed_roles"` // 允许的角色列表
UpstreamHeaders []string `json:"upstream_headers"` // 传递给上游的请求头
// 匹配规则
MatchRules expr.MatchRules `json:"match_rules"`
// 失败处理
FailureModeAllow bool `json:"failure_mode_allow"`
Fail
}
当前STS配置状况
在Higress中,STS服务只是作为配置选项存在.
需要一个配置项表明STS服务是可选的,通过设置非零端口值来启用.
STS服务实现
- STS服务主实现
// pkg/sts/server.go
package sts
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"istio.io/pkg/log"
)
type STSServer struct {
port int
jwtSecret []byte
tokenDuration time.Duration
oidcVerifier OIDCVerifier
roleValidator RoleValidator
}
type OIDCVerifier interface {
VerifyToken(ctx context.Context, token string) (*OIDCClaims, error)
}
type RoleValidator interface {
ValidateRole(ctx context.Context, roleArn string, subject string) error
}
type OIDCClaims struct {
Subject string `json:"sub"`
Issuer string `json:"iss"`
Audience string `json:"aud"`
ExpiresAt int64 `json:"exp"`
}
// AWS STS AssumeRoleWithWebIdentity请求结构
type AssumeRoleWithWebIdentityRequest struct {
RoleArn string `json:"RoleArn"`
RoleSessionName string `json:"RoleSessionName"`
WebIdentityToken string `json:"WebIdentityToken"`
DurationSeconds int `json:"DurationSeconds,omitempty"`
}
// AWS STS AssumeRole请求结构
type AssumeRoleRequest struct {
RoleArn string `json:"RoleArn"`
RoleSessionName string `json:"RoleSessionName"`
DurationSeconds int `json:"DurationSeconds,omitempty"`
}
// STS响应结构
type AssumeRoleResponse struct {
Credentials Credentials `json:"Credentials"`
AssumedRoleUser AssumedRoleUser `json:"AssumedRoleUser"`
PackedPolicySize int `json:"PackedPolicySize,omitempty"`
}
type Credentials struct {
AccessKeyId string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
Expiration string `json:"Expiration"`
}
type AssumedRoleUser struct {
AssumedRoleId string `json:"AssumedRoleId"`
Arn string `json:"Arn"`
}
func NewSTSServer(port int, jwtSecret []byte, tokenDuration time.Duration) *STSServer {
return &STSServer{
port: port,
jwtSecret: jwtSecret,
tokenDuration: tokenDuration,
oidcVerifier: &DefaultOIDCVerifier{},
roleValidator: &DefaultRoleValidator{},
}
}
func (s *STSServer) Start() error {
mux := http.NewServeMux()
// AWS STS兼容的端点
mux.HandleFunc("/", s.handleSTSRequest)
// 健康检查端点
mux.HandleFunc("/health", s.handleHealth)
server := &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: mux,
}
log.Infof("Starting STS server on port %d", s.port)
return server.ListenAndServe()
}
func (s *STSServer) handleSTSRequest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析AWS STS请求
action := r.FormValue("Action")
version := r.FormValue("Version")
if version != "2011-06-15" {
http.Error(w, "Unsupported API version", http.StatusBadRequest)
return
}
switch action {
case "AssumeRoleWithWebIdentity":
s.handleAssumeRoleWithWebIdentity(w, r)
case "AssumeRole":
s.handleAssumeRole(w, r)
default:
http.Error(w, "Unsupported action", http.StatusBadRequest)
}
}
func (s *STSServer) handleAssumeRoleWithWebIdentity(w http.ResponseWriter, r *http.Request) {
// 解析请求参数
roleArn := r.FormValue("RoleArn")
roleSessionName := r.FormValue("RoleSessionName")
webIdentityToken := r.FormValue("WebIdentityToken")
durationSeconds := 3600 // 默认1小时
if roleArn == "" || roleSessionName == "" || webIdentityToken == "" {
http.Error(w, "Missing required parameters", http.StatusBadRequest)
return
}
// 验证OIDC token
claims, err := s.oidcVerifier.VerifyToken(r.Context(), webIdentityToken)
if err != nil {
log.Errorf("Failed to verify OIDC token: %v", err)
http.Error(w, "Invalid web identity token", http.StatusUnauthorized)
return
}
// 验证角色权限
if err := s.roleValidator.ValidateRole(r.Context(), roleArn, claims.Subject); err != nil {
log.Errorf("Role validation failed: %v", err)
http.Error(w, "Access denied", http.StatusForbidden)
return
}
// 生成临时凭证
credentials, err := s.generateTemporaryCredentials(roleArn, roleSessionName, claims.Subject, durationSeconds)
if err != nil {
log.Errorf("Failed to generate credentials: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// 构建响应
response := AssumeRoleResponse{ Credentials: *credentials, AssumedRoleUser: AssumedRoleUser{
AssumedRoleId: fmt.Sprintf("AROA%s:%s", generateRandomString(16), roleSessionName),
Arn: fmt.Sprintf("%s/%s", roleArn, roleSessionName),
},
}
// 返回XML格式响应(AWS STS标准)
s.writeXMLResponse(w, &response)
}
func (s *STSServer) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
// 标准AssumeRole实现
roleArn := r.FormValue("RoleArn")
roleSessionName := r.FormValue("RoleSessionName")
durationSeconds := 3600
if roleArn == "" || roleSessionName == "" {
http.Error(w, "Missing required parameters", http.StatusBadRequest)
return
}
// 从请求头获取当前凭证信息
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
// 验证当前凭证并获取主体信息
subject, err := s.validateCurrentCredentials(authHeader)
if err != nil {
log.Errorf("Failed to validate current credentials: %v", err)
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// 验证角色权限
if err := s.roleValidator.ValidateRole(r.Context(), roleArn, subject); err != nil {
log.Errorf("Role validation failed: %v", err)
http.Error(w, "Access denied", http.StatusForbidden)
return
}
// 生成临时凭证
credentials, err := s.generateTemporaryCredentials(roleArn, roleSessionName, subject, durationSeconds)
if err != nil {
log.Errorf("Failed to generate credentials: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// 构建响应
response := AssumeRoleResponse{ Credentials: *credentials, AssumedRoleUser: AssumedRoleUser{
AssumedRoleId: fmt.Sprintf("AROA%s:%s", generateRandomString(16), roleSessionName),
Arn: fmt.Sprintf("%s/%s", roleArn, roleSessionName),
},
}
s.writeXMLResponse(w, &response)
}
func (s *STSServer) generateTemporaryCredentials(roleArn, sessionName, subject string, durationSeconds int) (*Credentials, error) {
// 生成临时访问密钥
accessKeyId := "ASIA" + generateRandomString(16)
secretAccessKey := generateRandomString(40)
// 生成会话令牌(JWT)
now := time.Now()
expiration := now.Add(time.Duration(durationSeconds) * time.Second)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "higress-sts",
"sub": subject,
"aud": "higress",
"exp": expiration.Unix(),
"iat": now.Unix(),
"role_arn": roleArn,
"session": sessionName,
"aki": accessKeyId,
})
sessionToken, err := token.SignedString(s.jwtSecret)
if err != nil {
return nil, fmt.Errorf("failed to sign session token: %w", err)
}
return &Credentials{
AccessKeyId: accessKeyId, SecretAccessKey: secretAccessKey, SessionToken: sessionToken, Expiration: expiration.UTC().Format(time.RFC3339),
}, nil
}
func (s *STSServer) validateCurrentCredentials(authHeader string) (string, error) {
// 简化实现:从Authorization头解析JWT token
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
tokenString := authHeader[7:]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.jwtSecret, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if sub, ok := claims["sub"].(string); ok {
return sub, nil
}
}
}
return "", fmt.Errorf("invalid authorization header")
}
func (s *STSServer) writeXMLResponse(w http.ResponseWriter, response *AssumeRoleResponse) {
w.Header().Set("Content-Type", "text/xml")
// 简化的XML响应
xml := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithWebIdentityResult>
<Credentials>
<AccessKeyId>%s</AccessKeyId>
<SecretAccessKey>%s</SecretAccessKey>
<SessionToken>%s</SessionToken>
<Expiration>%s</Expiration>
</Credentials>
<AssumedRoleUser>
<AssumedRoleId>%s</AssumedRoleId>
<Arn>%s</Arn>
</AssumedRoleUser>
</AssumeRoleWithWebIdentityResult>
</AssumeRoleWithWebIdentityResponse>`,
response.Credentials.AccessKeyId,
response.Credentials.SecretAccessKey,
response.Credentials.SessionToken,
response.Credentials.Expiration,
response.AssumedRoleUser.AssumedRoleId,
response.AssumedRoleUser.Arn,
)
w.Write([]byte(xml))
}
func (s *STSServer) handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func generateRandomString(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return base64.URLEncoding.EncodeToString(bytes)[:length]
}
- OIDC验证器实现
// pkg/sts/oidc.go
package sts
import (
"context"
"crypto/rsa"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
type DefaultOIDCVerifier struct {
httpClient *http.Client
jwksCache map[string]*JWKSResponse
}
type JWKSResponse struct {
Keys []JWK `json:"keys"`
}
type JWK struct {
Kty string `json:"kty"`
Use string `json:"use"`
Kid string `json:"kid"`
N string `json:"n"`
E string `json:"e"`
}
type OIDCConfiguration struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JwksURI string `json:"jwks_uri"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
}
func NewDefaultOIDCVerifier() *DefaultOIDCVerifier {
return &DefaultOIDCVerifier{
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
jwksCache: make(map[string]*JWKSResponse),
}
}
func (v *DefaultOIDCVerifier) VerifyToken(ctx context.Context, token string) (*OIDCClaims, error) {
// 解析JWT token而不验证签名(用于获取issuer和kid)
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
parsedToken, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid token claims")
}
issuer, ok := claims["iss"].(string)
if !ok {
return nil, fmt.Errorf("missing issuer in token")
}
// 获取kid(key ID)
kid, ok := parsedToken.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("missing kid in token header")
}
// 获取OIDC配置
oidcConfig, err := v.getOIDCConfiguration(ctx, issuer)
if err != nil {
return nil, fmt.Errorf("failed to get OIDC configuration: %w", err)
}
// 获取JWKS
jwks, err := v.getJWKS(ctx, oidcConfig.JwksURI)
if err != nil {
return nil, fmt.Errorf("failed to get JWKS: %w", err)
}
// 找到对应的公钥
publicKey, err := v.getPublicKey(jwks, kid)
if err != nil {
return nil, fmt.Errorf("failed to get public key: %w", err)
}
// 验证JWT签名
validatedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return publicKey, nil
})
if err != nil {
return nil, fmt.Errorf("failed to validate token: %w", err)
}
if !validatedToken.Valid {
return nil, fmt.Errorf("token is not valid")
}
// 提取claims
validatedClaims, ok := validatedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid validated token claims")
}
// 验证基本claims
if err := v.validateClaims(validatedClaims, issuer); err != nil {
return nil, fmt.Errorf("claims validation failed: %w", err)
}
// 构建OIDCClaims
oidcClaims := &OIDCClaims{
Subject: validatedClaims["sub"].(string),
Issuer: validatedClaims["iss"].(string),
}
if aud, ok := validatedClaims["aud"].(string); ok {
oidcClaims.Audience = aud
}
if exp, ok := validatedClaims["exp"].(float64); ok {
oidcClaims.ExpiresAt = int64(exp)
}
return oidcClaims, nil
}
func (v *DefaultOIDCVerifier) getOIDCConfiguration(ctx context.Context, issuer string) (*OIDCConfiguration, error) {
// 构建OIDC配置端点URL
configURL := strings.TrimSuffix(issuer, "/") + "/.well-known/openid_configuration"
req, err := http.NewRequestWithContext(ctx, "GET", configURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := v.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch OIDC configuration: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OIDC configuration request failed with status: %d", resp.StatusCode)
}
var config OIDCConfiguration
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
return nil, fmt.Errorf("failed to decode OIDC configuration: %w", err)
}
return &config, nil
}
func (v *DefaultOIDCVerifier) getJWKS(ctx context.Context, jwksURI string) (*JWKSResponse, error) {
// 检查缓存
if jwks, exists := v.jwksCache[jwksURI]; exists {
return jwks, nil
}
req, err := http.NewRequestWithContext(ctx, "GET", jwksURI, nil)
if err != nil {
return nil, fmt.Errorf("failed to create JWKS request: %w", err)
}
resp, err := v.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch JWKS: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("JWKS request failed with status: %d", resp.StatusCode)
}
var jwks JWKSResponse
if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil {
return nil, fmt.Errorf("failed to decode JWKS: %w", err)
}
// 缓存JWKS(简单实现,生产环境应考虑TTL)
v.jwksCache[jwksURI] = &jwks
return &jwks, nil
}
func (v *DefaultOIDCVerifier) getPublicKey(jwks *JWKSResponse, kid string) (*rsa.PublicKey, error) {
for _, key := range jwks.Keys {
if key.Kid == kid && key.Kty == "RSA" {
return v.parseRSAPublicKey(key)
} }
return nil, fmt.Errorf("public key not found for kid: %s", kid)
}
func (v *DefaultOIDCVerifier) parseRSAPublicKey(jwk JWK) (*rsa.PublicKey, error) {
// 这里需要实现JWK到RSA公钥的转换
// 简化实现,生产环境应使用专门的JWK库
return nil, fmt.Errorf("RSA public key parsing not implemented")
}
func (v *DefaultOIDCVerifier) validateClaims(claims jwt.MapClaims, expectedIssuer string) error {
// 验证issuer
if iss, ok := claims["iss"].(string); !ok || iss != expectedIssuer {
return fmt.Errorf("invalid issuer")
}
// 验证过期时间
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return fmt.Errorf("token has expired")
}
} else {
return fmt.Errorf("missing exp claim")
}
// 验证生效时间
if iat, ok := claims["iat"].(float64); ok {
if time.Now().Unix() < int64(iat) {
return fmt.Errorf("token not yet valid")
}
}
// 验证subject
if _, ok := claims["sub"].(string); !ok {
return fmt.Errorf("missing sub claim")
}
return nil
}
alibaba/higresshelm/core/values.yaml
324 token:
325 aud: istio-ca
326
327 sts:
328 # -- The service port used by Security Token Service (STS) server to handle token exchange requests.
329 # Setting this port to a non-zero value enables STS server.
330 servicePort: 0
- 角色验证器实现
// pkg/sts/role_validator.go
package sts
import (
"context"
"fmt"
"strings"
)
type DefaultRoleValidator struct {
roleMapping map[string][]string // subject -> allowed roles
}
func NewDefaultRoleValidator() *DefaultRoleValidator {
return &DefaultRoleValidator{
roleMapping: make(map[string][]string),
}
}
func (rv *DefaultRoleValidator) ValidateRole(ctx context.Context, roleArn string, subject string) error {
// 检查角色ARN格式
if !strings.HasPrefix(roleArn, "arn:aws:iam::") {
return fmt.Errorf("invalid role ARN format: %s", roleArn)
}
// 检查subject是否有权限假设该角色
allowedRoles, exists := rv.roleMapping[subject]
if !exists {
return fmt.Errorf("subject %s has no role mappings", subject)
}
for _, allowedRole := range allowedRoles {
if allowedRole == roleArn || allowedRole == "*" {
return nil
}
}
return fmt.Errorf("subject %s is not authorized to assume role %s", subject, roleArn)
}
func (rv *DefaultRoleValidator) AddRoleMapping(subject string, roles []string) {
rv.roleMapping[subject] = roles
}
- STS服务启动器
// pkg/sts/bootstrap.go
package sts
import (
"fmt"
"time"
)
func StartSTSServer(port int, jwtSecret string) error {
if port <= 0 {
return fmt.Errorf("invalid port: %d", port)
}
server := NewSTSServer(port, []byte(jwtSecret), time.Hour)
// 设置OIDC验证器
server.oidcVerifier = NewDefaultOIDCVerifier()
// 设置角色验证器
roleValidator := NewDefaultRoleValidator()
// 添加一些示例角色映射
roleValidator.AddRoleMapping("user@example.com", []string{
"arn:aws:iam::123456789012:role/HigressRole",
"arn:aws:iam::123456789012:role/ReadOnlyRole",
})
server.roleValidator = roleValidator
return server.Start()
}
这个的OIDC验证器实现包含了:
- OIDC配置获取:从
.well-known/openid_configuration端点获取OIDC提供商配置 - JWKS获取和缓存:获取JSON Web Key Set用于验证JWT签名
- JWT签名验证:使用RSA公钥验证JWT签名
- Claims验证:验证token的有效期、issuer、subject等标准claims
- 角色验证器:验证用户是否有权限假设指定角色