容器编排之争在 Kubernetes 一统天下局面形成后,K8S 成为了云原生时代的新一代操作系统。K8S 让一切变得简单了,但自身逐渐变得越来越复杂。【K8S Internals 系列专栏】围绕 K8S 生态的诸多方面,将由博云容器云研发团队定期分享有关调度、安全、网络、性能、存储、应用场景等热点话题。希望大家在享受 K8S 带来的高效便利的同时,又可以如庖丁解牛般领略其内核运行机制的魅力。
1. Pod Security Policy 简介Pod Security Policy 是一个赋予集群管理员控制 Pod 安全规范的内置准入控制器,可以让管理人员控制Pod实例安全的诸多方面,例如禁止采用root权限、防止容器逃逸等等。Pod Security Policy 定义了一组 Pod 运行时必须遵循的条件及相关字段的默认值,Pod 必须满足这些条件才能被成功创建,Pod Security Policy 对象 Spec 包含以下字段也即是 Pod Security Policy 能够控制的方面:
其中AppArmor 和seccomp 需要通过给PodSecurityPolicy对象添加注解的方式设定:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default'
seccomp.security.alpha.kubernetes.io/defaultProfileNames: 'docker/default'
apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
apparmor.security.beta.kubernetes.io/defaultProfileNames: 'runtime/default'
Pod Security Policy是集群级别的资源,我们看一下它的使用流程:
PSP使用流程
由于需要创建ClusterRole/Role和ClusterRoleBinding/RoleBinding绑定服务账号来使用PSP,这使得我们不能很容易的看出究竟使用了哪些PSP,更难看出Pod的创建被哪些安全规则限制。
2. 为什么出现Pod Security Admission通过对PodSecurityPolicy使用,应该也会发现它的问题,例如没有dry-run和审计模式、不方便开启和关闭等,并且使用起来也不那么清晰。种种缺陷造成的结果是PodSecurityPolicy在Kubernetes v1.21被标记为弃用,并且将在 v1.25中被移除,在kubernets v1.22中则增加了新特性Pod Security Admission。
3. Pod Security Admission介绍pod security admission是kubernetes内置的一种准入控制器,在kubernetes v1.23版本中这一特性门是默认开启的,在v1.22中需要通过kube-apiserver参数 --feature-gates="...,PodSecurity=true"
开启。在低于v1.22的kuberntes版本中也可以自行安装Pod Security Admission Webhook。
pod security admission是通过执行内置的 Pod Security Standards来限制集群中的pod的创建。
3.1 Pod Security Standards为了广泛的覆盖安全应用场景, Pod Security Standards渐进式的定义了三种不同的Pod安全标准策略:
详细内容参见Pod Security Standards。
3.2 Pod Security Standards实施方法在kubernetes集群中开启了pod security admission特性门之后,就可以通过给namespace设置label的方式来实施Pod Security Standards。其中有三种设定模式可选用:
label设置模板解释:
# 设定模式及安全标准策略等级
# MODE必须是 `enforce`, `audit`或`warn`其中之一。
# LEVEL必须是`privileged`, `baseline`或 `restricted`其中之一
pod-security.kubernetes.io/<MODE>: <LEVEL>
# 此选项是非必填的,用来锁定使用哪个版本的的安全标准
# MODE必须是 `enforce`, `audit`或`warn`其中之一。
# VERSION必须是一个有效的kubernetes minor version(例如v1.23),或者 `latest`
pod-security.kubernetes.io/<MODE>-version: <VERSION>
一个namesapce可以设定任意种模式或者不同的模式设定不同的安全标准策略。
通过准入控制器配置文件,可以为pod security admission设置默认配置:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
# Defaults applied when a mode label is not set.
#
# Level label values must be one of:
# - "privileged" (default)
# - "baseline"
# - "restricted"
#
# Version label values must be one of:
# - "latest" (default)
# - specific version like "v1.23"
defaults:
enforce: "privileged"
enforce-version: "latest"
audit: "privileged"
audit-version: "latest"
warn: "privileged"
warn-version: "latest"
exemptions:
# Array of authenticated usernames to exempt.
usernames: []
# Array of runtime class names to exempt.
runtimeClassNames: []
# Array of namespaces to exempt.
namespaces: []
pod security admission可以从username,runtimeClassName,namespace三个维度对pod进行安全标准检查的豁免。
3.3 Pod Security Standards实施演示- 环境: kubernetes v1.23
运行时的容器面临很多攻击风险,例如容器逃逸,从容器发起资源耗尽型攻击。
3.3.1 Baseline策略Baseline策略目标是应用于常见的容器化应用,禁止已知的特权提升,在官方的介绍中此策略针对的是应用运维人员和非关键性应用开发人员,在该策略中包括:
必须禁止共享宿主命名空间、禁止容器特权、 限制Linux能力、禁止hostPath卷、限制宿主机端口、设定AppArmor、SElinux、Seccomp、Sysctls等。
下面演示设定Baseline策略。
违反Baseline策略存在的风险:
- 特权容器可以看到宿主机设备
- 挂载procfs后可以看到宿主机进程,打破进程隔离
- 可以打破网络隔离
- 挂载运行时socket后可以不受限制的与运行时通信
等等以上风险都可能导致容器逃逸。
- 创建名为my-baseline-namespace的namespace,并设定enforce和warn两种模式都对应Baseline等级的Pod安全标准策略:
apiVersion: v1
kind: Namespace
metadata:
name: my-baseline-namespace
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.23
pod-security.kubernetes.io/warn: baseline
pod-security.kubernetes.io/warn-version: v1.23
-
创建pod
- 创建一个违反baseline策略的pod
apiVersion: v1 kind: Pod metadata: name: hostnamespaces2 namespace: my-baseline-namespace spec: containers: - image: bitnami/prometheus:2.33.5 name: prometheus securityContext: allowPrivilegeEscalation: true privileged: true capabilities: drop: - ALL hostPID: true securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault
- 执行apply命令,显示不能设置hostPID=true,securityContext.privileged=true,Pod创建被拒绝,特权容器的运行,并且开启hostPID,容器进程没有与宿主机进程隔离,容易造成Pod容器逃逸:
[root@localhost podSecurityStandard]# kubectl apply -f fail-hostnamespaces2.yaml Error from server (Forbidden): error when creating "fail-hostnamespaces2.yaml": pods "hostnamespaces2" is forbidden: violates PodSecurity "baseline:v1.23": host namespaces (hostPID=true), privileged (container "prometheus" must not set securityContext.privileged=true)
- 创建不违反baseline策略的pod,设定Pod的hostPID=false,securityContext.privileged=false
apiVersion: v1 kind: Pod metadata: name: hostnamespaces2 namespace: my-baseline-namespace spec: containers: - image: bitnami/prometheus:2.33.5 name: prometheus securityContext: allowPrivilegeEscalation: false privileged: false capabilities: drop: - ALL hostPID: false securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault
- 执行apply命令,pod被允许创建:
[root@localhost podSecurityStandard]# kubectl apply -f pass-hostnamespaces2.yaml pod/hostnamespaces2 created
Restricted策略目标是实施当前保护Pod的最佳实践,在官方介绍中此策略主要针对运维人员和安全性很重要的应用开发人员,以及不太被信任的用户。该策略包含所有的baseline策略的内容,额外增加: 限制可以通过 PersistentVolumes 定义的非核心卷类型、禁止(通过 SetUID 或 SetGID 文件模式)获得特权提升、必须要求容器以非 root 用户运行、Containers 不可以将 runAsUser 设置为 0、 容器组必须弃用 ALL capabilities 并且只允许添加 NET_BIND_SERVICE 能力。
restricted策略进一步的限制在容器内获取root权限,linux内核功能。例如针对kubernetes网络的中间人攻击需要拥有Linux系统的CAP_NET_RAW权限来发送ARP包。
-
创建名为my-restricted-namespace的namespace,并设定enforce和warn两种模式都对应Restricted等级的Pod安全标准策略:
apiVersion: v1 kind: Namespace metadata: name: my-restricted-namespace labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: v1.23 pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/warn-version: v1.23
-
创建pod
- 创建一个违反Restricted策略的pod
apiVersion: v1 kind: Pod metadata: name: runasnonroot0 namespace: my-restricted-namespace spec: containers: - image: bitnami/prometheus:2.33.5 name: prometheus securityContext: allowPrivilegeEscalation: false securityContext: seccompProfile: type: RuntimeDefault
- 执行apply命令,显示必须设置securityContext.runAsNonRoot=true,securityContext.capabilities.drop=["ALL"],Pod创建被拒绝,容器以root用户运行时容器获取权限过大,结合没有Drop linux内核能力有kubernetes网络中间人攻击的风险:
[root@localhost podSecurityStandard]# kubectl apply -f fail-runasnonroot0.yaml Error from server (Forbidden): error when creating "fail-runasnonroot0.yaml": pods "runasnonroot0" is forbidden: violates PodSecurity "restricted:v1.23": unrestricted capabilities (container "prometheus" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "prometheus" must set securityContext.runAsNonRoot=true)
- 创建不违反Restricted策略的pod,设定Pod的securityContext.runAsNonRoot=true,Drop所有linux能力。
apiVersion: v1 kind: Pod metadata: name: runasnonroot0 namespace: my-restricted-namespace spec: containers: - image: bitnami/prometheus:2.33.5 name: prometheus securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault
- 执行apply命令,pod被允许创建:
[root@localhost podSecurityStandard]# kubectl apply -f pass-runasnonroot0.yaml pod/runasnonroot0 created
如果你的集群中已经配置PodSecurityPolicy,考虑把它们迁移到pod security admission是需要一定的工作量的。
首先需要考虑当前的pod security admission是否适合你的集群,目前它旨在满足开箱即用的最常见的安全需求,与PSP相比它存在以下差异:
- pod security admission 只是对pod进行安全标准的检查,不支持对pod进行修改,不能为pod设置默认的安全配置。
- pod security admission 只支持官方定义的三种安全标准策略,不支持灵活的自定义安全标准策略。这使得不能完全将PSP规则迁移到pod security admission,需要进行具体的安全规则考量。
- pod security admission 不像PSP一样可以与具体的用户进行绑定,只支持豁免特定的用户或者RuntimeClass及namespace。
kubernetes准入控制器是在代码层面与API server逻辑解耦的插件,对象被创建、更新、或删除在etcd持久化之前可以对请求进行拦截执行特定的逻辑。一个请求到API server经典的流程如下图所示:
Api Request 处理流程图
podsecurityAdmission 代码流程图
pod security admission主体逻辑流程如图所示,准入控制器首先解析拦截到的请求,根据解析到的资源类型进行不同的逻辑处理:
- Namespace : 如果解析到的资源是Namespace,准入控制器先根据该namesapce的labels解析出配置安全标准策略的等级、模式及锁定的Pod安全标准策略版本等信息。检查如果过不包含Pod安全标准策略信息则直接允许请求通过,如果包含Pod安全标准策略信息则判断是create新的namespace,还是update旧的namespace,如果是create则判断配置是否正确,如果是update 则评估namespace中的pod是否符合新设定的安全标准策略。
- Pod: 如果解析到的资源是Pod,准入控制器先获取该Pod所处的namespace设定的Pod安全标准策略信息,如果该namespace未设定Pod安全标准策略则允许请求通过,否则评估该Pod是否符合安全标准策略。
- others:准入控制器先获取该资源所处的namespace设定的Pod安全策略信息,如果该namespace未设定Pod安全策略则允许请求通过,否则进一步解析该资源判断该资源是否是诸如PodTemplate,ReplicationController,ReplicaSet,Deployment,DaemonSet,StatefulSet,Job,CronJob等包含PodSpec的资源,解析出PodSpec后评估该资源是否符合Pod安全策略。
像大多数go程序一样,Pod security admission使用github.com/spf13/cobra创建了启动命令,在启动调用runServer初始化并启动webhook服务。入参Options中包含了DefaultClientQPSLimit,DefaultClientQPSBurst,DefaultPort,DefaultInsecurePort等默认配置。
// NewSchedulerCommand creates a *cobra.Command object with default parameters and registryOptions
func NewServerCommand() *cobra.Command {
opts := options.NewOptions()
cmdName := "podsecurity-webhook"
if executable, err := os.Executable(); err == nil {
cmdName = filepath.Base(executable)
}
cmd := &cobra.Command{
Use: cmdName,
Long: `The PodSecurity webhook is a standalone webhook server implementing the Pod
Security Standards.`,
RunE: func(cmd *cobra.Command, _ []string) error {
verflag.PrintAndExitIfRequested()
// 初始化并且启动webhook服务
return runServer(cmd.Context(), opts)
},
Args: cobra.NoArgs,
}
opts.AddFlags(cmd.Flags())
verflag.AddFlags(cmd.Flags())
return cmd
}
runserver函数中加载了准入控制器的配置,初始化了server, 最终启动server。
func runServer(ctx context.Context, opts *options.Options) error {
// 加载配置内容
config, err := LoadConfig(opts)
if err != nil {
return err
}
// 根据配置内容初始化server
server, err := Setup(config)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
stopCh := apiserver.SetupSignalHandler()
<-stopCh
cancel()
}()
// 启动server
return server.Start(ctx)
}
下面截取了Setup函数部分主要代码片段,Setup函数创建了Admission对象包含:
- PodSecurityConfig: 准入控制器配置内容,包括默认的Pod安全标准策略等级及设定模式和锁定对应kubernetes版本,以及豁免的Usernames、RuntimeClasses和Namespaces。
- Evaluator: 创建的评估器,即定义了检查安全标准策略的具体方法。
- Metrics: 用于收集Prometheus指标。
- PodSpecExtractor:用解析请求对象中的PodSpec。
- PodLister: 用于获取指定namespace中的Pods。
- NamespaceGetter:用户获取拦截到请求中的资源所处的namespace。
// Setup creates an Admission object to handle the admission logic.
func Setup(c *Config) (*Server, error) {
...
s.delegate = &admission.Admission{
Configuration: c.PodSecurityConfig,
Evaluator: evaluator,
Metrics: metrics,
PodSpecExtractor: admission.DefaultPodSpecExtractor{},
PodLister: admission.PodListerFromClient(client),
NamespaceGetter: admission.NamespaceGetterFromListerAndClient(namespaceLister, client),
}
...
return s, nil
}
准入控制器服务启动之后注册了HandleValidate方法进行准入检验逻辑的处理,在此方法中调用Validate方法进行具体Pod安全标准策略的检验。
//处理webhook拦截到的请求
func (s *Server) HandleValidate(w http.ResponseWriter, r *http.Request) {
defer utilruntime.HandleCrash(func(_ interface{}) {
// Assume the crash happened before the response was written.
http.Error(w, "internal server error", http.StatusInternalServerError)
})
...
// 进行具体的检验操作
response := s.delegate.Validate(ctx, attributes)
response.UID = review.Request.UID // Response UID must match request UID
review.Response = response
writeResponse(w, review)
}
4.3 准入检验处理逻辑
Validate方法根据获取请求包含的不同资源类型调用不同的检验方法进行具体的检验操作,以下三种处理方向最终都会调用EvaluatePod方法,对Pod进行安全标准策略评估。
// Validate admits an API request.
// The objects in admission attributes are expected to be external v1 objects that we care about.
// The returned response may be shared and must not be mutated.
func (a *Admission) Validate(ctx context.Context, attrs api.Attributes) *admissionv1.AdmissionResponse {
var response *admissionv1.AdmissionResponse
switch attrs.GetResource().GroupResource() {
case namespacesResource:
response = a.ValidateNamespace(ctx, attrs)
case podsResource:
response = a.ValidatePod(ctx, attrs)
default:
response = a.ValidatePodController(ctx, attrs)
}
return response
}
EvaluatePod方法中对namespace设定安全标准策略和版本进行判断,从而选取不同的检查方法对Pod进行安全性检验。
func (r *checkRegistry) EvaluatePod(lv api.LevelVersion, podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) []CheckResult {
// 如果设定的Pod安全标准策略等级是Privileged(宽松的策略)直接返回
if lv.Level == api.LevelPrivileged {
return nil
}
// 如果注册的检查策略最大版本号低于namespace设定策略版本号,则使用注册的检查策略的最大版本号
if r.maxVersion.Older(lv.Version) {
lv.Version = r.maxVersion
}
var checks []CheckPodFn
// 如果设定的Pod安全标准策略等级是Baseline
if lv.Level == api.LevelBaseline {
checks = r.baselineChecks[lv.Version]
} else {
// includes non-overridden baseline checks
// 其他走严格的Pod安全标准策略检查
checks = r.restrictedChecks[lv.Version]
}
var results []CheckResult
// 遍历检查方法,返回检查结果
for _, check := range checks {
results = append(results, check(podMetadata, podSpec))
}
return results
}
下面截取一个具体的检验方法来看一下是如何进行pod安全标准检查的,如下检查了Pod中的容器是否关闭了allowPrivilegeEscalation,AllowPrivilegeEscalation设置容器内的子进程是否可以提升权限,通常在设置非root用户(MustRunAsNonRoot)时进行设置。
func allowPrivilegeEscalation_1_8(podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec) CheckResult {
var badContainers []string
visitContainers(podSpec, func(container *corev1.Container) {
// 检查pod中容器安全上下文是否配置,AllowPrivilegeEscalation是否配置,及AllowPrivilegeEscalation是否设置为false.
if container.SecurityContext == nil || container.SecurityContext.AllowPrivilegeEscalation == nil || *container.SecurityContext.AllowPrivilegeEscalation {
badContainers = append(badContainers, container.Name)
}
})
if len(badContainers) > 0 {
// 存在违反Pod安全标准策略的内容,则返回具体结果信息
return CheckResult{
Allowed: false,
ForbiddenReason: "allowPrivilegeEscalation != false",
ForbiddenDetail: fmt.Sprintf(
"%s %s must set securityContext.allowPrivilegeEscalation=false",
pluralize("container", "containers", len(badContainers)),
joinQuote(badContainers),
),
}
}
return CheckResult{Allowed: true}
}
总结
在 kubernetes v1.23版本中 Pod Security Admission已经升级到beta版本,虽然目前功能不算强大,但该特性未来可期。