云原生安全Kubernetes基础篇

TL;DR

本文记录了我在学习Kubernetes及云原生架构安全中的笔记,大部分内容均来自Kubernetes官方文档,作为一个初学者非常推荐阅读官方文档来系统地学习Kubernetes,而本文的内容只是总结了部分和安全相关的知识点。

Kubernetes对象

(引用自:资源对象与基本概念解析 · Kubernetes Handbook - Kubernetes 中文指南/云原生应用架构实践手册 · Jimmy Song

类别 名称
资源对象 Pod、ReplicaSet、ReplicationController、Deployment、StatefulSet、DaemonSet、Job、CronJob、HorizontalPodAutoscaling、Node、Namespace、Service、Ingress、Label、CustomResourceDefinition
存储对象 Volume、PersistentVolume、Secret、ConfigMap
策略对象 SecurityContext、ResourceQuota、LimitRange
身份对象 ServiceAccount、Role、ClusterRole

可使用create、get、describe、delete、edit、replace等操纵上述对象(有的对象只支持部分操作或拥有其他对象没有的操作方法),较为通用的参数(-n 指定命名空间,-o 指定输出格式),对象的操作都是在默认命名空间下执行的,可指定--all-namespaces参数显示指定所有的命名空间。

部分对象示例:

1
2
# 查看所有节点
kubectl get node

image-20210910214006268

1
2
# 查看所有Pod
kubectl get pod [-o wide] [--all-namespace | -n namespace]

image-20210910214321106

1
2
# 查看暴露的服务
kubectl get servcie

image-20210910214621561

1
2
# 查看服务账户, 下述的命令指定了命名空间以及输出格式为yaml
kubectl get serviceaccount -n kubernetes-dashboard kubernetes-dashboard -o yaml

输出的资源清单中包含了该帐号所属的命名空间,元数据中包含了一些注释信息,以及对应的secret对象。

image-20210910214954475

1
2
# secret对象一般存储K8s集群中的证书、token等
kubectl get secret -n kubernetes-dashboard kubernetes-dashboard-token-tpzmz -o yaml

该token可用于Kubernetes API认证,Kubernetes默认使用RBAC鉴权(当使用kubectl命令时其实是底层通过证书认证的方式调用Kubernetes API)。

image-20210910215420010

1
2
# ConfigMap对象一般存储应用配置信息, 在创建Pod时或在容器中可以引用这些配置信息
kubectl get configMap --all-namespaces

image-20210910220103754

RBAC鉴权

基于角色(Role)的访问控制(RBAC)是一种基于组织中用户的角色来调节控制对 计算机或网络资源的访问的方法。
RBAC API 声明了四种 Kubernetes 对象:RoleClusterRoleRoleBindingClusterRoleBinding。你可以像使用其他 Kubernetes 对象一样, 通过类似 kubectl 这类工具 描述对象, 或修补对象。

先说RoleClusterRole的区别,Role用于在某个命名空间中设置访问权限,而ClusterRole用于设置集群范围内的访问权限(跨命名空间及非资源端点),简单理解只是作用域不同而已,ClusterRole作用域更加广泛。

再来说说RoleBindingClusterBinding,其实跟上面的描述差异不大,下面是官方文档的描述:

角色绑定(Role Binding)是将角色中定义的权限赋予一个或者一组用户。 它包含若干 主体(用户、组或服务账户)的列表和对这些主体所获得的角色的引用。 RoleBinding 在指定的名字空间中执行授权,而 ClusterRoleBinding 在集群范围执行授权。

角色绑定可以引用同一命名空间下的任何角色,或者角色绑定可以引用集群角色(ClusterRole),这样一来能够将集群角色(ClusterRole)绑定到RoleBinding所在的命名空间(其作用域就不再是所有命名空间了)。如果需要将某个集群角色(CLusterRole)绑定到所有命名空间,则需要使用ClusterBinding

Role示例

1
2
3
4
5
6
7
8
9
10
# pod-reader角色只拥有pod的读访问权限
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""] # "" 标明 core API 组
resources: ["pods"]
verbs: ["get", "watch", "list"]

RoleBinding示例

下面的例子中的 RoleBinding 将 “pod-reader” Role 授予在 “default” 名字空间中的用户 “jane”。 这样,用户 “jane” 就具有了读取 “default” 名字空间中 pods 的权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: rbac.authorization.k8s.io/v1
# 此角色绑定允许 "jane" 读取 "default" 名字空间中的 Pods
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
# 你可以指定不止一个“subject(主体)”
- kind: User
name: jane # "name" 是区分大小写的
apiGroup: rbac.authorization.k8s.io
roleRef:
# "roleRef" 指定与某 Role 或 ClusterRole 的绑定关系
kind: Role # 此字段必须是 Role 或 ClusterRole
name: pod-reader # 此字段必须与你要绑定的 Role 或 ClusterRole 的名称匹配
apiGroup: rbac.authorization.k8s.io

