前言

想象1个袜子乱穿的场景:

预发布(Dev/Staging)环境的Pod,意外拿到了生产环境(Prod)的 Redis、AK/SK 或制品库密钥,请求瞬间打进生产系统,引发数据污染甚至服务雪崩。

在Kubernetes环境中,需要管理的敏感信息越来越多:

  • GitLab/GitHub Token

  • Docker/Harbor登录凭证

  • 制品库账号

  • 云厂商AK/SK

  • 数据库、Redis 等基础设施密钥

这些秘钥一旦泄露,或者被用错环境,带来的后果往往是生产级故障。

Kubernetes权限

K8s采用RBAC权限模型,进行K8s资源的权限管理,RBAC就是先把权限打包成角色,再把角色发给人/人群/系统。

RBAC的设计原则是最小权限原则:每个Subject只获取完成任务所需的最少权限,避免滥用Role。

ServiceAccount决定你是谁,RBAC决定你能干什么?

RBAC组成

RBAC(基于角色的访问控制)主要由三类核心对象组成:

  • Role / ClusterRole:权限集合

  • RoleBinding / ClusterRoleBinding:绑定权限集合到Subject

  • Subject:人/人群/系统

RBAC 概念Django / WebKubernetes
主体(Subject) User User / ServiceAccount
角色(Role) Role 表 Role / ClusterRole
权限(Permission) Permission 表 rule(verbs + resources)
绑定关系 UserRole RoleBinding / ClusterRoleBinding
校验位置 代码里 API Server

 

Subject(权限承载者)

Subject指的是权限的接收者,可分为以下3类:

  • User:人类用户
  • Group:用户组
  • ServiceAccount:Kubernetes 内的系统身份(通常用于 Pod)

本质:Role/ClusterRole 是钥匙,Subject 是持钥者,RoleBinding 是钥匙发放单。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app1-sa    # SA 名称
  namespace: dev   # 所在 Namespace
#--------------
apiVersion: v1
kind: Pod
metadata:
  name: app1
  namespace: dev
spec:
  serviceAccountName: app1-sa   # 指定 SA
  containers:
  - name: app1-container
    image: nginx

查看当前Pod使用的SA

$ winpty kubectl.exe exec -it shouwang-7d98479d99-qp99j -n ops-dev -- //bin/sh
/app # cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6InlYUEQ0NXdSRVllVkVKSzE5dHJNY3l2Umxvd3FPZ1NwSmFQR0lPSENBVTAifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjIl0sImV4cCI6MTc5OTI5MDM1OSwiaWF0IjoxNzY3NzU0MzU5LCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMuZGVmYXVsdC5zdmMiLCJqdGkiOiJhODc3MzE4Zi1iZTYxLTRjMWQtOWRhZS0yMTBiMmZiNzIyNjkiLCJrdWJlcm5ldGVzLmlvIjp7Im5hbWVzcGFjZSI6Im9
wcy1kZXYiLCJub2RlIjp7Im5hbWUiOiJhcC1ub3J0aGVhc3QtMS4xMC4xMjcuMC4xMzUiLCJ1aWQiOiI1ODI5ZDFhNy0zMGQ5LTQzZTMtYjUyMC02Y2MxNjI2Zjg0MDkifSwicG9kIjp7Im5hbWUiOiJzaG91d2FuZy03ZDk4NDc5ZDk5LXFwOTlqIiwidWlkIjoiOTZkMjE3NzUtMGU1OC00YTVkLTlhZjUtNGQ4ZDNmYjQ4NzM0In0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiNTY5OGZlMDItODg5Ny00MzM0LThmNDctMTEzZDNhO
DhkNmZiIn0sIndhcm5hZnRlciI6MTc2Nzc1Nzk2Nn0sIm5iZiI6MTc2Nzc1NDM1OSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Om9wcy1kZXY6ZGVmYXVsdCJ9.gWyPAg_39ZsqT9ZyClKH6LnS-okoFJaqXoGkG_wfz4Y7sfKbhTof4652OJV9hYy4r4uW3jxKsMQY2PfsJA0wvbmALtYE9vp8YAiEtMoeHyyjGzGp_1AMgdzrYyhCytSEC2T7hd6qQAffd3C9NJHFkP6p6EvMxSlvegfJvFRr_5ffneb4L4Gz6ICcyxs3CATmHt1K-ZJYVyAhk60q6WFGcKXL3P
aStgrZCj0GAnwzuRHTtCK_JhJkKIHEclLueEVjZ_4-52tSWH5TF6jwwIHwF5QEATvMWlUNtoUZehlC5Tzae41cFs77O2LLr64WdYeJUOLGHuffW6_XptxJqoYnMg
View Code

Role/ClusterRole(权限集合)

Role:在1个Namespace内定义的一组权限(对资源的操作权限,如 Pod、ConfigMap等)。

ClusterRole:跨 Namespace或全局权限的集合,可用于集群级别的资源。

本质:把一组具体操作(verbs)打包成角色,便于统一管理。

示例

kind: Role
metadata:
  name: pod-reader
  namespace: dev
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]

RoleBinding/ClusterRoleBinding(权限绑定)

RoleBinding:把Role赋予指定的用户/用户组/ServiceAccount,在单个Namespace 内生效。

ClusterRoleBinding:把ClusterRole 赋给用户/用户组/ServiceAccount,全局生效。

