Kubernetes安全

机制说明

Kubernetes作为一个分布式集群的管理工具,保证集群的安全性是一个很重要的任务,API Server是集群内部各个组件通信的中介,也是外部控制的入口,因此Kubernetes的安全机制基本就是围绕保护API Server来设计的,在Kubernetes中,采用了认证(Authentication)、鉴权(Authorization)、准入控制(Admission Controll)三步来保证API Server的安全。

Kubernetes API访问控制

用户使用kubectl、客户端库或构造REST请求来访问Kubernetes API。用户和Kubernetes服务账户都可以被鉴权访问API。当请求到达API时,会经历多个阶段,如下图所示

4

在典型的Kubernetes集群中,API服务器在442端口上提供服务,受TLS保护,API服务器出出示证书。该证书可以使用私有证书颁发机构(CA)签名,也可以基于链接到公认的CA的公钥基础架构签名。如果你的集群使用私有证书颁发机构,你需要在客户端的~/.kube/config文件中提供该证书的副本,以便你可以信任该链接并确认连接没有被拦截。

Authentication

在Kubernetes中,认证分为下面三种

  • HTTP Token认证:通过一个Token来识别合法用户
    • HTTP Token的认证是用一个很长的特殊编码方式的并且难以被模仿的字符串,即Token来表达客户的一种方式,Token是一个很长的很复杂的字符串,每一个Token对一个一个用户名存放在API Server能访问的文件中,当客户端发起API调用请求时,需要在HTTP Header里放入Token
  • HTTP Base认证:通过用户名+密码的方式认证
    • 用户名+密码采用Base64算法进行编码后的字符串放在HTTP Request中的Heather Authorization域里发送给服务端,服务端收到后进行编码,获取用户名及密码。
  • HTTPS证书认证:基于CA根证书签名的客户端身份认证方式。这种方式最为严格,对于上面的两种方式,相当于只做了服务端认证客户端,并没有做客户端对服务端的认证。
需要认证的节点
  • Kubernetes组件对API Server的访问:kubectlController ManagerSchedulerkubeletkube-proxy
  • Kubernetes管理的Pod对容器的访问:Pod(dashboard也是以Pod形式运行)
安全性说明
  • Controller ManagerSchedulerAPI Server在同一台机器,所以直接使用API Server的非安全端口访问,--insecure-bind-address=127.0.0.1
  • kubectlkubeletkube-proxy访问API Server都需要证书进行HTTPS双向认证
证书颁发
  • 手动签发:通过Kubernetes集群根CA进行签发HTTPS证书
  • 自动签发:kubelet首次访问API Server时,使用Token做认证,通过后,Controller Manager会为kubelet生成一个证书,以后的访问都是用证书做认证。
kubeconfig

kubeconfig文件包含集群参数(CA证书,API Server地址),客户端参数(生成的证书和私钥),集群的context信息(集群名称、用户名)。Kubernetes组件通过启动时指定不同的kubeconfig文件可以切换到不同的集群。

查看kubeconfig文件

1

ServiceAccount

Pod中的容器访问API Server,因为Pod的创建、销毁是动态的,因此要为它手动生成证书就比较麻烦,Kubernetes使用了SA解决Pod访问API Server的认证问题。

Secret与SA的关系

Kubernetes设计了一种资源对象叫做Secret,分为两类,一种是用于ServiceAccountserver-account-token,另一种是用于保存用户自定义保密信息的OpaqueServiceAccount中包含三个部分。Token、ca.crt、namespace

  • Token是使用API Server私钥签名的JWT,用于访问API Server时,Server端认证
  • ca.crt根证书,用于Client端验证API Server发送的证书
  • namespace标识这个service-account-token的作用域名空间

查看ServiceAccount

默认情况下,每个namespace都会有一个ServiceAccount,如果Pod在创建时没有指定ServiceAccount,就会使用Pod所属的namespace的ServiceAccount。默认的挂载目录是/run/secrets/kubernetes.io/serviceaccount/

首先我们查看一下当前存在的命名空间

1
2
3
4
5
6
caoyifan@MacBookPro .kube % kubectl get ns
NAME STATUS AGE
default Active 18d
kube-node-lease Active 18d
kube-public Active 18d
kube-system Active 18d

接着查看命名空间为kube-system下的pod

1
2
3
4
5
6
7
8
9
10
11
caoyifan@MacBookPro ~ % kubectl get pod -n kube-system
NAME READY STATUS RESTARTS AGE
coredns-558bd4d5db-fpmvp 1/1 Running 6 18d
coredns-558bd4d5db-m8zcd 1/1 Running 6 18d
etcd-docker-desktop 1/1 Running 6 18d
kube-apiserver-docker-desktop 1/1 Running 7 18d
kube-controller-manager-docker-desktop 1/1 Running 6 18d
kube-proxy-zw8fj 1/1 Running 6 18d
kube-scheduler-docker-desktop 1/1 Running 52 18d
storage-provisioner 1/1 Running 56 18d
vpnkit-controller 1/1 Running 1034 18d

