多租户的Kubernetes集群里跑着OpenFaaS,本来以为网关认证加上namespace隔离就能挡住所有人,结果生产环境一上线,就被安全团队打回来说有越权漏洞。这个问题其实很常见,很多人把“认证”和“授权”混为一谈,觉得只要登录了、只要Pod分开了,就万事大吉。但实际上,OpenFaaS的网关认证只能证明“你是谁”,namespace隔离只能保证“你的函数跑在哪个房间”,至于“你能不能调用别人的函数”,这两个机制根本管不着。今天咱们就用大白话,把这个漏洞讲清楚,然后给出一个生产环境必须补上的安全方案:OAuth代理加上RBAC策略。
一、先搞懂问题出在哪:认证不等于授权
1.1 网关认证做了什么
OpenFaaS网关默认有个Basic Auth,或者你可以接入OIDC之类的身份认证。这个认证的作用是:当你请求网关API时,它先确认你有合法的用户名和密码或Token。如果认证通过,网关就认为你是“合法用户”。但网关本身并不知道这个用户能访问哪些函数,它只负责把请求转发给对应的函数Pod。
举个例子,假设有两个租户,租户A和租户B。租户A部署了函数func-a,租户B部署了函数func-b。两个函数分别放在不同的namespace里,比如tenant-a和tenant-b。OpenFaaS网关在转发请求时,会根据URL路径里的函数名去找到对应的服务。比如/function/func-a就会路由到tenant-a命名空间下的服务。
1.2 namespace隔离的局限性
namespace是Kubernetes用来做资源隔离的,它能把Pod、Service、ConfigMap等资源分开。但在网络层面,默认情况下,不同namespace的Pod是可以互相访问的,除非你定义了NetworkPolicy。也就是说,如果租户A的某个Pod被攻破了,它完全可以请求租户B的Service,只要它知道对方服务的DNS名称。这就是一个典型的横向越权。
更关键的是,OpenFaaS网关本身通常运行在openfaas这个namespace下,它需要访问所有租户的命名空间,才能把请求转发给对应的函数。所以网关事实上成了一个“超级代理”,只要通过了网关认证,就能调用所有函数。
1.3 越权的具体场景
场景一:租户A的用户甲,登录OpenFaaS后,直接构造请求调用租户B的函数。因为网关只验证了甲的登录身份,并没有验证甲是否有权限访问func-b。所以甲能成功拿到租户B的数据。
场景二:租户B的某个函数内部调用了OpenFaaS的REST API,使用了B用户的密钥。如果B用户泄露了密钥,攻击者就能用这个密钥调用A的函数。
场景三:某些函数需要管理权限,比如重启Pod、修改配置,这些操作如果通过OpenFaaS的API暴露出去,而网关认证又比较宽松,就会引发更大的风险。
所以结论很明确:网关认证只解决了“谁能进大门”的问题,namespace隔离只解决了“资源放在不同房间”的问题,但“谁能进哪个房间”这个问题,必须由额外的授权机制来控制。
二、生产环境的解法:OAuth代理 + RBAC策略
OAuth代理,比如oauth2-proxy,可以放在OpenFaaS网关前面,负责统一入口认证和会话管理。它本身支持很多身份提供商,比如GitHub、Okta、Azure AD等。RBAC(基于角色的访问控制)则是定义“什么角色能对什么资源做什么操作”。两者结合,就能实现“认证在前,授权在后”的完整链路。
整体架构大概是:用户请求 -> OAuth代理(验证身份,把用户信息放入Header)-> OpenFaaS网关(识别Header里的用户信息)-> 网关通过RBAC判断用户是否有权限访问特定函数 -> 有则转发,无则拒绝。
下面咱们用一个完整示例来说明,技术栈统一使用Go语言和Kubernetes相关工具。假设你已经有一个Kubernetes集群,并且安装了OpenFaaS。
2.1 部署OAuth代理
我们使用oauth2-proxy作为OAuth代理。它支持OIDC,可以对接很多身份提供商。这里以Google OAuth为例,但你只需要换成自己的Provider即可。
先创建一个Kubernetes Deployment和Service,用于运行oauth2-proxy。
# oauth2-proxy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: oauth2-proxy
namespace: openfaas
spec:
replicas: 1
selector:
matchLabels:
app: oauth2-proxy
template:
metadata:
labels:
app: oauth2-proxy
spec:
containers:
- name: oauth2-proxy
image: bitnami/oauth2-proxy:7.4.0
args:
- --provider=oidc
- --oidc-issuer-url=https://accounts.google.com
- --client-id=YOUR_CLIENT_ID
- --client-secret=YOUR_CLIENT_SECRET
- --cookie-secret=YOUR_COOKIE_SECRET
- --email-domain=yourdomain.com
- --http-address=0.0.0.0:4180
- --whitelist-domain=.yourdomain.com
# 关键:把用户邮箱和分组信息作为HTTP头传递给后端
- --set-xauthrequest=true
- --pass-user-headers=true
- --pass-access-token=true
- --upstream=http://gateway.openfaas.svc.cluster.local:8080
env:
- name: OAUTH2_PROXY_CLIENT_ID
value: "YOUR_CLIENT_ID"
- name: OAUTH2_PROXY_CLIENT_SECRET
value: "YOUR_CLIENT_SECRET"
ports:
- containerPort: 4180
readinessProbe:
httpGet:
path: /ping
port: 4180
---
apiVersion: v1
kind: Service
metadata:
name: oauth2-proxy
namespace: openfaas
spec:
selector:
app: oauth2-proxy
ports:
- port: 4180
targetPort: 4180
注意上面的--upstream=http://gateway.openfaas.svc.cluster.local:8080,意思是oauth2-proxy把经过认证的请求转发给OpenFaaS网关服务。同时通过--set-xauthrequest=true,oauth2-proxy会把用户的邮箱(X-Auth-Request-Email)等信息作为请求头发送给网关。
2.2 修改OpenFaaS网关,让它识别用户Header
OpenFaaS网关支持通过环境变量开启“用户上下文”传递。我们可以设置basic_auth为false,改为从Header中读取用户信息。这样网关在收到请求时,可以从X-Auth-Request-Email和X-Auth-Request-Groups这两个Header中获取用户身份和组信息。
用Helm安装OpenFaaS时,我们覆盖values.yaml:
# gateway-config.yaml
gateway:
basicAuth: false
# 允许从Header中读取用户身份
writeDebug: true
auth:
# 使用我们自定义的认证插件
image: ghcr.io/openfaas/openfaas-oidc-plugin:latest
args:
- "-uri=http://oauth2-proxy.openfaas.svc.cluster.local:4180/oauth2/auth"
# 告诉插件从哪个Header读取用户
env:
OIDC_AUTH_URI: "http://oauth2-proxy.openfaas.svc.cluster.local:4180/oauth2/auth"
实际上,OpenFaaS有一个官方插件叫openfaas-oidc-plugin,它可以验证OAuth代理传来的令牌。但更简单的做法是,我们直接在网关的配置里启用X-Auth-Request-*头的信任。如果你不想使用插件,咱们可以通过修改网关的env来启用http_headers作为用户输入。
为了简化,咱们直接利用oauth2-proxy的--set-xauthrequest=true,它会自动在请求头中加入X-Auth-Request-User和X-Auth-Request-Email。然后在OpenFaaS里,我们可以在函数调用请求中,把这些Header传给函数,但网关本身并不会对它们进行授权判断。这就需要我们自己做RBAC。
2.3 实现RBAC控制的网关插件
接下来是核心:我们需要一个授权中间件,它检查每个调用函数的请求,看用户是否有权限调用这个函数。我们用Go语言写一个简单的HTTP处理器,它作为OpenFaaS的网关代理,拦截所有请求,并在转发前执行RBAC检查。
下面是一个完整的Go示例,展示了如何实现这个中间件:
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
)
// 角色-权限映射表,模拟RBAC策略
// 这里为了方便演示,使用硬编码。在生产环境应该从配置中心或数据库读取。
var rolePolicies = map[string]map[string][]string{
"tenant-a-admin": {
"functions": {"func-a", "func-b"}, // 管理员可以访问两个函数
},
"tenant-a-user": {
"functions": {"func-a"}, // 普通用户只能访问自己的函数
},
"tenant-b-user": {
"functions": {"func-b"},
},
}
// 模拟用户-角色映射,实际应从OAuth代理传来的Header中获取用户组信息
func getUserRoles(headers http.Header) []string {
// oauth2-proxy会把用户所属组放到 X-Auth-Request-Groups 中
groupsHeader := headers.Get("X-Auth-Request-Groups")
if groupsHeader == "" {
return nil
}
// 假设组名是 "tenant-a-user" 这种格式
return strings.Split(groupsHeader, ",")
}
// 检查用户角色是否有权限访问某个函数
func hasPermission(roles []string, functionName string) bool {
for _, role := range roles {
if perms, ok := rolePolicies[role]; ok {
for _, allowed := range perms["functions"] {
if allowed == functionName || allowed == "*" {
return true
}
}
}
}
return false
}
func main() {
// 目标网关地址
target := os.Getenv("UPSTREAM_GATEWAY")
if target == "" {
target = "http://gateway.openfaas.svc.cluster.local:8080"
}
proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: target})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 只对函数调用路径做权限检查
if strings.HasPrefix(r.URL.Path, "/function/") {
// 提取函数名
functionPath := strings.TrimPrefix(r.URL.Path, "/function/")
functionName := strings.Split(functionPath, "/")[0]
// 从请求头中获取用户角色(oauth2-proxy已经注入)
roles := getUserRoles(r.Header)
if len(roles) == 0 {
// 没有角色信息,说明认证失败
http.Error(w, "Unauthorized: missing roles", http.StatusUnauthorized)
return
}
// 检查是否有权限
if !hasPermission(roles, functionName) {
http.Error(w, "Forbidden: you don't have access to this function", http.StatusForbidden)
return
}
}
// 如果通过检查,则转发请求到OpenFaaS网关
proxy.ServeHTTP(w, r)
})
fmt.Println("RBAC gateway proxy listening on :8081")
http.ListenAndServe(":8081", nil)
}
这个Go程序需要编译成镜像,然后部署到Kubernetes中,作为OpenFaaS网关前面的“授权中间件”。我们得把它放在OAuth代理和OpenFaaS网关之间,也就是让oauth2-proxy把请求先送到这个中间件,然后中间件再转发给真正的网关。
你可以用下面的Dockerfile构建这个中间件:
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod ./
COPY go.sum ./
COPY main.go ./
RUN go build -o rbac-proxy main.go
FROM alpine:3.18
COPY --from=builder /app/rbac-proxy /usr/local/bin/rbac-proxy
EXPOSE 8081
ENTRYPOINT ["rbac-proxy"]
然后用Kubernetes部署它:
# rbac-proxy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rbac-proxy
namespace: openfaas
spec:
replicas: 1
selector:
matchLabels:
app: rbac-proxy
template:
metadata:
labels:
app: rbac-proxy
spec:
containers:
- name: rbac-proxy
image: your-registry/rbac-proxy:latest
env:
- name: UPSTREAM_GATEWAY
value: "http://gateway.openfaas.svc.cluster.local:8080"
ports:
- containerPort: 8081
---
apiVersion: v1
kind: Service
metadata:
name: rbac-proxy
namespace: openfaas
spec:
selector:
matchLabels:
app: rbac-proxy
ports:
- port: 8081
targetPort: 8081
现在把oauth2-proxy的--upstream参数改成指向rbac-proxy服务:
--upstream=http://rbac-proxy.openfaas.svc.cluster.local:8081
这样,请求链路变为:用户 -> oauth2-proxy(认证)-> rbac-proxy(授权)-> OpenFaaS网关(转发函数)-> 函数Pod。
2.4 更细粒度的RBAC:用Kubernetes RBAC管理函数权限
上面的示例中,我们把RBAC策略硬编码在Go程序里。但生产环境需要动态管理,我们可以使用Kubernetes的RBAC机制,让OpenFaaS网关或中间件调用Kubernetes API来检查用户是否对某个CRD有权限。但这样会复杂很多。另一个更好的方式是,采用OpenFaaS官方的“函数访问控制”插件,或者使用OPA(Open Policy Agent)。
不过,为了保持示例简单,同时又贴近生产,我建议把RBAC策略存储在ConfigMap里,然后让Go中间件动态加载。这样运维人员修改ConfigMap,就可以调整权限,而不用重新构建镜像。
比如,定义一个ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: rbac-policies
namespace: openfaas
data:
policies.yaml: |
roles:
tenant-a-admin:
functions: ["func-a", "func-b"]
tenant-a-user:
functions: ["func-a"]
tenant-b-user:
functions: ["func-b"]
然后在Go代码中读取这个ConfigMap,建议用监听文件变化的方式动态更新。
三、技术优缺点和注意事项
3.1 这种“OAuth代理 + RBAC中间件”方案的优点
- 认证与授权分离:oauth2-proxy只负责认证,授权逻辑完全由自己控制,灵活性高。
- 兼容性强:OAuth代理支持各种身份源,RBAC也能适配大多数场景。
- 可审计:因为所有请求都会经过授权中间件,你可以在这里记录日志,跟踪谁在什么时间访问了哪个函数。
- 无需修改OpenFaaS核心:我们只是在前面加了一层,对OpenFaaS的侵入性很小。
3.2 缺点
- 性能损耗:多一次HTTP转发,多了授权检查,会有额外的延迟。当然,在内部集群中,这个延迟通常可以忽略。
- 复杂度增加:需要维护OAuth代理和授权中间件,运维成本变高。
- 策略管理还是略显原始:如果策略很多,用ConfigMap编辑效率低,也不容易做版本控制。生产环境建议用更专业的策略引擎如OPA。
3.3 注意事项
- 必须保护oauth2-proxy本身,因为它持有Client Secret和Cookie Secret,要用Kubernetes Secret存储,并设置合适的权限。
- 授权中间件必须放在受信任的网络中,不能被用户直接访问,否则用户就可以绕过授权。
- 如果OpenFaaS网关自己也开启了Basic Auth,记得关闭,否则会双重认证,而且容易造成混乱。
- 从Header中读取用户信息时,一定要信任来自oauth2-proxy的Header,防止用户伪造Header。通常oauth2-proxy会重写这些Header,并且在转发到上游之前清除所有客户端传递的同名Header。但为了安全,可以在授权中间件中再次确认请求来源IP是oauth2-proxy的Pod IP,或者使用网络策略锁死。
- 当函数需要接收用户身份时,授权中间件要把用户Header透传给函数,但也要注意防止下流服务伪造身份,可以在函数内部验证签名的令牌,而不只是依赖明文Header。
四、文章总结
生产环境的安全问题不是单靠一个组件就能解决的。OpenFaaS的网关认证提供的是“登录能力”,namespace隔离提供的是“资源隔离”,但要真正做到多租户之间的访问控制,必须把认证和授权结合起来。OAuth代理解决了认证的标准化和统一入口问题,而RBAC中间件则解决了细粒度的“谁可以调用哪个函数”的问题。这种组合虽然增加了部署的复杂度,但对于合规性要求较高的生产环境,是必不可少的。
以上示例展示了用Go语言实现一个简单的RBAC中间件,生产环境你可以换成更成熟的方案,比如使用OpenFaaS的插件生态或OPA。但核心思想都一样:不要信任网关的“认证通过”就代表授权,要在每个关键动作前显式地问一句“你有权限吗?”。
最后,安全是一个持续的过程,建议定期审计策略,监控访问日志,并做好密钥轮换。希望这篇文章能帮你在构建多租户OpenFaaS平台时,堵上这个越权漏洞。
评论
围绕“多租户使用OpenFaaS平台时,网关认证与函数namespace隔离并不能完全阻止越权,结合OAuth代理与RBAC策略控制访问边界,是生产环境必须补上的安全短板。”参与讨论