本质:把权限集合真正授予谁,实现授权。

示例

kind: RoleBinding
metadata:
  name: read-pods
  namespace: dev
subjects:
- kind: ServiceAccount
  name: app1-sa
  namespace: dev
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

云原生秘钥管理系统

本方案并非要替代成熟的秘钥管理系统,而是在工程实践中,利用Kubernetes已有的身份能力,解决秘钥乱用这一最常见、也最致命的问题。

1.namespace命名即环境规范

ops-dev
ops-prod
ops-test

2.客户端

对于需要长期运行的客户端服务,通常将客户端作为Pod的Sidecar部署,以持续刷新和注入短期凭证;

而对于1次性访问的场景,可以采用InitContainer部署客户端。(DevOps 系统通常只需一次性部署,因此适合使用 Init Container 方式。)

每次访问秘钥管理系统时,客户端会先读取本地挂载的PodSAToken再用PodSAToken请求秘钥管理系统获取短期凭证。

3.秘钥分发服务端

SSO服务保管私钥+公钥用它签发和验证JWT

秘钥分发服务持有SSO公钥只能验证JWT签名

优点:

对客户端进行JWT和ServiceAccount双重身份验证

秘钥分发服务无法伪造JWT也无法冒充SSO

安全性高,适合外部客户端和多个服务

服务端先验证客户端JWT是否合法?反查K8s的ApiServer判断Pod身份是否合法?

全部验证通过后,基于ns和serviceAccount授权秘钥。

关于秘钥生效时间

很多人误以为Conjur可以让固定密码短期生效,实际上它只是安全管理和分发密钥,至于这个密钥是长期有效还是短期有效,是由源系统本身决定的。

当Secret来源于阿里云STS服务等短期凭证系统时,凭证的生成与刷新由上游控制面服务(客户端)负责,如果凭证需要更新就需要重新写入CyberArkConjurSecretManagement,Conjur仅负责对最新凭证的安全存储与分发

from kubernetes import client, config

client_token = """eyJhbGciOiJSUzI1NiIsImtpZCI6InlYUEQ0NXdSRVllVkVKSzE5dHJNY3l2Umxvd3FPZ1NwSmFQR0lPSENBVTAifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjIl0sImV4cCI6MTc5OTI5MDM1OSwiaWF0IjoxNzY3NzU0MzU5LCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMuZGVmYXVsdC5zdmMiLCJqdGkiOiJhODc3MzE4Zi1iZTYxLTRjMWQtOWRhZS0yMTBiMmZiNzIyNjkiLCJrdWJlcm5ldGVzLmlvIjp7Im5hbWVzcGFjZSI6Im9
wcy1kZXYiLCJub2RlIjp7Im5hbWUiOiJhcC1ub3J0aGVhc3QtMS4xMC4xMjcuMC4xMzUiLCJ1aWQiOiI1ODI5ZDFhNy0zMGQ5LTQzZTMtYjUyMC02Y2MxNjI2Zjg0MDkifSwicG9kIjp7Im5hbWUiOiJzaG91d2FuZy03ZDk4NDc5ZDk5LXFwOTlqIiwidWlkIjoiOTZkMjE3NzUtMGU1OC00YTVkLTlhZjUtNGQ4ZDNmYjQ4NzM0In0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiNTY5OGZlMDItODg5Ny00MzM0LThmNDctMTEzZDNhO
DhkNmZiIn0sIndhcm5hZnRlciI6MTc2Nzc1Nzk2Nn0sIm5iZiI6MTc2Nzc1NDM1OSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Om9wcy1kZXY6ZGVmYXVsdCJ9.gWyPAg_39ZsqT9ZyClKH6LnS-okoFJaqXoGkG_wfz4Y7sfKbhTof4652OJV9hYy4r4uW3jxKsMQY2PfsJA0wvbmALtYE9vp8YAiEtMoeHyyjGzGp_1AMgdzrYyhCytSEC2T7hd6qQAffd3C9NJHFkP6p6EvMxSlvegfJvFRr_5ffneb4L4Gz6ICcyxs3CATmHt1K-ZJYVyAhk60q6WFGcKXL3P
aStgrZCj0GAnwzuRHTtCK_JhJkKIHEclLueEVjZ_4-52tSWH5TF6jwwIHwF5QEATvMWlUNtoUZehlC5Tzae41cFs77O2LLr64WdYeJUOLGHuffW6_XptxJqoYnMg"""

POLICY = {
    ("ops-dev", "default"): {
        "env": "dev",
        "allow_secrets": ["redis-dev"],
    },
    ("ops-prod", "default"): {
        "env": "prod",
        "allow_secrets": ["redis-prod"],
    },
}

config.load_kube_config()

auth_api = client.AuthenticationV1Api()

resp = auth_api.create_token_review(
    body=client.V1TokenReview(spec=client.V1TokenReviewSpec(token=client_token))
)

print(resp.status.authenticated)
username = resp.status.user.username
_, _, namespace, sa_name = username.split(":")
extra = resp.status.user.extra or {}
policy = POLICY.get((namespace, sa_name))
print(policy)
conjur核心逻辑

 

 

 

 

 

 

 

参考

posted on 2026-01-06 09:12  Martin8866  阅读(1)  评论(0)    收藏  举报