之后进入名为kube-proxy-zw8fj的pod中

1
kubectl exec -it kube-proxy-zw8fj -n kube-system -- /bin/sh

进入到挂载目录并查看token的详细信息

2

Authorization

在认证过程中,只是通信双方确认了对方是可信的,并且可以相互通信,而鉴权是确定请求方有哪些资源的权限,API Server目前支持以下几种授权策略(通过API Server的启动参数--authorization-mode设置)

  • AlwaysDeny:表示拒绝所有的请求,一般用于测试
  • AlwaysAllow:允许接收所有请求,如果集群不需要授权流程,则可以采用该策略
  • ABAC(Attribute-Based Access Control):基于属性的访问控制,表示使用用户配置的授权规则对用户请求进行匹配和控制
  • Webhook:通过调用外部REST服务对用户进行授权
  • RBAC(ROle-Based Access Control):基于角色的访问控制,现行默认规则
RBAC授权模式

RBAC在Kubernetes1.5中引入,现行版本成为默认标准,相对其他访问控制方式,拥有以下优势

  • 对集群中的资源和非资源均拥有完整的覆盖
  • 整个RBAC完全由几个API对象完成,同其他API对象一样,可以用kubectl或API进行操作
  • 可以在运行时进行调整,无需重启API Server

RBAC的API资源对象说明

RBAC引入了4个新的顶级资源对象:RoleClusterRoleRoleBindingClusterRoleBinding。四种对象类型均可以通过kubectl与API操作。部分关系如下图所示

3

需要注意的是Kubernetes并不会提供用户管理,对于User、Group、ServiceAccount指定的用户,Kubernetes组件(kubectl、kube-proxy)或是其他自定义的用户在向CA申请证书时,需要提供一个证书请求文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"CN":"admin",
"hosts":[],
"key":{
"algo":"rsa",
"size":2048
},
"names":[
{
"C":"CN",
"ST":"Hangzhou",
"L":"XS",
"O":"system:masters",
"OU":"System"
}
]
}

API Server会把客户端证书的CN字段作为User,把names.O字段作为Group

kubelet使用TLS Bootstrapping认证时,API Server可以使用Bootstrap Tokens或者Token authentication file验证token,无论哪一种,Kubernetes都会为token绑定一个默认的User和Group。Pod使用ServiceAccount认证时,service-account-token中的JWT会保存User信息,有了用户信息,再创建一对角色/角色绑定(集群角色/集群角色绑定)资源对象,就可以完成权限绑定了。

Role and ClusterRole

在RBAC API中,Role表示一组规则权限,权限只会增加(累加权限),不存在一个资源一开始就有很多权限而通过RBAC对其进行减少的操作,Role可以定义在一个namespace中,如果想要跨namespace则可以创建ClusterRole

1
2
3
4
5
6
7
8
9
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]

ClusterRole具有和Role相同的权限角色控制能力,不同的是,ClusterRole是集群级别的,可以用于

  • 集群级别的资源控制(例如node访问权限)
  • 非资源型 endpoints(例如/healthz访问)
  • 所有命名空间资源控制(例如pods)
1
2
3
4
5
6
7
8
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get","watch","list"]
RoleBinding and ClusterRoleBinding

RoleBinding可以将角色中定义的权限授予用户或用户组,RoleBinding包含一组权限列表(subjects),权限列表中包含有不同形式的待授予权限资源类型(users,groups,service accounts),RoleBinding同样包含对被Bind的Role引用,RoleBinding适用于某个命名空间内授权,而ClusterRoleBinding适用于集群范围内的授权。

例如,要将default命名空间的pod-reader Role授予elssm用户,此后elssm用户在default命名空间将具有pod-reader的权限

1
2
3
4
5
6
7
8
9
10
11
12
13
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: elssm
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io

RoleBinding同样可以引用ClusterRole来对当前namespace内用户、用户组或ServiceAccount进行授权,这种操作允许集群管理员在整个集群内定义一些通用的ClusterRole,然后在不同的namespace中使用RoleBinding来引用。

例如,以下RoleBinding引用了一个ClusterRole,这个ClusterRole具有整个集群内对secrets的访问权限,但是其授权用户warry只能访问development空间中的secrets(因为RoleBinding定义在development命名空间)

