前言
想象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 / Web | Kubernetes |
|---|---|---|
| 主体(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
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)
参考
浙公网安备 33010602011771号