熟悉又陌生的 k8s 字段:SecurityContext

本文涉及的产品
访问控制,不限时长
简介: pod 和 containers 中熟悉又陌生的字段 SecurityContext

前言

如果要投票在 Kubernetes 中很重要,但又最容易被初学者忽略的字段,那么我一定投给 SecurityContext。从 Security Context(安全上下文)的名字就可得知它和安全有关,那么它是如何控制容器安全的?又是如何实现的?本文我们就来探索一下 SecurityContext 这个字段。

SecurityContext

安全上下文(Security Context)定义 Pod 或 Container 的特权与访问控制设置。安全上下文包括但不限于:

  • 自主访问控制(Discretionary Access Control):基于用户 ID(UID)组 ID(GID)来判定对对象(例如文件)的访问权限。
  • 安全性增强的 Linux(SELinux):为对象赋予安全性标签。
  • 以特权模式或者非特权模式运行。
  • Linux 权能:为进程赋予 root 用户的部分特权而非全部特权。
  • AppArmor:使用程序框架来限制个别程序的权能。
  • Seccomp:过滤进程的系统调用。
  • AllowPrivilegeEscalation:控制进程是否可以获得超出其父进程的特权。 此布尔值直接控制是否为容器进程设置 no_new_privs 标志。当容器以特权模式运行或者具有 CAP_SYS_ADMIN 权能时,AllowPrivilegeEscalation 总是为 true
  • readOnlyRootFilesystem:以只读方式加载容器的根文件系统。

SecurityContext 可以应用于 Container 和 Pod 维度:

  • 在 Pod 上设置的安全性配置会应用到 Pod 中所有 Container 上,并且会还会影响 Volume
  • 在 Container 上设置的安全性配置仅适用于该容器本身,不会影响到其他容器以及 Pod 的 Volume

应用场景

上面的介绍来自官方文档,理解起来并不轻松,但总体思想是确定的:遵循权限最小化原则,除了业务运行所必需的系统权限,去除不必的其他权限。下面是笔者总结出的一些常见应用场景来帮助理解 SecurityContext

自定义对象访问权限

也就是上文中提到的自主访问控制(Discretionary Access Control),通过设置 UID 和 GID 来达到限制容器权限的目的。

...
securityContext:
  runAsUser: 1000
  runAsGroup: 3000
  fsGroup: 2000
...

如上这个配置,可以看到 Pod 中所有容器内的进程都是已用户 uid=1000 和主组 gid=3000来运行的,而创建的任何文件的属组都是 groups=2000。通过这样的方法,可以达到让用户的进程不使用默认的 Root (uid=0,gid=0)权限的目的。

禁止容器以 Root 身份运行

如果 runAsNonRoot 字段配置为 true,kubelet 在启动容器时会进行检查,如果以 UID 为 0 运行,则禁止容器启动,该 Pod 的 STATUS 变为 CreateContainerConfigError,并生成 Warning 类型事件 Error: container has runAsNonRoot and image will run as root。只有当设置 runAsUser 或者镜像本身就不是以 Root 身份运行时,该 Pod 才能正常启动。

系统 Capabilities 的控制

在某些时候,用户进程需要使用到 Root 用户的某些特权,但是又不想将全部特权都放给容器,这里就用到了 Linux Capabilities,用户可以在在 Container 的 securityContext 字段中添加 capabilities 字段。在 capabilities 字段中,可以添加或移除容器的 Capabilities。

更多关于 Capabilites 的内容可以在 capabilities man page 中查看。

Linux Capabilities 的定义的形式为 CAP_XXX。但是你在 Container 字段使用时,需要将名称中的 CAP_ 部分去掉。例如,要添加 CAP_SYS_TIME,可在 capabilities 列表中添加 SYS_TIME

只读访问根文件系统

使用 readOnlyRootFilesystem 字段,可以配阻止对容器根目录的写入,也十分的实用。

SELinux

可以通过 SELinux 的策略配置控制用户,进程等对文件等访问控制。若要给 Container 设置 SELinux 标签,可以在 Pod 或 Container 清单的 securityContext 中包含的 seLinuxOptions 字段进行设置。

