![对Kubernetes RBAC授权防御&攻击的部分思考插图 对Kubernetes RBAC授权防御&攻击的部分思考插图](https://blog.eswlnk.com/wp-content/uploads/wpcy/77b689cfdf76d6d326b3ec1c62111a8a.jpg)
写在前面
不久前看到一篇对Kubernetes授权管理的文章,笔者而后进行一些实验和思考,因此诞生了这篇学习笔记。本文章思路未必贴合实际应用场景,有概念错误的地方还望多多指正。
首先笔者对于Kubernetes用户的授权分为两个主体:对基础设施
的权限授予和对服务
的权限授予
分类讨论
0x01-基础设施
基础设施
的权限,理想情况下按照集群结构划分:
- master节点使用admin用户账户权限,这个权限是最大的
- node节点使用普通用户权限,普通用户权限仅能对集群中的部分资源控制,或者是对某个namespcace中的资源进行控制
0x02-服务
什么叫做对服务
进行划分呢?云计算的初衷是优雅地调配庞大规模的服务群,那么运维人员就需要对A、B、C…这些服务(Service)能够操纵集群权限这个能力进行考量,假如Service A由三台nginx容器构成,可能就不需要什么集群服务的资源。对于B服务而言,其定位是用来监控集群主机健康状态的,所以就需要很强的CURD
权限,起码是对Pod
、Deployment
有管理权限。
在Kubernetes
中,每个服务容器都会被kubelet
下发一个默认的ServiceAccount
账户(后文简称SA),而运维人员可以指定服务容器注入哪个SA账户。这意味着运维人员可以将不同SA账户注入给各类服务容器,来达到各类服务(A、B、C)权限细粒度化。
0x03-归根到底
基础设施
和服务
的权限授予有相同之处,但又不完全相同。它们相同的地方在于概念,都是为了控制对集群资源操纵的能力,这一过程在Kubernetes
中叫做授权(Authoriazation),可以参考下图;不同的地方在于控制单元,基础设施的权限是以用户账户(User Account)的权限为单位的,而服务的权限是以SA账户(ServiceAccount)为单位划分的。
那么,有没有办法来优雅地控制对这两种权限授予方式的统一呢?答案很显然是有,而且不止一种。这里摘取一种比较好的做法:通过ClusterRole
分发权限。
例如管理员事先建立Pod-read-Rule
、Pod-Write-Rule
规则,分别用来制约对Pod容器可读、可写这两种操作
,当然集群的资源可不止有Pod容器,我们可以通过kubectl api-resources
例举哪些资源可以被ClusterRole
约束。
而后,通过RoleBinding
或者ClusterBinding
的方式将这两个ClusterRole
分发给不同的User Account
或者ServiceAccount
,相当于是把锅碗瓢盆给到不同的角色。
假设一个场景,当User Account A
需要拥有所有Pod的可读权限,那么管理员(kubernetes admin)就通过RoleBinding
的形式将Pod-read-Rule
绑定给User Account A
;当User Account A
再想要要所有Pod的可写权限时,以相同的RoleBinding
方式将Pod-Write-Rule
绑定给User Account A
即可。
最终,我们创建一个理想化的场景:把集群内所有可利用资源(api-resources)的CURD
操作分别建立为不同的ClusterRole
,在创建Service Account
与User Account
时赋予它们最小权限,指定最少范围的namespace
,最好只作用于default namespcace。而后当SA
或者UA
需要对应权限时,管理员再经过审核制RoleBinding
,分配ClusterRole
给这些SA
或者UA
,而不是在初始化时就把权限塞满。
![对Kubernetes RBAC授权防御&攻击的部分思考插图3 Untitled](https://static.esw.eswlnk.com/2023/07/20230710044522817.png-esw)
实践
下面以两个实践为例,理解前文提到的权限分发思路。表格为实验拓扑:一台Centos7.x作为Master节点,剩下两台作为Node节点
主机 | IP地址 | 内核版本 | 用户账户 |
---|---|---|---|
k8s-master | 192.168.56.80 | 3.10.0-1160.el7.x86_64 | kubernetes-admin |
k8s-node1 | 192.168.56.81 | 3.10.0-1160.el7.x86_64 | userA |
k8s-node2 | 192.168.56.82 | 3.10.0-1160.el7.x86_64 | userB |
实践场景1-使用UserAccount
master节点使用kubernetes-admin用户进行管理,其他两个node节点使用userA、userB用户进行管理,且需要满足的条件:
- kubernetes-admin具有对集群操纵的全部权限
- userA用户仅拥有对tenantA命名空间的Pod容器完全操作权限
- userB用户仅拥有对tenantB命名空间的Pod容器完全操作权限
分析:由于master
节点的用户配置文件/etc/kubernetes/admin.conf
默认为kubernetes-admin权限,所以我们无需关心,只需满足UserA、B的需求。
第一步:建立两个Kubernetes User Account分别为userA、userB,参考文章:k8s创建用户账号——User Account – fat_girl_spring – 博客园 (cnblogs.com)。完整的创建过程如下,以相同的方式创建两个用户即可
[root@k8s-master tenantA]# (umask 077;openssl genrsa -out userA.key 2048)
[root@k8s-master tenantA]# openssl req -new -key userA.key -out userA.csr -subj "/O=k8s/CN=userA"
[root@k8s-master tenantA]# openssl x509 -req -in userA.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out userA.crt -days 365
[root@k8s-master tenantA]# kubectl config set-cluster k8s --server=https://192.168.56.80:6443 --certificate-authority=/etc/kubernetes/pki/ca.crt --embed-certs=true --kubeconfig=/root/k8s_tenants/userA/userA.conf
[root@k8s-master tenantA]# kubectl config set-credentials userA --client-certificate=userA.crt --client-key=userA.key --embed-certs=true --kubeconfig=/root/k8s_tenants/userA/userA.conf
[root@k8s-master tenantA]# kubectl config set-context kubernetes-userA@k8s --cluster=k8s --user=userA --kubeconfig=/root/k8s_tenants/userA/userA.conf
[root@k8s-master tenantA]# kubectl config use-context kubernetes-userA@k8s --kubeconfig=/root/k8s_tenants/userA/userA.conf
[root@k8s-master tenantA]# kubectl get pods --kubeconfig ./userA.conf
第二步:创建两个namespace
给userA
、userB
使用,这里笔者模拟的场景是租户根据namespace
隔离
[root@k8s-master ~]# kubectl create namespace tenant-a
namespace/tanent-a created
[root@k8s-master ~]# kubectl create namespace tenant-b
namespace/tanent-b created
第三步:创建ClusterRole
描述Pod容器完全操作权限
# cluster_pod_all_permission_role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-pod-all-permission-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
第四步:分别将cluster-pod-all-permission-role
权限绑定给用户A/B,并指定相对应的命名空间
# usera_pod_rolebiding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenanta-pod-all-permission-role-binding
namespace: tenant-a
subjects:
- kind: User
name: userA
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-pod-all-permission-role
apiGroup: rbac.authorization.k8s.io
---
# userb_pod_rolebiding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenantb-pod-all-permission-role-binding
namespace: tenant-b
subjects:
- kind: User
name: userB
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-pod-all-permission-role
apiGroup: rbac.authorization.k8s.io
没有看错,我们使用的是RoleBinding
而不是ClusterRolebinding
来完成权限绑定。前文提到,RoleBinding
是对某个namespace
进行的权限绑定,而ClusterRolebinding
是对整个集群做权限绑定,可以在RoleBinding
时可以通过roleRef
引用ClusterRolebinding
,从而将集群的限制策略转为对namespace
的权限绑定
![对Kubernetes RBAC授权防御&攻击的部分思考插图4 Untitled](https://static.esw.eswlnk.com/2023/07/20230710044522107.png-esw)
第五步:使用kubectl apply描述资源对象,就能向userA
、userB
赋予了两个不同namespace
的Pod完全执行权限,过程如下:
[root@k8s-master clusterrole]# kubectl apply -f cluster_pod_all_permission_role.yaml
clusterrole.rbac.authorization.k8s.io/cluster-pod-all-permission-role created
[root@k8s-master clusterrole]# kubectl apply -f user_pod_rolebind.yaml
rolebinding.rbac.authorization.k8s.io/tenanta-pod-all-permission-role-binding unchanged
rolebinding.rbac.authorization.k8s.io/tenantb-pod-all-permission-role-binding created
最后:我们来验证一下两个用户是否只能在当前租户空间(namespace)中操作Pod容器。如下图所示,k8s-node1使用userA作为用户账户,k8s-node2使用userB作为用户账户,它们仅能操作自身所在的namespace,除此之外无法访问任何namespace
![对Kubernetes RBAC授权防御&攻击的部分思考插图5 Untitled](https://static.esw.eswlnk.com/2023/07/20230710044522364.png-esw)
![对Kubernetes RBAC授权防御&攻击的部分思考插图6 Untitled](https://static.esw.eswlnk.com/2023/07/20230710044522426.png-esw)
实践场景2-使用ServiceAccount
在真实的渗透场景中,攻击者大多是通过Web网站拿到了某台容器的控制权限。在Kubernetes集群中,每台容器在启动时会被注入default权限的ServiceToken
,位置在/var/run/secrets/kubernetes.io/serviceaccount,它是ServiceAccount
(后文简称SA)的凭证
![对Kubernetes RBAC授权防御&攻击的部分思考插图7 Untitled](https://static.esw.eswlnk.com/2023/07/20230710044525606.png-esw)
为了展示较大权限的SA都能做哪些事情,现在我们在tanent-a
命名空间下创建一个Thinkphp Web Service,对外暴露30080
端口。其Pod容器使用的SA定义为phpsa
,并且phpsa
具有对当前tanent-a
命名空间下Pod完全执行权限。
创建Service
第一步创建phpsa,并且赋予其tanent-a命名空间下对Pod容器完全执行权限
#phpsa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: phpsa
namespace: tenant-a
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: phpsa-role-binding
namespace: tenant-a
subjects:
- kind: ServiceAccount
name: phpsa
namespace: tenant-a
roleRef:
kind: ClusterRole
name: cluster-pod-all-permission-role
apiGroup: rbac.authorization.k8s.io
第二步,通过如下资源描述文件创建漏洞环境的Deployment
,将其暴露为Service
服务。指定Pod容器默认注入的SA账户为phpsa
apiVersion: apps/v1
kind: Deployment
metadata:
name: thinkphp5023-rce
namespace: tenant-a
labels:
app: thinkphp5023-rce
spec:
replicas: 2
selector:
matchLabels: # 跟template.metadata.labels一致
app: thinkphp5023-rce
template:
metadata:
labels:
app: thinkphp5023-rce
spec:
containers:
- name: thinkphp5023-rce-container
image: vulhub/thinkphp:5.0.23
ports:
- containerPort: 80
name: thinkphp-port
serviceAccount: phpsa
---
apiVersion: v1
kind: Service
metadata:
name: thinkphp5023-vulnerable-service
namespace: tenant-a
spec:
type: NodePort
selector: # 更Deployment中的selector一致
app: thinkphp5023-rce
ports:
# By default and for convenience, the `targetPort` is set to the same value as the `port` field.
- port: 80
name: thinkphp-port
# Optional field
# By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
nodePort: 30081
![对Kubernetes RBAC授权防御&攻击的部分思考插图8 Untitled](https://static.esw.eswlnk.com/2023/07/20230710044525454.png-esw)
到这里,一个具有RCE漏洞的thinkphp5.0.23版本就搭建完毕了,攻击者可以通过thinkphp5.xRCE漏洞建立reverse shell
或上传webshell
后渗透利用
假设我们已经拿到thinkphp
服务的容器权限,那么在后续的实战中,攻击者大多通过cdk
等工具枚举当前SA账户phpsa
都具有哪些权限
但cdk
做的事情比较有限,如下图cdk
代码段所示,它通过访问api/v1/namespaces
端点来断言自己是否拥有list namespaces
权限,但却没有甄别当前SA账户处于哪个namespace
下。
由于当前容器的服务账号为phpsa
,具备cluster-pod-all-permission-role
权限,理论上拥有tenant-a
命名空间下Pod容器完全执行权限,如果仅用cdk
当脚本小子,无疑浪费大好的攻击机会。
但话说回来,攻击者在此面临两个问题:
- 盲视野攻击的状态下我们怎么能知道当前SA可以操作哪些命名空间呢?
- 如何确定SA账户所在的命名空间呢?
对于第一个问题,笔者暂时没有找到很好的解决方案。而第二个问题有一种取巧的解决方法:当我们尝试去访问api server
中任何越权的操作端点时,api server
会返回serviceaccount:x:y cannot list resource
错误,此时x代表当前SA账户所在的namespace
,y就代表当前SA账户的用户名
提取当前SA账户的namespace
所属为tenant-a
,接着就能列出命名空间tenant-a
中的所有容器,也能进行后续的渗透操作
curl --cacert ./ca.crt --header "Authorization: Bearer $(cat ./token)" -X GET https://kubernetes.default.svc/api/v1/namespaces/tenant-a/pods
![对Kubernetes RBAC授权防御&攻击的部分思考插图12 Untitled](https://static.esw.eswlnk.com/2023/07/20230710044527177.png-esw)
写在最后
本文对理想条件下的Kubernetes
授权进行了部分探究,但笔者比较好奇的是,业务态较广的云上业务,服务账号的授权行为不可能做到如此细粒度。那么有没有更快、更安全的做法将user group、namespace、准入控制玩出花活儿,从而让授权过程扁平化一些?先埋一个坑,等笔者有时间再探究一下
📮评论