ClusterRole

下面是一个 ClusterRole 的示例,可用来为任一特定名字空间中的 Secret 授予读访问权限, 或者跨名字空间的访问权限(取决于该角色是如何绑定的):

1
2
3
4
5
6
7
8
9
10
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
# "namespace" 被忽略,因为 ClusterRoles 不受名字空间限制
name: secret-reader
rules:
- apiGroups: [""]
# 在 HTTP 层面,用来访问 Secret 对象的资源的名称为 "secrets"
resources: ["secrets"]
verbs: ["get", "watch", "list"]

ClusterBinding

要跨整个集群完成访问权限的授予,你可以使用一个 ClusterRoleBinding。 下面的 ClusterRoleBinding 允许 “manager” 组内的所有用户访问任何名字空间中的 Secrets。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: rbac.authorization.k8s.io/v1
# 此集群角色绑定允许 “manager” 组中的任何人访问任何名字空间中的 secrets
kind: ClusterRoleBinding
metadata:
name: read-secrets-global
subjects:
- kind: Group
name: manager # 'name' 是区分大小写的
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: secret-reader
apiGroup: rbac.authorization.k8s.io

配置不当导致集群接管

当使用K8s提供的Dashboard(Web面板)来管理集群时,错误的配置将导致集群被接管,在K8s Master中可以使用如下命令安装Kubernetes官方提供的Dashboard。

1
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.3.1/aio/deploy/recommended.yaml

在官方提供的Yaml文件中可以看到其内置了一些默认帐号,而当使用K8s面板时开启enable-skip-login将会使用默认的kubernetes-dashboard帐号登陆。(面板通过服务暴露的方式映射为8443端口,如下图41-42行所示)

image-20210908205610062

而官方提供的示例文件中kubernetes-dashboard将拥有以下权限。

image-20210908210545342

案例演示

在学习Kubernetes的过程中总避免不了复杂的环境搭建,为了避免麻烦我将使用katacoda提供的在线演示环境,在其第一节环境中你就能够体验部署Kubernetes Dashboard的过程,当你部署完毕时访问Dashboard的url,你只需要点击Skip就能够登陆到Dashboard中。

image-20210908212745057

并且拥有所有权限。

image-20210911105029930

而其中的原因则来自于创建Dashboard Pod时开启的enable-skip-login以及授予默认账户的管理员权限。

image-20210908212410276

如何查找该Pod文件?只需要执行如下命令。

1
2
3
4
# 该命令将显示所有命名空间下的Pod,你只需要找到kubernetes-dashboard命名空间下以kubernetes-dashboard命名的Pod即可
kubectl get pods --all-namespaces
# 查看该Pod的配置清单,你可以使用-o 参数指定yaml格式
kubectl get pod -n kubernetes-dashboard kubernetes-dashboard-79d9cd965-bspdr -o yaml

查看默认的kubernetes-dashboard的账户权限可以通过查询(内置账户都是ServiceAccount,由K8s集群管理)ServiceAccountClusterRoleBindingRoleBinding等资源对象。

image-20210911104400622

引用的类型为集群角色,该角色名为cluster-admin(Kubernetes内置的集群超管角色)。

image-20210911104614675

拥有所有对象的操纵权限。

image-20210911104733360

Node逃逸

当通过Kubernetes Dashboard或其他攻击方式能够创建Pod或控制某个容器时,下一步的横向渗透将会是思考如何逃逸至(Node)节点中。在容器中的节点逃逸与平常的Docker逃逸并无差异(逃逸宿主机),而当获取到Kubernetes Dashboard权限时逃逸将变得更加轻松不过了,你只需要创建一个Pod,在容器中挂载宿主机的根目录以写入SSH公钥/定时任务等方式即可实现逃逸,下面的资源清单将演示这一过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1 # 版本
kind: Pod # 类型
metadata: # 元数据
name: test-pod # Pod名称
namespace: default # 命名空间
spec:
# 挂载卷
volumes:
- name: xx
hostPath:
path: /
containers:
- image: nginx
name: nginx-proxy
volumeMonuts:
# 容器中的挂载路径
- mountPath: /tmp/rr
name: xx

上述的Pod创建完毕运行后将把宿主机的根目录挂载到自己的/tmp/rr目录下,如果你想指定逃逸某个指定的Node或Master时,将会在下一节Pod的(亲和性、反亲和性)与(污点、容忍度)中详细说明。

亲和性与反亲和性

如果你的Kubernetes集群拥有多个Node(节点),你可能需要在创建Pod时指定由哪个Node调度该Pod。比较简单的方式是你可以通过nodeName属性和nodeSelector属性来指定Node,尝试创建下述的资源清单并运行该Pod。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
nodeName: node01
containers:
- image: nginx
name: web1
imagePullPolicy: IfNotPresent