...
seLinuxOptions:
  level: "s0:c123,c456"
...

要指定 SELinux,需要在宿主操作系统中装载 SELinux 安全性模块。

源码分析

SecurityContext 功能的实现更多是通过 runtime 来完成,kubelet 侧多是进行一些判断,将 SecurityContext 参数传递给 CRI。

禁止容器以 Root 身份运行

kubelet 在创建容器时,会调用 generateContainerConfig() 方法来获取容器的配置:

// github/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_container.go
func (m *kubeGenericRuntimeManager) generateContainerConfig(container *v1.Container, pod *v1.Pod, restartCount int, podIP, imageRef string, podIPs []string, nsTarget *kubecontainer.ContainerID) (*runtimeapi.ContainerConfig, func(), error) {
  ...
  // Verify RunAsNonRoot. Non-root verification only supports numeric user.
  if err := verifyRunAsNonRoot(pod, container, uid, username); err != nil {
    return nil, cleanupAction, err
  }
  ...
}

而验证 RunAsNonRoot 配置的就是 verifyRunAsNonRoot() 方法:

// verifyRunAsNonRoot verifies RunAsNonRoot.
func verifyRunAsNonRoot(pod *v1.Pod, container *v1.Container, uid *int64, username string) error {
  effectiveSc := securitycontext.DetermineEffectiveSecurityContext(pod, container)
  // If the option is not set, or if running as root is allowed, return nil.
  if effectiveSc == nil || effectiveSc.RunAsNonRoot == nil || !*effectiveSc.RunAsNonRoot {
    return nil
  }

  if effectiveSc.RunAsUser != nil {
    if *effectiveSc.RunAsUser == 0 {
      return fmt.Errorf("container's runAsUser breaks non-root policy (pod: %q, container: %s)", format.Pod(pod), container.Name)
    }
    return nil
  }

  switch {
  case uid != nil && *uid == 0:
    return fmt.Errorf("container has runAsNonRoot and image will run as root (pod: %q, container: %s)", format.Pod(pod), container.Name)
  case uid == nil && len(username) > 0:
    return fmt.Errorf("container has runAsNonRoot and image has non-numeric user (%s), cannot verify user is non-root (pod: %q, container: %s)", username, format.Pod(pod), container.Name)
  default:
    return nil
  }
}

这里的抛错是不是很眼熟?如果 uid 为 0 则返回我们上面说到的 container has runAsNonRoot and image will run as root 错误,并且阻止容器创建。

获取有效 SecurityContext

determineEffectiveSecurityContext() 方法用来确定 Pod 和 Container 中有效的 SecurityContext。

