原文:
zh.annas-archive.org/md5/525e77013d4f84e5576c98a6b161f26a
译者:飞龙
第五章:最小化微服务漏洞
在 Kubernetes 集群中操作的应用程序堆栈通常遵循微服务架构。“最小化微服务漏洞”领域涵盖了在 Pod 级别上实施安全设置的治理和强制执行。我们将介绍 Kubernetes 核心功能以及外部工具,帮助减少安全漏洞。此外,我们还将讨论运行微服务的 Pod 之间的加密网络通信。
在高层次上,本章涵盖以下概念:
-
设置适当的操作系统级安全域与安全上下文,Pod 安全审计(PSA)和 Open Policy Agent Gatekeeper
-
管理秘密
-
使用容器运行时沙盒,如 gVisor 和 Kata 容器
-
通过双向传输层安全协议(TLS)实现 Pod 之间的通信加密
设置适当的操作系统级安全域
Kubernetes 核心和 Kubernetes 生态系统均提供了定义、实施和管理 Pod 和容器级别安全设置的解决方案。本节将讨论安全上下文、Pod 安全审计和 Open Policy Agent Gatekeeper。您将学习如何通过示例应用每个功能和工具,展示它们在安全性方面的重要性。让我们从设置一个场景开始。
情景:攻击者滥用 root 用户容器访问权限
默认情况下,容器以 root 权限运行。应用程序中的漏洞可能会授予攻击者root
访问容器的权限。容器的 root 用户与主机上的 root 用户相同。攻击者不仅可以检查或修改应用程序,还可能安装额外的工具,使攻击者能够打破容器并以root
权限进入主机命名空间。攻击者还可以将主机文件系统的敏感数据复制到容器中。图 5-1 说明了该场景。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0501.png
图 5-1. 攻击者滥用 root 用户容器访问权限
因此,默认情况下以 root 用户运行容器是一个不好的选择。接下来的章节将解释如何为容器声明一个安全上下文,强制使用非 root 用户或特定用户和/或组标识符。我们还将讨论其他与从容器到主机的访问隔离相关的安全上下文设置。
理解安全上下文
作为容器编排引擎的 Kubernetes 可以应用额外的配置来增强容器安全性。通过定义安全上下文来实现。安全上下文定义了 Pod 或容器的特权和访问控制设置。以下列表提供了一些示例:
-
应用程序应使用的用户 ID 来运行 Pod 和/或容器
-
文件系统访问应使用的组 ID
-
授予容器内运行的进程部分 root 用户权限但不是全部权限
安全上下文不是 Kubernetes 的原语。它被建模为 Pod 规范内的一组属性,在指令 securityContext
下。在 Pod 级别定义的安全设置适用于在 Pod 中运行的所有容器;但容器级别的设置优先。有关 Pod 级别安全属性的更多信息,请参阅 PodSecurityContext API。容器级别的安全属性可以在 SecurityContext API 中找到。
强制使用非 root 用户
我们将看一个使用案例,使功能更透明。某些镜像,如用于开源反向代理服务器 nginx 的镜像,必须以 root
用户运行。假设你想强制容器不能作为 root
用户运行,以支持更合理的安全策略。文件 container-non-root-user-error.yaml
中的 YAML 清单文件(示例 5-1)定义了特定容器的安全配置。此安全上下文仅适用于此容器,而不适用于其他容器(如果你要定义更多的话)。
示例 5-1. 强制在需要以 root 用户运行的镜像上使用非 root 用户
apiVersion: v1
kind: Pod
metadata:
name: non-root-error
spec:
containers:
- image: nginx:1.23.1
name: nginx
securityContext:
runAsNonRoot: true
容器在启动过程中失败,状态为 CreateContainerConfigError
。查看 Pod 的事件日志显示,镜像尝试以 root
用户运行。配置的安全上下文不允许这样做:
$ kubectl apply -f container-non-root-user-error.yaml
pod/non-root-error created
$ kubectl get pod non-root-error
NAME READY STATUS RESTARTS AGE
non-root-error 0/1 CreateContainerConfigError 0 9s
$ kubectl describe pod non-root-error
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 24s default-scheduler Successfully \
assigned default/non-root to minikube
Normal Pulling 24s kubelet Pulling image \
"nginx:1.23.1"
Normal Pulled 16s kubelet Successfully \
pulled image "nginx:1.23.1" in 7.775950615s
Warning Failed 4s (x3 over 16s) kubelet Error: container \
has runAsNonRoot and image will run as root (pod: "non-root-error_default \
(6ed9ed71-1002-4dc2-8cb1-3423f86bd144)", container: secured-container)
Normal Pulled 4s (x2 over 16s) kubelet Container image \
"nginx:1.23.1" already present on machine
有可用的替代 nginx 容器镜像,不需要以 root
用户运行。例如 bitnami/nginx。示例 5-2 展示了文件 container-non-root-user-success.yaml
的内容。此文件的主要变更是为 spec.containers[].image
属性分配的值。
示例 5-2. 强制在支持使用用户 ID 运行的镜像上使用非 root 用户
apiVersion: v1
kind: Pod
metadata:
name: non-root-success
spec:
containers:
- image: bitnami/nginx:1.23.1
name: nginx
securityContext:
runAsNonRoot: true
使用 runAsNonRoot
指令启动容器将正常工作。容器转换为“运行”状态:
$ kubectl apply -f container-non-root-user-success.yaml
pod/non-root-success created
$ kubectl get pod non-root-success
NAME READY STATUS RESTARTS AGE
non-root-success 1/1 Running 0 7s
让我们快速检查容器使用哪个用户 ID。进入容器并运行 id
命令。输出显示用户 ID、组 ID 和附加组的 ID。镜像 bitnami/nginx
在构建容器镜像时通过指令将用户 ID 设置为 1001:
$ kubectl exec non-root-success -it -- /bin/sh
$ id
uid=1001 gid=0(root) groups=0(root)
$ exit
设置特定的用户和组 ID
许多容器镜像未设置显式的用户 ID 或组 ID。不使用 root
默认用户运行,可以设置所需的用户 ID 和组 ID,以最小化潜在的安全风险。文件 container-user-id.yaml
中存储的 YAML 清单文件(示例 5-3)将用户 ID 设置为 1000,组 ID 设置为 3000。
示例 5-3. 使用特定用户和组 ID 运行容器
apiVersion: v1
kind: Pod
metadata:
name: user-id
spec:
containers:
- image: busybox:1.35.0
name: busybox
command: ["sh", "-c", "sleep 1h"]
securityContext:
runAsUser: 1000
runAsGroup: 3000
创建 Pod 将无问题。容器转换为“运行”状态:
$ kubectl apply -f container-user-id.yaml
pod/user-id created
$ kubectl get pods user-id
NAME READY STATUS RESTARTS AGE
user-id 1/1 Running 0 6s
进入容器后,您可以检查用户 ID 和组 ID。当前用户无权在/
目录中创建文件。在/tmp
目录中创建文件将起作用,因为大多数用户都有写入权限:
$ kubectl exec user-id -it -- /bin/sh
/ $ id
uid=1000 gid=3000 groups=3000
/ $ touch test.txt
touch: test.txt: Permission denied
/ $ touch /tmp/test.txt
/ $ exit
避免使用特权容器
Kubernetes 在进程、网络、挂载、用户 ID 等方面在容器命名空间和主机命名空间之间建立了明确的分离。您可以配置容器的安全上下文以获取对主机命名空间某些方面的权限。使用特权容器时,请考虑以下影响:
-
容器内的进程几乎具有与主机上进程相同的权限。
-
容器具有访问主机上所有设备的权限。
-
容器中的
root
用户具有与主机上的root
用户类似的权限。 -
可以在容器中挂载主机文件系统上的所有目录。
-
可以通过使用
sysctl
命令更改内核设置。
使用特权模式的容器
配置容器以使用特权模式应该是一个罕见的情况。大多数运行在容器中的应用程序和进程不需要超出容器命名空间之外的提升权限。如果遇到配置为使用特权模式的 Pod,请联系负责的团队或开发者进行澄清,因为这将为攻击者打开入侵主机系统的漏洞。
让我们比较配置为非特权容器和配置为特权模式运行的容器的行为。首先,我们将设置一个常规的 Pod,如示例 5-4 所示。在 Pod 或容器级别未设置安全上下文。
示例 5-4. 使用非特权模式的容器中的 Pod
apiVersion: v1
kind: Pod
metadata:
name: non-privileged
spec:
containers:
- image: busybox:1.35.0
name: busybox
command: ["sh", "-c", "sleep 1h"]
创建 Pod 并确保其正确启动:
$ kubectl apply -f non-privileged.yaml
pod/non-privileged created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
non-privileged 1/1 Running 0 6s
为了演示容器命名空间与主机命名空间之间的隔离,我们将尝试使用sysctl
来更改主机名。正如您在命令输出中所看到的那样,容器明确执行受限权限:
$ kubectl exec non-privileged -it -- /bin/sh
/ # sysctl kernel.hostname=test
sysctl: error setting key 'kernel.hostname': Read-only file system
/ # exit
要使容器具有特权,请简单地将值true
分配给安全上下文属性privileged
。在示例 5-5 中的 YAML 清单中显示了一个示例。
示例 5-5. 配置为运行在特权模式中的容器的 Pod
apiVersion: v1
kind: Pod
metadata:
name: privileged
spec:
containers:
- image: busybox:1.35.0
name: busybox
command: ["sh", "-c", "sleep 1h"]
securityContext:
privileged: true
如常创建 Pod。Pod 应该转换为“Running”状态:
$ kubectl apply -f privileged.yaml
pod/privileged created
$ kubectl get pod privileged
NAME READY STATUS RESTARTS AGE
privileged 1/1 Running 0 6s
现在您可以看到,相同的sysctl
将允许您更改主机名:
$ kubectl exec privileged -it -- /bin/sh
/ # sysctl kernel.hostname=test
kernel.hostname = test
/ # exit
与特权模式相关的容器安全上下文配置是属性allowPrivilegeEscalation
。此属性将允许运行容器的进程获得比父进程更多的权限。该属性的默认值为false
,但如果看到将属性设置为true
,请对其使用进行严格审查。在大多数情况下,您不需要此功能。
情景:开发者不遵循 Pod 安全最佳实践
假设开发人员在 Kubernetes 功能方面,特别是适用于安全最佳实践的方面,没有广泛的知识是不公平的。在前一节中,我们了解了安全上下文以及要避免使用的设置。开发人员可能不知道这些最佳实践,没有持续的教育,因此可能会创建使用问题安全设置或根本不使用安全设置的 Pod。Figure 5-2 显示了一名开发人员使用从互联网上找到的复制清单在启用特权模式下创建 Pod。攻击者将乐意利用这种设置获取优势。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0502.png
图 5-2. 开发人员创建启用特权模式的 Pod
作为 Kubernetes 安全专家,您就是这里的关键人物。Kubernetes 生态系统提供了核心功能和外部工具,用于强制执行 Pod 的安全标准,以便没有正确配置的对象将被拒绝或至少会进行审计。下一节将探讨名为 Pod 安全入场的 Kubernetes 核心功能。
理解 Pod 安全入场(PSA)
Kubernetes 的旧版本附带了名为 Pod 安全策略(PSP)的功能。Pod 安全策略是一个概念,帮助强制执行 Pod 对象的安全标准。Kubernetes 1.21 弃用了 Pod 安全策略,并引入了其替代功能 Pod 安全入场。PSA 决定要遵循哪个 Pod 安全标准(PSS)。PSS 定义了从高度限制到高度宽松的安全策略范围。
然而,Kubernetes 1.25 完全删除了 Pod 安全策略。您可能仍然会在旧版本的 CKS 课程中看到此功能。我们在本书中将仅关注 Pod 安全入场。PSA 在 Kubernetes 1.23 中默认启用;但是,您需要声明哪些 Pod 应遵循安全标准。要选择 PSA 功能,您只需在命名空间中添加特定格式的标签。该命名空间中的所有 Pod 将必须遵循声明的标准。
标签包含三个部分:前缀、模式和级别。前缀 总是使用硬编码值 pod-security.kubernetes.io
,后面跟着一个斜线。模式 决定了违规处理方式。最后,级别 规定了遵循的安全标准的程度。这样的标签示例可能如下所示:
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted
模式允许设置三种不同的选项,如 Table 5-1 所示。
Table 5-1. Pod 安全入场模式
模式 | 行为 |
---|---|
enforce | 违规将导致 Pod 被拒绝。 |
audit | 允许创建 Pod。违规将被记录到审计日志中。 |
warn | 允许创建 Pod。违规将在控制台上显示。 |
Table 5-2 说明了由 PSA 设置的级别确定的安全策略。
Table 5-2. Pod 安全入场级别
级别 | 行为 |
---|---|
privileged | 完全无限制的策略。 |
baseline | 最低限度的限制性策略,覆盖了关键标准。 |
restricted | 遵循从安全角度加固 Pod 的最佳实践的严格限制性策略。 |
详细了解 PSA 的内容,请参阅 Kubernetes 文档。
为命名空间执行 Pod 安全标准
在 psa
命名空间中将 PSA 应用于一个 Pod。示例 5-6 展示了命名空间的定义及相关标签的声明。该标签将强制执行最高级别的安全标准。
Example 5-6. 强制执行最高级别安全标准的命名空间
apiVersion: v1
kind: Namespace
metadata:
name: psa
labels:
pod-security.kubernetes.io/enforce: restricted
确保 Pod 在 psa
命名空间中创建。示例 5-7 展示了运行 busybox
镜像的简单 Pod 的 YAML 清单。
Example 5-7. 违反 PSA 限制的 Pod
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: psa
spec:
containers:
- image: busybox:1.35.0
name: busybox
command: ["sh", "-c", "sleep 1h"]
在运行创建 Pod 的命令时,违规将在控制台中呈现。如下所示,该 Pod 无法被创建:
$ kubectl create -f psa-namespace.yaml
namespace/psa created
$ kubectl apply -f psa-violating-pod.yaml
Error from server (Forbidden): error when creating "psa-pod.yaml": pods \
"busybox" is forbidden: violates PodSecurity "restricted:latest": \
allowPrivilegeEscalation != false (container "busybox" must set \
securityContext.allowPrivilegeEscalation=false), unrestricted \
capabilities (container "busybox" must set securityContext. \
capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container \
"busybox" must set securityContext.runAsNonRoot=true), seccompProfile \
(pod or container "busybox" must set securityContext.seccompProfile. \
type to "RuntimeDefault" or "Localhost")
$ kubectl get pod -n psa
No resources found in psa namespace.
您需要配置 Pod 的安全上下文设置,以遵循非常严格的标准。示例 5-8 展示了一个示范 Pod 定义,它不违反 Pod 安全标准。
Example 5-8. 跟随 PSS 的 Pod
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: psa
spec:
containers:
- image: busybox:1.35.0
name: busybox
command: ["sh", "-c", "sleep 1h"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
runAsNonRoot: true
runAsUser: 2000
runAsGroup: 3000
seccompProfile:
type: RuntimeDefault
现在创建 Pod 对象按预期运行:
$ kubectl apply -f psa-non-violating-pod.yaml
pod/busybox created
$ kubectl get pod busybox -n psa
NAME READY STATUS RESTARTS AGE
busybox 1/1 Running 0 10s
PSA 是 Kubernetes 1.23 版本或更高版本中默认启用的内置功能。它易于采用,允许选择合适的策略标准,并可配置为强制执行或仅记录违规行为。
不幸的是,PSA 仅适用于具有预定义策略集的 Pod。您无法编写自定义规则、更改消息传递或变异 Pod 对象,如果不符合 PSS 的话。在下一节中,我们将研究超越 PSA 功能的工具。
了解开放策略代理(OPA)和 Gatekeeper
Open Policy Agent (OPA) 是一款开源的通用策略引擎,用于强制执行规则。OPA 不特定于 Kubernetes,可以在其他技术堆栈中使用。其好处之一是能够以非常灵活的方式定义策略。您可以使用名为 Rego 的查询语言编写自己的规则。在 Rego 中编写的验证逻辑确定是否接受或拒绝对象。
Gatekeeper 是 Kubernetes 的一个扩展,使用 OPA。Gatekeeper 允许为任何类型的 Kubernetes API 原语定义和强制执行自定义策略。因此,它比 PSA 更加灵活,但需要更复杂的知识来制定这些规则。Gatekeeper 参与了讨论的 准入控制 阶段,详见 “处理请求”。以下策略列表试图给您展示 Gatekeeper 的可能性:
-
确保所有 Service 对象都需要定义一个带有键
team
的标签。 -
确保 Pods 定义的所有容器镜像都从公司内部注册表中拉取。
-
确保 Deployments 需要至少控制三个副本。
在撰写本文时,Gatekeeper 允许通过拒绝对象创建来强制执行策略,如果未满足要求。Gatekeeper 的未来版本还可能提供一种在创建时改变对象的机制。例如,您可能希望为创建的任何对象添加特定的标签键值对。该变动将自动处理添加这些标签。
安装 Gatekeeper
安装 Gatekeeper 相对简单。您只需从 Gatekeeper 项目提供的 YAML 清单创建一堆 Kubernetes 对象即可。您需要有集群管理员权限来正确安装 Gatekeeper。以下命令展示了应用最新发布版的 kubectl
命令。更多信息,请参阅 安装手册。
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/\
gatekeeper/master/deploy/gatekeeper.yaml
Gatekeeper 对象已安装在命名空间 gatekeeper-system
中。确保在尝试使用 Gatekeeper 之前,命名空间中的所有 Pods 都转入“运行”状态:
$ kubectl get namespaces
NAME STATUS AGE
default Active 29h
gatekeeper-system Active 4s
...
实施 OPA 策略。
我们将使用一个具体的用例作为示例,演示定义自定义 OPA 策略所需的各部分。“使用网络策略限制 Pod 间通信” 解释了如何为命名空间分配标签,以便从网络策略中选择。在其核心,我们的自定义 OPA 策略将确定命名空间需要定义至少一个带有键 app
的标签,以表示命名空间托管的应用程序。
Gatekeeper 要求我们为自定义策略实现两个组件,约束模板 和 约束。简言之,约束模板使用 Rego 定义规则,并描述约束的模式。示例 5-9 展示了用于强制执行标签分配的约束模板定义。
示例 5-9. 使用 OPA 约束模板要求至少定义一个单一标签
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
validation:
openAPIV3Schema: <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: | <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("you must provide labels: %v", [missing])
}
声明用于约束的类型。
指定约束的验证模式。在这种情况下,我们允许传递名为 labels
的属性,其中捕获所需的标签键。
使用 Rego 检查标签的存在并将其与所需键的列表进行比较。
约束本质上是约束模板的实现。它使用约束模板定义的种类,并填充终端用户提供的数据。在 示例 5-10 中,种类是 K8sRequiredLabels
,我们在约束模板中定义了它。我们正在匹配命名空间,并期望它们定义具有键 app
的标签。
示例 5-10. 定义策略“数据”的 OPA 约束
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
metadata:
name: ns-must-have-app-label-key
spec:
match: <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters: <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
labels: ["app"]
使用约束模板定义的种类。
定义约束模板应用的 API 资源。
声明 labels
属性期望存在键 app
。
有了相关的 YAML 清单,让我们为约束模板和约束创建对象。假设约束模板写入文件 constraint-template-labels.yaml
,约束写入文件 constraint-ns-labels.yaml
:
$ kubectl apply -f constraint-template-labels.yaml
constrainttemplate.templates.gatekeeper.sh/k8srequiredlabels created
$ kubectl apply -f constraint-ns-labels.yaml
k8srequiredlabels.constraints.gatekeeper.sh/ns-must-have-app-label-key created
您可以通过快速运行的命令验证验证行为。以下命令尝试创建一个没有标签分配的新命名空间。 Gatekeeper 将呈现错误消息并阻止对象的创建:
$ kubectl create ns governed-ns
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" \
denied the request: [ns-must-have-app-label-key] you must provide labels: {"app"}
让我们确保我们实际上可以创建具有预期标签分配的命名空间。示例 5-11 展示了这样一个命名空间的 YAML 清单。
示例 5-11. 具有标签分配的命名空间的 YAML 清单
apiVersion: v1
kind: Namespace
metadata:
labels:
app: orion
name: governed-ns
以下命令从名为 namespace-app-label.yaml
的 YAML 清单文件创建对象:
$ kubectl apply -f namespace-app-label.yaml
namespace/governed-ns created
这个简单的例子演示了 OPA Gatekeeper 的使用。您可以在 OPA Gatekeeper Library 中找到许多其他示例。尽管 CKS 课程没有明确说明,您可能还想查看最近在 Kubernetes 社区中获得了很多关注的项目 Kyverno。
管理秘密
Kubernetes 安全功能讨论不完整,没有提到 Secrets。我假设你已经非常熟悉 API 原语 Secret 用于定义敏感数据以及在 Pod 中消费的不同选项。考虑到这个主题已经是 CKA 考试的一部分,我在这里不再赘述。更多信息,请参阅《Certified Kubernetes Administrator (CKA) Study Guide》中的相关章节或者Kubernetes 文档。我在“Configuring a Container with a ConfigMap or Secret”章节中讨论在容器中消费 ConfigMaps 和 Secrets 时的安全方面。
CKS 考试更加强调 Secret 管理的更专业方面。其中一个场景是处理可以分配给服务账户的 Secret,我们已经提到过。回顾“Creating a Secret for a service account”以刷新你对该主题的记忆。由于我们不会在这里讨论所有内置的 Secret 类型,请阅读相关章节了解它们的用途和创建方式在Kubernetes 文档中。
存储 Secrets 键值对的中心位置是 etcd。让我们来看看,如果攻击者能够访问 Kubernetes 后端存储集群数据,可能会出现的潜在问题。
场景:攻击者获得访问运行 etcd 的节点
etcd 运行的位置取决于你的 Kubernetes 集群的拓扑结构。为了这个场景,我们假设 etcd 运行在控制平面节点上。存储在 etcd 中的任何数据都以未加密形式存在,因此访问控制平面节点允许以明文形式读取 Secrets。图 5-3 展示了攻击者访问控制平面节点和因此在 etcd 中的未加密 Secrets。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0503.png
图 5-3. 攻击者获得访问 etcd 并读取 Secrets
缓解这种情况的一种方法是加密存储在 etcd 中的数据。无论是使用 etcdctl
访问 etcd 还是从文件系统读取 etcd 数据,都不会再暴露出人类可读的敏感信息。
访问 etcd 数据
我们将首先展示,攻击者在能够登录控制平面节点后如何读取 etcd 数据。首先,我们需要创建一个 Secret 对象以存储在 etcd 中。使用以下命令来创建一个条目:
$ kubectl create secret generic app-config --from-literal=password=passwd123
secret/app-config created
我们创建了一个带有键值对 password=passwd123
的 Secret。使用 SSH 登录控制平面节点。你可以轻松使用 etcd 客户端工具 etcdctl
从 etcd 中读取一个条目。
使用 etcd 客户端工具 etcdctl
很可能您尚未在控制平面节点上安装etcdctl
。请按照安装手册安装工具。在 Debian Linux 上,可以使用sudo apt install etcd-client
进行安装。要对 etcd 进行身份验证,您需要提供必需的命令行选项--cacert
、--cert
和--key
。您可以在通常位于/etc/kubernetes/manifests/kube-apiserver.yaml
的 API 服务器配置文件中找到相应的值。参数需要以--etcd
前缀开头。
以下命令使用必需的 CLI 选项从名为app-config
的秘密对象中读取内容。以下输出以十六进制格式显示文件内容。虽然不是 100%明显,但您仍然可以从输出中识别出明文的键值对:
$ sudo ETCDCTL_API=3 etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/\
etcd/server.key get /registry/secrets/default/app-config | hexdump -C
00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret|
00000010 73 2f 64 65 66 61 75 6c 74 2f 61 70 70 2d 63 6f |s/default/app-co|
00000020 6e 66 69 67 0a 6b 38 73 00 0a 0c 0a 02 76 31 12 |nfig.k8s.....v1.|
00000030 06 53 65 63 72 65 74 12 d9 01 0a b7 01 0a 0a 61 |.Secret........a|
00000040 70 70 2d 63 6f 6e 66 69 67 12 00 1a 07 64 65 66 |pp-config....def|
00000050 61 75 6c 74 22 00 2a 24 36 38 64 65 65 34 34 38 |ault".*$68dee448|
00000060 2d 34 39 62 37 2d 34 34 32 66 2d 39 62 32 66 2d |-49b7-442f-9b2f-|
00000070 33 66 39 62 39 62 32 61 66 66 36 64 32 00 38 00 |3f9b9b2aff6d2.8.|
00000080 42 08 08 97 f8 a4 9b 06 10 00 7a 00 8a 01 65 0a |B.........z...e.|
00000090 0e 6b 75 62 65 63 74 6c 2d 63 72 65 61 74 65 12 |.kubectl-create.|
000000a0 06 55 70 64 61 74 65 1a 02 76 31 22 08 08 97 f8 |.Update..v1"....|
000000b0 a4 9b 06 10 00 32 08 46 69 65 6c 64 73 56 31 3a |.....2.FieldsV1:|
000000c0 31 0a 2f 7b 22 66 3a 64 61 74 61 22 3a 7b 22 2e |1./{"f:data":{".|
000000d0 22 3a 7b 7d 2c 22 66 3a 70 61 73 73 77 6f 72 64 |":{},"f:password|
000000e0 22 3a 7b 7d 7d 2c 22 66 3a 74 79 70 65 22 3a 7b |":{}},"f:type":{|
000000f0 7d 7d 42 00 12 15 0a 08 70 61 73 73 77 6f 72 64 |}}B.....password|
00000100 12 09 70 61 73 73 77 64 31 32 33 1a 06 4f 70 61 |..passwd123..Opa|
00000110 71 75 65 1a 00 22 00 0a |que.."..|
接下来,我们将加密存储在 etcd 中的秘密,然后使用相同的命令验证现有条目。
加密 etcd 数据
您可以使用命令行选项--encryption-provider-config
控制 API 数据在 etcd 中如何加密,该选项提供给 API 服务器进程。分配给参数的值需要指向一个定义了EncryptionConfiguration
对象的配置文件。我们将首先创建配置文件,然后配置 API 服务器进程来消耗它。
生成一个 32 字节的随机密钥并对其进行 base64 编码。该值用于配置所谓的加密配置中的提供程序:
$ head -c 32 /dev/urandom | base64
W68xlPT/VXcOSEZJvWeIvkGJnGfQNFpvZYfT9e+ZYuY=
接下来,我们将使用 base64 编码的密钥并将其分配给加密配置中的提供程序,如示例 5-12 所示。将内容保存在文件/etc/kubernetes/enc/enc.yaml
中。
示例 5-12. 用于加密配置的 YAML 清单
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
providers:
- aescbc:
keys:
- name: key1
secret: W68xlPT/VXcOSEZJvWeIvkGJnGfQNFpvZYfT9e+ZYuY= <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
- identity: {}
定义要在 etcd 中加密的 API 资源。我们这里只加密秘密数据。
分配给 AES-CBC 加密提供程序的 base64 编码密钥。
编辑位于/etc/kubernetes/manifests/kube-apiserver.yaml
的清单,这是定义如何在 Pod 中运行 API 服务器的 YAML 清单。添加参数--encryption-provider-config
,并定义配置文件的 Volume 及其挂载路径,如下所示:
$ sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: \
192.168.56.10:6443
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
volumeMounts:
...
- name: enc
mountPath: /etc/kubernetes/enc
readonly: true
volumes:
...
- name: enc
hostPath:
path: /etc/kubernetes/enc
type: DirectoryOrCreate
...
运行 API 服务器的 Pod 应该会自动重新启动。这个过程可能需要几分钟时间。一旦完全重启,您应该能够查询它:
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
...
kube-apiserver-control-plane 1/1 Running 0 69s
新的秘密将自动加密。现有的秘密需要更新。您可以运行以下命令来跨所有命名空间执行秘密的更新。这包括default
命名空间中名为app-config
的秘密:
$ kubectl get secrets --all-namespaces -o json | kubectl replace -f -
...
secret/app-config replaced
运行我们之前使用的etcdctl
命令将显示aescbc
提供程序已用于加密数据。密码值不再能以明文读取:
$ sudo ETCDCTL_API=3 etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/\
etcd/server.key get /registry/secrets/default/app-config | hexdump -C
00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret|
00000010 73 2f 64 65 66 61 75 6c 74 2f 61 70 70 2d 63 6f |s/default/app-co|
00000020 6e 66 69 67 0a 6b 38 73 3a 65 6e 63 3a 61 65 73 |nfig.k8s:enc:aes|
00000030 63 62 63 3a 76 31 3a 6b 65 79 31 3a ae 26 e9 c2 |cbc:v1:key1:.&..|
00000040 7b fd a2 74 30 24 85 61 3c 18 1e 56 00 a1 24 65 |{..t0$.a<..V..$e|
00000050 52 3c 3f f1 24 43 9f 6d de 5f b0 84 32 18 84 47 |R<?.$C.m._..2..G|
00000060 d5 30 e9 64 84 22 f5 d0 0b 6f 02 af db 1d 51 34 |.0.d."...o....Q4|
00000070 db 57 c8 17 93 ed 9e 00 ea 9a 7b ec 0e 75 0c 49 |.W........{..u.I|
00000080 6a e9 97 cd 54 d4 ae 6b b6 cb 65 8a 5d 4c 3c 9c |j...T..k..e.]L<.|
00000090 db 9b ed bc ce bf 3c ef f6 2e cb 6d a2 53 25 49 |......<....m.S%I|
000000a0 d4 26 c5 4c 18 f3 65 bb a8 4c 0f 8d 6e be 7b d3 |.&.L..e..L..n.{.|
000000b0 24 9b a8 09 9c bb a3 f9 53 49 78 86 f5 24 e7 10 |$.......SIx..$..|
000000c0 ad 05 45 b8 cb 31 bd 38 b6 5c 00 02 b2 a4 62 13 |..E..1.8.\....b.|
000000d0 d5 82 6b 73 79 97 7e fa 2f 5d 3b 91 a0 21 50 9d |..ksy.~./];..!P.|
000000e0 77 1a 32 44 e1 93 9b 9c be bf 49 d2 f9 dc 56 23 |w.2D......I...V#|
000000f0 07 a8 ca a5 e3 e7 d1 ae 9c 22 1f 98 b1 63 b8 73 |........."...c.s|
00000100 66 3f 9f a5 6a 45 60 a7 81 eb 32 e5 42 4d 2b fd |f?..jE`...2.BM+.|
00000110 65 6c c2 c7 74 9f 1d 6a 1c 24 32 0e 7a 94 a2 60 |el..t..j.$2.z..`|
00000120 22 77 58 c9 69 c3 55 72 e8 fb 0b 63 9d 7f 04 31 |"wX.i.Ur...c...1|
00000130 00 a2 07 76 af 95 4e 03 0a 92 10 b8 bb 1e 89 94 |...v..N.........|
00000140 45 60 01 45 bf d7 95 df ff 2e 9e 31 0a |E`.E.......1.|
0000014d
要了解有关加密 etcd 数据的更多详细信息,请参阅Kubernetes 文档。在那里,您将找到关于其他加密提供者的额外信息,如何轮换解密密钥以及考虑用于高可用 (HA) 集群设置的过程。
理解容器运行时沙盒
容器在与主机环境隔离的容器运行时中运行。运行在容器中的进程或应用程序可以通过 syscalls 与内核交互。现在,我们可以在单个 Kubernetes 集群节点上运行多个容器(由 Pod 控制),因此使用相同的内核。在某些条件下,漏洞可能导致在容器运行的进程“突破”其隔离环境,并访问在同一主机上运行的另一个容器。容器运行时沙盒与常规容器运行时并行运行,但通过加强进程隔离添加了额外的安全层。
有几种情况下使用容器运行时沙盒可能是有意义的。例如,您的 Kubernetes 集群使用相同的基础设施处理不同客户的工作负载,这种称为多租户环境。另一个希望依赖更强容器隔离性的原因是,您可能不信任从公共注册表拉取的容器镜像中运行的进程或应用程序,特别是当您无法验证创建者或其运行时行为时。
情景:攻击者获取对另一个容器的访问权限
在这种情况下,我们面对的是开发者从公共注册表拉取容器镜像,如 Pod 所引用。该容器未经安全漏洞扫描。攻击者可以推送容器镜像的新标签来执行恶意代码。在实例化从该镜像运行的容器之后,运行在容器 1 的内核组中的恶意代码可以访问运行在容器 2 中的进程。正如您在图 5-4 中所看到的,两个容器使用同一主机系统的内核。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0504.png
图 5-4. 攻击者获取对另一个容器的访问权限
一般来说,盲目信任公共容器镜像并不是一个好主意。确保这样的容器镜像以更高的隔离运行的一种方法是容器运行时沙盒。下一节将向您介绍两种实现方式,这两种方式在课程中都有明确提到。
可用的容器运行时沙盒实现
在本书中,我们只讨论两种容器运行时沙箱实现,Kata Containers和gVisor。
安装和配置 gVisor
示例 5-14. 使用运行时类的 Pod 的 YAML 清单
创建和使用运行时类
接下来,配置用于签署归档和存储库的密钥。正如您在以下命令中所见,gVisor 托管在 Google 存储中:
$ sudo apt-get update && \
sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg
Kata containers 通过在轻量级虚拟机中运行来实现容器隔离。gVisor 采取了不同的方法。它有效地实现了在主机系统上运行的 Linux 内核。因此,主机系统上的所有容器不再共享系统调用。
$ curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/\
keyrings/gvisor-archive-keyring.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/\
gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases \
release main" | sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null
最后,重新启动 containerd 以使更改生效:
$ sudo apt-get update && sudo apt-get install -y runsc
现在我们可以在 Pod 的配置中引用运行时类名gvisor
。Example 5-14 展示了一个分配了spec.runtimeClassName
属性的 Pod 定义,指定了运行时类。
$ cat <<EOF | sudo tee /etc/containerd/config.toml
version = 2
[plugins."io.containerd.runtime.v1.linux"]
shim_debug = true
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
EOF
使用apply
命令创建运行时类和 Pod 对象:
$ sudo systemctl restart containerd
示例 5-13. 使用 runsc 处理程序定义运行时类的 YAML 清单
gVisor 包含一个名为 runsc 的 Open Container Initiative (OCI)运行时。runsc 运行时与 Docker 和 Kubernetes 等工具集成,用于运行容器运行时沙箱。以下命令从存储库安装可执行文件:
假设我们正在使用 containerd 作为容器运行时。您需要向 containerd 添加一些配置以使其意识到 runsc。您可以在 gVisor 文档中找到其他容器运行时的类似说明:
使用容器运行时沙箱在 Pod 中是一个两步骤的过程。首先,您需要创建一个运行时类。RuntimeClass 是一个 Kubernetes API 资源,用于定义容器运行时的配置。Example 5-13 展示了一个使用 runsc 处理程序的容器运行时的 YAML 清单。
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
以下instructions描述了使用apt
软件包管理器在 Linux 上安装 gVisor 所需的步骤。您需要在所有被声明为工作节点的主机机器上重复这些步骤。在考试中,您不需要安装 gVisor 或 Kata Containers。您可以假设容器运行时沙箱已经安装并配置好了。
从以下命令开始安装 gVisor 的依赖项:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
runtimeClassName: gvisor
containers:
- name: nginx
image: nginx:1.23.2
对于这些容器运行时沙箱实现的特性集或特定用例的深入讨论超出了本书的范围。我们将简单地学习如何使用一个解决方案作为示例,即 gVisor,并如何将其与 Kubernetes 集成。详细比较,请查看讲座“Kata Containers 和 gVisor:定量比较”。
$ kubectl apply -f runtimeclass.yaml
runtimeclass.node.k8s.io/gvisor created
$ kubectl apply -f pod.yaml
pod/nginx created
您可以通过设置容器运行时沙箱来验证容器是否正在运行。只需执行 dmesg
命令来检查内核环形缓冲区。命令的输出应该提到 gVisor,如下所示:
$ kubectl exec nginx -- dmesg
[ 0.000000] Starting gVisor...
[ 0.123202] Preparing for the zombie uprising...
[ 0.415862] Rewriting operating system in Javascript...
[ 0.593368] Reading process obituaries...
[ 0.741642] Segmenting fault lines...
[ 0.797360] Daemonizing children...
[ 0.831010] Creating bureaucratic processes...
[ 1.313731] Searching for needles in stacks...
[ 1.455084] Constructing home...
[ 1.834278] Gathering forks...
[ 1.928142] Mounting deweydecimalfs...
[ 2.109973] Setting up VFS...
[ 2.157224] Ready!
理解 Pod 与 Pod 之间的 mTLS 加密
在 “使用网络策略限制 Pod 与 Pod 通信” 中,我们谈论了 Pod 与 Pod 之间的通信。一个重要的要点是,除非你制定更严格的网络策略,否则每个 Pod 都可以通过定位其虚拟 IP 地址与任何其他 Pod 通信。默认情况下,两个 Pod 之间的通信是未加密的。
TLS 提供网络通信的加密,通常与 HTTP 协议一起使用。当我们谈论从浏览器对 Web 页面的调用时,就会涉及使用 HTTPS 协议。作为认证过程的一部分,客户端向服务器提供其客户端证书以证明其身份。但服务器不对客户端进行认证。
在加载 Web 页面时,客户端(即浏览器)的身份通常并不重要。重要的是 Web 页面证明其身份。双向 TLS(mTLS)类似于 TLS,但双方都必须进行身份验证。这种方法有以下几个好处。首先,通过加密实现安全通信。其次,可以验证客户端身份。攻击者不能轻易冒充另一个 Pod。
情景:攻击者监听两个 Pod 之间的通信
攻击者可以利用默认的未加密 Pod 与 Pod 之间的网络通信行为来加以利用。如你在 图 5-5 中所见,攻击者甚至不需要侵入 Pod。他们可以简单地通过冒充发送端或接收端来监听 Pod 与 Pod 的通信,提取敏感信息,然后用于更高级的攻击向量。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0505.png
图 5-5. 攻击者监听 Pod 与 Pod 之间的通信
通过设置 mTLS,你可以缓解这种情况。下一节将简要介绍实现这一目标的选项。
在 Kubernetes 中采用 mTLS
在 Kubernetes 集群中实现 mTLS 的棘手部分是证书的管理。正如你可以想象的,当实施微服务架构时,我们将不得不处理大量的证书。这些证书通常由官方证书颁发机构(CA)生成,以确保它们可以信任。请求证书涉及向 CA 发送证书签名请求(CSR)。如果 CA 批准请求,它将创建证书,然后签名并返回。建议在证书到期之前分配较短的生命周期,然后重新发行。这个过程称为证书轮换。
CKS 考试需要您了解 mTLS 的详细程度尚不太清楚。请求和批准证书的一般过程在Kubernetes 文档中有描述。
在大多数情况下,Kubernetes 管理员依赖于 Kubernetes 服务网格来实现 mTLS,而不是手动实现。Kubernetes 服务网格(如 Linkerd 或 Istio)是一种工具,用于为集群添加横切功能,如可观察性和安全性。
另一个选项是使用透明加密来确保流量不会在传输中未加密。一些流行的 CNI 插件,如Calico和Cilium,已经增加了对WireGuard的支持。WireGuard 是一种开源、轻量级和安全的虚拟专用网络(VPN)解决方案,无需配置或管理加密密钥或证书。许多团队更喜欢 WireGuard 而不是服务网格,因为它更易于管理。
考试不涵盖服务网格和 WireGuard。
总结
对于 Pod 来说,执行安全最佳实践非常重要。在本章中,我们审查了不同的选项。我们关注安全上下文以及如何在 Pod 和容器级别定义它们。例如,我们可以为容器配置一个安全上下文,使其以非 root 用户运行并防止使用特权模式。通常由开发人员负责定义这些设置。Pod 安全入场是 Kubernetes 的一个功能,可以进一步配置 Pod 安全设置。配置的安全标准可以强制执行、审计或仅记录到标准输出。Gatekeeper 是一个开源项目,实现了用于 Kubernetes 的 Open Policy Agent 的功能。不仅可以管理 Pod 对象的配置,还可以在创建时对其他类型的对象应用策略。
由 Secrets 定义的键值对以明文存储在 etcd 中。您应该配置 etcd 的加密,以确保攻击者无法从中读取敏感数据。要启用加密,创建一个 EncryptionConfiguration 的 YAML 清单,然后使用命令行选项--encryption-provider-config
将其传递给 API 服务器进程。
容器运行时沙盒比常规容器运行时更有效地隔离进程和应用程序。项目 Kata Containers 和 gVisor 是此类容器运行时沙盒的实现,可以安装并配置以与 Kubernetes 一起工作。我们尝试了 gVisor。安装和配置 gVisor 后,您需要创建一个指向 runsc 的 RuntimeClass 对象。在 Pod 配置中,通过名称指向 RuntimeClass 对象。
默认情况下,Pod 到 Pod 的通信是未加密且未认证的。双向 TLS 可以使这个过程更安全。互相通信的 Pods 需要提供证书来证明它们的身份。为具有数百个微服务的集群实现 mTLS 是一个繁琐的任务。每个运行微服务的 Pod 需要使用来自客户机机构的批准证书。服务网格可以作为 Kubernetes 集群中添加 mTLS 功能的一种方式。
考试要点
练习使用核心 Kubernetes 功能和外部工具来管理安全设置。
在本章的过程中,我们探讨了操作系统级别的安全设置及如何通过不同的核心功能和外部工具进行管理。您需要了解不同选项、它们的优势和局限性,并能够根据情境要求应用它们。练习使用安全上下文、Pod 安全审核和 Open Policy Agent Gatekeeper。Kubernetes 生态系统在这方面提供了更多的工具。可以自行探索以扩展视野。
了解 etcd 如何管理 Secrets 数据。
CKA 考试已经涵盖了使用 Secrets 将敏感配置数据注入到 Pods 的工作流程。我假设您已经知道如何操作。每个 Secret 的键值对都存储在 etcd 中。通过学习如何加密 etcd 来扩展您对 Secret 管理的知识,这样即使攻击者能访问运行 etcd 的主机,也无法以明文读取信息。
知道如何配置容器运行时沙盒的使用。
容器运行时沙盒有助于为容器增加更严格的隔离。您不需要安装容器运行时沙盒,如 Kata Containers 或 gVisor。但您需要了解如何通过 RuntimeClass 对象配置容器运行时沙盒的过程,并将 RuntimeClass 分配给 Pod 名称。
了解 mTLS 的重要性。
为所有在 Pod 中运行的微服务设置 mTLS 可能因证书管理而非常繁琐。在考试中,了解希望为 Pod 到 Pod 通信设置 mTLS 的一般用例。尽管您可能不需要手动实现它,但生产 Kubernetes 集群使用服务网格提供 mTLS 作为一种功能。
样例练习
这些练习的解决方案可以在 附录 中找到。
-
创建一个名为
busybox-security-context
的 Pod,使用容器镜像busybox:1.28
,运行命令sh -c sleep 1h
。添加一个类型为emptydir
的 Volume,并将其挂载到路径/data/test
。配置安全上下文,包括以下属性:runAsUser: 1000
,runAsGroup: 3000
,fsGroup: 2000
。此外,将属性allowPrivilegeEscalation
设置为false
。登入容器,导航到目录
/data/test
,并创建名为hello.txt
的文件。检查文件分配的群组。数值是多少?退出容器。 -
创建一个 Pod 安全性审核(PSA)规则。在名为
audited
的命名空间中,创建一个 Pod 安全性标准(PSS),其级别为baseline
,该信息应该渲染到控制台。尝试在违反 PSS 的命名空间中创建一个 Pod,并在控制台日志中输出消息。您可以提供任何名称、容器映像和安全配置。Pod 是否被创建?需要配置哪些 PSA 级别来防止创建该 Pod?
-
在集群上安装 Gatekeeper。创建一个 Gatekeeper ConstraintTemplate 对象,定义一个副本集所控制的副本数的最小和最大值。实例化一个使用该 ConstraintTemplate 的约束对象。设置最小副本数为 3,最大副本数为 10。
创建 创建一个部署对象,将副本数设置为 15。门禁不允许创建该部署、副本集和 Pod,应该渲染一个错误消息。尝试再次创建副本数为 7 的部署对象,验证是否成功创建了所有对象。
-
使用
aescbc
提供者为 etcd 配置加密。创建一个类型为Opaque
的密钥对象。提供键值对api-key=YZvkiWUkycvspyGHk3fQRAkt
。使用 etcdctl 查询该 Secret 的值,查看加密值是什么。 -
导航到从 GitHub 仓库bmuschko/cks-study-guidecheckout 的目录 app-a/ch05/gvisor,启动运行集群的虚拟机使用命令
vagrant up
。该集群包括一个名为kube-control-plane
的单个控制平面节点和一个名为kube-worker-1
的工作节点。完成后,使用vagrant destroy -f
关闭集群。gVisor 已经安装在虚拟机
kube-worker-1
上。进入该虚拟机并创建一个名为container-runtime-sandbox
的 RuntimeClass 对象,使用 runsc 作为处理程序。然后创建一个名为nginx
的 Pod,并使用容器映像nginx:1.23.2
分配该 RuntimeClass。先决条件:此练习需要安装工具 Vagrant 和 VirtualBox。
第六章:供应链安全
较早的章节主要集中在保护 Kubernetes 集群及其组件、用于运行集群节点的操作系统基础设施以及在现有容器镜像上运行工作负载的操作方面。本章退一步,深入探讨了设计、构建和优化容器镜像的过程、最佳实践和工具。
有时,你可能不想创建自己的容器镜像,而是消耗由其他团队或公司生产的现有镜像。在使用它们运行工作负载之前,手动或自动扫描已知漏洞的容器镜像应该成为你的审查过程的一部分。我们将讨论与 CKS 考试相关的一些选项,用于识别、分析和减轻预构建容器镜像的安全风险。
在高层次上,本章涵盖以下概念:
-
减小基础镜像的占用空间
-
保护供应链
-
使用用户工作负载的静态分析
-
扫描已知漏洞的镜像
减小基础镜像的占用空间
表面上看,构建容器镜像的过程似乎很简单;然而,魔鬼往往隐藏在细节中。对于新手来说,要避免构建过大、充满漏洞并且不优化容器层缓存的容器镜像可能并不明显。在本章的课程中,我们将在 Docker 容器引擎的帮助下解决所有这些问题。
场景:攻击者利用容器漏洞
在定义 Dockerfile 时,你需要做的第一个决定之一是选择一个基础镜像。基础镜像提供操作系统和额外的依赖项,并可能暴露 shell 访问权限。
在像 Docker Hub 这样的公共注册表上,你可以选择的一些基础镜像体积庞大,并且可能包含你不一定需要在其中运行应用程序的功能。操作系统本身以及基础镜像提供的任何依赖项都可能存在漏洞。
在 图 6-1 中,攻击者能够通过访问容器获取关于其详细信息的信息。这些漏洞现在可以被用作更高级攻击的跳板。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0601.png
图 6-1. 攻击者利用容器镜像漏洞
建议使用一个功能和依赖项最少的基础镜像。接下来的几节将解释创建更优化的基础镜像的方法,这将使构建速度更快,从容器注册表下载速度更快,并且通过减少臃肿的镜像来减小攻击面。接下来的几节将涉及最重要的技术。你可以在Docker 文档中找到更详细的 Dockerfile 写作最佳实践列表。
选择尺寸小的基础镜像
一些容器镜像可能有千兆字节甚至更多的大小。您真的需要这种容器镜像捆绑的所有功能吗?可能性不大。幸运的是,许多容器生产商为同一个版本上传了各种变体的容器镜像。其中一个变体是alpine
镜像,这是一个小巧、轻量且较少漏洞的 Linux 发行版。正如您在下面的输出中所看到的,下载的带有标签3.17.0
的alpine
容器镜像只有 7.05MB 的大小:
$ docker pull alpine:3.17.0
...
$ docker image ls alpine
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.17.0 49176f190c7e 3 weeks ago 7.05MB
alpine
容器镜像自带一个sh
shell,您可以用来排查容器内运行的进程。您可以使用以下命令在一个新的容器中打开交互式 shell:
$ docker run -it alpine:3.17.0 /bin/sh
/ # exit
虽然运行时的故障排除功能可能很有用,但将 shell 作为容器镜像的一部分会增加其大小,并可能为攻击者打开大门。此外,容器镜像内的软件越多,其漏洞也会越多。
您可以通过使用由 Google 提供的distroless 镜像进一步减小容器镜像的大小和攻击面。以下命令下载gcr.io/distroless/static-debian11
容器镜像的最新标签,并显示其详情。容器镜像的大小仅为 2.34MB:
$ docker pull gcr.io/distroless/static-debian11
...
$ docker image ls gcr.io/distroless/static-debian11:latest
REPOSITORY TAG IMAGE ID CREATED \
SIZE
gcr.io/distroless/static-debian11 latest 901590160d4d 53 years ago \
2.34MB
Distroless 容器镜像不包含任何 shell,您可以通过运行以下命令观察到这一点:
$ docker run -it gcr.io/distroless/static-debian11:latest /bin/sh
docker: Error response from daemon: failed to create shim task: OCI runtime \
create failed: runc create failed: unable to start container process: exec: \
"/bin/sh": stat /bin/sh: no such file or directory: unknown.
Kubernetes 提供了用于故障排除 distroless 容器的临时容器的概念。这些容器被设计为可丢弃的,可以用于排查通常不允许打开 shell 的最小化容器。讨论临时容器超出了本书的范围,但您可以在Kubernetes 文档中找到更多信息。
使用多阶段方法构建容器镜像
作为开发者,您可以决定将应用程序代码作为 Dockerfile 指令的一部分进行构建。这个过程可能包括编译代码和构建一个应该成为容器镜像入口点的二进制文件。拥有实施这一过程所需的所有工具和依赖项将自动增加容器镜像的大小,而且您在运行时也不再需要这些依赖项。
在 Docker 中使用多阶段构建的思想是将构建阶段与运行阶段分离。因此,构建阶段中需要的所有依赖项在完成进程后都会被丢弃,因此不会出现在最终的容器镜像中。这种方法通过去除所有不必要的内容,大大减小了容器镜像的大小。
尽管我们不会详细介绍制作和完全理解多阶段 Dockerfile 的细节,但我想在高层次上展示一下它们的区别。我们将首先展示一个 Dockerfile,它使用编程语言 Go 构建和测试一个简单的程序,如示例 6-1 所示。实质上,我们使用了一个包含 Go 1.19.4 的基础镜像。Go 运行时提供了go
命令,我们将调用它来执行测试并构建应用程序的二进制文件。
示例 6-1. 使用 Go 基础镜像构建和测试 Go 程序
FROM golang:1.19.4-alpine <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go test -v <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
RUN go build -o /go-sample-app . <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
CMD ["/go-sample-app"]
使用了一个 Go 基础镜像
执行针对应用代码的测试。
构建 Go 应用程序的二进制文件。
您可以使用docker build
命令生成镜像,如下所示:
$ docker build . -t go-sample-app:0.0.1
...
结果容器镜像的大小相当大,为 348MB,这其中有很好的理由。它包含了 Go 运行时,尽管在启动容器时实际上不再需要它。go build
命令生成的二进制文件将作为容器启动时的命令运行:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-sample-app 0.0.1 88175f3ab0d3 44 seconds ago 358MB
接下来,我们将看看多阶段方法。在多阶段的 Dockerfile 中,您至少定义了两个阶段。在示例 6-2 中,我们指定了一个别名为build
的阶段来运行测试并构建二进制文件,类似于之前的操作。第二个阶段将由阶段build
生成的二进制文件复制到一个专用目录中;然而,它使用alpine
基础镜像来运行它。在运行docker build
命令时,阶段build
将不再包含在最终的容器镜像中。
示例 6-2. 作为多阶段 Dockerfile 的一部分构建和测试 Go 程序
FROM golang:1.19.4-alpine AS build <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
RUN apk add --no-cache git
WORKDIR /tmp/go-sample-app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go test -v <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
RUN go build -o ./out/go-sample-app . <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
FROM alpine:3.17.0 <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/4.png>
RUN apk add ca-certificates
COPY --from=build /tmp/go-sample-app/out/go-sample-app /app/go-sample-app <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png>
CMD ["/app/go-sample-app"]
在名为build
的阶段中,使用了一个 Go 基础镜像来构建和测试程序。
执行针对应用代码的测试。
构建 Go 应用程序的二进制文件。
使用一个更小的基础镜像来运行容器中的应用程序。
复制在build
阶段生成的应用程序二进制文件,并将其用作启动容器时运行的命令。
使用 alpine
基础镜像时,生成的容器镜像大小显著较小,仅为 12MB。您可以通过再次运行 docker build
命令并列出容器镜像大小来验证结果:
$ docker build . -t go-sample-app:0.0.1
...
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-sample-app 0.0.1 88175f3ab0d3 44 seconds ago 12MB
我们不仅减少了大小,还通过包含更少的依赖项减少了攻击面。通过使用 distroless 基础镜像而不是 alpine
基础镜像,你可以进一步减小容器镜像的大小。
减少层数
每个 Dockerfile 都由一系列有序的指令组成。只有一些指令会在最终容器镜像中创建只读层。这些指令是 FROM
、COPY
、RUN
和 CMD
。所有其他指令不会创建层,因为它们会创建临时中间镜像。向容器镜像添加的层数越多,构建执行时间可能会越慢,容器镜像的大小可能会越大。因此,需要谨慎选择 Dockerfile 中的指令,以尽量减小容器镜像的占用空间。
通常会连续执行多个命令。您可以使用单独的 RUN
指令列表来定义这些命令,如 示例 6-3 所示。
示例 6-3. 指定多个 RUN
指令的 Dockerfile
FROM ubuntu:22.10
RUN apt-get update -y
RUN apt-get upgrade -y
RUN apt-get install -y curl
每个 RUN
指令将创建一个层,可能会增加容器镜像的大小。使用 &&
将它们串联在一起更有效,确保只生成单个层。示例 6-4 展示了这种优化技术的示例。
示例 6-4. 指定多个 RUN
指令的 Dockerfile
FROM ubuntu:22.10
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y curl
使用容器镜像优化工具
很容易忘记之前提到的优化实践。开源社区开发了一些工具,可以帮助检查生成的容器镜像。它们的功能提供了有用的提示,可以减少不必要的层、文件和文件夹:
DockerSlim
DockerSlim 将通过分析应用程序及其依赖项来优化和保护您的容器镜像。您可以在该工具的 GitHub 仓库 中获取更多信息。
Dive
Dive 是一个用于探索嵌入容器镜像中层的工具。它可以帮助您轻松识别不必要的层,进而进行进一步优化。Dive 的代码和文档可在 GitHub 仓库 中找到。
这只是容器镜像优化工具的简短列表。在 “工作负载的静态分析” 中,我们将看看其他专注于 Dockerfile 和 Kubernetes 清单的静态分析工具。
保护供应链
供应链自动化生产容器镜像并在运行时环境中操作的过程,在这种情况下是 Kubernetes。我们已经提到了一些可以集成到供应链中以支持容器镜像优化方面的工具。在本节中,我们将扩展到针对安全方面的实践。参考书籍 Container Security 作者 Liz Rice(O’Reilly)以获取更多信息。
签署容器镜像
在将容器镜像推送到容器注册表之前,您可以对其进行签名。签名可以通过docker trust sign
命令实现,该命令会向容器镜像添加一个签名,即所谓的图像摘要。图像摘要是根据容器镜像的内容派生的,通常以 SHA256 的形式表示。在消费容器镜像时,Kubernetes 可以将图像摘要与镜像内容进行比较,以确保其未被篡改。
场景:攻击者将恶意代码注入容器镜像
验证图像摘要的 Kubernetes 组件是 kubelet。如果您将 image pull policy 配置为 Always
,即使 kubelet 在之前已经下载和验证过容器镜像,它也会从容器注册表中查询图像摘要。
攻击者可以尝试修改现有容器镜像的内容,注入恶意代码,并将其上传到具有相同标签的容器注册表中,如 图 6-2 所示。然后,运行在容器中的恶意代码可能会将敏感信息发送到攻击者可访问的第三方服务器。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0602.png
图 6-2. 攻击者将恶意代码注入容器镜像
容器镜像的校验和验证不是自动进行的
图像摘要验证在 Kubernetes 中是一种选择性功能。在定义 Pod 时,请确保明确指定所有容器镜像的图像摘要。
验证容器镜像
在 Kubernetes 中,您可以将 SHA256 图像摘要附加到容器的规范中。例如,可以通过属性spec.containers[].image
来实现。图像摘要通常可以在容器注册表中找到。例如,图 6-3 显示了 Docker Hub 上 alpine:3.17.0
容器镜像的图像摘要(https://oreil.ly/ZAV_H)。在此示例中,图像摘要为 sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c
。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0603.png
图 6-3. Docker Hub 上 alpine:3.17.0
容器镜像的图像摘要
让我们看看图像摘要的作用。而不是使用标签,示例 6-5 通过附加图像摘要来指定容器镜像。
示例 6-5. 使用有效容器镜像摘要的 Pod
apiVersion: v1
kind: Pod
metadata:
name: alpine-valid
spec:
containers:
- name: alpine
image: alpine@sha256:c0d488a800e4127c334ad20d61d7bc21b40 \
97540327217dfab52262adc02380c
command: ["/bin/sh"]
args: ["-c", "while true; do echo hello; sleep 10; done"]
创建 Pod 将按预期工作。将验证镜像摘要,并将容器转换为“运行”状态:
$ kubectl apply -f pod-valid-image-digest.yaml
pod/alpine-valid created
$ kubectl get pod alpine-valid
NAME READY STATUS RESTARTS AGE
alpine-valid 1/1 Running 0 6s
示例 6-6 显示了相同的 Pod 定义;但是,已更改了镜像摘要,因此它与容器镜像的内容不匹配。
示例 6-6. 使用无效容器镜像摘要的 Pod
apiVersion: v1
kind: Pod
metadata:
name: alpine-invalid
spec:
containers:
- name: alpine
image: alpine@sha256:d006a643bccb6e9adbabaae668533c7f2e5 \
111572fffb5c61cb7fcba7ef4150b
command: ["/bin/sh"]
args: ["-c", "while true; do echo hello; sleep 10; done"]
您会发现 Kubernetes 仍然可以创建 Pod 对象,但无法正确验证容器镜像的哈希值。这将导致状态为“ErrImagePull”。正如事件日志中所示,甚至无法从容器注册表中拉取容器镜像:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
alpine-invalid 0/1 ErrImagePull 0 29s
$ kubectl describe pod alpine-invalid
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 13s default-scheduler Successfully assigned default \
/alpine-invalid to minikube
Normal Pulling 13s kubelet Pulling image "alpine@sha256: \
d006a643bccb6e9adbabaae668533c7f2e5111572fffb5c61cb7fcba7ef4150b"
Warning Failed 11s kubelet Failed to pull image \
"alpine@sha256:d006a643bccb6e9adbabaae668533c7f2e5111572fffb5c61cb7fcba7ef4 \
150b": rpc error: code = Unknown desc = Error response from daemon: manifest \
for alpine@sha256:d006a643bccb6e9adbabaae668533c7f2e5111572fffb5c61cb7fcba7e \
f4150b not found: manifest unknown: manifest unknown
Warning Failed 11s kubelet Error: ErrImagePull
Normal BackOff 11s kubelet Back-off pulling image \
"alpine@sha256:d006a643bccb6e9adbabaae668533c7f2e5111572fffb5c61cb7fcba7ef415 \
0b"
Warning Failed 11s kubelet Error: ImagePullBackOff
使用公共镜像注册表
每当创建一个 Pod 时,容器运行时引擎将从容器注册表下载声明的容器镜像(如果本地尚不可用)。可以使用镜像拉取策略来进一步调整此运行时行为。
图像名称中的前缀声明了注册表的域名;例如,gcr.io/google-containers/debian-base:v1.0.1
指的是 google-containers/debian-base:v1.0.1
在Google Cloud 容器注册表中,由 gcr.io
表示。如果在容器镜像声明中不指定域名,则容器运行时将尝试从 docker.io
,即Docker Hub 容器注册表 解析它。
情景:攻击者上传恶意容器镜像
虽然从公共容器注册表解析容器镜像很方便,但也伴随着风险。任何拥有这些容器注册表登录凭据的人都可以上传新的镜像。通常,使用容器镜像并不需要账户。
如图 6-4 所示(#公共注册表攻击者),攻击者可以使用窃取的凭据上传包含恶意代码的容器镜像到公共注册表。任何引用该注册表中容器镜像的容器都将自动运行恶意代码。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0604.png
图 6-4. 攻击者上传恶意容器镜像
在企业级别上,您需要控制信任的容器注册表。建议在公司网络内设置自己的容器注册表,可以完全控制和管理。或者,您可以在云提供商环境中设置一个私有容器注册表,不允许任何其他人访问。
您可以选择其中一个主要的二进制仓库管理器,例如JFrog Artifactory。该产品完全支持存储、扫描和提供容器镜像。任何使用容器镜像的消费者应仅允许从您的白名单容器注册表中拉取镜像。所有其他容器注册表都应该被拒绝。
使用 OPA GateKeeper 白名单允许的镜像注册表
通过 OPA Gatekeeper 控制容器注册表的使用方式。我们讨论了 “理解开放策略代理(OPA)和 Gatekeeper” 的安装过程和功能。在这里,我们将触及约束模板和允许信任容器注册表的约束条件。
示例 6-7 展示了我直接从 OPA Gatekeeper 库获取的 约束模板。作为输入属性,约束模板定义了一个字符串数组,表示容器注册表的前缀。Rego 规则不仅验证了属性 spec.containers[]
的分配容器镜像,还验证了 spec.initContainers[]
和 spec.ephemeralContainers[]
。
示例 6-7. 用于执行容器注册表强制约束的 OPA Gatekeeper 约束模板。
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
annotations:
metadata.gatekeeper.sh/title: "Allowed Repositories"
metadata.gatekeeper.sh/version: 1.0.0
description: >-
Requires container images to begin with a string from the specified list.
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
properties:
repos:
description: The list of prefixes a container image is allowed to have.
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo = input.parameters.repos[_] ; \
good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed \
repos are %v", [container.name, container.image, input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ; \
good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("initContainer <%v> has an invalid image repo <%v>, \
allowed repos are %v", [container.name, container.image, \
input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.ephemeralContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ; \
good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("ephemeralContainer <%v> has an invalid image repo <%v>, \
allowed repos are %v", [container.name, container.image, \
input.parameters.repos])
}
在 示例 6-8 中显示的约束负责定义我们想要允许的容器注册表。通常会选择公司网络中托管的服务器的域名。在这里,我们将使用 gcr.io/
作为演示目的。
示例 6-8. OPA Gatekeeper 约束,将 Google Cloud 注册表分配为受信任的存储库。
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: repo-is-gcr
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "gcr.io/"
让我们使用 apply
命令创建这两个对象:
$ kubectl apply -f allowed-repos-constraint-template.yaml
constrainttemplate.templates.gatekeeper.sh/k8sallowedrepos created
$ kubectl apply -f gcr-allowed-repos-constraint.yaml
k8sallowedrepos.constraints.gatekeeper.sh/repo-is-gcr created
在约束中,我们没有指定规则应适用于的命名空间。因此,它们将适用于集群中的所有命名空间。以下命令验证白名单规则是否按预期工作。以下命令尝试使用来自 Docker Hub 的 nginx
容器镜像创建一个 Pod。创建 Pod 被拒绝,并显示适当的错误消息:
$ kubectl run nginx --image=nginx:1.23.3
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" \
denied the request: [repo-is-gcr] container <nginx> has an invalid image \
repo <nginx:1.23.3>, allowed repos are ["gcr.io/"]
下一个命令创建一个 Pod,其容器镜像来自 Google Cloud 容器注册表。操作被允许,并且创建了 Pod 对象:
$ kubectl run busybox --image=gcr.io/google-containers/busybox:1.27.2
pod/busybox created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
busybox 0/1 Completed 1 (2s ago) 3s
使用 ImagePolicyWebhook Admission Controller 插件为允许的镜像注册表设置白名单。
另一种验证允许使用的镜像注册表的方法是拦截在创建 Pod 时向 API 服务器发出的调用。通过启用 Admission Controller 插件可以实现此机制。配置后,当 API 服务器接收请求时,会自动调用 Admission Controller 插件的逻辑,但在验证调用者身份后。我们已经讨论了 API 调用必须经过的阶段,见 “处理请求”。
Admission Controller 提供了在请求生效之前批准、拒绝或变更请求的方式。例如,我们可以检查与创建 Pod 的 API 请求一起发送的数据,迭代分配的容器镜像,并执行自定义逻辑以验证容器镜像的表示法。如果容器镜像不符合预期的约定,我们可以拒绝创建 Pod。图 6-5 展示了高级别的工作流程。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0605.png
图 6-5. 拦截特定 Pod API 调用并使用 Webhook 处理它
ImagePolicyWebhook 准入控制器插件是我们可以配置用于拦截 Kubernetes API 调用的插件之一。 您可以从其名称推断出插件的功能。 它为 Pod 中定义的所有容器镜像定义策略。 为了与定义的策略比较容器镜像,插件通过 HTTPS 调用到一个服务外部于 Kubernetes 的 webhook。 外部服务然后决定如何验证数据。 在准入控制器插件的上下文中,外部服务也被称为后端。
实施后端应用
后端应用可以使用您选择的编程语言实现。 它必须满足以下三个要求:
-
这是一个可以处理 HTTPS 请求的 Web 应用程序。
-
它可以接受和解析 JSON 请求有效载荷。
-
它可以发送一个 JSON 响应有效载荷。
实施后端应用不是 CKS 考试的一部分,但您可以在本书的GitHub 仓库中找到一个基于 Go 的示例实现。 请注意,该代码不被视为生产就绪。
以下命令演示了应用程序在https://localhost:8080/validate
上的运行时行为。 您可以在Kubernetes 文档中找到示例请求和响应 JSON 主体。
以下 curl
命令调用验证逻辑以验证容器镜像 nginx:1.19.0
。 如 JSON 响应所示,镜像被拒绝:
$ curl -X POST -H "Content-Type: application/json" -k -d \'{"apiVersion": \
"imagepolicy.k8s.io/v1alpha1", "kind": "ImageReview", "spec": \
{"containers": [{"image": "nginx:1.19.0"}]}}' https://localhost:8080/validate
{"apiVersion": "imagepolicy.k8s.io/v1alpha1", "kind": "ImageReview", \
"status": {"allowed": false, "reason": "Denied request: [container 1 \
has an invalid image repo nginx:1.19.0, allowed repos are [gcr.io/]]"}}
以下 curl
命令调用验证逻辑以验证容器镜像 gcr.io/nginx:1.19.0
。 如 JSON 响应所示,镜像被允许:
$ curl -X POST -H "Content-Type: application/json" -k -d '{"apiVersion": \
"imagepolicy.k8s.io/v1alpha1", "kind": "ImageReview", "spec": {"containers": \
[{"image": "gcr.io/nginx:1.19.0"}]}}' https://localhost:8080/validate
{"apiVersion": "imagepolicy.k8s.io/v1alpha1", "kind": "ImageReview", \
"status": {"allowed": true, "reason": ""}}
配置 ImagePolicyWebhook 准入控制器插件
考试中,您需要理解如何“连接”ImagePolicyWebhook 准入控制器插件。 本节将指导您完成此过程。 首先,您需要为准入控制器创建一个配置文件,以便它知道要使用哪些插件以及运行时应如何行为。 创建文件/etc/kubernetes/admission-control/image-policy-webhook-admission-config.yaml
并填入示例 6-9 中显示的内容。
示例 6-9. 准入控制器配置文件
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
configuration:
imagePolicy:
kubeConfigFile: /etc/kubernetes/admission-control/ \
imagepolicywebhook.kubeconfig <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: false <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
提供了 ImagePolicyWebhook 插件的配置。
指向用于配置后端的配置文件。
如果无法访问后端,则拒绝 API 请求。 默认值为 true,但将其设置为 false 更为明智。
接下来,创建由 plugins[].configuration.imagePolicy.kubeConfigFile
属性引用的文件。该文件的内容指向外部服务的 HTTPS URL。它还定义了磁盘上的客户端证书和密钥文件,以及 CA 文件。示例 6-10 显示了配置文件的内容。
示例 6-10. 镜像策略配置文件
apiVersion: v1
kind: Config
preferences: {}
clusters:
- name: image-validation-webhook
cluster:
certificate-authority: /etc/kubernetes/admission-control/ca.crt
server: https://image-validation-webhook:8080/validate <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
contexts:
- context:
cluster: image-validation-webhook
user: api-server-client
name: image-validation-webhook
current-context: image-validation-webhook
users:
- name: api-server-client
user:
client-certificate: /etc/kubernetes/admission-control/api-server-client.crt
client-key: /etc/kubernetes/admission-control/api-server-client.key
后端服务的 URL。必须使用 HTTPS 协议。
启用 API 服务器的 ImagePolicyWebhook 准入控制器插件,并将准入控制器指向配置文件。为实现此目的,请编辑 API 服务器的配置文件,通常位于 /etc/kubernetes/manifests/kube-apiserver.yaml
。
查找命令行选项 --enable-admission-plugins
,并在现有插件列表中追加值 ImagePolicyWebhook
,以逗号分隔。如果尚不存在,请提供命令行选项 --admission-control-config-file
,并将值设置为 /etc/kubernetes/admission-control/image-policy-webhook-admission-configuration.yaml
。由于配置文件位于新目录中,您将需要将其定义为 Volume 并将其挂载到容器中。示例 6-11 显示了 API 服务器容器的相关选项。
示例 6-11. API 服务器配置文件
...
spec:
containers:
- command:
- kube-apiserver
- --enable-admission-plugins=NodeRestriction,ImagePolicyWebhook
- --admission-control-config-file=/etc/kubernetes/admission-control/ \
image-policy-webhook-admission-configuration.yaml
...
volumeMounts:
...
- name: admission-control
mountPath: /etc/kubernetes/admission-control
readonly: true
volumes:
...
- name: admission-control
hostPath:
path: /etc/kubernetes/admission-control
type: DirectoryOrCreate
...
运行 API 服务器的 Pod 应自动重启。此过程可能需要几分钟时间。如果 API 服务器未能自行启动,请使用命令 sudo systemctl restart kubelet
重新启动 kubelet 服务。一旦完全重启,您应该能够查询它:
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
...
kube-apiserver-control-plane 1/1 Running 0 69s
现在,任何请求创建 Pod 的 API 调用将被路由到后端。根据验证结果,将允许或拒绝对象的创建。
工作负载的静态分析
在本书中,我们讨论了 Dockerfile 和 Kubernetes 清单的最佳实践。您可以手动检查这些文件,查找不良配置,并手动修复它们。这种方法需要大量复杂的知识,并且非常繁琐且易出错。通过适当的工具以自动化方式分析工作负载文件,会更加方便和高效。商业和开源静态分析工具的列表很长。在本节中,我们将介绍两个选项的功能,即 Haskell Dockerfile Linter 和 Kubesec。
在企业级别上,当您需要处理数百甚至数千个工作负载文件时,您可以借助持续交付管道的帮助来完成,如 图 6-6 所示。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0606.png
图 6-6. Kubernetes 的示例持续交付管道
相关的静态分析工具可以作为管道早期阶段的质量门控调用,甚至在构建容器镜像、推送到注册表或部署到 Kubernetes 集群之前。有关持续交付原则和实践的深入了解,请参阅 Jez Humble 和 David Farley 的优秀著作 Continuous Delivery(Addison-Wesley Professional)。
使用 Hadolint 分析 Dockerfile
Haskell Dockerfile Linter,也称为 hadolint,是 Dockerfile 的一个代码检查工具。该工具根据 Docker 文档页面上列出的 最佳实践 检查 Dockerfile。示例 6-12 展示了用于构建运行基于 Go 的应用程序的容器镜像的未优化 Dockerfile。
示例 6-12. 一个未优化的 Dockerfile
FROM golang
COPY main.go .
RUN go build main.go
CMD ["./main"]
Haskell Dockerfile Linter 支持一种操作模式,允许您将 hadolint
可执行文件指向磁盘上的 Dockerfile。您可以看到随后的命令执行,包括分析产生的发现的警告消息:
$ hadolint Dockerfile
Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
Dockerfile:2 DL3045 warning: `COPY` to a relative destination without \
`WORKDIR` set.
命令的输出建议您为基础镜像分配一个标签。现有的 Dockerfile 将拉取 latest
标签,该标签将解析为最新的 Go 容器镜像。这种做法可能导致 Go 运行时版本与应用程序代码不兼容。定义用于复制资源的工作目录有助于将操作系统特定的目录和文件与应用程序特定的目录和文件分开。示例 6-13 修复了 Haskell Dockerfile Linter 发现的警告消息。
示例 6-13. 一个优化后的 Dockerfile
FROM golang:1.19.4-alpine
WORKDIR /app
COPY main.go .
RUN go build main.go
CMD ["./main"]
对修改后的 Dockerfile 的另一次执行将导致成功的退出代码,并且不会呈现任何额外的警告消息:
$ hadolint Dockerfile
Dockerfile 现在遵循 hadolint 感知到的最佳实践。
使用 Kubesec 分析 Kubernetes 清单
Kubesec 是分析 Kubernetes 清单的工具。它可以作为二进制文件、Docker 容器、准入控制器插件甚至是 kubectl
插件执行。为了演示其用法,我们将设置一个 YAML 清单文件 pod-initial-kubesec-test.yaml
,如 示例 6-14 所示,作为起点。
示例 6-14. 使用初始安全设置的 Pod YAML 清单
apiVersion: v1
kind: Pod
metadata:
name: kubesec-demo
spec:
containers:
- name: kubesec-demo
image: gcr.io/google-samples/node-hello:1.0
securityContext:
readOnlyRootFilesystem: true
在检查 Pod 配置时,您可能已经根据前几章的内容有一些改进建议。让我们看看 Kubesec 将会推荐什么。
通过在 Docker 容器中运行逻辑的最简单方法来调用 Kubesec。以下命令将 YAML 清单的内容传送到标准输入流中。格式化为 JSON 的结果呈现评分,并提供结果的人类可读消息,并建议进行更改:
$ docker run -i kubesec/kubesec:512c5e0 scan /dev/stdin \
< pod-initial-kubesec-test.yaml
[
{
"object": "Pod/kubesec-demo.default",
"valid": true,
"message": "Passed with a score of 1 points",
"score": 1,
"scoring": {
"advise": [
{
"selector": ".spec .serviceAccountName",
"reason": "Service accounts restrict Kubernetes API access and \
should be configured with least privilege"
},
{
"selector": ".metadata .annotations .\"container.apparmor.security. \
beta.kubernetes.io/nginx\"",
"reason": "Well defined AppArmor policies may provide greater \
protection from unknown threats. WARNING: NOT PRODUCTION \
READY"
},
{
"selector": "containers[] .resources .requests .cpu",
"reason": "Enforcing CPU requests aids a fair balancing of \
resources across the cluster"
},
{
"selector": ".metadata .annotations .\"container.seccomp.security. \
alpha.kubernetes.io/pod\"",
"reason": "Seccomp profiles set minimum privilege and secure against \
unknown threats"
},
{
"selector": "containers[] .resources .limits .memory",
"reason": "Enforcing memory limits prevents DOS via resource \
exhaustion"
},
{
"selector": "containers[] .resources .limits .cpu",
"reason": "Enforcing CPU limits prevents DOS via resource exhaustion"
},
{
"selector": "containers[] .securityContext .runAsNonRoot == true",
"reason": "Force the running image to run as a non-root user to \
ensure least privilege"
},
{
"selector": "containers[] .resources .requests .memory",
"reason": "Enforcing memory requests aids a fair balancing of \
resources across the cluster"
},
{
"selector": "containers[] .securityContext .capabilities .drop",
"reason": "Reducing kernel capabilities available to a container \
limits its attack surface"
},
{
"selector": "containers[] .securityContext .runAsUser -gt 10000",
"reason": "Run as a high-UID user to avoid conflicts with the \
host's user table"
},
{
"selector": "containers[] .securityContext .capabilities .drop | \
index(\"ALL\")",
"reason": "Drop all capabilities and add only those required to \
reduce syscall attack surface"
}
]
}
}
]
示例 6-15 中可以找到原始 YAML 清单的修订版本。在这里,我们整合了 Kubesec 建议的一些推荐更改。
示例 6-15. 使用改进的安全设置的 Pod YAML 清单
apiVersion: v1
kind: Pod
metadata:
name: kubesec-demo
spec:
containers:
- name: kubesec-demo
image: gcr.io/google-samples/node-hello:1.0
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 20000
capabilities:
drop: ["ALL"]
对改变的 Pod YAML 清单执行相同的 Docker 命令将会呈现出一个改进的分数,并减少建议消息的数量:
$ docker run -i kubesec/kubesec:512c5e0 scan /dev/stdin \
< pod-improved-kubesec-test.yaml
[
{
"object": "Pod/kubesec-demo.default",
"valid": true,
"message": "Passed with a score of 9 points",
"score": 9,
"scoring": {
"advise": [
{
"selector": ".metadata .annotations .\"container.seccomp.security. \
alpha.kubernetes.io/pod\"",
"reason": "Seccomp profiles set minimum privilege and secure against \
unknown threats"
},
{
"selector": ".spec .serviceAccountName",
"reason": "Service accounts restrict Kubernetes API access and should \
be configured with least privilege"
},
{
"selector": ".metadata .annotations .\"container.apparmor.security. \
beta.kubernetes.io/nginx\"",
"reason": "Well defined AppArmor policies may provide greater \
protection from unknown threats. WARNING: NOT PRODUCTION \
READY"
}
]
}
}
]
扫描已知漏洞的镜像
记录和发现安全漏洞的首要来源之一是CVE 详情。该页面列出并按分数排名已知的漏洞(CVE)。自动化工具可以识别嵌入容器镜像中的软件组件,将其与中央漏洞数据库进行比较,并通过其严重性标记问题。
在 CKS 课程中明确提到的具备此能力的开源工具之一是Trivy。Trivy 可以以不同的操作模式运行:作为命令行工具、在容器中、作为 GitHub Action 配置在持续集成工作流中、作为 IDE VSCode 的插件以及作为 Kubernetes 操作者。关于可用安装选项和流程的概述,请参阅Trivy 文档。在考试期间,您无需安装此工具,它已经预先配置好。您只需要理解如何运行它、如何解释和修复找到的漏洞即可。
假设您已安装了 Trivy 的命令行实现。您可以使用以下命令检查 Trivy 的版本:
$ trivy -v
Version: 0.36.1
Vulnerability DB:
Version: 2
UpdatedAt: 2022-12-13 12:07:14.884952254 +0000 UTC
NextUpdate: 2022-12-13 18:07:14.884951854 +0000 UTC
DownloadedAt: 2022-12-13 17:09:28.866739 +0000 UTC
如您在图 6-7 中所见,Trivy 指示了从中央数据库下载已知漏洞副本的时间戳。Trivy 可以以各种形式扫描容器镜像。子命令image
期望您简单地拼写出镜像名称和标签,在本例中为python:3.4-alpine
。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0607.png
图 6-7. 使用 Trivy 扫描容器镜像生成的报告
输出中最重要的信息包括包含特定漏洞的库、其严重性以及修复问题所需的最低版本。应考虑修复任何具有高或关键严重性的发现漏洞。如果您无法控制容器镜像本身,则可以尝试升级到更新版本。
总结
Kubernetes 的主要目标是以可伸缩和安全的方式运行应用程序容器。在本章中,我们探讨了确保生成的容器镜像尺寸小且具有尽可能少的已知安全漏洞的过程、最佳实践和工具。
我们回顾了将容器映像占用空间减少到最小的一些最有效技术。首先选择一个小的基础映像来开始。甚至可以走极端,完全不提供 shell 以获得额外的大小减少。如果您正在使用 Docker,则可以利用多阶段方法,在容器内构建应用程序,而无需捆绑编译器、运行时和构建工具以实现目标。
在 Pod 中消费容器映像时,请确保仅从受信任的注册表中拉取容器映像。建议设置内部容器注册表以提供容器映像,从而消除对公共、可通过互联网访问的注册表的依赖,以消除潜在的安全风险。您可以通过 OPA Gatekeeper 帮助执行受信任容器注册表列表的使用。另一种安全措施是使用容器映像的 SHA256 哈希来验证其预期内容。
构建和扫描容器映像的过程可以整合到 CI/CD 流水线中。第三方工具可以在构建之前解析和分析可部署工件的资源文件。我们查看了用于 Dockerfile 的 Haskell Dockerfile Linter 和用于 Kubernetes 清单的 Kubesec。另一个需要在安全方面涵盖的用例是作为开发人员或您公司外部实体构建的现有容器映像的消费。在运行 Kubernetes Pod 中的容器映像之前,请确保扫描其内容以查找漏洞。Trivy 是可以识别并报告容器映像中漏洞的工具之一,以帮助您了解在计划在容器中操作时可能面临的安全风险。
考试要点
熟悉有助于减少容器映像占用空间的技术。
在本节中,我们描述了在构建时减小容器映像大小的一些技术。我建议您阅读 Docker 网页上提到的最佳实践,并尝试将它们应用于样本容器映像。比较应用技术前后生成的容器映像的大小。您可以尝试通过避免丢失关键功能的同时将容器映像减小到最小尺寸,挑战自己。
行走通过治理过程,其中容器映像可以解析。
OPA Gatekeeper 提供了定义允许用户解析容器映像的注册表的方法。设置约束模板和约束对象,并查看规则是否适用于定义主应用程序容器、初始化容器和临时容器的 Pod。为了扩展您的曝光度,还可以查看 Kubernetes 领域中提供类似功能的其他产品。其中一个产品是 Kyverno。
对一个容器镜像进行签名,并使用哈希值验证。
构建容器镜像后,请确保也为其创建摘要。将容器镜像及其摘要发布到注册表。练习如何在 Pod 定义中使用摘要,并验证 Kubernetes 在拉取容器镜像时的行为。
理解如何配置 ImagePolicyWebhook 准入控制器插件。
你不需要为 ImagePolicyWebhook 编写后端。这超出了考试范围,并需要掌握一门编程语言。但你确实需要了解如何在 API 服务器配置中启用该插件。即使没有运行的后端应用程序,我建议你练习这个工作流程。
知道如何修复静态分析工具产生的警告。
CKS 课程不指定用于分析 Dockerfile 和 Kubernetes 清单的特定工具。在考试期间,您可能会被要求运行一个特定的命令,该命令将生成一系列错误和/或警告消息。理解如何解释这些消息,并在相关资源文件中修复它们是很重要的。
练习使用 Trivy 来识别和修复安全漏洞。
CKS 的常见问题解答中列出了 Trivy 的文档页面。因此,可以合理地假设 Trivy 可能会在其中的一个问题中出现。您需要了解调用 Trivy 扫描容器镜像的不同方式。生成的报告将清楚地指示需要修复的内容及找到的漏洞的严重程度。考虑到您不能轻易修改容器镜像,您可能会被要求标记运行具有已知漏洞的容器镜像的 Pod。
示例练习
这些练习的解决方案可以在附录中找到。
-
看一下以下的 Dockerfile。你能找出减少生成的容器镜像占用空间的可能性吗?
FROM node:latest ENV NODE_ENV development WORKDIR /app COPY package.json . RUN npm install COPY . . EXPOSE 3001 CMD ["node", "app.js"]
将 Dockerfile 最佳实践应用于优化容器镜像占用空间。在进行优化之前和之后运行
docker build
命令。容器镜像的最终大小应更小,但应用程序仍应正常运行。 -
在你的 Kubernetes 集群中安装 Kyverno。您可以在文档中找到安装说明。
应用描述在文档页面上的“限制镜像注册表”策略。唯一允许的注册表应为
gcr.io
。禁止使用任何其他注册表。创建一个定义了容器镜像
gcr.io/google-containers/busybox:1.27.2
的 Pod。创建 Pod 应该失败。使用容器镜像busybox:1.27.2
创建另一个 Pod。Kyverno 应该允许创建该 Pod。 -
在 YAML 清单
pod-validate-image.yaml
中使用容器镜像nginx:1.23.3-alpine
定义一个 Pod。从 Docker Hub 检索容器镜像的摘要。使用 SHA256 哈希验证容器镜像内容。创建 Pod。Kubernetes 应能够成功拉取容器镜像。 -
使用 Kubesec 分析 YAML 清单文件
pod.yaml
中的以下内容:apiVersion: v1 kind: Pod metadata: name: hello-world spec: securityContext: runAsUser: 0 containers: - name: linux image: hello-world:linux
检查 Kubesec 生成的建议。忽略关于 seccomp 和 AppArmor 的建议。修复所有消息的根本原因,以确保再次执行工具时不会报告任何额外的建议。
-
转到已检出的 GitHub 仓库 bmuschko/cks-study-guide 的目录 app-a/ch06/trivy。执行命令
kubectl apply -f setup.yaml
。该命令在命名空间
r61
中创建了三个不同的 Pod。从命令行执行 Trivy 对这些 Pod 使用的容器镜像进行检查。删除所有具有“CRITICAL”漏洞的 Pod。哪些 Pod 仍在运行?
第七章:监控、日志和运行时安全
课程的最后一个领域主要涉及在 Kubernetes 集群中检测主机和容器级别的可疑活动。我们首先定义 行为分析 这个术语,并说明它如何应用于 Kubernetes 领域。理论澄清后,我们将介绍一个名为 Falco 的工具,它可以检测入侵场景。
一旦容器启动,其运行环境仍然可以修改。例如,作为操作员,您可以决定进入容器以手动安装额外工具或向容器的临时文件系统写入文件。在容器启动后修改容器可能会带来安全风险。您应该考虑创建 不可变容器,即启动后无法修改的容器。我们将学习如何配置 Pod 的正确设置,使其容器变为不可变。
最后,我们将讨论捕获在 Kubernetes 集群中发生事件的日志。这些日志可用于集群级别的故障排除,以重建集群配置何时以及如何发生变化,导致不希望或破损的运行时行为。日志条目还可用于跟踪正在发生的攻击,作为实施对策的手段。
在高层次上,本章涵盖以下概念:
-
进行行为分析以检测恶意活动
-
进行深入的分析调查和识别
-
在运行时确保容器的不可变性
-
使用审计日志监控访问
进行行为分析
除了管理和升级 Kubernetes 集群外,管理员还负责监视潜在的恶意活动。虽然您可以通过登录到集群节点并观察主机和容器级别的进程来手动执行此任务,但这是一项效率极低的工作。
行为分析 是观察集群节点是否存在异常活动的过程。自动化过程有助于过滤、记录和警报特定感兴趣的事件。
情景:Kubernetes 管理员可以观察攻击者采取的行动
攻击者通过在工作节点上运行的 shell 打开容器获得访问权,以在整个 Kubernetes 集群中启动额外的攻击。管理员不能轻易地通过手动检查每个容器来检测此事件。图 7-1 描述了这种情况。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0701.png
图 7-1。行为分析工具记录的恶意事件
管理员决定自行安装行为分析工具。该工具将持续监视特定事件并几乎即时记录它们。管理员现在拥有有效的机制来检测入侵并采取行动。
与考试相关的行为分析工具中包括Falco,Tracee和Tetragon。在本书中,我们将只关注 Falco,因为它是考试期间可用文档页面链接之一。
理解 Falco
Falco 通过观察主机和容器级活动来帮助检测威胁。以下是 Falco 可能监控的一些事件示例:
-
在文件系统中特定位置读取或写入文件
-
打开容器的 shell 二进制文件,例如
/bin/bash
打开 bash shell -
尝试向不良 URL 进行网络调用
Falco 部署了一组传感器来监听配置的事件和条件。每个传感器由一组规则组成,将事件映射到数据源。当规则匹配特定事件时会产生警报。警报将发送到输出通道以记录事件,例如标准输出、文件或 HTTPS 端点。Falco 允许同时启用多个输出通道。图 7-2 展示了 Falco 的高级架构。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0702.png
图 7-2. Falco 的高级架构
Falco 是一个功能丰富且有多种配置选项的工具。在本书中我们无法讨论所有功能,但我建议您花些时间了解 Falco 的高级概念。
关于 Falco 的另一个很好的学习资源可以在 Sysdig 培训门户网页上找到。“Falco 101”是一个免费的视频课程,详细解释了产品的所有细节。要开始学习,您只需注册一个账户。此外,我建议您阅读Practical Cloud Native Security with Falco,这本由 Loris Degioanni 和 Leonardo Grasso(O’Reilly 出版)合著的书籍采用了面向初学者的友好方法来学习 Falco。
安装 Falco
Falco 可以作为主机系统上的二进制文件或 Kubernetes 中的 DaemonSet 对象安装。您可以放心地假设 Falco 已经预安装在考试环境中。有关安装过程的更多信息,请查看Falco 文档的相关部分。以下步骤简要说明了在 Ubuntu 机器上安装二进制文件的过程。Falco 需要安装在 Kubernetes 集群的所有工作节点上。请注意,这些说明可能会随着 Falco 的未来版本而更改。
首先,您需要信任 Falco 的 GPG 密钥,配置特定于 Falco 的 apt 存储库,并更新软件包列表:
$ curl -s https://falco.org/repo/falcosecurity-packages.asc | apt-key add -
$ echo "deb https://download.falco.org/packages/deb stable main" | tee -a \
/etc/apt/sources.list.d/falcosecurity.list
$ apt-get update -y
然后,您可以使用以下命令安装内核头文件:
$ apt-get -y install linux-headers-$(uname -r)
最后,您需要安装版本为 0.33.1 的 Falco apt 包:
$ apt-get install -y falco=0.33.1
Falco 已成功安装,并作为一个后台的 systemd 服务运行。运行以下命令检查 Falco 服务的状态:
$ sudo systemctl status falco
● falco.service - Falco: Container Native Runtime Security
Loaded: loaded (/lib/systemd/system/falco.service; enabled; vendor preset: \
enabled)
Active: active (running) since Tue 2023-01-24 15:42:31 UTC; 43min ago
Docs: https://falco.org/docs/
Main PID: 8718 (falco)
Tasks: 12 (limit: 1131)
Memory: 30.2M
CGroup: /system.slice/falco.service
└─8718 /usr/bin/falco --pidfile=/var/run/falco.pid
Falco 服务应该处于“active”状态。它已经在监视系统中的事件。
配置 Falco
Falco 服务提供了一个操作环境,用于监视系统并带有一组默认规则。这些规则存储在 /etc/falco
目录中的 YAML 文件中。/etc/falco
中的文件和子目录列表如下:
$ tree /etc/falco
/etc/falco
├── aws_cloudtrail_rules.yaml
├── falco.yaml
├── falco_rules.local.yaml
├── falco_rules.yaml
├── k8s_audit_rules.yaml
├── rules.available
│ └── application_rules.yaml
└── rules.d
我想描述其中最重要文件的高级目的。
Falco 配置文件
名为 falco.yaml
的文件是 Falco 进程的配置文件。它控制在发生警报时将通知的通道。通道可以是标准输出或文件。此外,该文件定义了在日志中包含的警报的最低日志级别,以及如何配置用于实现 Falco 进程健康检查的嵌入式 Web 服务器。请参考 “Falco 配置选项” 获取所有可用选项的完整参考。
默认规则
名为 falco_rules.yaml
的文件定义了一组预安装规则。Falco 认为这些规则是默认应用的。其中包括检查当打开容器的 shell 或执行写操作到系统目录时创建警报的规则。您可以在 “规则示例”页面 上找到其他示例及其相应的规则定义。
自定义规则
名为 falco_rules.local.yaml
的文件允许您定义自定义规则并覆盖默认规则。在安装 Falco 时,该文件仅包含注释掉的规则,为您提供添加自己规则的起点。您可以在 Falco 文档中找到 编写自定义规则的指导。
Kubernetes 特定规则
名为 k8s_audit_rules.yaml
的文件定义了 Kubernetes 特定规则,除了记录系统调用事件。典型示例是“当命名空间被删除时记录事件”或“当创建 Role 或 ClusterRole 对象时记录事件”。
应用配置更改
修改配置文件的内容不会自动传播到 Falco 进程。您需要重新启动 Falco 服务,如下所示:
$ sudo systemctl restart falco
接下来,我们将触发 Falco 默认规则涵盖的一些事件。每个事件都会创建一个写入配置通道的警报。Falco 的初始设置将消息路由到标准输出。
生成事件并检查 Falco 日志
让我们看看 Falco 警报是如何工作的。Falco 默认规则之一会在用户尝试打开容器的 shell 时创建一个警报。我们需要执行这个活动来查看其日志条目。为了实现这一点,创建一个名为 nginx
的新 Pod,打开容器的 bash shell,然后退出容器:
$ kubectl run nginx --image=nginx:1.23.3
pod/nginx created
$ kubectl exec -it nginx -- bash
root@nginx:/# exit
通过检查其运行时详细信息来确定 Pod 运行在哪个集群节点上:
$ kubectl get pod nginx -o jsonpath='{.spec.nodeName}'
kube-worker-1
此 Pod 正在名为kube-worker-1
的集群节点上运行。您需要检查该机器上的 Falco 日志以找到相关的日志条目。您可以直接在kube-worker-1
集群节点上使用journalctl
命令检查记录的事件。以下命令搜索包含关键字falco
的条目:
$ sudo journalctl -fu falco
...
Jan 24 18:03:37 kube-worker-1 falco[8718]: 18:03:14.632368639: Notice A shell \
was spawned in a container with an attached terminal (user=root user_loginuid=0 \
nginx (id=18b247adb3ca) shell=bash parent=runc cmdline=bash pid=47773 \
terminal=34816 container_id=18b247adb3ca image=docker.io/library/nginx)
如果您尝试修改容器状态,将会发现会引入额外的规则。比如说,您在nginx
容器中使用apt
安装了 Git 包:
root@nginx:/# apt update
root@nginx:/# apt install git
Falco 为这些系统级操作添加了日志条目。以下输出呈现了警报:
$ sudo journalctl -fu falco
Jan 24 18:55:48 ubuntu-focal falco[8718]: 18:55:05.173895727: Error Package \
management process launched in container (user=root user_loginuid=0 \
command=apt update pid=60538 container_id=18b247adb3ca container_name=nginx \
image=docker.io/library/nginx:1.23.3)
Jan 24 18:55:48 ubuntu-focal falco[8718]: 18:55:11.050925982: Error Package \
management process launched in container (user=root user_loginuid=0 \
command=apt install git-all pid=60823 container_id=18b247adb3ca \
container_name=nginx image=docker.io/library/nginx:1.23.3)
...
在下一节中,我们将检查触发警报创建的 Falco 规则及其语法。
理解 Falco 规则文件基础知识
Falco 规则文件通常由 YAML 中定义的三个元素组成:规则、宏和列表。您需要在高层次上理解这些元素,并知道如何修改它们以达到特定的目标。
编写您自己的 Falco 规则
考试期间,您可能不需要自己编写 Falco 规则。您将获得一组现有的规则。如果您想更深入地了解 Falco 规则,请查看相关的文档页面。
规则
规则是生成警报的条件。它还定义了警报的输出消息。输出消息可以包含硬编码的消息并结合内置变量。警报记录在WARNING
日志级别上。示例 7-1 展示了一个规则的 YAML,监听试图从除了传统视频会议软件(如 Skype 或 Webex)之外的进程访问机器摄像头的事件。
示例 7-1. 监控摄像头访问的 Falco 规则
- rule: access_camera
desc: a process other than skype/webex tries to access the camera
condition: evt.type = open and fd.name = /dev/video0 and not proc.name in \
(skype, webex)
output: Unexpected process opening camera video device (command=%proc.cmdline)
priority: WARNING
宏
宏是可重复使用的规则条件,有助于避免在多个规则之间复制粘贴类似的逻辑。如果规则文件变得很长,并且您希望提高可维护性,宏非常有用。
假设您正在简化一个规则文件的过程中。您注意到多个规则使用相同的条件监听摄像头访问。示例 7-2 展示了如何将逻辑分解为宏。
示例 7-2. 定义可重复条件的 Falco 宏
- macro: camera_process_access
condition: evt.type = open and fd.name = /dev/video0 and not proc.name in \
(skype, webex)
现在可以通过名称在规则定义中引用宏,如示例 7-3 所示。
示例 7-3. 包含宏的 Falco 规则
- rule: access_camera
desc: a process other than skype/webex tries to access the camera
condition: camera_process_access
output: Unexpected process opening camera video device (command=%proc.cmdline)
priority: WARNING
列表
列表是可以包含在规则、宏和其他列表中的项目集合。将列表视为传统编程语言中的数组。示例 7-4 创建了一个与视频会议软件关联的进程名称列表。
示例 7-4. Falco 列表
- list: video_conferencing_software
items: [skype, webex]
示例 7-5 展示了如何在宏中使用列表。
示例 7-5. 使用列表的 Falco 宏
- macro: camera_process_access
condition: evt.type = open and fd.name = /dev/video0 and not proc.name in \
(video_conferencing_software)
解析现有规则
Falco 预装默认规则的原因是为了缩短生产集群启动时间。与其自己定义规则和正确的语法,您可以简单地安装 Falco,并从一开始就受益于最佳实践。
让我们回到我们在 “生成事件并检查 Falco 日志” 中触发的事件之一。撰写时,我正在使用 Falco 版本 0.33.1。随附的规则文件 /etc/falco/falco_rules.yaml
包含一个名为 “Terminal shell in container” 的规则。它观察打开容器的 shell 事件。您可以通过搜索日志消息的一部分,例如 “在容器中生成了一个 shell” 来轻松找到该规则。示例 7-6 展示了规则定义的语法,以及 YAML 注释部分。
示例 7-6。监视 shell 活动到容器的 Falco 规则
- macro: spawned_process <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
condition: (evt.type in (execve, execveat) and evt.dir=<)
- macro: container <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
condition: (container.id != host)
- macro: container_entrypoint <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
condition: (not proc.pname exists or proc.pname in (runc:[0:PARENT], \
runc:[1:CHILD], runc, docker-runc, exe, docker-runc-cur))
- macro: user_expected_terminal_shell_in_container_conditions <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
condition: (never_true)
- rule: Terminal shell in container <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
desc: A shell was used as the entrypoint/exec point into a container with an \
attached terminal.
condition: > <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
and not user_expected_terminal_shell_in_container_conditions
output: > <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/4.png>
A shell was spawned in a container with an attached terminal (user=%user.name \
user_loginuid=%user.loginuid %container.info
shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline pid=%proc.pid \
terminal=%proc.tty container_id=%container.id image=%container.image.repository)
priority: NOTICE <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png>
tags: [container, shell, mitre_execution] <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/6.png>
定义宏,一种可在多个规则中重复使用的条件,通过名称引用。
指定规则的名称。
由多个宏组成的聚合条件。
如果发生事件,警报消息应该。消息可能使用内置字段引用运行时值。
指示规则违反的严重程度。
将规则集分为相关规则组,以便管理。
有时候您可能希望更改现有的规则。下一节将解释如何最好地覆盖默认规则。
覆盖现有规则
不要直接修改 /etc/falco/falco_rules.yaml
中的规则定义,我建议您在 /etc/falco/falco_rules.local.yaml
中重新定义规则。这样做可以帮助您在想要消除修改或在过程中出现任何错误时回退到原始规则定义。规则定义需要在集群的所有工作节点上进行更改。
在 示例 7-7 中展示的规则定义通过将优先级更改为 ALERT
并将输出格式更改为通过 内置字段 来包含新格式来覆盖名为 “Terminal shell in container” 的规则。
示例 7-7。修改后的监视 shell 活动到容器的规则
- rule: Terminal shell in container
desc: A shell was used as the entrypoint/exec point into a container with an \
attached terminal.
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
and not user_expected_terminal_shell_in_container_conditions
output: >
Opened shell: %evt.time,%user.name,%container.name <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
priority: ALERT <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
tags: [container, shell, mitre_execution]
简化违规时生成的日志输出。
以 ALERT
优先级处理违反规则。
在向 falco_rules.local.yaml
添加规则后,我们需要让 Falco 获取这些更改。请使用以下命令重启 Falco 服务:
$ sudo systemctl restart falco
因此,任何尝试 shell 进入容器的行为都将以不同的输出格式和优先级记录,如下所示:
$ sudo journalctl -fu falco
...
Jan 24 21:19:13 kube-worker-1 falco[100017]: 21:19:13.961970887: Alert Opened \
shell: 21:19:13.961970887,<NA>,nginx
除了覆盖现有的 Falco 规则,您还可以在 /etc/falco/falco_rules.local.yaml
中定义自己的自定义规则。编写自定义规则不在本书的讨论范围内,但您可以在 Falco 文档中找到大量相关信息。
确保容器的不可变性
容器默认是可变的。在容器启动后,您可以打开一个 shell 连接到它,安装现有软件的补丁,修改文件或更改其配置。可变的容器允许攻击者通过下载或安装恶意软件来获取对容器的访问权限。
要对抗这种情况,请确保容器处于不可变状态。这意味着防止对容器文件系统的写操作,甚至禁止对容器进行 shell 访问。如果需要对容器进行任何重大更改,比如更新依赖项或集成新的应用功能,应该发布容器镜像的新标签,而不是手动修改容器本身。然后,您可以将容器镜像的新标签分配给 Pod,而无需直接修改其内部。
情景:攻击者安装恶意软件
图 7-3 描述了一个情景,攻击者利用窃取的凭据访问容器。攻击者下载并安装了恶意软件,因为容器允许对根文件系统进行写操作。恶意软件继续监视容器中的活动,例如可以解析应用程序日志以获取敏感信息,然后将信息发送到 Kubernetes 集群外的服务器。因此,数据被用作登录系统其他部分的手段。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0703.png
图 7-3. 攻击者 shell 进入容器并安装恶意软件
在下一节中,您将学习如何通过使用 Distroless 容器镜像、通过 ConfigMap 或 Secret 注入配置数据以及配置只读容器文件系统来防止这种情况的发生。这些设置将使容器真正成为不可变的。
使用 Distroless 容器镜像
Distroless 容器镜像在容器的世界中越来越受欢迎。它们仅捆绑了您的应用程序及其运行时依赖项,同时尽可能地去除操作系统的大部分内容,例如 shell、软件包管理器和库。Distroless 容器镜像不仅尺寸更小,而且更安全。攻击者无法进入容器,因此文件系统无法被滥用安装恶意软件。使用 Distroless 容器镜像是创建不可变容器的第一防线。我们已经在“选择尺寸小的基础镜像”中介绍了 Distroless 容器镜像。更多信息请参阅该部分。
配置容器使用 ConfigMap 或 Secret
最佳实践是在不同的部署环境中使用相同的容器镜像,即使它们的运行时配置可能不同。任何特定于环境的配置,如凭据和系统其他部分的连接 URL,都应该是外部化的。在 Kubernetes 中,您可以通过 ConfigMap 或 Secret 将配置数据作为环境变量或通过 Volumes 挂载为文件注入到容器中。图 7-4 展示了在开发和生产集群中配置 Pod 时重用相同的容器镜像。环境特定的配置数据由 ConfigMap 提供。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0704.png
图 7-4. 在多个环境中使用相同的容器镜像
避免创建特定于环境的容器镜像简化了创建过程,减少了引入意外安全风险的风险,并使功能测试更加容易。在容器启动后注入运行时值无需更改容器,因此是使其不可变的关键。
当在容器中将 Secret 作为环境变量使用时,请确保避免意外将值记录到标准输出,例如在编写日志消息时作为明文值。任何可以访问容器日志的人都可以解析其中的 Secret 值。作为抽查,识别应用程序代码中使用 Secret 的位置,并评估其暴露风险。
要了解创建、配置和使用 ConfigMaps 和 Secrets 的最新信息,请查阅Kubernetes 文档。
配置只读容器根文件系统
容器不可变性的另一个方面是防止对容器文件系统的写访问。您可以通过将值true
分配给属性spec.containers[].securityContext.readOnlyRootFilesystem
来配置此运行时行为。
仍然有一些应用程序需要写入权限以满足其功能需求。例如,nginx 需要写入到 /var/run
、/var/cache/nginx
和 /usr/local/nginx
目录。与设置 readOnlyRootFilesystem
为 true
结合使用,您可以声明卷使这些目录可写。Example 7-8 展示了运行 nginx 的不可变容器的 YAML 清单。
Example 7-8. 一个禁止对根文件系统进行写入访问的容器
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.21.6
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: nginx-run
mountPath: /var/run
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-data
mountPath: /usr/local/nginx
volumes:
- name: nginx-run
emptyDir: {}
- name: nginx-data
emptyDir: {}
- name: nginx-cache
emptyDir: {}
在创建 Pod 之前,确定应用程序的文件系统读/写需求。使用卷来配置写入挂载路径。任何其他文件系统路径应设为只读。
使用审计日志监控访问
对于 Kubernetes 管理员来说,记录集群中发生的事件是非常重要的。这些记录可以帮助实时检测入侵,或者用于跟踪配置更改以进行故障排除。审计日志 提供了 API 服务器接收到的事件的时间顺序视图。
情景:管理员可以实时监控恶意事件
Figure 7-5 展示了监控 Kubernetes API 事件的好处。在这种情况下,攻击者试图调用 API 服务器。审计日志机制已捕获到感兴趣的事件。管理员可以随时查看这些事件,识别入侵尝试,并采取对策。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0705.png
图 7-5. 通过观察审计日志监控的攻击者
我们只在这里回顾了一个用例,即适用于安全问题的用例。不能低估跟踪公司内部 API 请求的能力。通过审查审计日志,管理员可以为试图创建 Kubernetes 对象的应用程序开发人员提供指导,或者重建可能导致集群行为故障的配置更改。
理解审计日志
Kubernetes 可以存储由最终用户触发的对 API 服务器的任何请求或由控制平面本身发出的事件的记录。审计日志中的条目以 JSON Lines 格式存在,可能包括但不限于以下信息:
-
发生了什么事件?
-
是谁触发了这个事件?
-
它是什么时候触发的?
-
哪个 Kubernetes 组件处理了该请求?
事件类型及其对应的请求数据由 审计策略 定义。审计策略是一个指定这些规则的 YAML 清单,并且必须提供给 API 服务器进程。
审计后端 负责存储根据审计策略定义的记录的审计事件。对于后端,您有两个可配置的选项:
-
一个日志后端,将事件写入文件。
-
Webhook 后端通过 HTTP(S) 将事件发送到外部服务,例如集中式日志记录和监控系统集成的目的。 这样的后端可以帮助调试运行时问题,例如应用程序崩溃。
图 7-6 汇总了配置审计日志所需的所有要素。 下面的章节将解释配置它们的详细信息。
https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0706.png
图 7-6. 高级审计日志架构
让我们深入了解审计策略文件及其配置选项。
创建审计策略文件
审计策略文件实际上是用于 Policy
资源的 YAML 清单。 API 服务器接收到的任何事件都会按照策略文件中定义的顺序匹配规则。 如果能找到匹配的规则,则记录事件及其声明的审计级别。 表 7-1 列出了所有可用的审计级别。
表 7-1. 审计级别
Level | Effect |
---|---|
None | 不记录与此规则匹配的事件。 |
Metadata | 仅记录事件的请求元数据。 |
Request | 记录事件的元数据和请求体。 |
RequestResponse | 记录事件的元数据、请求和响应体。 |
示例 7-9 展示了一个示例审计策略。 规则被指定为具有名为 rules
的属性的项目数组。 每个规则声明一个级别,适用于的资源类型和 API 组,以及一个可选的命名空间。
示例 7-9. 审计策略文件的内容
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived" <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
rules:
- level: RequestResponse <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
resources:
- group: ""
resources: ["pods"]
- level: Metadata <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
resources:
- group: ""
resources: ["pods/log", "pods/status"]
阻止在 RequestReceived
阶段为所有请求生成日志
记录 RequestResponse
级别的 Pod 变更
记录特定的 Pod 事件,例如日志和状态请求,在 Metadata
级别
先前的审计策略并不是很详细,但应该让您对其格式有所了解。 有关更多示例和详细信息,请参阅 Kubernetes 文档。
一旦创建了审计策略文件,它就可以被 API 服务器进程消费。 在文件 /etc/kubernetes/manifests/kube-apiserver.yaml
中的 API 服务器进程中添加标志 --audit-policy-file
。 参数分配的值是审计策略文件的完全限定路径。
接下来,我们将详细介绍配置 API 服务器的审计日志记录所需的设置,包括文件日志后端和 Webhook 后端。
配置日志后端
要设置基于文件的日志后端,您需要向文件 /etc/kubernetes/manifests/kube-apiserver.yaml
添加三个配置项。 以下列表总结了配置内容:
-
向 API 服务器进程提供两个标志:标志
--audit-policy-file
指向审计策略文件;标志--audit-log-path
指向日志输出文件。 -
为审计日志策略文件和日志输出目录添加卷挂载路径。
-
为审计日志策略文件和日志输出目录在主机路径上添加卷定义。
示例 7-10 显示了 API 服务器配置文件的修改内容。
示例 7-10。配置审计策略文件和审计日志文件
...
spec:
containers:
- command:
- kube-apiserver
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
- --audit-log-path=/var/log/kubernetes/audit/audit.log <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png>
...
volumeMounts:
- mountPath: /etc/kubernetes/audit-policy.yaml <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
name: audit
readOnly: true
- mountPath: /var/log/kubernetes/audit/ <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png>
name: audit-log
readOnly: false
...
volumes:
- name: audit <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
hostPath:
path: /etc/kubernetes/audit-policy.yaml
type: File
- name: audit-log <https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png>
hostPath:
path: /var/log/kubernetes/audit/
type: DirectoryOrCreate
将策略文件和日志文件的位置提供给 API 服务器进程。
将策略文件和审计日志目录挂载到给定路径。
定义策略文件和审计日志目录的卷。
通过向 API 服务器进程传递附加标志,可以进一步自定义日志后端的运行时行为。例如,您可以通过提供标志--audit-log-maxage
指定保留旧审计日志文件的最大天数。请参考Kubernetes 文档查看完整的标志列表。
现在是产生一些日志条目的时候了。以下kubectl
命令向 API 服务器发送一个请求,以创建名为nginx
的 Pod:
$ kubectl run nginx --image=nginx:1.21.6
pod/nginx created
在上一步中,我们配置了审计日志文件位于/var/log/kubernetes/audit/audit.log
。根据审计策略中的规则,条目数量可能会非常庞大,这使得查找特定事件变得困难。过滤已配置事件的简单方法是搜索分配给apiVersion
属性的值audit.k8s.io/v1
。以下命令查找相关的日志条目,一个是RequestResponse
级别的,另一个是Metadata
级别的:
$ sudo grep 'audit.k8s.io/v1' /var/log/kubernetes/audit/audit.log
...
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"RequestResponse", \
"auditID":"285f4b99-951e-405b-b5de-6b66295074f4","stage":"ResponseComplete", \
"requestURI":"/api/v1/namespaces/default/pods/nginx","verb":"get", \
"user":{"username":"system:node:node01","groups":["system:nodes", \
"system:authenticated"]},"sourceIPs":["172.28.116.6"],"userAgent": \
"kubelet/v1.26.0 (linux/amd64) kubernetes/b46a3f8","objectRef": \
{"resource":"pods","namespace":"default","name":"nginx","apiVersion":"v1"}, \
"responseStatus":{"metadata":{},"code":200},"responseObject":{"kind":"Pod", \
"apiVersion":"v1","metadata":{"name":"nginx","namespace":"default", \
...
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID": \
"5c8e5ecc-0ce0-49e0-8ab2-368284f2f785","stage":"ResponseComplete", \
"requestURI":"/api/v1/namespaces/default/pods/nginx/status","verb":"patch", \
"user":{"username":"system:node:node01","groups":["system:nodes", \
"system:authenticated"]},"sourceIPs":["172.28.116.6"],"userAgent": \
"kubelet/v1.26.0 (linux/amd64) kubernetes/b46a3f8","objectRef": \
{"resource":"pods","namespace":"default","name":"nginx","apiVersion":"v1", \
"subresource":"status"},"responseStatus":{"metadata":{},"code":200}, \
...
配置 Webhook 后端
配置 Webhook 后端与配置日志后端有所不同。我们需要告诉 API 服务器向外部服务发送 HTTP(S)请求,而不是文件。与我们在“配置 ImagePolicyWebhook Admission Controller 插件”中所做的类似,将配置到外部服务的配置、Webhook 和用于认证的凭据定义在一个kubeconfig 文件中。
在文件/etc/kubernetes/manifests/kube-apiserver.yaml
中向 API 服务器进程添加标志--audit-webhook-config-file
,并指向 kubeconfig 文件的位置。标志--audit-webhook-initial-backoff
定义了在初始请求后等待外部服务重试之前的时间。您仍然需要分配标志--audit-policy-file
来指向审计策略文件。
总结
在 Kubernetes 集群中监视和记录事件是每个管理员的重要职责。我们使用 Falco 来识别和过滤与安全相关的事件。您了解了不同配置文件的目的和语法,以及如何在日志中找到相关警报。
除了使用行为分析工具外,您还需要为到达 API 服务器的请求设置审计日志记录。审计日志记录配置的事件到后端,可以是控制平面节点上的文件,也可以通过 HTTP(S)调用发送到外部服务。我们已经完成了为 API 服务器进程启用审计日志记录的过程。
朝向更安全容器的明智步骤是使其不可变。不可变容器仅支持只读文件系统,因此潜在攻击者无法安装恶意软件。如果容器内运行的应用程序需要写入数据,则挂载一个卷。使用 distroless 容器镜像阻止攻击者能够进入容器的 shell。
考试要点
练习如何配置和操作 Falco。
Falco 肯定会成为考试中的一个主题。您需要了解如何读取和修改配置文件中的规则。我建议您详细浏览语法和选项,以防需要自己编写规则。运行 Falco 的主要入口点是命令行工具。可以合理地假设它已经预装在考试环境中。
知道如何识别不可变容器。
不可变容器是本考试领域的核心主题。了解如何为 Pod 设置spec.containers[].securityContext.readOnlyRootFilesystem
属性,以及如何挂载卷到特定路径,以防容器进程需要写操作。
深入了解审计日志配置选项。
设置审计日志记录包括两个步骤。首先,您需要了解审计策略文件的语法和结构。另一个方面是如何配置 API 服务器以消耗审计策略文件,提供到后端的引用,并挂载相关的文件系统卷。确保进行所有这些方面的实际操作练习。
示例练习
-
导航到已检出的 GitHub 存储库bmuschko/cks-study-guide的app-a/ch07/falco目录。使用命令
vagrant up
启动运行集群的虚拟机(VMs)。该集群包括一个名为kube-control-plane
的单个控制平面节点和一个名为kube-worker-1
的工作节点。完成后,使用vagrant destroy -f
关闭集群。Falco 已作为一个 systemd 服务正在运行。检查运行在名为
malicious
的现有 Pod 中的进程。查看 Falco 日志,看看是否有规则为该进程创建了日志。通过更改输出为
<timestamp>,<username>,<container-id>
来重新配置创建事件日志的现有规则。在 Falco 日志中找到已更改的日志条目。重新配置 Falco,使其将日志写入到文件
/var/logs/falco.log
。禁用标准输出通道。确保 Falco 将新消息追加到日志文件中。先决条件: 这项练习需要安装工具 Vagrant 和 VirtualBox。
-
转到已检出的 GitHub 仓库 bmuschko/cks-study-guide 的目录 app-a/ch07/immutable-container。使用命令
kubectl apply -f setup.yaml
执行操作。检查 YAML 清单在
default
命名空间中创建的 Pod。对 Pod 进行相关更改,以便其容器被视为不可变。 -
转到已检出的 GitHub 仓库 bmuschko/cks-study-guide 的目录 app-a/ch07/audit-log。使用命令
vagrant up
启动运行集群的虚拟机。该集群包括一个名为kube-control-plane
的单控制平面节点和一个名为kube-worker-1
的工作节点。完成后,使用vagrant destroy -f
停止集群。编辑位于
/etc/kubernetes/audit/rules/audit-policy.yaml
的现有审计策略文件。添加一个规则,以Metadata
级别记录 ConfigMaps 和 Secrets 的事件。添加另一个规则,以Request
级别记录 Services 的事件。配置 API 服务器以消耗审计策略文件。日志应写入到文件
/var/log/kubernetes/audit/logs/apiserver.log
。定义最多保留审计日志文件五天。确保已创建日志文件并包含至少一个与配置的事件匹配的条目。
先决条件: 这项练习需要安装工具 Vagrant 和 VirtualBox。