在 Kubernetes 中安全地运行应用

在 Kubernetes 中安全地运行应用

背景

在近期的工作当中我大量接触 Kubernetes 集群以及容器化应用,在完成 Operator 拓展的开发和发布后,有海外用户反映在 OpenShift 平台上运行我们的容器化应用会遇到安全性问题,具体而言是我们的容器化应用默认需要 root 用户运行,而 OpenShift 平台如果不进行专门的设置是不允许容器使用 root 用户的。为了解决该用户的问题我们花费了一些功夫,正好我也想以此为契机进一步了解如何在 Kubernetes 中安全地运行应用。本文将记录我在调研和学习过程中的一些心得体会。

容器安全

Docker Init CLI

Docker Init 命令是用来创建遵循最佳实践的 Docker 配置文件的命令行工具。在使用时通过选择需要运行的应用类型(例如 Go、Python、Node、Rust 等),Docker 会自动帮助用户创建出符合最佳实践的 Dockerfile 和 compose.yaml 文件。

我的操作系统是 macOS, Docker 版本为 25.0.5,后续的版本中支持的应用类型可能会更多。

docker init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ docker init

Welcome to the Docker Init CLI!

This utility will walk you through creating the following files with sensible defaults for your project:
- .dockerignore
- Dockerfile
- compose.yaml

Let's get started!

? What application platform does your project use? [Use arrows to move, type to filter]
Go - suitable for a Go server application
Python - suitable for a Python server application
Node - suitable for a Node server application
Rust - suitable for a Rust server application
> Other - general purpose starting point for containerizing your application
Don't see something you need? Let us know!
Quit

以 Go 语言应用为例

通过 Docker Init 创建出 Go 语言程序的 Dockerfile 示例如下,

Dockerfile
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
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.22
FROM golang:${GO_VERSION} AS build
WORKDIR /src

RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download -x

RUN --mount=type=cache,target=/go/pkg/mod/ \
--mount=type=bind,target=. \
CGO_ENABLED=0 go build -o /bin/server ./k8s-safety

FROM alpine:latest AS final

RUN --mount=type=cache,target=/var/cache/apk \
apk --update add \
ca-certificates \
tzdata \
&& \
update-ca-certificates

ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
USER appuser

COPY --from=build /bin/server /bin/

EXPOSE 8080

ENTRYPOINT [ "/bin/server" ]

首先设置好 Go 的版本和工作目录后,通过 cache 和 bind 两种挂载将 go mod download -x 命令所依赖的文件缓存和 go.mod、go.sum 文件挂载到容器中,然后通过 go build -o /bin/server ./k8s-safety 命令编译出二进制文件。这里使用了 --mount=type=cache--mount=type=bind 两种挂载方式,前者是将缓存文件挂载到容器中,后者是将本地文件挂载到容器中。这样做的好处是可以减少容器构建时间,提高构建效率。如果直接使用 COPY 或者 ADD 命令将本地文件拷贝到容器中,每次构建都会重新拷贝一次,效率较低,而且这两个命令可能会在镜像中残留一些不必要的信息,增加了潜在的安全风险。

该文件使用了两阶段构建,这是生产环境镜像常用的构建方法。如果直接采用 golang 的镜像作为运行镜像,其体积大不说(282 MB 左右),因为其中包含了编译环境,还会暴露一些不必要的信息,增加了潜在的安全风险。使用 alpine 镜像作为运行镜像,体积小(5.6 MB 左右),减小镜像体积的同时减小了攻击面。

在 alpine 镜像中通过 apk --update add 命令安装了一些必要的软件包,涉及了 TLS 证书和时区信息。再通过 adduser 命令创建了一个非 root 用户 appuser,并将其设置为容器运行时的用户。该用户没有设置密码、Home 目录和登录 shell,拥有最小的权限和资源,提高了容器的安全性。最后通过 COPY 命令从构建镜像中把编译好的二进制文件拷贝到运行镜像中,设置了容器监听的端口和启动命令。

通过 Docker Init 创建的这个 Dockerfile 遵循了一些最佳实践,例如使用多阶段构建、使用最小化的基础镜像、设置非 root 用户等,提高了容器的安全性。

配置管理

ConfigMap 和 Secret

在 Kubernetes 中,配置管理是一个非常重要的环节。在应用部署时,我们需要将应用的配置信息注入到容器中,以便应用能够正常运行。在配置管理中,我们需要合理的使用 ConfigMap 和 Secret 两种资源对象。其中 ConfigMap 用来存储应用的配置信息,Secret 用来存储应用的敏感信息,例如密码、证书等。它们都可以通过环境变量或者存储卷挂载的方式进行注入。

在我们的开发中常用 Write Ahead ConfigMap/Secret 的形式来存储配置信息,在应用启动时从 ConfigMap 或者 Secret 中读取配置信息到内存中,运行时若有修改,先修改 ConfigMap 或者 Secret,再修改内存中的数据,这样可以在应用重启时也能保持原有的配置,相当于用 K8s 的 Etcd 来存储配置信息。但需要注意的是 ConfigMap 和 Secret 一般有内容大小限制(1MB 左右),如果配置信息过大,可能会导致存储失败。

另外,我们还可以通过 ConfigMap/Secret 的更新机制来实现配置的热更新,避免了应用重启的问题。例如 local-path-provisioner 采用了轮询的方式来检测挂载的 ConfigMap 是否变化。

命名空间

Kubernetes 中的命名空间是用来对集群中的资源进行逻辑隔离的资源。通过命名空间,我们可以将集群中的资源划分为不同的逻辑单元,提高了资源的管理和安全性。在实际的应用部署过程中,我们可以根据业务需求和安全要求,将不同的资源放置在不同的命名空间中,以便更好的管理、控制资源和隔离风险。最直观的是 ConfigMap、Secret、Service 等都是按照命名空间划分的资源,分别决定了应用的配置、密钥和服务访问等。

