为 Pod 或容器配置安全上下文
安全上下文(Security Context)定义 Pod 或 Container 的特权与访问控制设置。 安全上下文包括但不限于:
自主访问控制(Discretionary Access Control): 基于用户 ID(UID)和组 ID(GID) 来判定对对象(例如文件)的访问权限。
安全性增强的 Linux(SELinux): 为对象赋予安全性标签。
以特权模式或者非特权模式运行。
Linux 权能: 为进程赋予 root 用户的部分特权而非全部特权。
AppArmor:使用程序配置来限制个别程序的权能。
Seccomp:过滤进程的系统调用。
allowPrivilegeEscalation
:控制进程是否可以获得超出其父进程的特权。 此布尔值直接控制是否为容器进程设置 no_new_privs标志。 当容器满足一下条件之一时,allowPrivilegeEscalation
总是为 true:- 以特权模式运行,或者
- 具有
CAP_SYS_ADMIN
权能
readOnlyRootFilesystem
:以只读方式加载容器的根文件系统。
以上条目不是安全上下文设置的完整列表 — 请参阅 SecurityContext 了解其完整列表。
准备开始
你必须拥有一个 Kubernetes 的集群,且必须配置 kubectl 命令行工具让其与你的集群通信。 建议运行本教程的集群至少有两个节点,且这两个节点不能作为控制平面主机。 如果你还没有集群,你可以通过 Minikube 构建一个你自己的集群,或者你可以使用下面的 Kubernetes 练习环境之一:
要获知版本信息,请输入 kubectl version
.
为 Pod 设置安全性上下文
要为 Pod 设置安全性设置,可在 Pod 规约中包含 securityContext
字段。securityContext
字段值是一个 PodSecurityContext 对象。你为 Pod 所设置的安全性配置会应用到 Pod 中所有 Container 上。 下面是一个 Pod 的配置文件,该 Pod 定义了 securityContext
和一个 emptyDir
卷:
pods/security/security-context.yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
volumes:
- name: sec-ctx-vol
emptyDir: {}
containers:
- name: sec-ctx-demo
image: busybox:1.28
command: [ "sh", "-c", "sleep 1h" ]
volumeMounts:
- name: sec-ctx-vol
mountPath: /data/demo
securityContext:
allowPrivilegeEscalation: false
在配置文件中,runAsUser
字段指定 Pod 中的所有容器内的进程都使用用户 ID 1000 来运行。runAsGroup
字段指定所有容器中的进程都以主组 ID 3000 来运行。 如果忽略此字段,则容器的主组 ID 将是 root(0)。 当 runAsGroup
被设置时,所有创建的文件也会划归用户 1000 和组 3000。 由于 fsGroup
被设置,容器中所有进程也会是附组 ID 2000 的一部分。 卷 /data/demo
及在该卷中创建的任何文件的属主都会是组 ID 2000。 此外,当 supplementalGroups
字段被指定时,容器的所有进程也会成为所指定的组的一部分。 如果此字段被省略,则表示为空。
创建该 Pod:
kubectl apply -f https://k8s.io/examples/pods/security/security-context.yaml
检查 Pod 的容器处于运行状态:
kubectl get pod security-context-demo
开启一个 Shell 进入到运行中的容器:
kubectl exec -it security-context-demo -- sh
在你的 Shell 中,列举运行中的进程:
ps
输出显示进程以用户 1000 运行,即 runAsUser
所设置的值:
PID USER TIME COMMAND
1 1000 0:00 sleep 1h
6 1000 0:00 sh
...
在你的 Shell 中,进入 /data
目录列举其内容:
cd /data
ls -l
输出显示 /data/demo
目录的组 ID 为 2000,即 fsGroup
的设置值:
drwxrwsrwx 2 root 2000 4096 Jun 6 20:08 demo
在你的 Shell 中,进入到 /data/demo
目录下创建一个文件:
cd demo
echo hello > testfile
列举 /data/demo
目录下的文件:
ls -l
输出显示 testfile
的组 ID 为 2000,也就是 fsGroup
所设置的值:
-rw-r--r-- 1 1000 2000 6 Jun 6 20:08 testfile
运行下面的命令:
id
输出类似于:
uid=1000 gid=3000 groups=2000,3000,4000
从输出中你会看到 gid
值为 3000,也就是 runAsGroup
字段的值。 如果 runAsGroup
被忽略,则 gid
会取值 0(root),而进程就能够与 root 用户组所拥有以及要求 root 用户组访问权限的文件交互。 你还可以看到,除了 gid
之外,groups
还包含了由 fsGroup
和 supplementalGroups
指定的组 ID。
退出你的 Shell:
exit
容器镜像内 /etc/group
中定义的隐式组成员身份
默认情况下,Kubernetes 会将 Pod 中的组信息与容器镜像内 /etc/group
中定义的信息合并。
pods/security/security-context-5.yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
supplementalGroups: [4000]
containers:
- name: sec-ctx-demo
image: registry.k8s.io/e2e-test-images/agnhost:2.45
command: [ "sh", "-c", "sleep 1h" ]
securityContext:
allowPrivilegeEscalation: false
此 Pod 的安全上下文包含 runAsUser
、runAsGroup
和 supplementalGroups
。
然而,你可以看到,挂接到容器进程的实际附加组将包括来自容器镜像中 /etc/group
的组 ID。
创建 Pod:
kubectl apply -f https://k8s.io/examples/pods/security/security-context-5.yaml
验证 Pod 的 Container 正在运行:
kubectl get pod security-context-demo
打开一个 Shell 进入正在运行的 Container:
kubectl exec -it security-context-demo -- sh
检查进程身份:
$ id
输出类似于:
uid=1000 gid=3000 groups=3000,4000,50000
你可以看到 groups
包含组 ID 50000
。 这是因为镜像中定义的用户(uid=1000
)属于在容器镜像内 /etc/group
中定义的组(gid=50000
)。
检查容器镜像中的 /etc/group
:
$ cat /etc/group
你可以看到 uid 1000
属于组 50000
。
...
user-defined-in-image:x:1000:
group-defined-in-image:x:50000:user-defined-in-image
退出你的 Shell:
exit
说明:
隐式合并的附加组可能会导致安全问题, 特别是在访问卷时(有关细节请参见 kubernetes/kubernetes#112879)。
如果你想避免这种问题,请查阅以下章节。
配置 Pod 的细粒度 SupplementalGroups 控制
特性状态: Kubernetes v1.31 [alpha]
通过为 kubelet 和 kube-apiserver 设置 SupplementalGroupsPolicy
特性门控, 并为 Pod 设置 .spec.securityContext.supplementalGroupsPolicy
字段,此特性可以被启用。
supplementalGroupsPolicy
字段为 Pod 中的容器进程定义了计算附加组的策略。 此字段有两个有效值:
Merge
:为容器的主用户在/etc/group
中定义的组成员身份将被合并。 如果不指定,这就是默认策略。Strict
:仅将fsGroup
、supplementalGroups
或runAsGroup
字段中的组 ID 挂接为容器进程的附加组。这意味着容器主用户在/etc/group
中的组成员身份将不会被合并。
当此特性被启用时,它还会在 .status.containerStatuses[].user.linux
字段中暴露挂接到第一个容器进程的进程身份。这对于检测是否挂接了隐式组 ID 非常有用。
pods/security/security-context-6.yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
supplementalGroups: [4000]
supplementalGroupsPolicy: Strict
containers:
- name: sec-ctx-demo
image: registry.k8s.io/e2e-test-images/agnhost:2.45
command: [ "sh", "-c", "sleep 1h" ]
securityContext:
allowPrivilegeEscalation: false
此 Pod 清单定义了 supplementalGroupsPolicy=Strict
。 你可以看到没有将 /etc/group
中定义的组成员身份合并到容器进程的附加组中。
创建 Pod:
kubectl apply -f https://k8s.io/examples/pods/security/security-context-6.yaml
验证 Pod 的 Container 正在运行:
kubectl get pod security-context-demo
检查进程身份:
kubectl exec -it security-context-demo -- id
输出类似于:
uid=1000 gid=3000 groups=3000,4000
查看 Pod 的状态:
kubectl get pod security-context-demo -o yaml
你可以看到 status.containerStatuses[].user.linux
字段暴露了挂接到第一个容器进程的进程身份。
...
status:
containerStatuses:
- name: sec-ctx-demo
user:
linux:
gid: 3000
supplementalGroups:
- 3000
- 4000
uid: 1000
...
说明:
请注意,status.containerStatuses[].user.linux
字段的值是第一个挂接到容器中第一个容器进程的进程身份。 如果容器具有足够的权限来进行与进程身份相关的系统调用 (例如 setuid(2)、 setgid(2) 或 setgroups(2) 等), 则容器进程可以更改其身份。因此,实际进程身份将是动态的。
实现
说明: 本部分链接到提供 Kubernetes 所需功能的第三方项目。Kubernetes 项目作者不负责这些项目。此页面遵循CNCF 网站指南,按字母顺序列出项目。要将项目添加到此列表中,请在提交更改之前阅读内容指南。
已知以下容器运行时支持细粒度的 SupplementalGroups 控制。
CRI 级别:
- containerd,自 v2.0 起
- CRI-O,自 v1.31 起
你可以在 Node 状态中查看此特性是否受支持。
apiVersion: v1
kind: Node
...
status:
features:
supplementalGroupsPolicy: true
为 Pod 配置卷访问权限和属主变更策略
特性状态: Kubernetes v1.23 [stable]
默认情况下,Kubernetes 在挂载一个卷时,会递归地更改每个卷中的内容的属主和访问权限, 使之与 Pod 的 securityContext
中指定的 fsGroup
匹配。 对于较大的数据卷,检查和变更属主与访问权限可能会花费很长时间,降低 Pod 启动速度。 你可以在 securityContext
中使用 fsGroupChangePolicy
字段来控制 Kubernetes 检查和管理卷属主和访问权限的方式。
fsGroupChangePolicy - fsGroupChangePolicy
定义在卷被暴露给 Pod 内部之前对其 内容的属主和访问许可进行变更的行为。此字段仅适用于那些支持使用 fsGroup
来 控制属主与访问权限的卷类型。此字段的取值可以是:
OnRootMismatch
:只有根目录的属主与访问权限与卷所期望的权限不一致时, 才改变其中内容的属主和访问权限。这一设置有助于缩短更改卷的属主与访问 权限所需要的时间。Always
:在挂载卷时总是更改卷中内容的属主和访问权限。
例如:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
fsGroupChangePolicy: "OnRootMismatch"
说明:
此字段对于 secret、 configMap 和 emptydir 这类临时性存储无效。
将卷权限和所有权更改委派给 CSI 驱动程序
特性状态: Kubernetes v1.26 [stable]
如果你部署了一个容器存储接口 (CSI) 驱动,而该驱动支持 VOLUME_MOUNT_GROUP
NodeServiceCapability
, 在 securityContext
中指定 fsGroup
来设置文件所有权和权限的过程将由 CSI 驱动而不是 Kubernetes 来执行。在这种情况下,由于 Kubernetes 不执行任何所有权和权限更改, fsGroupChangePolicy
不会生效,并且按照 CSI 的规定,CSI 驱动应该使用所指定的 fsGroup
来挂载卷,从而生成了一个对 fsGroup
可读/可写的卷.
为 Container 设置安全性上下文
若要为 Container 设置安全性配置,可以在 Container 清单中包含 securityContext
字段。securityContext
字段的取值是一个 SecurityContext 对象。你为 Container 设置的安全性配置仅适用于该容器本身,并且所指定的设置在与 Pod 层面设置的内容发生重叠时,会重写 Pod 层面的设置。Container 层面的设置不会影响到 Pod 的卷。
下面是一个 Pod 的配置文件,其中包含一个 Container。Pod 和 Container 都有 securityContext
字段:
pods/security/security-context-2.yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo-2
spec:
securityContext:
runAsUser: 1000
containers:
- name: sec-ctx-demo-2
image: gcr.io/google-samples/node-hello:1.0
securityContext:
runAsUser: 2000
allowPrivilegeEscalation: false
创建该 Pod:
kubectl apply -f https://k8s.io/examples/pods/security/security-context-2.yaml
验证 Pod 中的容器处于运行状态:
kubectl get pod security-context-demo-2
启动一个 Shell 进入到运行中的容器内:
kubectl exec -it security-context-demo-2 -- sh
在你的 Shell 中,列举运行中的进程:
ps aux
输出显示进程以用户 2000 运行。该值是在 Container 的 runAsUser
中设置的。 该设置值重写了 Pod 层面所设置的值 1000。
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
2000 1 0.0 0.0 4336 764 ? Ss 20:36 0:00 /bin/sh -c node server.js
2000 8 0.1 0.5 772124 22604 ? Sl 20:36 0:00 node server.js
...
退出你的 Shell:
exit
为 Container 设置权能
使用 Linux 权能, 你可以赋予进程 root 用户所拥有的某些特权,但不必赋予其全部特权。 要为 Container 添加或移除 Linux 权能,可以在 Container 清单的 securityContext
节包含 capabilities
字段。
首先,看一下不包含 capabilities
字段时候会发生什么。 下面是一个配置文件,其中没有添加或移除容器的权能:
pods/security/security-context-3.yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo-3
spec:
containers:
- name: sec-ctx-3
image: gcr.io/google-samples/node-hello:1.0
创建该 Pod:
kubectl apply -f https://k8s.io/examples/pods/security/security-context-3.yaml
验证 Pod 的容器处于运行状态:
kubectl get pod security-context-demo-3
启动一个 Shell 进入到运行中的容器:
kubectl exec -it security-context-demo-3 -- sh
在你的 Shell 中,列举运行中的进程:
ps aux
输出显示容器中进程 ID(PIDs):
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4336 796 ? Ss 18:17 0:00 /bin/sh -c node server.js
root 5 0.1 0.5 772124 22700 ? Sl 18:17 0:00 node server.js
在你的 Shell 中,查看进程 1 的状态:
cd /proc/1
cat status
输出显示进程的权能位图:
...
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
...
记下进程权能位图,之后退出你的 Shell:
exit
接下来运行一个与前例中容器相同的容器,只是这个容器有一些额外的权能设置。
下面是一个 Pod 的配置,其中运行一个容器。配置为容器添加 CAP_NET_ADMIN
和 CAP_SYS_TIME
权能:
pods/security/security-context-4.yaml
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo-4
spec:
containers:
- name: sec-ctx-4
image: gcr.io/google-samples/node-hello:1.0
securityContext:
capabilities:
add: ["NET_ADMIN", "SYS_TIME"]
创建 Pod:
kubectl apply -f https://k8s.io/examples/pods/security/security-context-4.yaml
启动一个 Shell,进入到运行中的容器:
kubectl exec -it security-context-demo-4 -- sh
在你的 Shell 中,查看进程 1 的权能:
cd /proc/1
cat status
输出显示的是进程的权能位图:
...
CapPrm: 00000000aa0435fb
CapEff: 00000000aa0435fb
...
比较两个容器的权能位图:
00000000a80425fb
00000000aa0435fb
在第一个容器的权能位图中,位 12 和 25 是没有设置的。在第二个容器中,位 12 和 25 是设置了的。位 12 是 CAP_NET_ADMIN
而位 25 则是 CAP_SYS_TIME
。 参见 capability.h 了解权能常数的定义。
说明:
Linux 权能常数定义的形式为 CAP_XXX
。但是你在 container 清单中列举权能时, 要将权能名称中的 CAP_
部分去掉。例如,要添加 CAP_SYS_TIME
, 可在权能列表中添加 SYS_TIME
。
为容器设置 Seccomp 配置
若要为容器设置 Seccomp 配置(Profile),可在你的 Pod 或 Container 清单的 securityContext
节中包含 seccompProfile
字段。该字段是一个 SeccompProfile 对象,包含 type
和 localhostProfile
属性。 type
的合法选项包括 RuntimeDefault
、Unconfined
和 Localhost
。 localhostProfile
只能在 type: Localhost
配置下才可以设置。 该字段标明节点上预先设定的配置的路径,路径是相对于 kubelet 所配置的 Seccomp 配置路径(使用 --root-dir
设置)而言的。
下面是一个例子,设置容器使用节点上容器运行时的默认配置作为 Seccomp 配置:
...
securityContext:
seccompProfile:
type: RuntimeDefault
下面是另一个例子,将 Seccomp 的样板设置为位于 <kubelet-根目录>/seccomp/my-profiles/profile-allow.json
的一个预先配置的文件。
...
securityContext:
seccompProfile:
type: Localhost
localhostProfile: my-profiles/profile-allow.json
为 Container 设置 AppArmor 配置
要为 Container 设置 AppArmor 配置,请在 Container 的 securityContext
节中包含 appArmorProfile
字段。 appArmorProfile
字段是一个 AppArmorProfile 对象,由 type
和 localhostProfile
组成。type
的有效选项包括 RuntimeDefault
(默认)、Unconfined
和 Localhost
。 只有当 type
为 Localhost
时,才能设置 localhostProfile
。
它表示节点上预配的配置文件的名称。 此配置需要被加载到所有适合 Pod 的节点上,因为你不知道 Pod 将被调度到哪里。
关于设置自定义配置的方法,参见使用配置文件设置节点。
注意:如果 containers[*].securityContext.appArmorProfile.type
被显式设置为 RuntimeDefault
,那么如果 AppArmor 未在 Node 上被启用,Pod 将不会被准入。
然而,如果 containers[*].securityContext.appArmorProfile.type
未被指定, 则只有在节点已启用 AppArmor 时才会应用默认值(也是 RuntimeDefault
)。
如果节点已禁用 AppArmor,Pod 将被准入,但 Container 将不受 RuntimeDefault
配置的限制。
以下是将 AppArmor 配置设置为节点的容器运行时默认配置的例子:
...
containers:
- name: container-1
securityContext:
appArmorProfile:
type: RuntimeDefault
以下是将 AppArmor 配置设置为名为 k8s-apparmor-example-deny-write
的预配配置的例子:
...
containers:
- name: container-1
securityContext:
appArmorProfile:
type: Localhost
localhostProfile: k8s-apparmor-example-deny-write
有关更多细节参见使用 AppArmor 限制容器对资源的访问。
为 Container 赋予 SELinux 标签
若要给 Container 设置 SELinux 标签,可以在 Pod 或 Container 清单的 securityContext
节包含 seLinuxOptions
字段。 seLinuxOptions
字段的取值是一个 SELinuxOptions 对象。下面是一个应用 SELinux 标签的例子:
...
securityContext:
seLinuxOptions:
level: "s0:c123,c456"
说明:
要指定 SELinux,需要在宿主操作系统中装载 SELinux 安全性模块。
高效重打 SELinux 卷标签
特性状态: Kubernetes v1.28 [beta]
说明:
Kubernetes v1.27 引入了此行为的早期受限形式,仅适用于使用 ReadWriteOncePod
访问模式的卷(和 PersistentVolumeClaim)。
作为一项 Alpha 特性,你可以启用 SELinuxMount
特性门控, 将性能改进扩展到其他类型的 PersistentVolumeClaim,如下文详细解释。
默认情况下,容器运行时递归地将 SELinux 标签赋予所有 Pod 卷上的所有文件。 为了加快该过程,Kubernetes 使用挂载可选项 -o context=<label>
可以立即改变卷的 SELinux 标签。
要使用这项加速功能,必须满足下列条件:
必须启用
ReadWriteOncePod
和SELinuxMountReadWriteOncePod
特性门控。Pod 必须使用带有对应的
accessModes
和特性门控 的 PersistentVolumeClaim。- 卷具有
accessModes: ["ReadWriteOncePod"]
,并且SELinuxMountReadWriteOncePod
特性门控已启用。 - 或者卷可以使用任何其他访问模式,并且必须启用
SELinuxMountReadWriteOncePod
和SELinuxMount
特性门控。
- 卷具有
Pod(或其中使用 PersistentVolumeClaim 的所有容器)必须设置
seLinuxOptions
。对应的 PersistentVolume 必须是:
- 使用传统树内(In-Tree)
iscsi
、rbd
或fs
卷类型的卷。 - 或者是使用 {< glossary_tooltip text=”CSI” term_id=”csi” >}} 驱动程序的卷 CSI 驱动程序必须能够通过在 CSIDriver 实例中设置
spec.seLinuxMount: true
以支持-o context
挂载。
- 使用传统树内(In-Tree)
对于所有其他卷类型,重打 SELinux 标签的方式有所不同: 容器运行时为卷中的所有节点(文件和目录)递归地修改 SELinux 标签。 卷中的文件和目录越多,重打标签需要耗费的时间就越长。
管理对 /proc
文件系统的访问
特性状态: Kubernetes v1.12 [alpha]
对于遵循 OCI 运行时规范的运行时,容器默认运行模式下,存在多个被屏蔽且只读的路径。 这样做的结果是在容器的 mount 命名空间内会存在这些路径,并且这些路径的工作方式与容器是隔离主机时类似, 但容器进程无法写入它们。 被屏蔽的和只读的路径列表如下:
被屏蔽的路径:
/proc/asound
/proc/acpi
/proc/kcore
/proc/keys
/proc/latency_stats
/proc/timer_list
/proc/timer_stats
/proc/sched_debug
/proc/scsi
/sys/firmware
/sys/devices/virtual/powercap
只读的路径:
/proc/bus
/proc/fs
/proc/irq
/proc/sys
/proc/sysrq-trigger
对于某些 Pod,你可能希望绕过默认的路径屏蔽。 最常见的情况是你尝试在 Kubernetes 容器内(在 Pod 内)运行容器。
securityContext
字段 procMount
允许用户请求容器的 /proc
为 Unmasked
, 或者由容器进程以读写方式挂载。这一设置也适用于不在 /proc
内的 /sys/firmware
路径。
...
securityContext:
procMount: Unmasked
说明:
将 procMount
设置为 Unmasked 需要将 Pod 规约中的 spec.hostUsers
的值设置为 false
。换句话说:希望使用未被屏蔽的 /proc
或 /sys
路径的容器也必须位于 user 命名空间中。 Kubernetes v1.12 到 v1.29 没有强制执行该要求。
讨论
Pod 的安全上下文适用于 Pod 中的容器,也适用于 Pod 所挂载的卷(如果有的话)。 尤其是,fsGroup
和 seLinuxOptions
按下面的方式应用到挂载卷上:
fsGroup
:支持属主管理的卷会被修改,将其属主变更为fsGroup
所指定的 GID, 并且对该 GID 可写。进一步的细节可参阅 属主变更设计文档。seLinuxOptions
:支持 SELinux 标签的卷会被重新打标签,以便可被seLinuxOptions
下所设置的标签访问。通常你只需要设置level
部分。 该部分设置的是赋予 Pod 中所有容器及卷的 多类别安全性(Multi-Category Security,MCS)标签。
警告:
在为 Pod 设置 MCS 标签之后,所有带有相同标签的 Pod 可以访问该卷。 如果你需要跨 Pod 的保护,你必须为每个 Pod 赋予独特的 MCS 标签。
清理
删除之前创建的所有 Pod:
kubectl delete pod security-context-demo
kubectl delete pod security-context-demo-2
kubectl delete pod security-context-demo-3
kubectl delete pod security-context-demo-4
接下来
- PodSecurityContext API 定义
- SecurityContext API 定义
- CRI 插件配置指南
- 安全上下文的设计文档(英文)
- 属主管理的设计文档(英文)
- Pod 安全性准入
- AllowPrivilegeEscalation 的设计文档(英文)
- 关于在 Linux 系统中的安全机制的更多信息,可参阅 Linux 内核安全性能力概述(注意:部分信息已过时)。
- 了解 Linux Pod 的 user 命名空间。
- OCI 运行时规范中的被屏蔽的路径