1
2
3
4
5
6
7
8
9
10
11
12
13
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: read-secrets
namespace: development
subjects:
- kind: User
name: warry
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: secret-reader
apiGroup: rbac.authorization.k8s.io

除此以外,还可以使用ClusterRoleBinding对整个集群中的所有命名空间资源权限进行授权,以下ClusterRoleBinding例子展示了授权manager组内所有用户在全部命名空间中对secrets进行访问

1
2
3
4
5
6
7
8
9
10
11
12
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: read-secrets-global
subjects:
- kind: Group
name: manager
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: secret-reader
apiGroup: rbac.authorization.k8s.io

Admission Controll

准入控制是API Server的插件集合,通过添加不同的插件,实现额外的准入控制规则,甚至于API Server的一些主要的功能都需要通过Admission Controllers,比如ServiceAccount

一些插件的功能

  • Namespace Lifecycle:防止在不存在的namespace上创建对象,防止删除系统预置的namespace
  • LimitRanger:确保请求的资源不会超过所在namespace的LimitRange的限制
  • Service Account:实现了自动化添加ServiceAccount
  • ResourceQuota:确保请求的资源不会超过资源的ResourceQuota限制

配置节点的安全上下文

我们可以在Pod或其所属容器的描述中通过security-Context选项配置其他与安全性相关的特性,这个选项可以适用于整个pod,或者每个pod中单独的容器。

配置安全上下文可以使我们完成很多事情,例如

  • 指定容器中运行进程的用户(用户ID)
  • 阻止容器使用root用户运行
  • 使用特权模式运行容器,使其对宿主节点的内核具有完全的访问权限
  • 通过添加或禁用内核功能,配置细粒度的内核访问权限
  • 设置SELinux(安全增强型Linux)选项,加强对容器的限制
  • 阻止进程写入容器的根文件系统
运行没有配置安全上下文的pod

创建一个名为pod-with-defaults的YAML文件,内容如下

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
name: pod-with-defaults
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep","999999"]

启动pod

1
2
caoyifan@MacBookPro ~ % kubectl create -f pod-with-defaults.yaml 
pod/pod-with-defaults created

查看这个容器中的用户ID和组ID以及它所属的用户组

1
2
caoyifan@MacBookPro ~ % kubectl exec pod-with-defaults -- id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

发现这个容器在用户ID(uid)为0的用户,用户组ID(gid)为0的用户组下运行,它同样还属于一些其他的用户组。

运行指定用户的pod

为了使用一个与镜像中不同的用户ID来运行pod,需要设置该pod的securityContext.runAsUser选项,可以通过以下代码来运行一个使用guest用户运行的容器。

创建一个名为pod-as-user-guest的YAML文件,内容如下,其中id405对应guest用户

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: pod-as-user-guest
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep","999999"]
securityContext:
runAsUser: 405

启动pod后查看,发现该容器在guest用户下运行

1
2
caoyifan@MacBookPro ~ % kubectl exec pod-as-user-guest -- id
uid=405(guest) gid=100(users)
阻止容器以root用户运行

创建一个名为pod-run-as-non-root的YAML文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: pod-run-as-non-root
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep","999999"]
securityContext:
runAsNonRoot: true

启动pod后查看pod信息

1
2
3
caoyifan@MacBookPro DockerTest % kubectl get pod pod-run-as-non-root     
NAME READY STATUS RESTARTS AGE
pod-run-as-non-root 0/1 CreateContainerConfigError 0 5m16s

发现pod并没有运行,通过describe查看具体信息

1
2
3
4
5
6
7
8
9
10
11
caoyifan@MacBookPro DockerTest % kubectl describe pod pod-run-as-non-root      
Name: pod-run-as-non-root
Namespace: default
Priority: 0
Node: docker-desktop/192.168.65.4
Start Time: Tue, 26 Oct 2021 13:53:32 +0800
Labels: <none>
Annotations: <none>
Status: Pending
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Error: container has runAsNonRoot and image will run as root (pod: "pod-run-as-non-root_default", container: main)
运行使用特权模式的pod

为了获取宿主机内核的完整权限,该pod需要在特权模式下运行,这可以通过将容器的securityContext中的privileged设置为true实现。

创建一个名为pod-privileged的YAML文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: pod-privileged
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep","999999"]
securityContext:
privileged: true

部署好这个pod之后,我们与之前部署的非特权模式的pod做对比。

首先使用之前的名为pod-with-defaults的pod,通过列出/dev目录下文件的方式查看非特权模式容器中的设备,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
caoyifan@MacBookPro DockerTest % kubectl exec pod-with-defaults -- ls /dev
core
fd
full
mqueue
null
ptmx
pts
random
shm
stderr
stdin
stdout
termination-log
tty
urandom
zero