// github/kubernetes/pkg/kubelet/kuberuntime/security_context.go
func (m *kubeGenericRuntimeManager) determineEffectiveSecurityContext(pod *v1.Pod, container *v1.Container, uid *int64, username string) *runtimeapi.LinuxContainerSecurityContext {
  // 合成有效的 SecurityContext,如果 Pod 和 Container 都设置了,则 Container 优先
  effectiveSc := securitycontext.DetermineEffectiveSecurityContext(pod, container)
  synthesized := convertToRuntimeSecurityContext(effectiveSc)
  if synthesized == nil {
    synthesized = &runtimeapi.LinuxContainerSecurityContext{
      MaskedPaths:   securitycontext.ConvertToRuntimeMaskedPaths(effectiveSc.ProcMount),
      ReadonlyPaths: securitycontext.ConvertToRuntimeReadonlyPaths(effectiveSc.ProcMount),
    }
  }

  // 设置 SeccompProfilePath.
  synthesized.SeccompProfilePath = m.getSeccompProfile(pod.Annotations, container.Name, pod.Spec.SecurityContext, container.SecurityContext)

  // 设置 ApparmorProfile.
  synthesized.ApparmorProfile = apparmor.GetProfileNameFromPodAnnotations(pod.Annotations, container.Name)

  // 设置 RunAsUser.
  if synthesized.RunAsUser == nil {
    if uid != nil {
      synthesized.RunAsUser = &runtimeapi.Int64Value{Value: *uid}
    }
    synthesized.RunAsUsername = username
  }

  // 设置 namespace 选项和 Supplemental Groups
  synthesized.NamespaceOptions = namespacesForPod(pod)
  podSc := pod.Spec.SecurityContext
  if podSc != nil {
    if podSc.FSGroup != nil {
      synthesized.SupplementalGroups = append(synthesized.SupplementalGroups, int64(*podSc.FSGroup))
    }

    if podSc.SupplementalGroups != nil {
      for _, sg := range podSc.SupplementalGroups {
        synthesized.SupplementalGroups = append(synthesized.SupplementalGroups, int64(sg))
      }
    }
  }
  if groups := m.runtimeHelper.GetExtraSupplementalGroupsForPod(pod); len(groups) > 0 {
    synthesized.SupplementalGroups = append(synthesized.SupplementalGroups, groups...)
  }

  // 判断是否应该添加 no_new_privs 选项
  synthesized.NoNewPrivs = securitycontext.AddNoNewPrivileges(effectiveSc)

  // 将 ProcMountType 转换为指定或默认的 masked paths
  synthesized.MaskedPaths = securitycontext.ConvertToRuntimeMaskedPaths(effectiveSc.ProcMount)
  // 将 ProcMountType 转换为指定或默认的 readonly paths
  synthesized.ReadonlyPaths = securitycontext.ConvertToRuntimeReadonlyPaths(effectiveSc.ProcMount)

  return synthesized
}

筛选出有效的配置会被用来生成 Linux 容器配置。

docker Seccomp 配置

如果 runtime 为 docker,在 CreateContainer() 中:

// github/kubernetes/pkg/kubelet/dockershim/docker_container.go
func (ds *dockerService) CreateContainer(_ context.Context, r *runtimeapi.CreateContainerRequest) (*runtimeapi.CreateContainerResponse, error) {
  ...
  securityOpts, err := ds.getSecurityOpts(config.GetLinux().GetSecurityContext().GetSeccompProfilePath(), securityOptSeparator)
  if err != nil {
    return nil, fmt.Errorf("failed to generate security options for container %q: %v", config.Metadata.Name, err)
  }

  hc.SecurityOpt = append(hc.SecurityOpt, securityOpts...)
  ...
}

getSecurityOpts() 方法就是将 SecurityContext 中 seccomp 有关配置格式化:

// github/kubernetes/pkg/kubelet/dockershim/helpers_linux.go
func (ds *dockerService) getSecurityOpts(seccompProfile string, separator rune) ([]string, error) {
  // Apply seccomp options.
  seccompSecurityOpts, err := getSeccompSecurityOpts(seccompProfile, separator)
  if err != nil {
    return nil, fmt.Errorf("failed to generate seccomp security options for container: %v", err)
  }

  return seccompSecurityOpts, nil
}
// getSeccompSecurityOpts gets container seccomp options from container seccomp profile.
// It is an experimental feature and may be promoted to official runtime api in the future.
func getSeccompSecurityOpts(seccompProfile string, separator rune) ([]string, error) {
  seccompOpts, err := getSeccompDockerOpts(seccompProfile)
  if err != nil {
    return nil, err
  }
  return fmtDockerOpts(seccompOpts, separator), nil
}
// fmtDockerOpts formats the docker security options using the given separator.
func fmtDockerOpts(opts []dockerOpt, sep rune) []string {
  fmtOpts := make([]string, len(opts))
  for i, opt := range opts {
    fmtOpts[i] = fmt.Sprintf("%s%c%s", opt.key, sep, opt.value)
  }
  return fmtOpts
}

最终通过 createResp, createErr := ds.client.CreateContainer(createConfig) 传递给 docker runtime client,用来创建容器。

结语

SecurityContext 相关内容更多是和 Linux 知识挂钩,内容比较庞杂且资料较少,花费了1周多的时间也没能写出令人满意的内容。由于精力有限,只能做一个阶段性的总结,希望今后有空能更深入的解析这部分内容。