通过命名空间的划分,将开发、测试和生产的资源分开,将提供不同服务的应用分开,将不同业务线的应用分开,有条不紊地将集群中的资源组织起来是非常重要的。

资源配额

Pod 的模板中可以为每个容器设置资源请求(Request)和限制(Limit)。资源请求是容器启动时所需的资源,资源限制是容器能够使用的资源的上限。通过设置资源请求和限制,我们可以更好的控制容器的资源使用,避免资源的浪费和滥用。如果使用 VSCode 进行开发并且安装了 Kubernetes 插件,在识别到 Pod 模板没有填写 Request 和 Limit 时会有警告提示,这也一定程度上说明了最佳实践是如何。当然如果是部分设置,需要区分情况:

  • 没有设置 Limit,Pod 可以使用集群中的所有资源,可能会导致节点上其他 Pod 无法正常运行。
  • 没有设置 Request,K8s 无法决定在哪个节点上调度 Pod,可能会导致该 Pod 无法正常运行。

总之,Pod 中资源请求和限制的设置是非常重要的,是 Kubernetes 正确调度 Pod 的基础。

访问控制

Kubernetes 内置的 RBAC(Role-Based Access Control)是一种基于角色的访问控制机制,用来控制用户对集群资源的访问权限,是一种成熟的访问控制方案。通过 RBAC,我们可以为用户(ServiceAccount)分配不同的角色,不同的角色拥有不同的权限,从而实现对集群资源的精细化控制。其核心是通过 Role、RoleBinding、ClusterRole 和 ClusterRoleBinding 四种资源对象来实现。在 Role 中定义一组权限,包括对何种资源能够采取何种操作,然后通过 RoleBinding 将 Role 绑定到用户上,从而实现对用户的授权。ClusterRole 和 ClusterRoleBinding 与 Role 和 RoleBinding 类似,只是作用于集群级别。

RBAC 相关的资源在 rbac.authorization.k8s.io API 组中,通过 kubectl api-resources 命令可以查看到。下面这是一个简单的 RBAC 角色配置。

rbac-role.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-deployment-manager
rules:
- apiGroups: [""] # 空字符串表示 core API 组
resources:
- pods
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups: ["apps"]
resources:
- deployments
verbs: ["*"] # * 表示所有操作

上述配置定义了一个名为 pod-deployment-manager 的 Role,该 Role 具有对 Pod 和 Deployment 资源的 get、list、watch、create、update、patch 和 delete 操作权限。然后通过 RoleBinding 将该 Role 绑定到用户上。

rbac-role-binding.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: default
name: pod-deployment-manager-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: pod-deployment-manager
subjects:
- kind: User
name: pod-deployment-manager
- kind: ServiceAccount
name: pod-deployment-manager-sa

上述配置定义了一个名为 pod-deployment-manager-binding 的 RoleBinding,将 pod-deployment-manager Role 绑定到 pod-deployment-manager 用户和 pod-deployment-manager-sa ServiceAccount 上。这样,pod-deployment-manager 用户和 pod-deployment-manager-sa ServiceAccount 就具有了对 Pod 和 Deployment 资源的 get、list、watch、create、update、patch 和 delete 操作权限。将具有权限的 ServiceAccount 分配给 Pod,就可以实现对 Pod 的授权。

在生产环境的部署中,需要按照“最小权限”地原则给每个应用和用户单独分配权限,避免权限过大导致的安全风险。

准入控制

准入控制是 Kubernetes 中一种用来控制集群中资源的创建和修改的机制,可以通过准入控制器来实现。准入控制器对请求的资源进行验证和审批,只有通过了验证和审批的资源才能被创建或修改。准入控制器可以通过 Webhook 的方式实现,也可以通过 Admission Controller 的方式实现。在自定义 Kubernetes 拓展时往往涉及到引入自定义资源 CRD(Custom Resource Definition),同大多数 K8s 资源会对配置进行校验一样,我们也可以通过准入控制器对 CRD 的配置进行校验,保证配置的正确性和安全性。

准入控制器的工作原理是当 K8s APIServer 收到请求时,会将请求发送给准入控制器,准入控制器对请求进行验证和审批,然后返回给 APIServer,APIServer 根据准入控制器的结果决定是否允许请求。准入控制器可以对请求的资源进行各种验证,例如验证资源的名称、标签、注解、配置等,在资源不符合校验条件时拒绝请求;也可以对请求的资源进行变更,例如自动添加标签、注解、配置等。这两种准入控制逻辑在 K8s 中分别由 validatingwebhookconfigurationsmutatingwebhookconfigurations 实现。API Server 通过调用 Webhook 服务来实现准入控制逻辑,Webhook 服务可以是内部服务也可以是外部服务,如果使用 kubebuilder 等 Operator 构建框架来进行 Kubernetes 的拓展,Webhook 服务是自动集成到 Operator 中的。

小结

本文从容器安全、配置管理、访问控制和准入控制几方面介绍了在 Kubernetes 中安全地运行应用的一些方法和最佳实践。目前涉及到的实体交互尚停留在容器本身的运行和配置管理层面,后续将在另外的文章中继续对 Kubernetes 中的网络安全、存储安全、日志安全进行学习。

参考

在 Kubernetes 中安全地运行应用

https://powerfooi.github.io/2024/05/19/RunAppSafelyInK8s/

作者

PowerfooI

发布于

2024-05-19

更新于

2024-08-04

许可协议

评论