接下来我们列出特权模式容器/dev目录下的文件,可以发现,特权模式的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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
caoyifan@MacBookPro DockerTest % kubectl exec pod-privileged -- ls /dev
cachefiles
core
cpu
cpu_dma_latency
cuse
fd
full
fuse
hpet
hwrng
input
kmsg
loop-control
loop0
loop1
loop2
loop3
loop4
loop5
loop6
loop7
mapper
mem
mqueue
nbd0
nbd1
nbd10
nbd11
nbd2
nbd3
nbd4
nbd5
net
null
nvram
port
psaux
ptmx
pts
ram0
ram1
ram10
ram2
ram3
ram4
ram5
tty11
tty12
tty13
tty14
tty15
tty16
tty17
...
为容器单独添加内核功能

相比于让容器运行在特权模式下以给予其无限的权限,一个更安全的做法是只给予它是用真正需要的内核功能的权限,Kubernetes允许为特定的容器添加内核功能,或禁用部分内核功能,以允许对容器进行更加精细的权限控制,从而限制攻击之恶潜在侵入的一些影响。

例如,一个容器通常不允许修改系统时间,我们可以通过修改名为pod-with-defaults的pod中的时间来验证。

1
2
caoyifan@MacBookPro DockerTest % kubectl exec pod-with-defaults -- date +%T -s "12:00:00"
date: can't set date: Operation not permitted

如果需要允许容器修改系统时间,可以在容器的securityContext.capabilities里add一项名为CAP_SYS_TIME的功能。

首先创建一个名为pod-add-settime-capability的YAML文件,内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
name: pod-add-settime-capability
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep","999999"]
securityContext:
capabilities:
add:
- SYS_TIME

注意Linux内核功能的名称通常以CAP_开头,但是在pod spec中指定内核功能时,必须省略CAP_前缀

启动好pod之后,在新的容器中运行同样的命令,发现可以成功修改系统时间

1
2
3
4
caoyifan@MacBookPro DockerTest % kubectl exec pod-add-settime-capability -- date +%T -s "12:00:00" 
12:00:00
caoyifan@MacBookPro DockerTest % kubectl exec pod-add-settime-capability -- date
Tue Oct 26 12:00:05 UTC 2021
在容器中禁用内核功能

默认情况下,容器拥有CAP_CHOWN权限,允许进程修改文件系统中文件的所有者。如下示例,可以在pod-with-defaults中将/tmp目录的所有者改为guest用户

1
2
3
caoyifan@MacBookPro DockerTest % kubectl exec pod-with-defaults -- chown guest /tmp
caoyifan@MacBookPro DockerTest % kubectl exec pod-with-defaults -- ls -la / | grep tmp
drwxrwxrwt 1 guest root 4096 Aug 27 11:05 tmp

为了阻止容器的这种行为,可以在容器的securityContext.capabilities里drop一项名为CHOWN的功能

首先创建一个名为pod-drop-chown-capability的YAML文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
name: pod-drop-chown-capability
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep","999999"]
securityContext:
capabilities:
drop:
- CHOWN

禁用CHOWN内核功能后,则不允许在这个pod中修改文件所有者

1
2
3
caoyifan@MacBookPro DockerTest % kubectl exec pod-drop-chown-capability -- chown guest /tmp
chown: /tmp: Operation not permitted
command terminated with exit code 1
阻止对容器根文件系统的写入

因为安全原因,可以需要组织容器中的进程对容器的根文件系统进行写入,仅允许他们写入挂载的存储卷。我们可以通过将容器的securityContext.readOnlyRootFilesystem设置为true来实现。

创建一个名为pod-with-readonly-filesystem的YAML文件,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: pod-with-readonly-filesystem
spec:
containers:
- name: main
image: alpine
command: ["/bin/sleep","999999"]
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: my-volume
mountPath: /volume
readOnly: false
volumes:
- name: my-volume
emptyDir:

这个pod中的容器虽然以root用户运行,拥有/目录的写权限,但在该目录下写入一个文件会失败,如下所示

1
2
3
caoyifan@MacBookPro DockerTest % kubectl exec pod-with-readonly-filesystem -- touch /new-file
touch: /new-file: Read-only file system
command terminated with exit code 1

但是对于挂载的卷的写入时允许的,如下所示

1
2
3
caoyifan@MacBookPro DockerTest % kubectl exec pod-with-readonly-filesystem -- touch /volume/new-file
caoyifan@MacBookPro DockerTest % kubectl exec pod-with-readonly-filesystem -- ls -la /volume/new-file
-rw-r--r-- 1 root root 0 Oct 26 06:59 /volume/new-file