如果node01节点满足Pod的资源需求,则节点会运行该Pod。而nodeSelector与上述功能相同,也能够将Pod分配给指定的节点调度,其通过匹配标签(Label)的形式分配。

1
2
# 该命令为node01节点添加一个标签
kubectl label nodes node01 disktype=ssd

image-20210911112003477

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- image: nginx
name: web1
imagePullPolicy: IfNotPresent
nodeSelector:
disktype: ssd

节点亲和性

亲和性相比nodeSelecetor的表现而言更加灵活,亲和性支持InNotInExistsDoesNotExistsGtLt等操作符。其类型可区分为requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution,前者为必须满足的要求,而后者则无需强制满足。

下面是一个使用节点亲和性的Pod示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/e2e-az-name
operator: In
values:
- e2e-az1
- e2e-az2
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value
containers:
- name: with-node-affinity
image: k8s.gcr.io/pause:2.0

此节点亲和性表示该Pod只能被具备kubernetes.io/e2e-az-name标签且值为e2e-az1e2e-az2,在满足该条件的情况下还将在具备another-node-label-key标签且值为another-node-label-value节点上优先调度。

如果同时指定了nodeSelectornodeAffinity的话则两者都必须满足条件才会被调度。
如果同时指定了多个nodeSelectorTerms的话则只需要满足其中一个即可具备调度条件。

如果同时指定了多个matchExpressions则必须所有匹配表达式都满足才能够调度。

preferredDuringSchedulingIgnoredDuringExecution 中的 weight 字段值的 范围是 1-100。 对于每个符合所有调度要求(资源请求、RequiredDuringScheduling 亲和性表达式等) 的节点,调度器将遍历该字段的元素来计算总和,并且如果节点匹配对应的 MatchExpressions,则添加“权重”到总和。 然后将这个评分与该节点的其他优先级函数的评分进行组合。 总分最高的节点是最优选的。「kubernetes.io」

Pod间亲和性与反亲和性

Pod 间亲和性与反亲和性使你可以 基于已经在节点上运行的 Pod 的标签 来约束 Pod 可以调度到的节点,而不是基于节点上的标签。 规则的格式为“如果 X 节点上已经运行了一个或多个 满足规则 Y 的 Pod, 则这个 Pod 应该(或者在反亲和性的情况下不应该)运行在 X 节点”。 Y 表示一个具有可选的关联命令空间列表的 LabelSelector; 与节点不同,因为 Pod 是命名空间限定的(因此 Pod 上的标签也是命名空间限定的), 因此作用于 Pod 标签的标签选择算符必须指定选择算符应用在哪个命名空间。 从概念上讲,X 是一个拓扑域,如节点、机架、云供应商可用区、云供应商地理区域等。 你可以使用 topologyKey 来表示它,topologyKey 是节点标签的键以便系统 用来表示这样的拓扑域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: v1
kind: Pod
metadata:
name: with-pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S1
topologyKey: topology.kubernetes.io/zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: security
operator: In
values:
- S2
topologyKey: topology.kubernetes.io/zone
containers:
- name: with-pod-affinity
image: k8s.gcr.io/pause:2.0

在这个Pod的亲和性规则中定义了一条亲和性规则和反亲和性规则,在该示例中亲和性的规则要求满足security标签且值为S1,而反亲和性规则如果security标签为S2则Pod不应该被调度到此节点上,(topology.kubernetes.io/zone是内置的标签,表示一个可用区),更多内置标签、注解、污点,请查看完整文档:常见的标签、注解和污点 | Kubernetes


下述例子将演示在拥有三个节点的Kubernetes集群上,如何使用Pod亲和性与反亲和性让每个节点上运行一组应用(Redis + Web Server),而不会在一个节点上重复运行。(这需要用到内置标签kubernetes.io/hostname,标签值为主机名生成)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-cache
spec:
selector:
matchLabels:
app: store
replicas: 3
template:
metadata:
labels:
app: store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: redis-server
image: redis:3.2-alpine

web-serverDeployment控制器配置了Pod亲和性与反亲和性,这将通知调度器将它的所有副本与具有app=store选择器标签的Pod放置在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
selector:
matchLabels:
app: web-store
replicas: 3
template:
metadata:
labels:
app: web-store
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-store
topologyKey: "kubernetes.io/hostname"
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- store
topologyKey: "kubernetes.io/hostname"
containers:
- name: web-app
image: nginx:1.16-alpine

最终的运行结果为如下所示:

node01 node02 node03
webserver-1 webserver-2 webserver-3
cache-1 cache-2 cache-3

污点与容忍度

节点亲和性是Pod的一种属性,它使Pod被吸引到一类特定的节点上。而污点(Taint)恰恰相反,它能够使节点排斥某一类Pod。