欢迎扫描二维码关注公众号,了解更多云原生知识

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
4月前
|
Kubernetes 调度 数据中心
在K8S中,Pod中关于资源有request和limit两个字段?这么设计的原因是什么?
在K8S中,Pod中关于资源有request和limit两个字段?这么设计的原因是什么?
|
4月前
|
Kubernetes 负载均衡 容器
在K8S中,nodePort的externalTrafficPolicy字段有什么作用?
在K8S中,nodePort的externalTrafficPolicy字段有什么作用?
|
存储 Kubernetes 安全
Kubernetes 中的对象是如何删除的:Finalizers 字段介绍
Kubernetes 中的对象删除并不像表面上看起来那么简单,删除对象涉及一系列过程,例如对象的级联和非级联删除,在删除之前检查以确定是否可以安全删除对象等等。这些都是通过称为 `Finalizers`(终结器)的 API 对象实现的。
1165 0
Kubernetes 中的对象是如何删除的:Finalizers 字段介绍
|
存储 Kubernetes Linux
【探索 Kubernetes|作业管理篇 系列 8】探究 Pod 的 API 对象属性级别与重要字段用法
【探索 Kubernetes|作业管理篇 系列 8】探究 Pod 的 API 对象属性级别与重要字段用法
118 1
|
Kubernetes 容器 Perl
kubernetes:字段选择器(field-selector)标签选择器(labels-selector)和筛选 Kubernetes 资源
kubernetes:字段选择器(field-selector)标签选择器(labels-selector)和筛选 Kubernetes 资源
1508 0
kubernetes:字段选择器(field-selector)标签选择器(labels-selector)和筛选 Kubernetes 资源
|
JSON Kubernetes 前端开发
熟悉又陌生的 k8s 字段:finalizers
metadata 中熟悉又陌生的字段 finalizers
6712 0
|
2天前
|
Prometheus Kubernetes 监控
OpenAI故障复盘 - 阿里云容器服务与可观测产品如何保障大规模K8s集群稳定性
聚焦近日OpenAI的大规模K8s集群故障,介绍阿里云容器服务与可观测团队在大规模K8s场景下我们的建设与沉淀。以及分享对类似故障问题的应对方案:包括在K8s和Prometheus的高可用架构设计方面、事前事后的稳定性保障体系方面。
|
4天前
|
Kubernetes 网络协议 应用服务中间件
Kubernetes Ingress:灵活的集群外部网络访问的利器
《Kubernetes Ingress:集群外部访问的利器-打造灵活的集群网络》介绍了如何通过Ingress实现Kubernetes集群的外部访问。前提条件是已拥有Kubernetes集群并安装了kubectl工具。文章详细讲解了Ingress的基本组成(Ingress Controller和资源对象),选择合适的版本,以及具体的安装步骤,如下载配置文件、部署Nginx Ingress Controller等。此外,还提供了常见问题的解决方案,例如镜像下载失败的应对措施。最后,通过部署示例应用展示了Ingress的实际使用方法。
20 2
|
16天前
|
存储 Kubernetes 关系型数据库
阿里云ACK备份中心,K8s集群业务应用数据的一站式灾备方案
本文源自2024云栖大会苏雅诗的演讲,探讨了K8s集群业务为何需要灾备及其重要性。文中强调了集群与业务高可用配置对稳定性的重要性,并指出人为误操作等风险,建议实施周期性和特定情况下的灾备措施。针对容器化业务,提出了灾备的新特性与需求,包括工作负载为核心、云资源信息的备份,以及有状态应用的数据保护。介绍了ACK推出的备份中心解决方案,支持命名空间、标签、资源类型等维度的备份,并具备存储卷数据保护功能,能够满足GitOps流程企业的特定需求。此外,还详细描述了备份中心的使用流程、控制台展示、灾备难点及解决方案等内容,展示了备份中心如何有效应对K8s集群资源和存储卷数据的灾备挑战。
|
1月前
|
Kubernetes 监控 Cloud Native
Kubernetes集群的高可用性与伸缩性实践
Kubernetes集群的高可用性与伸缩性实践
74 1

热门文章

最新文章

推荐镜像

更多