容忍度是应用在Pod上的属性,它与污点相互配合,可以避免Pod被分配到不具备污点的节点上。每个节点都应用一个或多个污点。

1
2
3
4
# 为节点添加污点
kubectl taint nodes node01 key1=value1:NoSchedule
# 去除污点
kubectl taint nodes node01 key1=value1:NoSchedule-

image-20210911145329245

这是一个使用了容忍度的Pod,当创建时它只会被node01节点调度,因为其他节点不具备该污点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"

operator属性的默认值是Equal,这表示键的值必须与value属性的值一致。而如果operatorExists的话则容忍度不能指定value,而污点上的任意值都能够匹配该污点(前提是key匹配)。

如果一个容忍度的key为空且operatorExists,则表示这个容忍度与任意的keyvalueeffect都能够匹配,即这个容忍度能容忍任意污点(taint)。

如果污点未指定effect且容忍度中effect属性为空,则只需键匹配即可(eg: key1=value1)。

上述例子中的key1effectNoSchedule,还可以使用另一个值PerferNoSchedule,其表示尽量避免将Pod调度到存在不能容忍污点的节点上。以及NoExecute值,在不能容忍污点的节点上该Pod会被驱逐(已运行的情况下)。

示例

使用污点和容忍度能够使Pod灵活的避开某些节点或者将某些Pod从节点上驱逐。例如Kubernetes Master拥有默认的污点(这也是为什么Master上不会运行Pod的部分原因)。

image-20210911153402580

因此可以通过创建一个具有node-role.kubernetes.io/master:NoSchedule的容忍度让Pod被Kubernetes Master所调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"

在上述的Pod中如果将宿主机的根目录挂载到容器中(利用volumes与volumeMounts)即可逃逸至Kubernetes Master中接管集群。

apiserver

未授权

Kubernetes API服务器在2个端口上提供服务,分别为8080与6443(安全端口),安全端口使用TLS加密,默认IP是第一个非本地网络接口,安全端口的请求需经身份认证和鉴权组件处理,请求必须经准入控制模块处理。

而8080端口(localhost 非安全端口)默认情况下用于测试,不使用TLS加密。因此如果暴露该端口则有可能导致未经授权的请求调用API Server。

使用kubectl可以指定IP和端口调用存在未授权漏洞的API Server。

1
kubectl -s ip:port get nodes

证书认证

在Kubernetes Master中使用 kubectl 命令行工具来管理Kubernetes集群,kubectl$HOME/.kube目录中查找一个 config 的配置文件(默认情况)。你也可以使用 --kubeconfig 参数来指定其他配置文件,配置文件中包含了证书文件的地址,实际上也可以将证书内容硬编码在配置文件中(效果都是一样的)。

image-20210930112527943

Kubelet

Kubelet运行在每个Node节点上,其基于 PodSpec 工作。Kubelet通过接收api server提供的一组 PodSpec ,并确保 PodSpec 中的容器处于运行状态且运行状况良好,简言之,Kubelet负责管理容器的生命周期。

Kubelet服务默认监听10250与10255端口,前者为安全端口并使用HTTP/S,而后者则为无需身份验证/鉴权的只读端口,Kubelet的配置文件默认位于 /var/lib/kubelet/config.yaml

如果Kubelet配置了允许匿名访问的话,则会以 system:anonymous 用户名和system:unauthenticated组的身份请求API。(下图是关闭状态)

image-20210930171625968

Kubeletctl

Kubeletctl是基于Kubelet API实现的Cli工具,通过其可以快速实现对Kubelet的攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 命令示例

# 扫描Kubelet API与Kubernetes API Server
kubeletctl scan --cidr 172.17.0.1/24
# 列出Pods
kubeletctl pods
# 扫描存在RCE漏洞的Pods
kubeletctl scan rce
# 扫描所有容器中的Token
kubeletctl scan token
# 在指定容器上执行命令
kubeletctl exec "command" -p [pod_name] -c [container_name]
# 在所有Pod上运行命令
kubeletctl run "uname -a" --all-pods

总结

实际上在真正的云原声环境中渗透远不止本文所提及的内容,在云环境的渗透场景中错综复杂。但攻击者视角所关注的应远不止于容器应用中的提权(指容器逃逸),信息收集同样十分重要。

  • 在主机中全局搜索 config 配置文件(find / -iname “*config*“ | grep -RiE “server:\s*https?://.*?:8443”)。
  • 使用CDK工具进行横向渗透及信息收集

References

为Kubernetes集群添加用户 - 知乎

Kubernetes 文档 | Kubernetes

Learn Kubernetes using Interactive Browser-Based Labs | Katacoda

Kubernetes API Reference Docs

kubernetes(k8s)kubectl使用详解 - 好库文摘

说明 · Kubernetes 加固指南(美国国家安全局出品)- Jimmy Song