Kubernetes-认证安全专家-CKS-学习指南-全-

Kubernetes 认证安全专家(CKS)学习指南(全)

原文:zh.annas-archive.org/md5/525e77013d4f84e5576c98a6b161f26a

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

Kubernetes 认证计划自 2018 年以来已经存在五年。在这段时间里,安全性变得越来越重要,包括 Kubernetes 领域。最近,增加了认证 Kubernetes 安全专家 (CKS) 角色来满足需求。安全性可以有不同的方面,解决这些问题的方式可以非常多样化。这就是 Kubernetes 生态系统发挥作用的地方。除了 Kubernetes 内置的安全功能外,许多工具已经发展起来,帮助识别和修复安全风险。作为 Kubernetes 管理员,你需要熟悉广泛的概念和工具,以加固你的集群和应用程序。

CKS 认证计划旨在验证安全相关主题的能力,需要通过认证 Kubernetes 管理员 (CKA) 考试后才能注册。如果你对 Kubernetes 认证程序完全陌生,我建议首先了解 CKA 或认证 Kubernetes 应用开发者 (CKAD) 计划。

在本学习指南中,我将探讨 CKS 考试涵盖的主题,以全面准备你通过认证考试。我们将研究何时以及如何应用 Kubernetes 的核心概念和外部工具来保护集群组件、集群配置以及在 Pod 中运行的应用程序。我还将提供一些提示,帮助你更好地准备考试,并分享我准备考试的个人经验的所有方面。

CKS 不同于其他认证的典型多项选择格式。它完全基于表现,并要求你在巨大的时间压力下展示对手头任务的深入知识。你准备好一次通过考试了吗?

适合读者

本书适合已经通过 CKA 考试并希望在安全领域扩展知识的任何人。考虑到注册 CKS 前需要通过 CKA 考试,你应该已经熟悉考试问题的格式和环境。第一章 简要回顾了考试课程的一般方面,但重点强调了适用于 CKS 考试的信息。如果你还没有参加 CKA 考试,我建议你先阅读认证 Kubernetes 管理员 (CKA) 学习指南(O'Reilly)。这本书将为你提供开始学习 CKS 所需的基础。

学到什么

本书内容压缩了与 CKS 考试相关的最重要方面。不需要考虑云提供商特定的 Kubernetes 实现,如 AKS 或 GKE。鉴于 Kubernetes 中提供了大量的配置选项,几乎不可能涵盖所有用例和场景而不重复官方文档。建议考生参考Kubernetes 文档以获取更广泛的知识。与 CKS 考试相关的外部工具(如 Trivy 或 Falco)仅在高层次上进行了介绍。请参考它们的文档以探索更多功能、功能和配置选项。

本书结构

本书的大纲严格遵循 CKS 课程。虽然可能存在更自然的教学结构来学习 Kubernetes 的一般知识,但课程大纲将帮助考生通过专注于特定主题来准备考试。因此,您可能会根据自己的知识水平交叉参考本书的其他章节。

请注意,本书仅涵盖与 CKS 考试相关的概念。不讨论基础的 Kubernetes 概念和原语。如果您希望深入了解,请参考 Kubernetes 文档或其他书籍。

Kubernetes 的实际经验对于通过考试至关重要。每章都包含名为“示例练习”的部分,其中包含练习题。这些问题的解决方案可以在附录中找到。

本书使用的约定

本书使用以下排版约定:

斜体

表示新术语、URL 和电子邮件地址。

常量宽度

用于文件名、文件扩展名和程序清单,以及段落中用于引用程序元素(如变量或函数名称、数据库、数据类型、环境变量、语句和关键字)。

常量宽度粗体

显示用户应直接输入的命令或其他文本。

提示

此元素表示提示或建议。

注意

此元素表示一般说明。

警告

此元素表示警告或注意事项。

使用代码示例

本书中的一些代码片段使用反斜杠字符(\)来将单行代码分解为多行,以便适应页面。如果您直接从书中内容复制粘贴到终端或编辑器中,则需要手动整理代码。更好的选择是参考代码书的GitHub 存储库,那里已经有了正确的格式。

GitHub 存储库根据 Apache License 2.0 分发。代码可以在商业和开源项目中免费使用。如果在源代码中遇到问题或有疑问,请在GitHub 问题跟踪器中提出问题。我很乐意进行交流并解决可能出现的任何问题。

本书旨在帮助您完成工作。一般而言,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了代码的大部分,否则无需获得我们的许可。例如,编写一个使用本书多个代码块的程序不需要许可。销售或分发 O’Reilly 书籍中的示例代码需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书大量示例代码整合到您产品的文档中需要许可。我们感谢您的署名,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Certified Kubernetes Security Specialist (CKS) Study Guide by Benjamin Muschko (O’Reilly)。版权 2023 年 Automated Ascent, LLC, 978-1-098-13297-2。”

如果您认为您对代码示例的使用超出了公平使用或以上给出的权限,请随时通过permissions@oreilly.com与我们联系。

O’Reilly 在线学习

注意

超过 40 年来,O’Reilly Media提供技术和商业培训、知识和见解,帮助企业成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专业知识。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深入的学习路径、交互式编码环境以及来自 O’Reilly 和 200 多家其他出版商的大量文本和视频。有关更多信息,请访问http://oreilly.com

如何联系我们

请将关于本书的评论和问题发送至出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • CA 95472 Sebastopol

  • 800-889-8969(美国或加拿大)

  • 707-829-7019(国际或本地)

  • 707-829-0104(传真)

  • support@oreilly.com

  • https://www.oreilly.com/about/contact.xhtml

我们有本书的网页,列出勘误、示例和任何额外信息。您可以访问此页面:https://oreil.ly/cks-study-guide

有关我们的书籍和课程的新闻和信息,请访问http://oreilly.com

在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://youtube.com/oreillymedia

在 Twitter 上关注作者:https://twitter.com/bmuschko

在 GitHub 上关注作者:https://github.com/bmuschko

关注作者的博客:https://bmuschko.com

致谢

每本书籍项目都是一段漫长的旅程,没有编辑部和技术审阅人员的帮助是不可能完成的。特别感谢 Robin Smorenburg,Werner Dijkerman,Michael Kehoe 和 Liz Rice 提供的详细技术指导和反馈。我还要感谢 O’Reilly Media 的编辑 John Devins 和 Michele Cronin,他们一直以来的支持和鼓励。

第一章:考试细节和资源

本章介绍了备考 Certified Kubernetes Security Specialist (CKS) 考试 时候候候候候候候候候候候候候候候候候候候候候候候候候候候候候候候。我们将讨论该认证的目标受众、课程设置和考试环境,以及技巧和额外的学习资源。如果您对该认证计划已经很熟悉,可以直接跳到覆盖技术概念的章节。

Kubernetes 认证学习路径

CNCF 提供四种不同的 Kubernetes 认证。Figure 1-1 根据目标受众对它们进行了分类。您会发现 CKS 是您可以获得的最高级别认证。它是唯一一个有通过其他认证作为先决条件的认证;其他所有认证都是独立的项目。

ckss 0101

图 1-1。Kubernetes 认证学习路径

让我们简要地查看每个认证的详细信息,看看 CKS 是否适合您。

Kubernetes and Cloud Native Associate (KCNA)

KCNA 是一个面向任何对云原生应用开发、运行时环境和工具感兴趣的入门级认证项目。虽然考试涵盖 Kubernetes,但并不期望您实际解决实际问题。该考试适合对该主题有广泛了解的候选人。

Kubernetes and Cloud Native Security Associate (KCSA)

该认证侧重于安全概念的基础知识及其在 Kubernetes 集群中的应用。该程序的广度和深度与 KCNA 相当,因为它不要求实际动手解决问题。

Certified Kubernetes Application Developer (CKAD)

CKAD 考试侧重于验证您构建、配置和部署基于微服务的应用程序到 Kubernetes 的能力。您不需要实际实现应用程序;然而,该考试适合熟悉应用架构、运行时和编程语言等主题的开发人员。

Certified Kubernetes Administrator (CKA)

CKA 考试的目标受众包括 DevOps 从业者、系统管理员和可靠性工程师。该考试测试您在 Kubernetes 管理员角色中执行任务的能力,包括集群、网络、存储和初级安全管理,重点放在故障排除场景上。

Certified Kubernetes Security Specialist (CKS)

CKS 考试扩展了 CKA 考试验证的主题。在您甚至能够报名参加 CKS 考试之前,通过 CKA 考试是必须的先决条件。对于这个认证,您需要对 Kubernetes 安全方面有更深入的了解。课程涵盖的主题包括应用构建容器化应用程序的最佳实践和确保安全的 Kubernetes 运行时环境。

考试目标

如果被利用,软件和 IT 基础设施中的漏洞可能对组织构成重大威胁。云原生计算基金会(CNCF)开发了认证 Kubernetes 安全专家(CKS)证书,以验证 Kubernetes 管理员在保护 Kubernetes 集群及其中运行的云原生软件方面的能力。作为 CKS 考试的一部分,您应该了解 Kubernetes 核心安全功能,以及用于保护应用程序和基础设施的第三方工具和已建立的实践。

考试期间使用的 Kubernetes 版本

在撰写本书时,考试基于 Kubernetes 1.26 版。本书的所有内容将遵循该特定版本的功能、API 和命令行支持。未来版本有可能会破坏向后兼容性。在准备认证时,请查阅Kubernetes 发布说明,并使用考试中使用的 Kubernetes 版本进行练习,以避免不愉快的意外。

在本书中,我将通过提供特定用例来解释每种安全威胁。我们将从讨论允许攻击者访问集群、注入恶意代码或使用漏洞入侵系统的场景开始。然后,我们将涉及可以防止该情况发生的概念、实践和/或工具。通过这种方法,您将能够评估安全风险的严重性以及实施安全措施的必要性。

课程

以下概述列出了 CKS 考试的高级部分,也称为领域,以及它们的评分权重:

  • 10%:集群设置

  • 15%:集群硬化

  • 15%:系统硬化

  • 20%:最小化微服务漏洞

  • 20%:供应链安全

  • 20%:监控、日志和运行时安全性

书籍工作原理

本书的大纲严格遵循 CKS 课程。虽然一般来说,学习 Kubernetes 可能会有更自然、更教学性的组织结构,但课程大纲将帮助考试参与者通过专注于特定主题来准备考试。因此,根据你的现有知识水平,你会发现自己需要参考本书的其他章节。

让我们在接下来的章节详细分析每个领域。

集群设置

本节涵盖了 CKA 考试已经涵盖的 Kubernetes 概念;然而,它们假定您已经理解基础知识,并期望您能够深入了解。在这里,您将被测试网络策略及其对在同一命名空间内和跨多个命名空间之间的 Pod 之间的网络通信禁止和授予的影响。主要关注点将放在限制通信以最小化攻击面上。此外,“集群设置”领域将验证您设置带有传输层安全性(TLS)终止的 Ingress 对象的知识。

强调通过检查集群设置来识别和修复安全漏洞。像 kube-bench 这样的外部工具可以帮助自动化该过程。执行工具针对集群的结果将为您提供一个可执行的漏洞列表。根据建议更改集群的配置设置可以显著降低安全风险。

最后,锁定集群节点端点、端口和图形用户界面(GUI)可以帮助防止攻击者控制集群。您需要了解默认集群设置,以便尽可能限制对它们的访问。需要检查 Kubernetes 的二进制文件和可执行文件(如kubectlkubeadm和 kubelet)的校验和,以确保它们没有被第三方篡改。您需要了解如何获取二进制文件的校验和文件以及如何使用它来验证可执行文件的有效性。

集群加固

大多数组织从允许开发人员和管理员管理 Kubernetes 安装、配置和管理任何对象的集群开始。虽然这对于熟悉 Kubernetes 的团队来说是一种便利的方法,但并不安全,因为它可能会为攻击者打开闸门。一旦访问了集群,就可以执行任何恶意操作。

基于角色的访问控制(RBAC)将权限映射到用户或进程。考试要求深入了解涉及的 API 资源。领域“集群加固”还集中讨论保持集群版本最新以确保获取最新错误修复的主题。Kubernetes 通过端点公开 API 服务器。您应了解最小化其对外界暴露的策略。

系统加固

此领域的重点在于理解如何最小化对主机系统和外部网络的访问,以减少攻击面。这就是像 AppArmor 和 seccomp 这样的操作系统级工具发挥作用的地方。您需要展示它们的使用以满足要求。此领域还涉及在亚马逊云环境中运行的集群中使用 AWS IAM 角色的使用。

最小化微服务的漏洞

安全上下文定义了容器的特权和访问控制。平台和安全团队可以在组织级别上管理和执行所需的安全措施。考试要求您了解 Pod 安全策略和 OPA Gatekeeper 的目的。此外,您将被要求演示定义不同类型的 Secrets 并从 Pod 中使用它们以注入敏感的运行时信息。

有时,您可能希望从未经验证的来源或潜在不安全的来源实验容器镜像。像 gVisor 和 Kata Containers 这样的容器运行时沙箱可以在 Kubernetes 中配置,以使用非常受限的权限执行容器镜像。配置和使用这样的容器运行时沙箱是本领域的一部分。此外,您需要了解 mTLS Pod 对 Pod 加密的好处以及如何配置它。

供应链安全

容器安全始于基础镜像。您需要了解构建容器镜像的最佳实践,以最小化从一开始引入安全漏洞的风险。最优的做法是,您只允许从已经扫描过镜像漏洞的组织内部容器注册表中拉取受信任的容器镜像。仅允许这些注册表至关重要,并且是本领域重要主题之一。像 Trivy 这样的工具可以帮助扫描镜像以查找漏洞,并被列为通过考试的要求之一。

监控、日志记录和运行时安全

此领域的重点之一是行为分析,即观察异常和恶意事件的过程。Falco 是本节中应熟悉的主要工具。容器启动后不应该可变,以避免为攻击者打开额外的后门。您需要了解最佳实践,并能够在容器的配置中应用它们。

审计日志记录对于实时查看集群事件或调试目的很有帮助。配置 Kubernetes 集群的审计日志记录是考试的一部分。

涉及的 Kubernetes 原语

一些考试目标可以通过理解相关的核心 Kubernetes 原语来实现。预计考试将在单个问题中结合多个概念。参考图 1-2 作为适用的 Kubernetes 资源及其关系的粗略指南。

ckss 0102

图 1-2. 与考试相关的 Kubernetes 原语

除了 Kubernetes 核心原语外,您还需要掌握开源项目提供的特定自定义资源定义(CRD)。例如,Open Policy Agent(OPA)Gatekeeper 提供了原语的 ConstraintTemplate。

涉及的外部工具

考试中有相当一部分内容需要展示你对外部安全工具的专业知识。课程中明确列出了一些工具,但还有其他属于同一功能类别的工具。至少,你需要熟悉以下工具:

文档

考试期间,你可以打开明确定义的一系列网页作为参考。你可以自由浏览这些页面并在考试终端中复制粘贴代码。CKS 的 常见问题解答 (FAQ) 列出了一些允许的 URL。

Kubernetes 官方文档包括参考手册、GitHub 网站和博客:

对于外部工具,允许你打开并浏览以下网址:

候选技能

CKS 认证假设你已经具备 Kubernetes 的管理员级理解。CNCF 要求你先获得 CKA 认证作为先决条件。没有这些证书,你将无法报名参加 CKS 考试。如果你还没有通过 CKA 考试,或者想复习相关主题,我建议你看一下我的书籍 Certified Kubernetes Administrator (CKA) Study Guide

在本书的其余部分,我将假设你已经掌握了通过 CKA 所需的知识。因此,我不会再重复涉及重叠主题的基础知识。为方便起见,我将在需要时指向 CKA 书中相关信息。请重新查看 CKA 书中有关考试环境和时间管理的部分。它们同样适用于 CKS 考试。

练习和实践考试

在通过考试时,动手练习非常重要。为此,你需要一个功能正常的 Kubernetes 集群环境。以下选项非常突出:

  • 我发现使用 VagrantVirtualBox 运行一个或多个虚拟机非常有用。这些工具帮助创建一个易于引导和按需处理的孤立 Kubernetes 环境。本书的一些实践练习使用此设置作为起点。

  • 在你的开发者机器上安装一个简单的 Kubernetes 集群相对来说是很容易的。Kubernetes 文档根据你的操作系统提供各种安装选项。Minikube 在实验更高级功能(如 Ingress 或存储类)时非常有用,因为它提供了作为附加组件的必要功能,可以通过单个命令安装。

  • 如果你是O'Reilly 学习平台的订阅者,你可以无限制地访问运行 Kubernetes 环境的实验室。此外,你可以通过以互动实验室形式的 CKS 实践测试来测试你的知识。

你可能还想尝试以下一些商业化的学习和练习资源:

总结

CKS 考试验证你在 Kubernetes 中与安全相关的实际操作知识。你需要理解核心 Kubernetes 基元和概念,以满足安全需求,如 RBAC、网络策略和 Ingress。考试还涉及有用的第三方安全工具。你需要展示如何有效地使用这些工具。通过 CKA 考试是 CKS 考试的必要条件。如果你还没有通过 CKA 考试,请确保先通过 CKA。

以下章节与考试大纲一致,这样你可以将内容映射到学习目标上。每章结束时,你会找到一些示例练习来巩固你的知识。每个领域的讨论以对最重要的学习要点的简短总结结束。

第二章:聚集设置

考试的第一个领域涉及与 Kubernetes 集群设置和配置相关的问题。在本章中,我们将仅深入探讨与安全相关的方面,而不是 Kubernetes 管理员的标准责任。

在高层次上,本章涵盖以下概念:

  • 使用网络策略来限制 Pod 到 Pod 的通信

  • 运行 CIS 基准工具以识别集群组件的安全风险

  • 设置带有 TLS 支持的入口对象

  • 保护节点端口、API 端点和 GUI 访问

  • 验证平台二进制文件与它们的校验和

使用网络策略来限制 Pod 到 Pod 的通信

在 Kubernetes 中,为了使微服务架构正常运行,Pod 需要能够在同一节点上或不同节点上运行的另一个 Pod 进行通信,而不需要网络地址转换(NAT)。Kubernetes 在每个 Pod 创建时从其节点的 Pod CIDR 范围内为其分配唯一的 IP 地址。该 IP 地址是临时的,因此不能长期稳定。每次 Pod 重新启动时,都会租用一个新的 IP 地址。建议使用 Pod 到服务的通信而不是 Pod 到 Pod 的通信,以便依赖于一致的网络接口。

分配给 Pod 的 IP 地址在所有节点和命名空间中是唯一的。这是通过在注册节点时为每个节点分配专用子网来实现的。在节点上创建新的 Pod 时,IP 地址是从分配的子网中租赁的。这由容器网络接口(CNI)插件处理。因此,节点上的 Pod 可以与集群中任何其他节点上运行的所有其他 Pod 进行通信。

网络策略类似于防火墙规则,但用于 Pod 到 Pod 的通信。规则可以包括网络流量的方向(入站和/或出站),一个或多个命名空间内或跨不同命名空间的多个 Pod 的目标端口。关于网络策略基础的深入覆盖,请参阅书籍 Certified Kubernetes Application Developer (CKAD) Study Guide(O’Reilly)或 Kubernetes 文档。CKS 考试主要侧重于使用网络策略限制集群级访问。

正确定义网络策略的规则可能具有挑战性。网站 networkpolicy.io 提供了一个网络策略的可视化编辑器,在浏览器中呈现图形表示。

场景:攻击者获得对 Pod 的访问权限

假设你在为一家运营 Kubernetes 集群的公司工作,该集群有三个工作节点。工作节点 1 当前作为微服务架构的一部分运行两个 Pod。考虑到 Kubernetes 对 Pod 到 Pod 网络通信的默认行为,Pod 1 可以无限制地与 Pod 2 进行通信,反之亦然。

如您在图 2-1 中所见,攻击者已经访问了 Pod 1. 如果没有定义网络策略,攻击者可以简单地与 Pod 2 进行通信,并造成额外的损害。这种漏洞不限于单个命名空间。Pod 3 和 Pod 4 也可以被访问和受到威胁。

ckss 0201

图 2-1. 已经访问 Pod 1 的攻击者可以访问其他 Pod 的网络

观察默认行为

我们将设置三个 Pod 来演示实践中无限制的 Pod-to-Pod 网络通信。如您在示例 2-1 中所见,YAML 清单定义了命名空间 g04 中名为 backendfrontend 的 Pods。other Pod 位于 default 命名空间中。观察命名空间和 Pods 的标签分配。稍后在本章节中定义网络策略时我们会引用它们。

示例 2-1. 位于不同命名空间中的三个 Pods 的 YAML 清单
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app: orion
  name: g04
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    tier: backend
  name: backend
  namespace: g04
spec:
  containers:
  - image: bmuschko/nodejs-hello-world:1.0.0
    name: hello
    ports:
    - containerPort: 3000
  restartPolicy: Never
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    tier: frontend
  name: frontend
  namespace: g04
spec:
  containers:
  - image: alpine
    name: frontend
    args:
    - /bin/sh
    - -c
    - while true; do sleep 5; done;
  restartPolicy: Never
---
apiVersion: v1
kind: Pod
metadata:
  labels:
    tier: outside
  name: other
spec:
  containers:
  - image: alpine
    name: other
    args:
    - /bin/sh
    - -c
    - while true; do sleep 5; done;
  restartPolicy: Never

首先,使用声明性的 kubectl apply 命令从现有的 YAML 清单创建对象:

$ kubectl apply -f setup.yaml
namespace/g04 created
pod/backend created
pod/frontend created
pod/other created

让我们验证命名空间 g04 运行正确的 Pods。使用 -o wide CLI 选项来确定分配给 Pods 的虚拟 IP 地址。backend Pod 使用 IP 地址 10.0.0.43,而 frontend Pod 使用 IP 地址 10.0.0.193:

$ kubectl get pods -n g04 -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP           NODE     \
  NOMINATED NODE   READINESS GATES
backend    1/1     Running   0          15s   10.0.0.43    minikube \
  <none>           <none>
frontend   1/1     Running   0          15s   10.0.0.193   minikube \
  <none>           <none>

default 命名空间处理单个 Pod:

$ kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
other   1/1     Running   0          4h45m

frontend Pod 可以与 backend Pod 进行通信,因为没有设置通信限制:

$ kubectl exec frontend -it -n g04 -- /bin/sh
/ # wget --spider --timeout=1 10.0.0.43:3000
Connecting to 10.0.0.43:3000 (10.0.0.43:3000)
remote file exists
/ # exit

位于 default 命名空间中的 other Pod 可以与 backend Pod 进行通信而没有问题:

$ kubectl exec other -it -- /bin/sh
/ # wget --spider --timeout=1 10.0.0.43:3000
Connecting to 10.0.0.43:3000 (10.0.0.43:3000)
remote file exists
/ # exit

在接下来的章节中,我们将讨论如何通过拒绝所有网络策略规则来限制 Pod-to-Pod 网络通信的最大程度。然后,我们将仅针对微服务架构正常运行所需的网络通信打开入口和/或出口通信。

拒绝定向网络流量

限制 Pod-to-Pod 网络流量的最佳方法是使用最小权限原则。最小权限意味着 Pods 应该以最低特权进行网络通信。通常情况下,您会从禁止任何方向的流量开始,然后开放应用架构所需的流量。

Kubernetes 文档提供了几个有用的 YAML 清单示例。示例 2-2 展示了一个拒绝命名空间 g04 中所有 Pods 入口流量的网络策略。

示例 2-2. 默认拒绝所有入口网络策略
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: g04
spec:
  podSelector: {}
  policyTypes:
  - Ingress

选择所有 Pods 由赋值给 spec.podSelector 属性的值 {} 表示。spec.policyTypes 属性的值定义了流量的拒绝方向。对于入站流量,您可以将 Ingress 添加到数组中。出站流量可以通过 Egress 指定。在这个特定的例子中,我们禁止所有入站流量。出站流量仍然允许。

“拒绝所有”网络策略的内容已保存在文件deny-all-ingress-network-policy.yaml中。以下命令从文件创建对象:

$ kubectl apply -f deny-all-ingress-network-policy.yaml
networkpolicy.networking.k8s.io/default-deny-ingress created

让我们看看这如何改变 Pod 到 Pod 网络通信的运行时行为。frontend Pod 无法再与backend Pod 通信,通过运行与之前相同的wget命令观察到这一点。网络调用在一秒后超时,由 CLI 选项--timeout定义:

$ kubectl exec frontend -it -n g04 -- /bin/sh
/ # wget --spider --timeout=1 10.0.0.43:3000
Connecting to 10.0.0.43:3000 (10.0.0.43:3000)
wget: download timed out
/ # exit

此外,运行在不同命名空间中的 Pod 也无法再连接到backend Pod。以下wget命令从运行在default命名空间中的other Pod 到backend Pod 的 IP 地址的调用:

$ kubectl exec other -it -- /bin/sh
/ # wget --spider --timeout=1 10.0.0.43:3000
Connecting to 10.0.0.43:3000 (10.0.0.43:3000)
wget: download timed out

此调用也超时。

允许细粒度入站流量

网络策略是可加的。要为网络通信授予更多权限,只需创建另一个具有更精细规则的网络策略。例如,我们想要仅允许来自同一命名空间中的frontend Pod 的入口流量访问backend Pod。独立于它们运行的命名空间,应拒绝所有其他 Pod 的入口流量。

网络策略大量使用标签选择来定义规则。识别g04命名空间中的标签及其运行在同一命名空间中的 Pod 对象,以便在网络策略中使用它们:

$ kubectl get ns g04 --show-labels
NAME   STATUS   AGE   LABELS
g04    Active   12m   app=orion,kubernetes.io/metadata.name=g04
$ kubectl get pods -n g04 --show-labels
NAME       READY   STATUS    RESTARTS   AGE     LABELS
backend    1/1     Running   0          9m46s   tier=backend
frontend   1/1     Running   0          9m46s   tier=frontend

g04命名空间的标签分配包括键值对app=orionbackend Pod 的标签集包括键值对tier=backend,而frontend Pod 则包括键值对tier=frontend

创建一个新的网络策略,允许frontend Pod 只在端口 3000 上与backend Pod 通信。不允许其他任何通信。在示例 2-3 中,YAML 清单显示了完整的网络策略定义。

示例 2-3. 允许入口流量的网络策略
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-ingress
  namespace: g04
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          app: orion
      podSelector:
        matchLabels:
          tier: frontend
    ports:
    - protocol: TCP
      port: 3000

网络策略的定义已存储在文件backend-ingress-network-policy.yaml中。从文件创建对象:

$ kubectl apply -f backend-ingress-network-policy.yaml
networkpolicy.networking.k8s.io/backend-ingress created

frontend Pod 现在可以与backend Pod 通信:

$ kubectl exec frontend -it -n g04 -- /bin/sh
/ # wget --spider --timeout=1 10.0.0.43:3000
Connecting to 10.0.0.43:3000 (10.0.0.43:3000)
remote file exists
/ # exit

仍在g04命名空间之外运行的 Pod 无法连接到backend Pod。wget命令超时:

$ kubectl exec other -it -- /bin/sh
/ # wget --spider --timeout=1 10.0.0.43:3000
Connecting to 10.0.0.43:3000 (10.0.0.43:3000)
wget: download timed out

应用 Kubernetes 组件安全最佳实践

管理本地 Kubernetes 集群可以完全控制应用于集群组件(如 API 服务器、etcd、kubelet 等)的配置选项。当创建集群节点时,使用kubeadm的默认配置设置通常并不罕见。其中一些默认设置可能会暴露集群组件,使其面临不必要的攻击机会。

加固集群安全措施是任何寻求最小化攻击向量的 Kubernetes 管理员的关键活动。如果你了解最佳实践,可以手动执行此活动,或者使用自动化过程。

互联网安全中心(CIS)是一个非营利性组织,发布网络安全最佳实践。其中包括他们的最佳实践之一是 Kubernetes CIS Benchmark,这是针对 Kubernetes 环境的最佳实践目录。您将在他们的网页上找到针对集群组件推荐的详细安全设置列表。

云提供商 Kubernetes 环境的 CIS 基准测试

Kubernetes CIS Benchmark 针对自管理的 Kubernetes 安装。云提供商的 Kubernetes 环境(例如 Amazon Elastic Kubernetes Service (EKS) 和 Google Kubernetes Engine (GKE))提供了由其自己的命令行工具支持的托管控制平面。因此,Kubernetes CIS Benchmark 提出的安全建议可能不太适用。一些工具,如下讨论的 kube-bench,专门为云提供商提供验证检查。

使用 kube-bench

使用工具kube-bench可以自动化地检查 Kubernetes 集群组件是否符合 CIS 基准最佳实践。kube-bench 可以通过多种方式执行。例如,您可以将其安装为平台特定的二进制文件,如 RPM 或 Debian 文件。直接在 Kubernetes 集群上的 Pod 中运行 kube-bench 是最方便和直接的验证过程。为此,使用工具的 GitHub 仓库中提供的 YAML 文件创建一个 Job 对象。

首先,根据需求从文件 job-master.yamljob-node.yaml 创建 Job,以检查控制平面节点或工作节点。以下命令对控制平面节点执行验证检查:

$ kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/\
main/job-master.yaml
job.batch/kube-bench-master created

在 Job 执行期间,可以通过其名称在 default 命名空间中标识运行验证过程的 Pod。Pod 的名称以 kube-bench 前缀开始,然后附加节点类型及末尾的哈希。以下输出使用名为 kube-bench-master-8f6qh 的 Pod:

$ kubectl get pods
NAME                      READY   STATUS      RESTARTS   AGE
kube-bench-master-8f6qh   0/1     Completed   0          45s

等待 Pod 转换为“已完成”状态,以确保所有验证检查已完成。您可以通过转储 Pod 的日志查看基准结果:

$ kubectl logs kube-bench-master-8f6qh

有时,将验证结果写入文件可能更为方便。您可以将 kubectl logs 命令的输出重定向到文件,例如使用命令 kubectl logs kube-bench-master-8f6qh > control-plane-kube-bench-results.txt

kube-bench 验证结果

生成的验证结果可能会很长,但包括以下关键元素:被检查节点的类型,被检查的组件,通过检查的列表,未通过检查的列表,警告列表和高级别摘要:

[INFO] 1 Control Plane Security Configuration ![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/1.png)
[INFO] 1.1 Control Plane Node Configuration Files
[PASS] 1.1.1 Ensure that the API server pod specification file permissions are \
set to 644 or more restrictive (Automated) ![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/2.png)
...
[INFO] 1.2 API Server
[WARN] 1.2.1 Ensure that the --anonymous-auth argument is set to false \
(Manual) ![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/3.png)
...
[FAIL] 1.2.6 Ensure that the --kubelet-certificate-authority argument is set \
as appropriate (Automated) ![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/4.png)

== Remediations master ==
...
1.2.1 Edit the API server pod specification file /etc/kubernetes/manifests/ \
kube-apiserver.yaml on the control plane node and set the below parameter.
--anonymous-auth=false
...
1.2.6 Follow the Kubernetes documentation and setup the TLS connection between ![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png)
the apiserver and kubelets. Then, edit the API server pod specification file ![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png)
/etc/kubernetes/manifests/kube-apiserver.yaml on the control plane node and \ ![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png)
set the --kubelet-certificate-authority parameter to the path to the cert \ ![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png)
file for the certificate authority. ![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png)
--kubelet-certificate-authority=<ca-string> ![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/5.png)

...
== Summary total == ![6](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/6.png)
42 checks PASS
9 checks FAIL
11 checks WARN
0 checks INFO

1

在本例中,被检查的节点是控制平面节点。

2

一个通过的检查。这里是 API 服务器配置文件的文件权限。

3

提示消息,提示您手动检查提供给 API 服务器可执行文件的参数值。

4

一个失败的检查。例如,应为 API 服务器可执行文件设置标志--kubelet-certificate-authority

5

修复问题所需的纠正操作。失败或警告的编号,例如 1.2.1,对应于纠正操作分配的编号。

6

所有通过和失败检查的摘要,以及警告和信息消息。

修复检测到的安全问题

最初,报告的警告和失败检查列表可能会让人有些不知所措。请记住,您不必一次性修复所有问题。一些检查只是指南或提示,用于验证配置的分配值。以下步骤将引导您完成消除警告消息的过程。

控制平面组件的配置文件可以在控制平面节点的主机系统上的目录/etc/kubernetes/manifests中找到。假设您想修复由 kube-bench 报告的警告 1.2.12:

[INFO] 1.2 API Server
...
[WARN] 1.2.12 Ensure that the admission control plugin AlwaysPullImages is \
set (Manual)

== Remediations master ==
...
1.2.12 Edit the API server pod specification file /etc/kubernetes/manifests/ \
kube-apiserver.yaml
on the control plane node and set the --enable-admission-plugins parameter \
to include AlwaysPullImages.
--enable-admission-plugins=...,AlwaysPullImages,...

如纠正操作所建议的,您应编辑 API 服务器的配置文件并将值AlwaysPullImages添加到准入插件列表中。继续编辑文件kube-apiserver.yaml

$ sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml

在参数--enable-admission-plugins中添加值AlwaysPullImages后,结果可能如下所示:

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
    - --advertise-address=192.168.56.10
    - --allow-privileged=true
    - --authorization-mode=Node,RBAC
    - --client-ca-file=/etc/kubernetes/pki/ca.crt
    - --enable-admission-plugins=NodeRestriction,AlwaysPullImages
...

将更改保存到文件。在kube-system命名空间中运行 API 服务器的 Pod 将自动重启。启动过程可能需要几秒钟。因此,执行以下命令可能需要一段时间才能成功:

$ kubectl get pods -n kube-system
NAME                           READY   STATUS    RESTARTS   AGE
...
kube-apiserver-control-plane   1/1     Running   0          71m
...

在验证更改结果之前,您需要删除现有的作业对象:

$ kubectl delete job kube-bench-master
job.batch "kube-bench-master" deleted

验证检查 1.2.12 现在报告通过的结果:

$ kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/\
main/job-master.yaml
job.batch/kube-bench-master created
$ kubectl get pods
NAME                      READY   STATUS      RESTARTS   AGE
kube-bench-master-5gjdn   0/1     Completed   0          10s
$ kubectl logs kube-bench-master-5gjdn | grep 1.2.12
[PASS] 1.2.12 Ensure that the admission control plugin AlwaysPullImages is \
set (Manual)

创建带有 TLS 终止的 Ingress

Ingress 会根据匹配的 URL 上下文路径,将来自集群外部的 HTTP 和/或 HTTPS 流量路由到一个或多个服务。您可以在图 2-2 中看到其功能。

ckss 0202

图 2-2. 通过 HTTP(S)管理对服务的外部访问

Ingress 已配置为接受来自集群外部的 HTTP 和 HTTPS 流量。如果调用方提供上下文路径/app,则流量路由到服务 1。如果调用方提供上下文路径/api,则流量路由到服务 2。重要的是指出,一旦流过 Ingress,通信通常使用未加密的 HTTP 网络通信。

考虑到 Ingress API 资源是 CKAD 和 CKA 考试的一部分,我们不再在这里讨论基础知识。有关详细讨论,请参阅Certified Kubernetes Administrator (CKA) Study GuideKubernetes 文档中的信息。

Ingress 控制器的角色

请记住,没有 Ingress 控制器,Ingress 无法工作。Ingress 控制器评估由 Ingress 定义的规则集,确定流量路由。一个生产级别的 Ingress 控制器示例是F5 NGINX Ingress ControllerAKS Application Gateway Ingress Controller。您可以在Kubernetes 文档中找到其他选项。如果您使用 minikube,请确保启用 Ingress 插件

CKS 的主要焦点是设置带有 TLS 终止的 Ingress 对象。配置 Ingress 以进行 HTTPS 通信可以免去在服务级别上处理网络通信安全性的麻烦。在本书的本节中,您将学习如何创建 TLS 证书和密钥,如何将证书和密钥提供给 TLS 类型的 Secret 对象,以及如何配置 Ingress 对象以支持 HTTPS 通信。

设置 Ingress 后端

在 Ingress 的上下文中,后端是服务名称和端口的组合。在创建 Ingress 之前,我们将处理服务、部署和运行 nginx 的 Pod,以便稍后演示将 HTTPS 流量路由到实际应用程序。所有这些对象都应存在于命名空间 t75 中。示例 2-4 将所有这些资源定义为单个 YAML 清单文件 setup.yaml,以便快速创建 Ingress 后端。

示例 2-4. 通过服务暴露 nginx 的 YAML 清单
apiVersion: v1
kind: Namespace
metadata:
  name: t75
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: t75
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: accounting-service
  namespace: t75
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

使用以下命令从 YAML 文件创建对象:

$ kubectl apply -f setup.yaml
namespace/t75 created
deployment.apps/nginx-deployment created
service/accounting-service created

让我们快速验证对象已正确创建,并且 Pod 已转换为“Running”状态。执行get all命令时,您应该看到一个名为 nginx-deployment 的控制三个副本的部署,以及一个类型为 ClusterIP 的名为 accounting-service 的服务:

$ kubectl get all -n t75
NAME                                    READY   STATUS    RESTARTS   AGE
pod/nginx-deployment-6595874d85-5rdrh   1/1     Running   0          108s
pod/nginx-deployment-6595874d85-jmhvh   1/1     Running   0          108s
pod/nginx-deployment-6595874d85-vtwxp   1/1     Running   0          108s

NAME                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S) \
  AGE
service/accounting-service   ClusterIP   10.97.101.228   <none>        80/TCP \
  108s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-deployment   3/3     3            3           108s

当在同一节点上的另一个 Pod 中调用服务端点时,应该从 nginx Pod 获得成功响应。这里我们使用wget命令来验证这种行为:

$ kubectl run tmp --image=busybox --restart=Never -it --rm \
  -- wget 10.97.101.228:80
Connecting to 10.97.101.228:80 (10.97.101.228:80)
saving to 'index.xhtml'
index.xhtml           100% |************|   612  0:00:00 ETA
'index.xhtml' saved
pod "tmp" deleted**********

有了这些对象并按预期运行,我们现在可以集中精力创建带有 TLS 终止的 Ingress。 ****## 创建 TLS 证书和密钥

在创建 TLS Secret 之前,我们需要生成 TLS 证书和密钥。为此,我们将使用 OpenSSL 命令。生成的文件名为 accounting.crtaccounting.key

$ openssl req -nodes -new -x509 -keyout accounting.key -out accounting.crt \
  -subj "/CN=accounting.tls"
Generating a 2048 bit RSA private key
...........................+
..........................+
writing new private key to 'accounting.key'
-----
$ ls
accounting.crt accounting.key

用于生产环境,您需要生成一个密钥文件,并使用它从证书颁发机构(CA)获取 TLS 证书。有关创建 TLS 证书和密钥的更多信息,请参阅 OpenSSL 文档

创建 TLS 类型的 Secret

创建 Secret 最简单的方法是使用命令式命令的帮助。此创建方法不需要您手动对证书和密钥值进行 base64 编码。编码在对象创建时会自动发生。以下命令使用 Secret 选项 tls 并使用选项 --cert--key 分配证书和密钥文件名:

$ kubectl create secret tls accounting-secret --cert=accounting.crt \
  --key=accounting.key -n t75
secret/accounting-secret created

示例 2-5 显示了如果要声明性地创建对象,则 TLS Secret 的 YAML 表示。

示例 2-5. 使用类型 kubernetes.io/tls 的 Secret
apiVersion: v1
kind: Secret
metadata:
  name: accounting-secret
  namespace: t75
type: kubernetes.io/tls
data:
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk...
  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk...

确保将 tls.crttls.key 属性的值分配为单行的 base64 编码值。要生成 base64 编码的值,只需将 base64 命令指向要转换内容的文件名即可。以下示例将文件 accounting.crt 的内容进行了 base64 编码:

$ base64 accounting.crt
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNyakNDQ...

创建 Ingress

您可以使用一行命令的命令式方法创建 Ingress,如以下片段所示。制定 --rule 参数的值是件难事。您可能需要参考 create ingress 命令的 --help 选项,因为它需要特定的表达式。创建 Ingress 对象与 TLS Secret 之间连接的相关信息是附加的参数 tls=accounting-secret

$ kubectl create ingress accounting-ingress \
  --rule="accounting.internal.acme.com/*=accounting-service:80, \
  tls=accounting-secret" -n t75
ingress.networking.k8s.io/accounting-ingress created

示例 2-6 显示了 Ingress 的 YAML 表示。定义 TLS 信息的属性是 spec.tls[]

示例 2-6. 定义 TLS 终止 Ingress 的 YAML 清单
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: accounting-ingress
  namespace: t75
spec:
  tls:
  - hosts:
    - accounting.internal.acme.com
    secretName: accounting-secret
  rules:
  - host: accounting.internal.acme.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: accounting-service
            port:
              number: 80

使用命令式或声明式方法创建 Ingress 对象后,您应该能够在命名空间 t75 中找到它。正如您在以下输出中看到的那样,端口 443 列在 “PORT” 列中,表示已启用 TLS 终止:

$ kubectl get ingress -n t75
NAME                 CLASS   HOSTS                          ADDRESS       \
  PORTS     AGE
accounting-ingress   nginx   accounting.internal.acme.com   192.168.64.91 \
  80, 443   55s

描述 Ingress 对象显示,后端可以映射到路径 / 并通过名为 accounting-service 的 Service 将流量路由到 Pod:

$ kubectl describe ingress accounting-ingress -n t75
Name:             accounting-ingress
Labels:           <none>
Namespace:        t75
Address:          192.168.64.91
Ingress Class:    nginx
Default backend:  <default>
TLS:
  accounting-secret terminates accounting.internal.acme.com
Rules:
  Host                          Path  Backends
  ----                          ----  --------
  accounting.internal.acme.com
                                /   accounting-service:80 \
                                (172.17.0.5:80,172.17.0.6:80,172.17.0.7:80)
Annotations:                    <none>
Events:
  Type    Reason  Age               From                      Message
  ----    ------  ----              ----                      -------
  Normal  Sync    1s (x2 over 31s)  nginx-ingress-controller  Scheduled for sync

调用 Ingress

要在您的本地 Kubernetes 集群上测试行为,您需要首先找出节点的 IP 地址。以下命令在 minikube 环境中显示 IP 地址:

$ kubectl get nodes -o wide
NAME       STATUS   ROLES           AGE     VERSION   INTERNAL-IP   \
  EXTERNAL-IP   OS-IMAGE               KERNEL-VERSION   CONTAINER-RUNTIME
minikube   Ready    control-plane   3d19h   v1.24.1   192.168.64.91 \
  <none>        Buildroot 2021.02.12   5.10.57          docker://20.10.16

接下来,您需要将 IP 地址添加到主机名映射到您的 /etc/hosts 文件中:

$ sudo vim /etc/hosts
...
192.168.64.91   accounting.internal.acme.com

您现在可以使用分配的域名发送 HTTPS 请求到 Ingress,并收到 HTTP 响应码 200:

$ wget -O- https://accounting.internal.acme.com --no-check-certificate
--2022-07-28 15:32:43--  https://accounting.internal.acme.com/
Resolving accounting.internal.acme.com (accounting.internal.acme.com)... \
192.168.64.91
Connecting to accounting.internal.acme.com (accounting.internal.acme.com) \
|192.168.64.91|:443... connected.
WARNING: cannot verify accounting.internal.acme.com's certificate, issued \
by ‘CN=Kubernetes Ingress Controller Fake Certificate,O=Acme Co’:
  Self-signed certificate encountered.
WARNING: no certificate subject alternative name matches
	requested host name ‘accounting.internal.acme.com’.
HTTP request sent, awaiting response... 200 OK
```****  ****# 保护节点元数据和端点

Kubernetes 集群公开用于与集群组件通信的端口。例如,API 服务器默认使用端口 6443,以便客户端(如 `kubectl`)在执行命令时可以与其通信。

Kubernetes 文档列出了这些端口在 [“端口和协议”](https://oreil.ly/iN993) 中。以下两个表格显示了每个节点的默认端口分配。

表 2-1 显示了集群节点上的默认入站端口。

表 2-1\. 入站控制平面节点端口

| 端口范围 | 用途 |
| --- | --- |
| 6643 | Kubernetes API 服务器 |
| 2379–2380 | etcd 服务器客户端 API |
| 10250 | Kubelet API |
| 10259 | kube-scheduler |
| 10257 | kube-controller-manager |

许多这些端口是可配置的。例如,您可以通过在配置文件 `/etc/kubernetes/manifests/kube-apiserver.yaml` 中使用 `--secure-port` 标志提供不同的值来修改 API 服务器端口,如在集群组件的 [文档](https://oreil.ly/TTzAz) 中所述。有关所有其他集群组件,请参阅它们各自的文档。

表 2-2 列出了工作节点上的默认入站端口。

表 2-2\. 入站工作节点端口

| 端口范围 | 用途 |
| --- | --- |
| 10250 | Kubelet API |
| 30000–32767 | NodePort 服务 |

要保护集群组件使用的端口,设置防火墙规则以减少攻击面。例如,您可以决定不将 API 服务器暴露给企业内部以外的任何人。只有在 VPN 登录时,使用 `kubectl` 的客户端才能对 Kubernetes 集群运行命令,从而使集群更不容易受到攻击。

云提供商 Kubernetes 集群(例如 AWS、Azure 或 Google Cloud)公开了所谓的元数据服务。元数据服务是可以提供敏感数据(如认证令牌)的 API,供 VM 或 Pod 在不需要额外授权的情况下消费。在 CKS 考试中,您需要了解这些节点端点和云提供商元数据服务。此外,您应该对如何防止未经授权访问它们有高层次的理解。

## 场景:被攻击的 Pod 可以访问元数据服务器

图 2-3 显示了在云提供商 Kubernetes 集群中一个节点上运行的 Pod 的攻击者。

![ckss 0203](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0203.png)

###### 图 2-3\. 攻击者已经获得对 Pod 的访问权限,因此可以访问元数据服务器。

对元数据服务器的访问未受到任何限制。攻击者可以检索敏感信息,这可能会打开其他入侵可能性。

## 使用网络策略保护元数据服务器访问

让我们选择一个暴露元数据端点的云提供商。在 AWS 中,可以使用 IP 地址 169.254.169.254 访问元数据服务器,详见 [AWS 文档](https://oreil.ly/6DsIx)。所暴露的端点可以提供访问 EC2 实例元数据的权限。例如,您可以获取实例的本地 IP 地址以便通过脚本管理外部应用程序的连接或与实例联系。参见相应的 [文档页面](https://oreil.ly/Bwdej),了解使用 curl 命令行工具调用这些端点的详细信息。

要防止命名空间中的任何 Pod 访问元数据服务器的 IP 地址,需设置一个网络策略,允许所有 IP 地址的出站流量,除了 169.254.169.254\. 示例 2-7 展示了带有此规则集的 YAML 清单。

##### 示例 2-7\. 默认拒绝向 IP 地址 169.254.169.254 的出站网络策略

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress-metadata-server
namespace: a12
spec:
podSelector: {}
policyTypes:

  • Egress
    egress:
  • to:
    • ipBlock:
      cidr: 0.0.0.0/0
      except:
      • 169.254.169.254/32

创建网络策略后,命名空间 `a12` 中的 Pod 不应再能访问元数据端点。有关使用 curl 访问端点的详细示例,请参阅相关的 [AWS 文档](https://oreil.ly/fQ07b)。

# 保护 GUI 元素

`kubectl` 工具并非管理集群的唯一用户界面(UI)。尽管 `kubectl` 允许进行精细操作,但大多数组织更喜欢使用更便捷的图形用户界面(GUI)来管理集群中的对象。您可以从多种选择中进行选择。[Kubernetes 仪表板](https://oreil.ly/ABDQo) 是一个免费的基于 Web 的应用程序。其他用于 Kubernetes 的 GUI 仪表板如 [Portainer](https://oreil.ly/i_FJv) 则通过添加事件追踪或硬件资源消耗可视化等功能,扩展了基本功能。在本节中,我们将重点介绍 Kubernetes 仪表板,因为它易于安装和配置。

## 情景:攻击者获得了对仪表板功能的访问权限。

Kubernetes 仪表板作为集群内的一个 Pod 运行。安装仪表板还会创建一个 `ClusterIP` 类型的 Service,只允许从集群内部访问该端点。要使仪表板对最终用户可访问,必须将该 Service 暴露到集群外部。例如,可以切换到 `NodePort` Service 类型或者部署一个 Ingress。图 2-4 展示了部署和访问仪表板的高级架构。

![ckss 0204](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0204.png)

###### 图 2-4\. 成功访问仪表板的攻击者

一旦将仪表板暴露给外部世界,攻击者可能会获取访问权限。如果没有正确的安全设置,对象可以被删除、修改或用于恶意目的。这种攻击的最著名受害者是特斯拉,在 2018 年成为黑客攻击的牺牲品,黑客们成功访问了其未受保护的仪表板以进行加密货币挖掘。从那时起,仪表板的新版本更改了默认设置,以使其从一开始就更安全。

## 安装 Kubernetes 仪表板

安装 Kubernetes 仪表板非常简单。您可以使用项目 GitHub 存储库中提供的 YAML 清单创建相关对象。以下命令安装所有必要的对象:

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/
v2.6.0/aio/deploy/recommended.yaml


# 渲染仪表板中的度量数据

如果您有兴趣检查仪表板功能中的资源消耗度量标准,您可能还希望安装[度量服务器](https://oreil.ly/3Rtkl)。

您可以在`kubernetes-dashboard`命名空间中找到清单创建的对象。其中包括 Deployments、Pods 和 Services。以下命令列出所有这些对象:

$ kubectl get deployments,pods,services -n kubernetes-dashboard
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/dashboard-metrics-scraper 1/1 1 1 11m
deployment.apps/kubernetes-dashboard 1/1 1 1 11m

NAME READY STATUS RESTARTS AGE
pod/dashboard-metrics-scraper-78dbd9dbf5-f8z4x 1/1 Running 0 11m
pod/kubernetes-dashboard-5fd5574d9f-ns7nl 1/1 Running 0 11m

NAME TYPE CLUSTER-IP EXTERNAL-IP
PORT(S) AGE
service/dashboard-metrics-scraper ClusterIP 10.98.6.37
8000/TCP 11m
service/kubernetes-dashboard ClusterIP 10.102.234.158
80/TCP 11m


## 访问 Kubernetes 仪表板

`kubectl proxy`命令可帮助临时创建代理,允许您在浏览器中打开仪表板。此功能仅用于故障排除目的,不适用于生产环境。您可以在[文档](https://oreil.ly/gGsqX)中找到有关`proxy`命令的信息:

$ kubectl proxy
Starting to serve on 127.0.0.1:8001


使用[*http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy*](http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy)网址在浏览器中打开。仪表板将要求您提供身份验证方法和凭据。配置仪表板的推荐方式是通过承载令牌。

## 创建具有管理权限的用户

在登录屏幕中进行身份验证之前,您需要创建一个 ServiceAccount 和 ClusterRoleBinding 对象,授予管理员权限。首先创建名为`admin-user-serviceaccount.yaml`的文件,并填充其中显示的内容,该内容如示例 2-8 所示。

##### 示例 2-8\. 管理权限的服务账户

apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kubernetes-dashboard


然后,将示例 2-9 的内容存储到名为`admin-user-⁠clusterrole​bind⁠ing.yaml`的文件中,以将 ClusterRole 命名为`cluster-admin`映射到 ServiceAccount。

##### 示例 2-9\. 用于管理员权限的 ClusterRoleBinding

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:

  • kind: ServiceAccount
    name: admin-user
    namespace: kubernetes-dashboard

使用以下声明性命令创建这两个对象:

$ kubectl create -f admin-user-serviceaccount.yaml
serviceaccount/admin-user created
$ kubectl create -f admin-user-clusterrolebinding.yaml
clusterrolebinding.rbac.authorization.k8s.io/admin-user created


您现在可以使用以下命令为管理员用户创建承载令牌。该命令将为提供的 ServiceAccount 对象生成一个令牌,并在控制台上呈现它:

$ kubectl create token admin-user -n kubernetes-dashboard
eyJhbGciOiJSUzI1NiIsImtpZCI6...


# 服务账户令牌的过期时间

默认情况下,此令牌将在 24 小时后过期。这意味着一旦“生存时间”(TTL)过去,令牌对象将自动删除。您可以通过提供命令行选项 `--ttl` 来更改令牌的 TTL。例如,`40h` 的值将在 40 小时后使令牌过期。值 `0` 表示令牌永不过期。

将命令的输出复制并粘贴到登录屏幕的“输入令牌”字段中,如 Figure 2-5 所示。

![ckss 0205](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0205.png)

###### 图 2-5\. 仪表板登录界面中令牌的使用

单击“登录”按钮将带您进入 Figure 2-6 中显示的仪表板。

![ckss 0206](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0206.png)

###### 图 2-6\. 特定命名空间中 Pods 的仪表板视图

您现在可以管理端用户和集群对象而无需任何限制。

## 创建具有受限权限的用户

在上一节中,您学习了如何创建具有全局集群管理权限的用户。但是,仪表板的大多数用户可能只需要一组受限权限。例如,实施和操作云原生应用程序的开发人员可能只需要在 Kubernetes 集群上执行其任务所需的部分管理权限。为仪表板创建具有受限权限的用户包括以下三个步骤:

1.  创建一个 ServiceAccount 对象。

1.  创建定义权限的 ClusterRole 对象。

1.  创建将 ClusterRole 映射到 ServiceAccount 的 ClusterRoleBinding。

如您所见,该流程与我们为管理员用户所经历的流程非常相似。第 2 步是新的,因为我们需要明确我们要授予哪些权限。接下来的 YAML 清单将模拟作为开发人员工作的用户,该用户仅允许只读权限(例如获取、列出和观察资源)。

首先创建文件 `restricted-user-serviceaccount.yaml`,并填入示例 Example 2-10 中显示的内容。

##### 示例 2-10\. 用于受限权限的 ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
name: developer-user
namespace: kubernetes-dashboard


Example 2-11 中的 ClusterRole 仅允许获取、列出和观察资源。不允许执行其他任何操作。将内容存储在文件 `restricted-user-clusterrole.yaml` 中。

##### 示例 2-11\. 用于受限权限的 ClusterRole

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
name: cluster-developer
rules:

  • apiGroups:
    • '*'
      resources:
    • '*'
      verbs:
    • get
    • list
    • watch
  • nonResourceURLs:
    • '*'
      verbs:
    • get
    • list
    • watch

最后,将 ServiceAccount 映射到文件 `restricted-user-clusterrolebinding.yaml` 中的 ClusterRole,如 Example 2-12 所示。

##### 示例 2-12\. 用于受限权限的 ClusterRoleBinding

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: developer-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-developer
subjects:

  • kind: ServiceAccount
    name: developer-user
    namespace: kubernetes-dashboard

使用以下声明性命令创建所有对象:

$ kubectl create -f restricted-user-serviceaccount.yaml
serviceaccount/restricted-user created
$ kubectl create -f restricted-user-clusterrole.yaml
clusterrole.rbac.authorization.k8s.io/cluster-developer created
$ kubectl create -f restricted-user-clusterrolebinding.yaml
clusterrolebinding.rbac.authorization.k8s.io/developer-user created


使用以下命令生成具有受限用户的 Bearer 令牌:

$ kubectl create token developer-user -n kubernetes-dashboard
eyJhbGciOiJSUzI1NiIsImtpZCI6...


GUI 中,对于当前登录用户不允许的操作将不会显示为禁用选项。你仍然可以选择该选项,但会显示错误消息。图 2-7 展示了如果尝试通过没有权限执行操作的用户删除 Pod 时,Dashboard 的行为。

![ckss 0207](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0207.png)

###### 图 2-7\. 尝试调用允许操作时显示的错误消息

## 避免不安全的配置参数

在生产环境中保护 Dashboard 包括使用[执行参数](https://oreil.ly/gS1hE),用于正确配置身份验证和授权。默认情况下,启用登录功能,并且 HTTPS 端点将在 8443 端口上公开。如果不希望自动生成 TLS 证书,可以使用`--tls-cert-file`和`--tls-cert-key`命令行选项提供 TLS 证书。

避免设置命令行参数`--insecure-port`来公开 HTTP 端点和`--enable-insecure-login`来启用以 HTTP 而非 HTTPS 提供登录页面。此外,请确保*不要*使用`--enable-skip-login`选项,因为这将允许通过在登录屏幕上单击跳过按钮来规避认证方法。

# 验证 Kubernetes 平台的二进制文件

Kubernetes 项目会在每个发布中发布客户端和服务器二进制文件。客户端二进制文件指的是可执行的`kubectl`。服务器二进制文件包括`kubeadm`,以及 API 服务器、调度器和 kubelet 的可执行文件。你可以在[Kubernetes GitHub 仓库的“tags”部分](https://oreil.ly/vHpAV)或发布页面[*https://dl.k8s.io*](https://dl.k8s.io)找到这些文件。

## 场景:攻击者向二进制文件注入恶意代码

可执行文件`kubectl`和`kubeadm`对于与 Kubernetes 交互至关重要。`kubectl`允许你针对 API 服务器运行命令,例如管理对象。`kubeadm`在将集群节点从一个版本升级到另一个版本时是必需的。假设你正在[升级集群版本的过程中](https://oreil.ly/hTJ57),从 1.23 版本升级到 1.24 版本。作为过程的一部分,你将需要升级`kubeadm`二进制文件。官方升级文档详细说明了升级二进制文件所需的命令。

假设攻击者成功修改了版本为 1.24 的`kubeadm`可执行文件,并诱使你以为需要从放置恶意二进制文件的位置下载该二进制文件。正如图 2-8 所示,每次调用修改后的`kubeadm`可执行文件都会暴露你于运行恶意代码的风险。例如,你可能会向集群外的服务器发送凭据,这会为入侵你的 Kubernetes 环境打开新的途径。

![ckss 0208](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0208.png)

###### 图 2-8\. 一个将恶意代码注入二进制文件的攻击者

## 验证二进制文件与哈希值匹配

您可以借助 MD5 或 SHA 等哈希码验证二进制文件的有效性。Kubernetes 为每个二进制文件发布 SHA256 哈希码。在首次使用之前,应对各个二进制文件进行哈希验证。如果生成的哈希码与您下载的哈希码不匹配,则表示二进制文件存在问题。可能是第三方修改了二进制文件,或者您未正确使用特定类型或版本的哈希码。

您可以从[*https://dl.k8s.io*](https://dl.k8s.io)下载与二进制文件对应的哈希码。哈希码的完整 URL 反映了二进制文件的版本、操作系统和架构信息。以下列表展示了适用于 Linux AMD64 平台二进制文件的示例 URL:

+   `kubectl`: [*https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubectl.sha256*](https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubectl.sha256)

+   `kubeadm`: [*https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubeadm.sha256*](https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubeadm.sha256)

+   `kubelet`: [*https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubelet.sha256*](https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubelet.sha256)

+   `kube-apiserver`: [*https://dl.k8s.io/v1.26.1/bin/linux/amd64/kube-apiserver.sha256*](https://dl.k8s.io/v1.26.1/bin/linux/amd64/kube-apiserver.sha256)

您需要使用特定于操作系统的哈希码验证工具来检查二进制文件的有效性。如果您的机器上还没有该工具,可能需要先安装它。下面的命令展示了在不同操作系统上使用工具的示例,详细说明请参考[Kubernetes 文档](https://oreil.ly/2FmVm):

+   Linux: `echo "$(cat kubectl.sha256) kubectl" | sha256sum --check`

+   MacOSX: `echo "$(cat kubectl.sha256) kubectl" | shasum -a 256 --check`

+   Windows with Powershell: `$($(CertUtil -hashfile .\kubectl.exe SHA256)[1] -replace " ", "") -eq $(type .\kubectl.exe.sha256)`

以下命令演示如何下载版本为 1.26.1 的`kubeadm`二进制文件及其对应的 SHA256 哈希文件:

$ curl -LO "https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubeadm"
$ curl -LO "https://dl.k8s.io/v1.26.1/bin/linux/amd64/kubeadm.sha256"


验证工具`shasum`可用于确认校验和是否匹配:

$ echo "$(cat kubeadm.sha256) kubeadm" | shasum -a 256 --check
kubeadm: OK


前一个命令返回“OK”消息。二进制文件未被篡改。任何其他消息都可能表示在执行二进制文件时存在潜在的安全风险。

# 总结

“集群设置” 领域关注于设置 Kubernetes 集群相关的安全方面。即使您可能正在使用 `kubeadm` 从头开始创建集群,这并不意味着您一定在遵循最佳实践。使用 kube-bench 检测潜在的安全风险是一个很好的起点。逐个修复工具报告的问题。您还可以检查客户端和服务器二进制文件的校验和,以确保它们未被攻击者修改。一些组织使用仪表板来管理集群及其对象。确保仪表板的身份验证和授权限制访问的小部分利益相关者。

网络通信是一个重要的安全方面。默认情况下,Pod 之间的通信是不受限制的。仔细查看运行在 Kubernetes 内的应用架构。只允许符合架构要求的 Pod 之间的单向网络流量。拒绝所有其他网络流量。在将应用程序暴露到集群外部时,请确保已配置带有 TLS 终止的 Ingress 对象。这将确保数据双向加密,使攻击者无法观察客户端和 Kubernetes 集群之间发送的密码等敏感信息。

# 考试要点

理解网络策略的目的和影响

默认情况下,Pod 之间的通信是无限制的。使用最小特权原则实例化默认拒绝规则以限制 Pod 之间的网络流量。网络策略的 `spec.podSelector` 属性根据标签选择选择目标 Pod 应用规则。入站和出站规则定义了允许进出流量的 Pod、命名空间、IP 地址和端口。网络策略可以进行聚合。默认拒绝规则可能禁止入站和/或出站流量。可以使用更精细的定义打开这些规则的其他网络策略。

练习使用 kube-bench 检测集群组件的漏洞

Kubernetes CIS 基准是在生产 Kubernetes 环境中推荐的安全设置的最佳实践集合。您可以利用工具 kube-bench 自动化检测安全风险的过程。运行 kube-bench 生成的报告描述了修复检测到问题所需的详细补救措施。学习如何解释结果以及如何缓解问题。

知道如何配置具有 TLS 终止的 Ingress

可以通过暴露 HTTPS 端点来配置 Ingress 发送和接收加密数据。为此,您需要创建一个 TLS Secret 对象,并为其分配一个 TLS 证书和密钥。然后,可以通过 Ingress 使用属性 `spec.tls[]` 消费这个 Secret。

知道如何为安全访问配置 GUI 元素

GUI 元素,例如 Kubernetes 仪表盘,提供了管理对象的便捷方式。如果应用程序没有受到未经授权访问的保护,攻击者可能会对您的集群造成危害。在考试中,您需要知道如何为特定利益相关者正确设置 RBAC。此外,您还应该大致了解与安全相关的命令行参数。练习安装仪表盘的过程,学习如何调整其命令行参数,并了解为不同用户设置权限的影响。

知道如何检测修改过的平台二进制文件

可以根据其相应的哈希码验证平台二进制文件,如 `kubectl` 和 `kubeadm`。了解如何查找哈希文件以及如何使用验证工具来识别二进制文件是否已被篡改。

# 示例练习

这些练习的解决方案可以在 附录 中找到。

1.  创建一个网络策略,禁止出口流量到集群外的任何域。网络策略适用于具有标签 `app=backend` 的 Pod,并且还允许 UDP 和 TCP 的端口 53 的出口流量到任何其他命名空间中的 Pod。

1.  创建一个名为 `allowed` 的 Pod,在端口 80 上运行 `busybox:1.36.0` 镜像,并为其分配标签 `app=frontend`。对 `http://google.com` 进行 `curl` 调用。网络调用应该被允许,因为网络策略不适用于该 Pod。

1.  创建另一个名为 `denied` 的 Pod,在端口 80 上运行 `busybox:1.36.0` 镜像,并为其分配标签 `app=backend`。对 `http://google.com` 进行 `curl` 调用。网络调用应该被阻止。

1.  安装 Kubernetes 仪表盘或确保已经安装。在命名空间 `kubernetes-dashboard` 中,创建一个名为 `observer-user` 的 ServiceAccount。此外,创建相应的 ClusterRole 和 ClusterRoleBinding。ServiceAccount 应只被允许查看部署(Deployments)。所有其他操作应被拒绝。例如,使用以下命令在 `default` 命名空间中创建名为 `deploy` 的部署:`kubectl create deployment deploy --image=nginx --replicas=3`。

1.  为名为 `observer-user` 的 ServiceAccount 创建一个永不过期的令牌。使用该令牌登录仪表盘。确保仅可以查看部署(Deployments),而不能查看任何其他类型的资源。

1.  在 Linux AMD64 上下载 API 服务器版本 1.26.1 的二进制文件。下载 API 服务器可执行文件版本 1.23.1 的 SH256 校验和文件。运行特定于操作系统的验证工具,并观察结果。


# 第三章:集群加固

“集群加固”领域涉及一些在集群初始化设置和配置后保持尽可能安全的重要主题。作为本章讨论的一部分,您可能会注意到,我将引用通常由 Kubernetes 管理员负责的概念和实践。在适当的情况下,我将提供链接到已涵盖 CKA 考试的主题。

在高层次上,本章涵盖以下概念:

+   限制对 Kubernetes API 的访问

+   配置基于角色的访问控制(RBAC)以最小化暴露

+   在使用服务账户时要谨慎

+   经常更新 Kubernetes

# 与 Kubernetes API 交互

API 服务器是访问 Kubernetes 集群的网关。任何人类用户、客户端(例如`kubectl`)、集群组件或服务账户都将通过 HTTPS 进行 RESTful API 调用访问 API 服务器。这是执行操作(如创建 Pod 或删除 Service)的*中心点*。

在本节中,我们将专注于与 API 服务器相关的安全特定方面。关于 API 服务器的内部工作方式以及 Kubernetes API 的使用详细讨论,请参考 Brendan Burns 和 Craig Tracey(O'Reilly)的书籍[*管理 Kubernetes*](https://learning.oreilly.com/library/view/managing-kubernetes/9781492033905)。

## 处理请求

图 3-1 说明了向 API 服务器发出调用时请求经历的阶段。有关更多信息,请参阅[Kubernetes 文档](https://oreil.ly/DuLdf)。

![ckss 0301](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0301.png)

###### 图 3-1\. API 服务器请求处理

请求处理的第一个阶段是*身份验证*。通过检查客户端证书或令牌验证来验证调用者的身份。如果令牌与服务账户关联,则将在此处验证。

第二阶段确定了第一阶段提供的身份是否可以访问动词和 HTTP 路径请求。因此,第二阶段处理请求的*授权*,使用标准的 Kubernetes RBAC 模型实现。在这里,我们要确保服务账户被允许列出 Pods 或者根据请求创建新的 Service 对象。

请求处理的第三阶段涉及*准入控制*。准入控制验证请求是否格式正确,并在处理请求之前可能需要进行修改。例如,准入控制策略可以确保创建 Pod 的请求包含特定标签的定义。如果没有定义该标签,则请求将被拒绝。

最后一个阶段确保了请求中包含的资源是有效的。请求*验证*可以作为准入控制的一部分来实现,但不是必须的。例如,这个阶段确保了服务对象的名称遵循提供的 DNS 名称的标准 Kubernetes 命名规则。

## 连接到 API 服务器

运行以下命令很容易确定 API 服务器的端点:

$ kubectl cluster-info
Kubernetes control plane is running at https://172.28.40.5:6443
...


对于给定的 Kubernetes 集群,API 服务器已通过 URL [*https://172.28.40.5:6443*](https://172.28.40.5:6443) 暴露。此外,您还可以查看 API 服务器配置文件中的命令行选项 `--advertise-address` 和 `--secure-port` 来确定端点。您可以在 `/etc/kubernetes/manifests/kube-apiserver.yaml` 找到 API 服务器配置文件。

# 配置 API 服务器的不安全端口

可以配置 API 服务器使用不安全端口(例如,80)已在 Kubernetes 1.10 版本中弃用。在版本 1.24 中,不安全端口标志 `--port` 和 `--insecure-port` 已完全删除,因此不能再用于配置 API 服务器。有关更多信息,请参阅[发布说明](https://oreil.ly/OTsmV)。

### 使用 Kubernetes 服务

Kubernetes 使得特定用例更方便访问 API 服务器。例如,您可能希望从 Pod 发送请求到 Kubernetes API。而不是使用 API 服务器的 IP 地址和端口,您可以简单地引用名为 `kubernetes.default.svc` 的服务。这个特殊的服务位于 `default` 命名空间中,并由集群自动启动。删除服务将自动重新创建它。您可以通过以下命令轻松找到该服务:

$ kubectl get service kubernetes
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 443/TCP 32s


检查此服务的端点时,您将看到它指向 API 服务器的 IP 地址和端口,如通过执行以下命令所示:

$ kubectl get endpoints kubernetes
NAME ENDPOINTS AGE
kubernetes 172.28.40.5:6443 4m3s


服务的 IP 地址和端口也通过环境变量暴露给 Pod。您可以从容器内运行的程序中读取环境变量的值。服务的 IP 地址由环境变量 `KUBERNETES_SERVICE_HOST` 反映。端口可以使用环境变量 `KUBERNETES_SERVICE_PORT` 访问。要渲染环境,请简单地使用 `env` 命令在临时 Pod 中访问环境变量:

$ kubectl run kubernetes-envs --image=alpine:3.16.2 -it --rm --restart=Never
-- env
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443


我们将在 “最小化服务账户权限” 部分中使用 `kubernetes` 服务。

### 匿名访问

以下命令使用 `curl` 命令行工具匿名调用 API 列出所有命名空间。选项 `-k` 避免验证服务器的 TLS 证书:

$ curl https://172.28.40.5:6443/api/v1/namespaces -k
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "namespaces is forbidden: User "system:anonymous" cannot list
resource "namespaces" in API group "" at the cluster scope",
"reason": "Forbidden",
"details": {
"kind": "namespaces"
},
"code": 403
}


如你从 JSON 格式的 HTTP 响应体中看到的那样,API 服务器接受匿名调用,但没有适当的操作权限。在内部,Kubernetes 将调用映射到[用户名 `system:anonymous`](https://oreil.ly/_HrbF),这实际上没有授权执行该操作。

### 使用客户端证书访问

要作为授权用户发出请求,您需要创建一个新用户或使用具有管理员权限的现有默认用户 `kubernetes-admin`。我们现在不会详细介绍创建新用户的过程。有关创建用户的更多信息,请参阅“限制用户权限”。

以下命令列出了所有可用用户,包括其客户端证书和密钥:

$ kubectl config view --raw
apiVersion: v1
clusters:

  • cluster:
    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL... 1
    server: https://172.28.132.5:6443
    name: kubernetes
    contexts:
  • context:
    cluster: kubernetes
    user: kubernetes-admin
    name: kubernetes-admin@kubernetes
    current-context: kubernetes-admin@kubernetes
    kind: Config
    preferences: {}
    users:
  • name: kubernetes-admin 2
    user:
    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL... 3
    client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktL... 4

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_cluster_hardening_CO1-1)

证书颁发机构的 base64 编码值

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_cluster_hardening_CO1-2)

默认创建的具有管理员权限的用户条目

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_cluster_hardening_CO1-3)

用户客户端证书的 base64 编码值

![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_cluster_hardening_CO1-4)

用户私钥的 base64 编码值

要使用用户 `kubernetes-admin` 进行调用,我们需要将 CA、客户端证书和私钥的 base64 编码值提取到文件中作为 base64 解码值。以下命令将复制 base64 编码值,并使用工具 `base64` 对其进行解码,然后写入文件。CA 值将存储在文件 `ca` 中,客户端证书值将存储在 `kubernetes-admin.crt` 中,私钥将存储在 `kubernetes-admin.key` 中:

$ echo LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL... | base64 -d > ca
$ echo LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL... | base64 -d > kubernetes-admin.crt
$ echo LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktL... | base64 - \

kubernetes-admin.key


现在,您可以使用相关命令行选项将 `curl` 命令指向这些文件。向 API 服务器的请求应正确进行身份验证,并返回所有现有的命名空间,因为 `kubernetes-admin` 具有适当的权限:

$ curl --cacert ca --cert kubernetes-admin.crt --key kubernetes-admin.key
https://172.28.132.5:6443/api/v1/namespaces
{
"kind": "NamespaceList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "2387"
},
"items": [
...
]
}


# 限制访问 API 服务器

如果您正在将 API 服务器暴露到互联网,请问您是否有必要这样做。一些云提供商提供创建私有集群的选项,这将限制或完全禁用对 API 服务器的公共访问。有关更多信息,请参阅[EKS](https://oreil.ly/W4Oma)和[GKE](https://oreil.ly/c7G-g)的文档页面。

如果您正在运营本地 Kubernetes 集群,您将需要实例化防火墙规则以阻止对 API 服务器的访问。设置防火墙规则超出了考试范围,因此本书不会讨论此内容。

## 场景:攻击者可以通过互联网调用 API 服务器

云服务提供商有时会将 API 服务器暴露在互联网上,以简化管理访问。攻击者可以尝试通过拒绝提供客户端证书或令牌来向 API 服务器端点发起匿名请求。如果攻击者幸运地捕获到用户凭据,那么可以执行经过身份验证的调用。根据分配给用户的权限,可以执行恶意操作。图 3-2 说明了一个攻击者从互联网调用 API 服务器的情况。

![ckss 0302](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0302.png)

###### 图 3-2\. 一个攻击者从互联网调用 API 服务器

在本章中,我们将看一下如何限制对 API 服务器的访问,并通过示例实现具有有限权限的 RBAC。“理解 Open Policy Agent(OPA)和 Gatekeeper”将通过 OPA Gateway 帮助审计控制。

## 限制用户权限

我们已经看到我们可以使用`kubernetes-admin`用户的凭据来调用 Kubernetes API。这个用户应该非常节俭地使用,也不应该与很多人共享凭据。如果凭据落入错误的手中,可能会造成很大的损害。将这个用户专门保留给负责集群管理的人员使用。

对于您的 Kubernetes 集群的其他利益相关者,您应该设置一个具有有限权限集的专用用户。您可能在组织中有特定的角色可以映射到。例如,您可能有一个开发者角色,应该被允许管理部署、Pod、ConfigMaps、Secrets 和 Services,但不允许其他操作。要创建一个新用户并分配相关的 RBAC 权限,请参考[Kubernetes 文档](https://oreil.ly/n8EMD)。简而言之,有四个步骤:

1.  创建一个私钥。

1.  创建并批准一个 CertificateSigningRequest。

1.  创建一个 Role 和一个 RoleBinding。

1.  将用户添加到[kubeconfig 文件](https://oreil.ly/OKs9g)中。

我们将详细介绍这个过程,但会在“最小化服务账户权限”中更详细地讨论 RBAC 概念。

### 创建一个私钥

使用`openssl`可执行文件创建一个私钥。提供一个有意义的文件名,比如`<username>.key`:

$ openssl genrsa -out johndoe.key 2048
Generating RSA private key, 2048 bit long modulus
...+
......................................................................+
e is 65537 (0x10001)


在带有扩展名`.csr`的文件中创建一个证书签名请求(CSR)。您需要提供上一步的私钥。当要求输入“通用名称”值时,以下命令使用用户名`johndoe`。所有其他输入请求都是可选的,可以根据需要填写:

$ openssl req -new -key johndoe.key -out johndoe.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.

Country Name (2 letter code) []:
State or Province Name (full name) []:
Locality Name (eg, city) []:
Organization Name (eg, company) []:
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:johndoe
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:


使用以下命令检索 CSR 文件内容的 base64 编码值。在下一步创建 CertificateSigningRequest 对象时会需要它:

$ cat johndoe.csr | base64 | tr -d "\n"
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tL...


### 创建和批准一个 CertificateSigningRequest

以下脚本创建一个 CertificateSigningRequest 对象。[CertificateSigningRequest 资源](https://oreil.ly/ltFbE) 用于请求由指定签名者签名的证书:

$ cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: johndoe
spec:
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tL...
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400
usages:

  • client auth
    EOF
    certificatesigningrequest.certificates.k8s.io/johndoe created

属性 `spec.signerName` 的值 `kubernetes.io/kube-apiserver-client` 签署证书,这些证书将被 API 服务器作为客户端证书接受。使用上一步的 Base64 编码值并将其分配给属性 `spec.request` 的值。最后,可选属性 `spec.expirationSeconds` 确定证书的生存期。分配的值 `86400` 使证书有效期为一天。根据你希望证书持续的时间长短增加或减少到期时间,或者简单地不添加这个属性。

创建 CertificateSigningRequest 对象后,条件将显示为“待处理”。你需要在 24 小时内批准签名请求,否则对象将被自动删除,作为清理集群中不必要对象的手段。

$ kubectl get csr johndoe
NAME AGE SIGNERNAME REQUESTOR
REQUESTEDDURATION CONDITION
johndoe 6s kubernetes.io/kube-apiserver-client minikube-user
24h Pending


使用 `certificate approve` 命令批准签名请求。结果,条件变更为“已批准,已发放”:

$ kubectl certificate approve johndoe
certificatesigningrequest.certificates.k8s.io/johndoe approved
$ kubectl get csr johndoe
NAME AGE SIGNERNAME REQUESTOR
REQUESTEDDURATION CONDITION
johndoe 17s kubernetes.io/kube-apiserver-client minikube-user
24h Approved,Issued


最后,导出从已批准的 CertificateSigningRequest 对象颁发的证书:

$ kubectl get csr johndoe -o jsonpath={.status.certificate}| base64
-d > johndoe.crt


### 创建一个角色和一个角色绑定

是时候分配 RBAC 权限了。在这一步,你将为用户创建一个角色和一个角色绑定。角色模拟了组织内的“应用开发者”角色。开发者只能允许获取、列出、更新和删除 Pod。以下的命令创建了角色对象:

$ kubectl create role developer --verb=create --verb=get --verb=list
--verb=update --verb=delete --resource=pods
role.rbac.authorization.k8s.io/developer created


接下来,我们将把角色绑定到名为 `johndoe` 的用户上。使用命令 `create rolebinding` 来完成这个操作:

$ kubectl create rolebinding developer-binding-johndoe --role=developer
--user=johndoe
rolebinding.rbac.authorization.k8s.io/developer-binding-johndoe created


### 将用户添加到 kubeconfig 文件中

在最后一步,你需要将用户添加到 kubeconfig 文件中,并为其创建一个用户上下文。请注意,在下面的命令中集群名称是 `minikube`,因为我们正在 minikube 安装中尝试这个操作:

$ kubectl config set-credentials johndoe --client-key=johndoe.key
--client-certificate=johndoe.crt --embed-certs=true
User "johndoe" set.
$ kubectl config set-context johndoe --cluster=minikube --user=johndoe
Context "johndoe" created.


### 验证权限

是时候切换到名为`johndoe`的用户上下文了:

$ kubectl config use-context johndoe
Switched to context "johndoe".


使用 `kubectl` 作为客户端向 API 服务器发出调用,我们将验证这个操作是否被允许。用于在 `default` 命名空间中列出所有 Pod 的 API 调用已经被验证和授权:

$ kubectl get pods
No resources found in default namespace.


命令的输出指示当前`default`命名空间在这个时间点上不包含任何 Pod 对象,但是调用是成功的。我们还将测试负面案例。列出命名空间对于用户来说是一个不允许的操作。执行相关的 `kubectl` 命令将返回一个错误消息:

$ kubectl get namespaces
Error from server (Forbidden): namespaces is forbidden: User "johndoe" cannot
list resource "namespaces" in API group "" at the cluster scope


当你完成权限验证后,你可能想要切换回拥有管理员权限的上下文:

$ kubectl config use-context minikube
Switched to context "minikube".


## 情景:攻击者可以通过服务账号调用 API 服务器

用户代表经常使用 `kubectl` 可执行文件或 UI 仪表板与 Kubernetes 集群进行交互的真实人员。在某些罕见条件下,运行在 Pod 容器内的应用程序需要与 Kubernetes API 进行交互。这种需求的典型示例是包管理器[Helm](https://helm.sh)。Helm 根据捆绑在 Helm 图表中的 YAML 清单管理 Kubernetes 资源。Kubernetes 使用服务账户通过身份验证令牌将 Helm 服务进程与 API 服务器进行身份验证。可以将此服务账户分配给 Pod,并映射到 RBAC 规则。

如果攻击者能够访问 Pod,很可能也能够滥用服务账户调用 Kubernetes API,如图 3-3 所示。

![ckss 0303](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0303.png)

###### 图 3-3\. 攻击者使用服务账户调用 API 服务器。

## 最小化服务账户的权限。

重要的是只限制那些对应用程序真正必要的服务账户的权限。接下来的章节将解释如何实现这一点,以最小化潜在的攻击面。

要使这种场景正常工作,您需要创建一个 ServiceAccount 对象,并将其分配给 Pod。服务账户可以与 RBAC 绑定,并分配角色和角色绑定以定义它们应该允许执行的操作。

### 将服务账户绑定到 Pod。

作为起点,我们将设置一个 Pod,通过调用 Kubernetes API 列出命名空间 `k97` 中所有 Pods 和 Deployments。该调用作为每十秒无限循环的一部分进行。来自 API 调用的响应将写入标准输出,可通过 Pod 的日志访问。

为了对 API 服务器进行身份验证,我们将发送与 Pod 使用的服务账户相关联的令牌。服务账户的默认行为是自动挂载 API 凭据到路径 `/var/run/secrets/kubernetes.io/serviceaccount/token`。我们将使用 `cat` 命令行工具简单地获取文件内容,并将其作为 HTTP 请求的标头一并发送。示例 3-1 定义了命名空间、服务账户和 Pod,在一个名为 `setup.yaml` 的 YAML 文件中。

##### 示例 3-1\. 为将服务账户分配给 Pod 的 YAML 文件清单。

apiVersion: v1
kind: Namespace
metadata:
name: k97

apiVersion: v1
kind: ServiceAccount
metadata:
name: sa-api
namespace: k97

apiVersion: v1
kind: Pod
metadata:
name: list-objects
namespace: k97
spec:
serviceAccountName: sa-api
containers:

  • name: pods
    image: alpine/curl:3.14
    command: ['sh', '-c', 'while true; do curl -s -k -m 5 -H
    "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/
    serviceaccount/token)" https://kubernetes.default.svc.cluster.
    local/api/v1/namespaces/k97/pods; sleep 10; done']
  • name: deployments
    image: alpine/curl:3.14
    command: ['sh', '-c', 'while true; do curl -s -k -m 5 -H
    "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/
    serviceaccount/token)" https://kubernetes.default.svc.cluster.
    local/apis/apps/v1/namespaces/k97/deployments; sleep 10; done']

使用以下命令从 YAML 文件创建对象:

$ kubectl apply -f setup.yaml
namespace/k97 created
serviceaccount/sa-api created
pod/list-objects created


### 验证默认权限。

名为 `list-objects` 的 Pod 在专用容器中调用 API 服务器以检索 Pods 和 Deployments 的列表。容器 `pods` 执行调用以列出 Pods。容器 `deployments` 向 API 服务器发送请求以列出 Deployments。

如 [Kubernetes 文档](https://oreil.ly/gBp30) 所述,默认的 RBAC 策略不会授予 `kube-system` 命名空间之外的服务帐户任何权限。容器 `pods` 和 `deployments` 的日志返回错误消息,指示服务帐户 `sa-api` 未被授权列出资源:

$ kubectl logs list-objects -c pods -n k97
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "pods is forbidden: User "system:serviceaccount:k97:sa-api"
cannot list resource "pods" in API group "" in the
namespace "k97"",
"reason": "Forbidden",
"details": {
"kind": "pods"
},
"code": 403
}
$ kubectl logs list-objects -c deployments -n k97
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "deployments.apps is forbidden: User
"system:serviceaccount:k97:sa-api" cannot list resource
"deployments" in API group "apps" in the namespace
"k97"",
"reason": "Forbidden",
"details": {
"group": "apps",
"kind": "deployments"
},
"code": 403
}


接下来,我们将创建一个 ClusterRole 和 RoleBinding 对象,并赋予执行所需 API 调用的权限。

### 创建 ClusterRole

首先,定义名为 `list-pods-clusterrole` 的 ClusterRole,如 示例 3-2 中所示,在文件 `clusterrole.yaml` 中。规则集仅添加 Pod 资源和动词 `list`。

##### 示例 3-2\. 允许列出 Pods 的 ClusterRole 的 YAML 清单

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: list-pods-clusterrole
rules:

  • apiGroups: [""]
    resources: ["pods"]
    verbs: ["list"]

通过指向相应的 YAML 清单文件来创建对象:

$ kubectl apply -f clusterrole.yaml
clusterrole.rbac.authorization.k8s.io/list-pods-clusterrole created


### 创建 RoleBinding

示例 3-3 在 `rolebinding.yaml` 文件中定义了 RoleBinding 的 YAML 清单。RoleBinding 将 ClusterRole `list-pods-clusterrole` 映射到名为 `sa-pod-api` 的服务帐户,并且仅适用于命名空间 `k97`。

##### 示例 3-3\. 附加到服务帐户的 RoleBinding 的 YAML 清单

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: serviceaccount-pod-rolebinding
namespace: k97
subjects:

  • kind: ServiceAccount
    name: sa-api
    roleRef:
    kind: ClusterRole
    name: list-pods-clusterrole
    apiGroup: rbac.authorization.k8s.io

使用 `apply` 命令创建 RoleBinding 对象:

$ kubectl apply -f rolebinding.yaml
rolebinding.rbac.authorization.k8s.io/serviceaccount-pod-rolebinding created


### 验证授予的权限

通过授予 `list` 权限,服务帐户现在可以正确检索 `k97` 命名空间中的所有 Pods。`pods` 容器中的 `curl` 命令成功执行,如下所示:

$ kubectl logs list-objects -c pods -n k97
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "628"
},
"items": [
{
"metadata": {
"name": "list-objects",
"namespace": "k97",
...
}
]
}


我们没有授予服务帐户其他资源的任何权限。在 `k97` 命名空间中列出 Deployments 仍然失败。以下输出显示了在 `deployments` 命名空间中使用 `curl` 命令的响应:

$ kubectl logs list-objects -c deployments -n k97
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "deployments.apps is forbidden: User
"system:serviceaccount:k97:sa-api" cannot list resource
"deployments" in API group "apps" in the namespace
"k97"",
"reason": "Forbidden",
"details": {
"group": "apps",
"kind": "deployments"
},
"code": 403
}


随意修改 ClusterRole 对象,以允许列出 Deployment 对象。

### 禁用服务帐户令牌的自动装载

前一节中描述的 Pod 使用服务帐户的令牌作为针对 API 服务器进行身份验证的手段。将令牌文件挂载到 `/var/run/secrets/kubernetes.io/serviceaccount/token` 是每个服务帐户的标准行为。只有当 Pod 实际与 Kubernetes API 交互时,您才真正需要文件的内容。在所有其他情况下,此行为可能构成潜在的安全风险,因为访问 Pod 将直接将攻击者引导至令牌。

您可以通过将属性 `automountServiceAccountToken` 的值设置为 `false` 来禁用服务帐户对象的自动装载行为,如 示例 3-4 所示。

##### 示例 3-4\. 选择退出服务帐户的令牌自动装载行为

apiVersion: v1
kind: ServiceAccount
metadata:
name: sa-api
namespace: k97
automountServiceAccountToken: false


如果要为单个 Pod 禁用自动装载行为,请在 Pod 定义中使用 `spec.automountServiceAccountToken` 属性。示例 3-5 展示了一个 Pod 的 YAML 清单。

##### 示例 3-5\. 在 Pod 中禁用服务帐户的令牌自动装载

apiVersion: v1
kind: Pod
metadata:
name: list-objects
namespace: k97
spec:
serviceAccountName: sa-api
automountServiceAccountToken: false
...


### 生成服务账户令牌

有多种用例表明希望创建一个禁用令牌自动挂载的服务账户。例如,您可能需要从外部工具或连续交付流水线访问 Kubernetes API,以查询现有对象的信息。在这些场景中,仍需要使用令牌对 API 服务器进行身份验证。所列场景不一定运行分配了服务账户的 Pod,而只是从诸如`curl`之类的工具执行 RESTful API 调用。

要手动创建令牌,请执行`create token`命令,并将服务账户的名称作为参数提供。该命令的输出呈现令牌:

$ kubectl create token sa-api
eyJhbGciOiJSUzI1NiIsImtpZCI6IjBtQkJzVWlsQjl...


您需要将令牌存储在安全的位置,例如密码管理器中。如果丢失令牌,将无法重新获取。您只能使用相同的命令重新创建令牌,这将自动使先前的令牌失效。所有使用该令牌的引用都必须更改。

对于自动化流程,使用有限生命周期生成令牌可能很有帮助。`--duration`将在“生命周期”结束后自动使令牌失效:

$ kubectl create token sa-api --duration 10m
eyJhbGciOiJSUzI1NiIsImtpZCI6IjBtQkJzVWlsQjl...


### 创建服务账户的密钥

在 Kubernetes 1.24 中,ServiceAccount 对象不再自动创建包含令牌的对应 Secret 对象。有关更多信息,请参阅 [发行说明](https://oreil.ly/MSPuX)。列出 ServiceAccount 对象会呈现 0 个密钥的数量。该对象在 YAML 表示中也不再包含`secrets`属性:

$ kubectl get serviceaccount sa-api -n k97
NAME SECRETS AGE
sa-api 0 42m


您可以使用`create token`命令生成令牌,如 “生成服务账户令牌”中所述,或手动创建相应的密钥。示例 3-6 显示了这种密钥的 YAML 清单。

##### 示例 3-6\. 手动创建服务账户的密钥

apiVersion: v1
kind: Secret
metadata:
name: sa-api-secret
namespace: k97
annotations:
kubernetes.io/service-account.name: sa-api
type: kubernetes.io/service-account-token


要为服务账户分配密钥,需添加带有键`kubernetes.io/service-account.name`的注释。以下命令创建密钥对象:

$ kubectl create -f secret.yaml
secret/sa-api-secret created


描述 Secret 对象时,您可以在“数据”部分找到令牌:

$ kubectl describe secret sa-api-secret -n k97
...
Data

ca.crt: 1111 bytes
namespace: 3 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjBtQkJzVWlsQjl...


# 频繁更新 Kubernetes

安装具有特定版本的 Kubernetes 集群并非一劳永逸的操作。即使在安装时使用了最新的长期支持 (LTS) 版本,也不能保证您的集群没有安全漏洞。

随着时间的推移,将发现与安全相关的漏洞和弱点。此声明包括底层操作系统以及集群节点运行的依赖项。攻击者可以轻易在公开披露的 [通用漏洞和暴露 (CVE) 数据库](https://oreil.ly/FHhXD) 中查找安全漏洞并利用它们。

## 版本控制方案

将 Kubernetes 版本定期更新到所有节点上,这由集群管理员负责。Kubernetes 遵循[语义化版本控制方案](https://semver.org)。语义化版本包括主版本号、次版本号和补丁版本号。例如,对于 Kubernetes 版本 1.24.3,主版本号是 1,次版本号是 24,补丁版本号是 3。

每个版本号的部分都有特定的含义。主版本号的变更表示有不兼容的改动。递增次版本号表示以向后兼容的方式新增了功能。补丁版本号仅修复了一个 bug。

# Kubernetes 中通过次版本更新引入的不兼容变更

需要指出的是,Kubernetes 并不总是严格遵循语义化版本控制的解释。例如,PodSecurityPolicy(PSP)准入控制器已经在[版本 1.25.0](https://oreil.ly/JE-i8)中被 Pod Security Admission 概念取代。按照传统,这些更改只应该发生在主版本更新时。参考[Kubernetes 废弃政策](https://oreil.ly/on9lu)以更好地理解 API、标志或功能如何逐步淘汰。

## 发布节奏

您可以预期每三个月发布[一个新的次版本](https://oreil.ly/LGIn5)的 Kubernetes。新发布版本可能包含新功能和额外的 bug 修复。必要时,安全修复也会被实施到最新版本的 Kubernetes,并将回溯到之前的两个次版本。始终保持更新到您的集群的最新版本需要相当多的人力投入。因此,您需要相应地预留时间来执行这些活动。

## 执行升级过程

建议从一个次版本升级到更高的次版本(例如,从 1.23 到 1.24),或者从一个补丁版本升级到更近期的一个(例如,从 1.24.1 到 1.24.3)。避免跨越多个次版本进行升级,以避免意外的副作用。

您可以在官方 Kubernetes 文档中找到关于[升级步骤](https://oreil.ly/RxC9j)的完整描述。图 3-4 概述了高层次的升级过程。

集群版本升级过程已经是 CKA 考试的一部分。考虑到您必须通过 CKA 作为先决条件,我假设您已经知道如何执行这个过程。有关详细描述,请参考[*Certified Kubernetes Administrator (CKA) Study Guide*](https://oreil.ly/cka-study-guide)。

![ckss 0304](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0304.png)

###### 图 3-4\. 集群版本升级流程

# 概述

用户、客户端应用程序(例如`kubectl`或`curl`)、使用服务账户的 Pod,以及集群组件都通过 API 服务器与其通信,以管理对象。确保安全地保护 API 服务器,以防止恶意访问,这至关重要。

为了最小化攻击面积,避免使用防火墙规则将 API 服务器暴露给互联网。对于每个用户或服务账户,使用 RBAC 规则将权限限制到最低限度,以执行对 Kubernetes API 的操作。权限最小化可以在攻击者获得凭据访问时造成更少的损害。

确保升级您的 Kubernetes 集群版本。集成错误和安全修复将减少暴露给攻击者利用的不必要漏洞的风险。

# 考试要点

练习与 Kubernetes API 交互。

本章演示了与 Kubernetes API 交互的不同方式。我们通过切换到用户上下文执行 API 请求,并借助使用`curl`进行 RESTful API 调用。您需要了解如何确定 API 服务器的端点以及如何使用不同的身份验证方法,例如客户端凭据和 Bearer 令牌。探索 Kubernetes API 及其端点,以便获得更广泛的暴露。

理解为用户和服务账户定义 RBAC 规则的影响。

匿名用户对 Kubernetes API 的请求不会允许任何实质性操作。对于来自用户或服务账户的请求,您需要仔细分析授予主体的权限。通过创建相关对象来控制权限,了解定义 RBAC 规则的方方面面。服务账户在 Pod 中使用时会自动挂载令牌。如果打算从 Pod 中进行 API 调用,则仅暴露令牌作为卷。

注意 Kubernetes 的发布节奏和升级集群的必要性。

出于安全原因,Kubernetes 集群需要随着时间的推移进行维护。攻击者可能会利用已知的过时 Kubernetes 版本中的漏洞。版本升级过程是每个管理员的职责的一部分,不应忽视。

# 示例练习

这些练习的解答在附录中提供。

1.  为名为`jill`的用户在`observer`组中创建客户端证书和密钥。使用管理员上下文为用户`jill`创建上下文。

1.  对于组(而不是用户!),在`default`名称空间中定义一个允许资源 Pods、ConfigMaps 和 Secrets 的动词`get`、`list`和`watch`的角色和角色绑定。创建这些对象。

1.  切换到用户上下文,并执行允许已授权操作的`kubectl`命令,以及一个不应允许的`kubectl`命令。切换回`admin`上下文。

1.  在名称空间`t23`中创建一个名为`service-list`的 Pod。该容器使用`alpine/curl:3.14`镜像,并在无限循环中向 Kubernetes API 发出一个列出`default`名称空间中 Service 对象的`curl`调用。创建并附加服务账户`api-call`。在 Pod 启动后检查容器日志。您期望从`curl`命令中看到什么响应?

1.  为服务账号分配 ClusterRole 和 RoleBinding,仅允许 Pod 所需的操作。查看 `curl` 命令的响应。

1.  配置 Pod 以禁用自动挂载服务账号令牌。检索令牌值并直接在 `curl` 命令中使用。确保 `curl` 命令仍然能够授权操作。

1.  导航到 GitHub 仓库 [*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ) 的 *app-a/ch03/upgrade-version* 目录。使用命令 `vagrant up` 启动运行集群的虚拟机。将集群中所有节点从 Kubernetes 1.25.6 升级到 1.26.1\. 集群包括名为 `kube-control-plane` 的单一控制平面节点和名为 `kube-worker-1` 的工作节点。完成后,使用 `vagrant destroy -f` 关闭集群。

    *先决条件:* 这个练习需要安装工具 [Vagrant](https://oreil.ly/FiyeH) 和 [VirtualBox](https://oreil.ly/WW8IK)。


# 第四章:系统加固

“系统加固”领域涉及到运行 Kubernetes 集群节点的底层主机系统的安全相关方面。本章讨论的主题涉及基本的 Linux 核心功能,包括禁用服务和删除软件包、管理用户和组、禁用端口以及设置防火墙规则。最后,本章讨论了可以限制容器中运行的进程在主机级别上执行操作的 Linux 内核加固工具。

从高层次上看,本章涵盖以下概念:

+   减少主机操作系统的占用

+   减少 IAM 角色

+   减少对网络的外部访问

+   使用像 AppArmor 和 seccomp 这样的内核加固工具

# 减少主机操作系统的占用

集群节点运行在物理机或虚拟机上。在大多数情况下,这些机器上的操作系统是 Linux 发行版。显然,操作系统可能会暴露安全漏洞。

随着时间的推移,您需要将操作系统的版本保持最新,以包含最新的安全修复。例如,这个过程可能涉及将节点的操作系统从 Ubuntu 18 升级到 22。本书不涵盖操作系统的升级;有关更多信息,请参阅相关的 Linux 文档。

许多 Linux 发行版,如 Ubuntu,自带额外的工具、应用程序和服务,并非操作 Kubernetes 集群所必需。作为管理员,您的任务是识别安全风险,禁用或移除可能暴露漏洞的任何操作系统特定功能,并保持操作系统补丁,以包含最新的安全修复。操作系统的功能越少,风险就越小。

# Ubuntu Linux 的 CIS 基准

作为参考指南,您可以将操作系统的配置与 [Ubuntu Linux 的 CIS 基准](https://oreil.ly/AeAAE) 进行比较。

## 场景:攻击者利用软件包漏洞

图 4-1 描述了攻击者如何利用系统上安装的软件包的漏洞。例如,该应用程序可能是软件包管理器 [snapd](https://oreil.ly/ZOFTj)。假设攻击者利用已知的漏洞 [USN-5292-1](https://oreil.ly/lw_MV),可能会向攻击者公开敏感信息。

![ckss 0401](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0401.png)

###### 图 4-1\. 攻击者利用操作系统级别的漏洞

下一节将解释如何通过简单禁用或移除不真正需要的服务和软件包,来减少操作 Kubernetes 所需的服务和软件包的安全风险。

## 禁用服务

在 Linux 上,许多应用程序作为后台服务运行。可以使用命令行工具 `systemctl` 管理服务。以下 `systemctl` 命令列出所有正在运行的服务:

$ systemctl | grep running
...
snapd.service loaded active running Snap Daemon


在操作群集节点时我们不需要的一个服务是软件包管理器 snapd。 有关该服务的更多详细信息,请使用`status`子命令检索其状态:

$ systemctl status snapd
● snapd.service - Snap Daemon
Loaded: loaded (/lib/systemd/system/snapd.service; enabled; vendor
preset: enabled)
Active: active (running) since Mon 2022-09-19 22:49:56 UTC; 30min ago
TriggeredBy: ● snapd.socket
Main PID: 704 (snapd)
Tasks: 12 (limit: 2339)
Memory: 45.9M
CGroup: /system.slice/snapd.service
└─704 /usr/lib/snapd/snapd


您可以使用`systemctl`子命令`stop`停止服务:

$ sudo systemctl stop snapd
Warning: Stopping snapd.service, but it can still be activated by:
snapd.socket


执行`disable`子命令以防止系统重新启动时再次启动服务:

$ sudo systemctl disable snapd
Removed /etc/systemd/system/multi-user.target.wants/snapd.service.


服务现已停止并已禁用:

$ systemctl status snapd
● snapd.service - Snap Daemon
Loaded: loaded (/lib/systemd/system/snapd.service; disabled; vendor
preset: enabled)
Active: inactive (dead) since Mon 2022-09-19 23:22:22 UTC; 4min 4s ago
TriggeredBy: ● snapd.socket
Main PID: 704 (code=exited, status=0/SUCCESS)


## 删除不需要的软件包

既然服务已停用,保留软件包也就没有意义了。 您可以删除软件包以释放额外的磁盘空间和内存。 您可以使用`apt purge`命令删除软件包及其传递的软件包,如下所示:

$ sudo apt purge --auto-remove snapd
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages will be REMOVED:
snapd* squashfs-tools*
0 upgraded, 0 newly installed, 2 to remove and 116 not upgraded.
After this operation, 147 MB disk space will be freed.
Do you want to continue? [Y/n] y
...


即使软件包不受服务控制,您也可以使用相同的命令。 确定您不需要的软件包,然后简单地删除它们。 您应该能够获得系统更加精简的足迹。

潜在攻击者无法再使用 snapd 服务来利用系统。 您应该对任何不需要的服务重复此过程。 结果,snapd 服务将不再存在于系统中:

$ systemctl status snapd
Unit snapd.service could not be found.


# 最小化 IAM 角色

系统级别的身份验证和访问管理(IAM)涉及管理 Linux 用户、他们所属的组以及授予他们的权限。 任何目录和文件都将为用户分配文件权限。

适当的用户和访问管理是每个系统管理员的经典责任。 尽管您作为 Kubernetes 管理员的角色可能不直接涉及系统级别的 IAM,但理解安全性的影响至关重要。 您可能需要与同行合作以加固运行 Kubernetes 集群的系统。

本节将简要介绍如何管理用户和组。 我们还将讨论如何设置文件权限和所有权以尽可能减少访问权限。 在本书中,我们只是涉及这个主题的皮毛。 如需更多信息,请参考您选择的 Linux 文档。

## 场景:攻击者使用凭据获取文件访问权限

安全漏洞可能导致用户凭据被窃取。 获得有效用户凭据使攻击者可以打开其他攻击向量的大门。 图 4-2 显示一个攻击者使用窃取的用户凭据登录到群集节点,现在可以与分配给用户权限的所有文件和目录进行交互。

![ckss 0402](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0402.png)

###### 图 4-2。 攻击者使用窃取的凭据访问文件

建议遵循最小特权原则。 只向有限的用户组授予管理权限。 所有其他用户应只被允许执行其工作所需的操作。

## 理解用户管理

每个用户必须对系统进行身份验证才能使用它。 认证用户根据分配的权限访问资源。 本节将为您介绍管理用户所需的主要操作。

### 用户列表

要列出系统上的所有用户,请渲染文件`/etc/passwd`的内容。每个条目遵循一般模式`username:password:UID:GID:comment:home:shell`。模式内的某些字段可能为空:

$ cat /etc/passwd
root❌0:0:root:/root:/bin/bash
nobody❌65534:65534:nobody:/nonexistent:/usr/sbin/nologin
...


命令输出将用户`root`呈现在输出的第一个位置。字符串的最后一部分`/bin/bash`表示用户被允许使用`bash` shell 登录系统。其他用户可能根本无法登录。对于这些用户,您将在`shell`字段中找到分配给`/usr/sbin/nologin`的字符串。

在任何给定时间点,您可以看到由用户启动的哪些进程。以下命令显示所有`bash`进程,包括启动它的相应用户:

$ ps aux | grep bash
root 956 0.0 0.4 22512 19200 pts/0 Ss 17:57 0:00 -bash
root 7064 0.0 0.0 6608 2296 pts/0 S+ 18:08 0:00 grep
--color=auto bash


### 添加用户

在某些时候,您可能希望让团队成员访问运行集群节点的机器,并限制权限。您可以使用`adduser`命令将新用户添加到系统。添加标志`--shell /sbin/nologin`以禁用用户的 Shell 访问权限。以下命令创建用户`ben`:

$ sudo adduser ben
Adding user ‘ben’ ...
Adding new group ‘ben’ (1001) ...
Adding new user ‘ben’ (1001) with group ‘ben’ ...
Creating home directory ‘/home/ben’ ...
Copying files from ‘/etc/skel’ ...
New password:
Retype new password:
...


用户条目已添加到文件`/etc/passwd`中:

$ cat /etc/passwd
...
ben❌1001:1001:,,,:/home/ben:/bin/bash


### 切换到用户

您可以使用`su`命令在 Shell 中更改用户。以下命令切换到之前创建的用户`ben`。系统将要求您输入用户的密码:

$ su ben
Password:
ben@controlplane:/root$ pwd
/root


Shell 将通过其提示指示当前用户。您将从运行`su`命令时使用的帐户继承环境变量。要创建新环境,请在`su`命令中添加连字符:

$ su - ben
ben@controlplane:~$ pwd
/home/ben


另一种临时切换用户的方法是使用`sudo`命令。您需要有提升的权限来执行该命令。因此,`sudo`命令等效于“以管理员身份运行此命令”:

$ sudo -u ben pwd
/root


### 删除用户

由系统中的用户代表的团队成员转移到其他团队,或者可能只是离开公司。您将希望撤销用户的访问权限,以防止未经授权使用凭据。以下命令删除用户,包括用户的主目录:

$ sudo userdel -r ben


## 理解组管理

系统管理员更方便地将具有类似访问要求的用户分组,以控制个别用户级别的权限。Linux 系统提供了组的概念,用于基于团队或特定组织角色组织用户。我们将简要介绍组管理的最重要方面。

### 列出组

可以通过检查文件`/etc/group`的内容来列出组。每个条目遵循一般模式`groupname:password:GID:group members`:

$ cat /etc/group
root❌0:
plugdev❌46:packer
nogroup❌65534:
...


正如您在输出中看到的那样,某些字段可能为空。唯一具有分配成员的组是`plugdev`,其名称为`packer`。

### 添加组

使用命令`groupadd`添加新组。以下示例添加组`kube-developers`:

$ sudo groupadd kube-developers


该组现在将在文件`/etc/group`中列出。请注意,组标识符为 1004:

$ cat /etc/group
...
kube-developers❌1004:


### 将用户分配到一个组

要将组分配给用户,请使用`usermod`命令。以下命令将用户`ben`添加到组`kube-developers`:

$ sudo usermod -g kube-developers ben


组标识符 1004 充当`kube-developers`组的替代品:

$ cat /etc/passwd | grep ben
ben❌1001:1004:,,,:/home/ben:/bin/bash


### 删除组

有时您希望完全摆脱一个组。也许是指向一个不再存在的 Linux 组的组织角色。使用`groupdel`命令删除一个组。如果成员仍然是该组的一部分,您将收到错误消息:

$ sudo groupdel kube-developers
groupdel: cannot remove the primary group of user ben


在删除组之前,您应该使用`usermod`命令将组成员重新分配到另一个组。以下命令将组从`kube-developers`更改为`kube-admins`。假设组`kube-admins`已经在之前创建好:

$ sudo usermod -g kube-admins ben
$ sudo groupdel kube-developers


## 理解文件权限和所有权

将文件权限分配为尽可能小的访问权限对于最大化安全性至关重要。这就是 Linux 文件权限和所有权发挥作用的地方。我只会高层次地讨论相关操作。更多详细信息,请参考[Linux Foundation 关于 Linux 文件权限的博文](https://oreil.ly/3IpRT)。

### 查看文件权限和所有权

每个用户都可以创建新的目录和文件。例如,您可以使用`touch`命令创建一个空文件。以下命令在当前目录中创建一个名为`my-file`的文件:

$ touch my-file


要查看目录内容的“长”格式,请使用`ls`命令。由于请求的`-l`命令行参数,输出的长格式呈现文件权限和文件所有权:

$ ls -l
total 0
-rw-r--r-- 1 root root 0 Sep 26 17:53 my-file


输出的重要部分是`-rw-r--r--`。第一个字符是特殊的权限字符,可以根据系统而变化,后跟三个以`rwx`表示的组合。前三个字符代表所有者权限,第二组三个字符是组权限,最后三个字符表示所有用户的权限。符号`r`表示读取权限,`w`表示写入权限,`x`表示执行权限。在前面的示例中,用户`root`可以读取和写入文件,而组和其他用户只能读取文件。

### 更改文件所有权

使用`chown`命令更改文件或目录的用户和组分配。命令的语法遵循`chown owner:group filename`的模式。以下命令将文件的所有权更改为用户`ben`,但不重新分配组。执行`chown`命令的用户需要具有写权限:

$ chown ben my-file
$ ls -l
total 0
-rw-r--r-- 1 ben root 0 Sep 26 17:53 my-file


### 更改文件权限

您可以使用`chmod`命令以多种表示法添加或删除权限。例如,使用以下命令为文件所有者删除写权限:

$ chmod -w file1
$ ls -l
total 0
-r--r--r-- 1 ben root 0 Sep 26 17:53 my-file


# 减少网络对外部访问

只有允许对 Kubernetes 操作必需的端口进行集群节点的外部访问。我们已经在“保护节点元数据和端点”中讨论了标准的 Kubernetes 端口。所有其他端口的访问应被阻止。

## 辨识和停用开放端口

像 FTP 服务器、Web 服务器以及诸如 Samba 之类的文件和打印服务这样的应用程序通过开放端口来向客户端公开通信终点。运行打开网络通信的应用程序可能会带来安全风险。您可以通过简单地禁用服务并卸载应用程序来消除风险。

假设我们在控制平面节点上[安装了 Apache 2 HTTP Web 服务器](https://oreil.ly/t-np3),使用以下命令:

$ sudo apt update
$ sudo apt install apache2


# 关于 netstat 命令的更新

`netstat`命令已经弃用,推荐使用更快速、更易读的`ss`命令。有关更多信息,请参阅您使用的操作系统的文档。

我们可以使用命令行工具`ss`检查所有开放端口,该工具类似于`netstat`。以下命令显示了所有开放端口及其进程,其中包括 Apache 2 暴露的端口 80:

$ sudo ss -ltpn
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
...
LISTEN 0 511 *:80 : users:
(("apache2",pid=18435,fd=4),("apache2",pid=18434,fd=4),("apache2", ]
pid=18432,fd=4))


您可能仅临时需要 Web 服务器,并且可能只是忘记了安装它。该过程当前由服务器管理。您可以使用`systemctl status`命令查看服务的状态:

$ sudo systemctl status apache2
● apache2.service - The Apache HTTP Server
Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor
preset: enabled)
Active: active (running) since Tue 2022-09-20 22:25:25 UTC; 39s ago
Docs: https://httpd.apache.org/docs/2.4/
Main PID: 18432 (apache2)
Tasks: 55 (limit: 2339)
Memory: 5.6M
CGroup: /system.slice/apache2.service
├─18432 /usr/sbin/apache2 -k start
├─18434 /usr/sbin/apache2 -k start
└─18435 /usr/sbin/apache2 -k start


Kubernetes 不需要 Apache 2。我们决定关闭该服务并卸载该软件包:

$ sudo systemctl stop apache2
$ sudo systemctl disable apache2
Synchronizing state of apache2.service with SysV service script with
/lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install disable apache2
Removed /etc/systemd/system/multi-user.target.wants/apache2.service.
$ sudo apt purge --auto-remove apache2


验证端口是否不再被使用。`ss`命令再也找不到暴露端口 80 的应用程序:

$ sudo ss -ltpn | grep :80


## 设置防火墙规则

控制端口的另一种方式是使用操作系统级防火墙的帮助。在 Linux 上,您可以使用[Uncomplicated Firewall (UFW)](https://oreil.ly/iqiwv)。本节将简要介绍如何启用 UFW 及配置防火墙规则。

遵循最小权限原则,建议首先启用防火墙,并为*任何*传入和传出的网络流量设置拒绝规则。以下命令演示了实现这一目标的步骤:

$ sudo ufw allow ssh
Rules updated
Rules updated (v6)
$ sudo ufw default deny outgoing
Default outgoing policy changed to deny
(be sure to update your rules accordingly)
$ sudo ufw default deny incoming
Default incoming policy changed to deny
(be sure to update your rules accordingly)
$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup


你需要允许像`kubectl`这样的外部工具连接到运行在 6443 端口的 API 服务器。在控制平面节点上,执行以下命令以允许访问 API 服务器端口:

$ sudo ufw allow 6443
Rule added
Rule added (v6)


您需要重复相同的过程以打开控制平面和工作节点上的其他端口。确保所有不需要用于操作 Kubernetes 的其他端口都被阻止。

# 使用内核硬化工具

容器内运行的应用程序或进程可以进行系统调用。典型示例可能是 `curl` 命令执行 HTTP 请求。系统调用是用户空间中用于请求内核服务的程序抽象。我们可以使用内核硬化工具限制允许进行的系统调用。CKS 考试明确提到了两种工具,AppArmor 和 seccomp。我们将讨论这两种工具及其与 Kubernetes 集成的机制。

## 使用 AppArmor

[AppArmor](https://apparmor.net) 提供对在 Linux 系统上运行的程序的访问控制。该工具在用户空间调用的应用程序和底层系统功能之间实现了一个额外的安全层。例如,我们可以限制网络调用或文件系统交互。许多 Linux 发行版(如 Debian、Ubuntu、openSUSE)已经预装了 AppArmor。因此,不需要手动安装 AppArmor。不支持 AppArmor 的 Linux 发行版使用 [Security-Enhanced Linux (SELinux)](https://oreil.ly/CKBr7) 代替,其与 AppArmor 采取了类似的方法。了解 SELinux 超出了 CKS 考试的范围。

### 理解配置文件

定义程序能够执行或无法执行的规则在 AppArmor 配置文件中定义。每个配置文件在生效之前都需要加载到 AppArmor 中。AppArmor 提供了一个命令行工具,用于检查已加载的配置文件。执行命令 `aa-status` 可以查看所有加载配置文件的摘要。您会看到 AppArmor 已经带有一组默认的应用程序配置文件,用于保护 Linux 服务:

$ sudo aa-status
apparmor module is loaded.
31 profiles are loaded.
31 profiles are in enforce mode.
/snap/snapd/15177/usr/lib/snapd/snap-confine
...
0 profiles are in complain mode.
14 processes have profiles defined.
14 processes are in enforce mode.
/pause (11934) docker-default
...
0 processes are in complain mode.
0 processes are unconfined but have a profile defined.


配置文件模式决定运行时对规则的处理,如果匹配事件发生。AppArmor 区分两种配置文件模式:

强制执行

系统强制执行规则,并将违规情况报告并记录到系统日志中。您应该使用此模式来防止程序进行特定的调用。

抱怨

系统不强制执行规则,但会将违规情况记录到日志中。如果您希望了解程序调用的调用,请使用此模式。

示例 4-1 定义了一个自定义配置文件 `k8s-deny-write`,用于限制文件写入访问权限。该文件应放置在每个执行工作负载的工作节点的目录 `/etc/apparmor.d` 中。本书不详细解释所有规则。要了解更多信息,请参阅 [AppArmor wiki](https://oreil.ly/mNuWB)。

##### 示例 4-1\. 限制文件写入访问的 AppArmor 配置文件

include <tunables/global>

profile k8s-deny-write flags=(attach_disconnected) { 1

include <abstractions/base>

file, 2

deny /** w, 3
}


![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO1-1)

`profile` 关键字后的标识符是配置文件的名称。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO1-2)

应用到文件操作。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO1-3)

拒绝所有文件写入。

### 设置自定义配置文件

要将配置文件加载到 AppArmor 中,请在工作节点上运行以下命令:

$ sudo apparmor_parser /etc/apparmor.d/k8s-deny-write


该命令默认使用强制执行模式。要加载投诉模式的配置文件,请使用 `-C` 选项。现在 `aa-status` 命令将列出该配置文件以及默认配置文件。从输出中可以看到,配置文件以强制执行模式列出:

$ sudo aa-status
apparmor module is loaded.
32 profiles are loaded.
32 profiles are in enforce mode.
k8s-deny-write
...


AppArmor 还支持作为一个实用程序包的附加方便命令。如果你想使用它们,可以手动安装该包,并执行以下命令:

$ sudo apt-get update
$ sudo apt-get install apparmor-utils


安装完成后,您可以使用 `aa-enforce` 命令以强制执行模式加载配置文件,使用 `aa-complain` 命令以投诉模式加载配置文件。对于考试来说,直接使用标准的 `apparmor_parser` 命令可能更容易。

### 将配置文件应用到容器上

在使用 Pod 定义中的 AppArmor 规则之前,您需要确保一些前提条件。首先,容器运行时需要支持 AppArmor,以便规则生效。此外,运行 Pod 的工作节点上需要安装 AppArmor。最后,请确保加载了配置文件,如前一节所述。

示例 4-2 展示了一个在文件 `pod.yaml` 中定义的 Pod 的 YAML 清单。要将配置文件应用到容器上,需要设置一个特定的注解。注解键需要使用格式 `container.apparmor.security.beta.kubernetes.io/<container-name>` 中的键。在我们的案例中,容器名称为 `hello`。完整的键是 `container.apparmor.security.beta.kubernetes.io/hello`。注解的值遵循模式 `localhost/<profile-name>`。这里我们想要使用的自定义配置文件是 `k8s-deny-write`。有关配置选项的更多信息,请参阅 [Kubernetes 文档](https://oreil.ly/1o3zO)。

##### 示例 4-2\. 应用一个 AppArmor 配置文件到一个容器的 Pod

apiVersion: v1
kind: Pod
metadata:
name: hello-apparmor
annotations:
container.apparmor.security.beta.kubernetes.io/hello: \ 1
localhost/k8s-deny-write 2
spec:
containers:

  • name: hello 3
    image: busybox:1.28
    command: ["sh", "-c", "echo 'Hello AppArmor!' && sleep 1h"]

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO2-1)

由硬编码前缀和由斜杠字符分隔的容器名称组成的注解键。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO2-2)

在当前节点上指示的配置文件名称,由 `localhost` 表示。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO2-3)

容器名称。

我们已经准备好创建 Pod 了。运行 `apply` 命令,并指向 YAML 清单。等待 Pod 转换为“Running”状态:

$ kubectl apply -f pod.yaml
pod/hello-apparmor created
$ kubectl get pod hello-apparmor
NAME READY STATUS RESTARTS AGE
hello-apparmor 1/1 Running 0 4s


现在,您可以进入容器并执行文件写入操作:

$ kubectl exec -it hello-apparmor -- /bin/sh
/ # touch test.txt
touch: test.txt: Permission denied


如果尝试向容器文件系统写入文件,AppArmor 将会阻止此操作,并显示“Permission denied”的消息。

## 使用 seccomp

Seccomp,即“安全计算模式”,是另一个 Linux 内核特性,可以限制从用户空间到内核的调用。Seccomp 配置文件是定义限制系统调用及其参数的规则的机制。使用 Seccomp 可以降低利用 Linux 内核漏洞的风险。有关 Kubernetes 上 Seccomp 的更多信息,请参阅 [文档](https://oreil.ly/B8I5L)。

### 将默认的容器运行时配置文件应用到容器上

容器运行时(如 Docker Engine 或 containerd)附带默认的 seccomp 配置文件。默认的 seccomp 配置文件允许应用程序使用的最常用系统调用,同时禁止被认为危险的系统调用。

Kubernetes 在创建 Pod 时不会将默认容器运行时配置文件应用于容器,但可以通过设置安全上下文属性`seccompProfile`将 seccomp 配置文件类型设置为`RuntimeDefault`来启用它。您可以使用`SeccompDefault` [功能门](https://oreil.ly/m9g0G)在 Pod 级别启用该功能。示例 4-3 演示了其使用方法。

##### 示例 4-3. 一个应用了容器运行时配置文件提供的默认 seccomp 配置文件的 Pod

apiVersion: v1
kind: Pod
metadata:
name: hello-seccomp
spec:
securityContext:
seccompProfile:
type: RuntimeDefault 1
containers:

  • name: hello
    image: busybox:1.28
    command: ["sh", "-c", "echo 'Hello seccomp!' && sleep 1h"]

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO3-1)

应用默认的容器运行时配置文件。

使用`apply`命令并指向 YAML 清单文件可以启动 Pod。Pod 应该会转换为“运行”状态:

$ kubectl apply -f pod.yaml
pod/hello-seccomp created
$ kubectl get pod hello-seccomp
NAME READY STATUS RESTARTS AGE
hello-seccomp 1/1 Running 0 4s


在容器中执行的`echo`命令在安全性方面被默认的 seccomp 配置文件视为无问题。以下命令检查容器的日志:

$ kubectl logs hello-seccomp
Hello seccomp!


允许调用并导致将消息“Hello seccomp!”写入标准输出。

### 设置自定义配置文件

除了默认容器运行时配置文件之外,您还可以创建和设置自定义配置文件。这些文件的标准目录是`/var/lib/kubelet/seccomp`。我们将在子目录`profiles`中组织我们的自定义配置文件。如果目录不存在,则创建该目录:

$ sudo mkdir -p /var/lib/kubelet/seccomp/profiles


我们决定在配置文件目录中的文件`mkdir-violation.json`中创建我们的自定义配置文件。示例 4-4 显示了配置文件定义的详细信息。简而言之,规则集禁止使用`mkdir`系统调用。

##### 示例 4-4. 阻止执行`mkdir`系统调用的 seccomp 配置文件

{
"defaultAction": "SCMP_ACT_ALLOW", 1
"architectures": ![2
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
],
"syscalls": [
{
"names": [
"mkdir"
],
"action": "SCMP_ACT_ERRNO" 3
}
]
}


![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO4-1)

默认操作适用于所有系统调用。在这里,我们将使用`SCMP_ACT_ALLOW`允许所有系统调用。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO4-2)

您可以过滤默认操作应用于的特定体系结构。该字段的定义是可选的。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO4-3)

默认操作可以通过声明更细粒度的规则来覆盖。`SCMP_ACT_ERRNO`操作将阻止执行`mkdir`系统调用。

将自定义配置文件放入目录`/var/lib/kubelet/seccomp`中不会自动将规则应用于 Pod。您仍然需要配置 Pod 以使用它。

### 将自定义配置文件应用于容器

应用自定义配置文件与应用默认的容器运行时配置文件遵循类似的模式,但存在一些小差异。正如您在示例 4-5 中所看到的,我们将安全配置文件的`seccompProfile`属性指向文件`mkdir-violation.json`,并将类型设置为`Localhost`。

##### 示例 4-5\. 应用自定义 seccomp 配置文件的 Pod 阻止了`mkdir`系统调用。

apiVersion: v1
kind: Pod
metadata:
name: hello-seccomp
spec:
securityContext:
seccompProfile:
type: Localhost 1
localhostProfile: profiles/mkdir-violation.json 2
containers:

  • name: hello
    image: busybox:1.28
    command: ["sh", "-c", "echo 'Hello seccomp!' && sleep 1h"]
    securityContext:
    allowPrivilegeEscalation: false

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO5-1)

指的是当前节点上的一个配置文件。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_system_hardening_CO5-2)

应用名称为`mkdir-violation.json`的配置文件位于子目录`profiles`中。

使用声明性的`apply`命令创建 Pod。等待 Pod 转换为“运行”状态。

$ kubectl apply -f pod.yaml
pod/hello-seccomp created
$ kubectl get pod hello-seccomp
NAME READY STATUS RESTARTS AGE
hello-seccomp 1/1 Running 0 4s


进入容器的 Shell 中,验证 seccomp 是否正确执行了应用的规则:

$ kubectl exec -it hello-seccomp -- /bin/sh
/ # mkdir test
mkdir: can't create directory test: Operation not permitted


正如您在输出中所看到的,在尝试执行`mkdir`命令时,操作会生成一个错误消息。违反了自定义配置文件中的规则。

# 概要

处理安全方面不仅限于 Kubernetes 集群组件或工作负载。您可以在主机系统层面上做很多事情。我们讨论了不同的操作系统功能以及如何利用它们来减少潜在的安全漏洞。

许多操作系统提供了丰富的软件包和服务,以提供更丰富的用户体验。识别不需要操作 Kubernetes 集群的功能非常重要。彻底清理不必要的软件包和服务,并关闭不需要的端口。您还需要严格控制哪些用户被允许访问特定目录、文件和应用程序。利用 Linux 的用户管理来限制权限。

容器中运行的应用程序和进程通常会进行系统调用,这是非常普遍的。您可以使用 Linux 内核加固工具如 AppArmor 和 seccomp 来限制这些调用。只允许那些对您应用运行所需至关重要的系统调用。

# 考试要点

具备基本的 Linux 操作系统工具使用理解。

CKS 考试主要关注 Kubernetes 中的安全功能。该领域跨越边界到 Linux 操作系统安全特性。熟悉 Linux 特定的工具和安全方面的高级内容不会有害。独立于本章涵盖的内容,建议熟悉 Linux 上的服务、包管理、用户管理和网络管理。

知道如何将 Linux 内核加固工具与 Kubernetes 集成。

AppArmor 和 seccomp 只是一些可以与 Kubernetes 集成的内核加固工具,用于限制容器中发出的系统调用。实践加载配置文件并将其应用于容器的过程。为了拓宽视野,您可能还希望探索与 Kubernetes 并行工作的其他内核功能,如[SELinux](https://oreil.ly/DrGbB)或[sysctl](https://oreil.ly/GyUoc)。

# 样例练习

这些练习的解决方案可以在附录中找到。

1.  进入已检出的 GitHub 代码库 [*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ) 的目录 *app-a/ch04/close-ports*。使用命令 `vagrant up` 启动运行集群的虚拟机(VMs)。该集群包含一个名为 `kube-control-plane` 的控制平面节点和一个名为 `kube-worker-1` 的工作节点。完成后,使用 `vagrant destroy -f` 关闭集群。

    确定在 VM `kube-worker-1` 上监听端口 21 的进程。为降低攻击者利用该端口的风险,决定关闭该端口对应的进程。

    *先决条件:* 此练习需要安装工具 [Vagrant](https://oreil.ly/FiyeH) 和 [VirtualBox](https://oreil.ly/WW8IK)。

1.  进入已检出的 GitHub 代码库 [*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ) 的目录 *app-a/ch04/apparmor*。使用命令 `vagrant up` 启动运行集群的虚拟机(VMs)。该集群包含一个名为 `kube-control-plane` 的控制平面节点和一个名为 `kube-worker-1` 的工作节点。完成后,使用 `vagrant destroy -f` 关闭集群。

    创建一个名为 `network-deny` 的 AppArmor 配置文件。该配置文件应禁止任何入站和出站网络流量。将该配置添加到处于强制执行模式的 AppArmor 规则集中。将该配置应用于运行在 `default` 命名空间中的名为 `network-call` 的 Pod。检查 Pod 的日志以确保无法进行网络调用。

    *先决条件:* 此练习需要安装工具 [Vagrant](https://oreil.ly/FiyeH) 和 [VirtualBox](https://oreil.ly/WW8IK)。

1.  进入已检出的 GitHub 代码库 [*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ) 的目录 *app-a/ch04/seccomp*。使用命令 `vagrant up` 启动运行集群的虚拟机(VMs)。该集群包含一个名为 `kube-control-plane` 的控制平面节点和一个名为 `kube-worker-1` 的工作节点。完成后,使用 `vagrant destroy -f` 关闭集群。

    创建一个名为 `audit.json` 的 seccomp 配置文件,记录标准 seccomp 目录中的所有系统调用。将该配置应用于运行在 `default` 命名空间中的名为 `network-call` 的 Pod。检查日志文件 `/var/log/syslog` 查看日志条目。

    *先决条件:* 此练习需要安装工具 [Vagrant](https://oreil.ly/FiyeH) 和 [VirtualBox](https://oreil.ly/WW8IK)。

1.  创建一个名为 `sysctl-pod` 的新 Pod,使用镜像 `nginx:1.23.1`。设置 sysctl 参数 `net.core.somaxconn` 为 1024 和 `debug.iotrace` 为 `1`。检查 Pod 的状态。


# 第五章:最小化微服务漏洞

在 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 说明了该场景。

![ckss 0501](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](https://oreil.ly/cJWXA)。容器级别的安全属性可以在 [SecurityContext API](https://oreil.ly/XOy2a) 中找到。

## 强制使用非 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

容器在启动过程中失败,状态为 `CreateContainer​Confi⁠gError`。查看 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](https://oreil.ly/EnvzT)。示例 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`命令](https://oreil.ly/YEcOs)更改内核设置。

# 使用特权模式的容器

配置容器以使用特权模式应该是一个罕见的情况。大多数运行在容器中的应用程序和进程不需要超出容器命名空间之外的提升权限。如果遇到配置为使用特权模式的 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。攻击者将乐意利用这种设置获取优势。

![ckss 0502](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 文档](https://oreil.ly/DYziy)。

## 为命名空间执行 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)](https://oreil.ly/oK9pI) 是一款开源的通用策略引擎,用于强制执行规则。OPA 不特定于 Kubernetes,可以在其他技术堆栈中使用。其好处之一是能够以非常灵活的方式定义策略。您可以使用名为 [Rego](https://oreil.ly/0_mA8) 的查询语言编写自己的规则。在 Rego 中编写的验证逻辑确定是否接受或拒绝对象。

[Gatekeeper](https://oreil.ly/AyVjP) 是 Kubernetes 的一个扩展,使用 OPA。Gatekeeper 允许为任何类型的 Kubernetes API 原语定义和强制执行自定义策略。因此,它比 PSA 更加灵活,但需要更复杂的知识来制定这些规则。Gatekeeper 参与了讨论的 *准入控制* 阶段,详见 “处理请求”。以下策略列表试图给您展示 Gatekeeper 的可能性:

+   确保所有 Service 对象都需要定义一个带有键 `team` 的标签。

+   确保 Pods 定义的所有容器镜像都从公司内部注册表中拉取。

+   确保 Deployments 需要至少控制三个副本。

在撰写本文时,Gatekeeper 允许通过拒绝对象创建来强制执行策略,如果未满足要求。Gatekeeper 的未来版本还可能提供一种在创建时改变对象的机制。例如,您可能希望为创建的任何对象添加特定的标签键值对。该变动将自动处理添加这些标签。

## 安装 Gatekeeper

安装 Gatekeeper 相对简单。您只需从 Gatekeeper 项目提供的 YAML 清单创建一堆 Kubernetes 对象即可。您需要有集群管理员权限来正确安装 Gatekeeper。以下命令展示了应用最新发布版的 `kubectl` 命令。更多信息,请参阅 [安装手册](https://oreil.ly/CyZ1c)。

$ 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 1
validation:
openAPIV3Schema: 2
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: | 3
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])
    }

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO1-1)

声明用于约束的类型。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO1-2)

指定约束的验证模式。在这种情况下,我们允许传递名为 `labels` 的属性,其中捕获所需的标签键。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO1-3)

使用 Rego 检查标签的存在并将其与所需键的列表进行比较。

约束本质上是约束模板的实现。它使用约束模板定义的种类,并填充终端用户提供的数据。在 示例 5-10 中,种类是 `K8sRequiredLabels`,我们在约束模板中定义了它。我们正在匹配命名空间,并期望它们定义具有键 `app` 的标签。

##### 示例 5-10\. 定义策略“数据”的 OPA 约束

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels 1
metadata:
name: ns-must-have-app-label-key
spec:
match: 2
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters: 3
labels: ["app"]


![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO2-1)

使用约束模板定义的种类。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO2-2)

定义约束模板应用的 API 资源。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO2-3)

声明 `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](https://oreil.ly/1VV5e) 中找到许多其他示例。尽管 CKS 课程没有明确说明,您可能还想查看最近在 Kubernetes 社区中获得了很多关注的项目 [Kyverno](https://kyverno.io)。

# 管理秘密

Kubernetes 安全功能讨论不完整,没有提到 Secrets。我假设你已经非常熟悉 API 原语 Secret 用于定义敏感数据以及在 Pod 中消费的不同选项。考虑到这个主题已经是 CKA 考试的一部分,我在这里不再赘述。更多信息,请参阅《[*Certified Kubernetes Administrator (CKA) Study Guide*](https://oreil.ly/cka-study-guide)》中的相关章节或者[Kubernetes 文档](https://oreil.ly/1afoc)。我在“Configuring a Container with a ConfigMap or Secret”章节中讨论在容器中消费 ConfigMaps 和 Secrets 时的安全方面。

CKS 考试更加强调 Secret 管理的更专业方面。其中一个场景是处理可以分配给服务账户的 Secret,我们已经提到过。回顾“Creating a Secret for a service account”以刷新你对该主题的记忆。由于我们不会在这里讨论所有内置的 Secret 类型,请阅读相关章节了解它们的用途和创建方式在[Kubernetes 文档](https://oreil.ly/YU7Yy)中。

存储 Secrets 键值对的中心位置是 etcd。让我们来看看,如果攻击者能够访问 Kubernetes 后端存储集群数据,可能会出现的潜在问题。

## 场景:攻击者获得访问运行 etcd 的节点

etcd 运行的位置取决于你的 Kubernetes 集群的[拓扑结构](https://oreil.ly/bf5Gt)。为了这个场景,我们假设 etcd 运行在控制平面节点上。存储在 etcd 中的任何数据都以未加密形式存在,因此访问控制平面节点允许以明文形式读取 Secrets。图 5-3 展示了攻击者访问控制平面节点和因此在 etcd 中的未加密 Secrets。

![ckss 0503](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`。请按照[安装手册](https://oreil.ly/wpCkO)安装工具。在 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 1
      providers:
    • aescbc:
      keys:
      - name: key1
      secret: W68xlPT/VXcOSEZJvWeIvkGJnGfQNFpvZYfT9e+ZYuY= 2
    • identity: {}

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO3-1)

定义要在 etcd 中加密的 API 资源。我们这里只加密秘密数据。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_minimizing_microservice_vulnerabilities_CO3-2)

分配给 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 文档](https://oreil.ly/uIylK)。在那里,您将找到关于其他加密提供者的额外信息,如何轮换解密密钥以及考虑用于高可用 (HA) 集群设置的过程。

# 理解容器运行时沙盒

容器在与主机环境隔离的容器运行时中运行。运行在容器中的进程或应用程序可以通过 syscalls 与内核交互。现在,我们可以在单个 Kubernetes 集群节点上运行多个容器(由 Pod 控制),因此使用相同的内核。在某些条件下,漏洞可能导致在容器运行的进程“突破”其隔离环境,并访问在同一主机上运行的另一个容器。*容器运行时沙盒*与常规容器运行时并行运行,但通过加强进程隔离添加了额外的安全层。

有几种情况下使用容器运行时沙盒可能是有意义的。例如,您的 Kubernetes 集群使用相同的基础设施处理不同客户的工作负载,这种称为多租户环境。另一个希望依赖更强容器隔离性的原因是,您可能不信任从公共注册表拉取的容器镜像中运行的进程或应用程序,特别是当您无法验证创建者或其运行时行为时。

## 情景:攻击者获取对另一个容器的访问权限

在这种情况下,我们面对的是开发者从公共注册表拉取容器镜像,如 Pod 所引用。该容器未经安全漏洞扫描。攻击者可以推送容器镜像的新标签来执行恶意代码。在实例化从该镜像运行的容器之后,运行在容器 1 的内核组中的恶意代码可以访问运行在容器 2 中的进程。正如您在图 5-4 中所看到的,两个容器使用同一主机系统的内核。

![ckss 0504](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0504.png)

###### 图 5-4\. 攻击者获取对另一个容器的访问权限

一般来说,盲目信任公共容器镜像并不是一个好主意。确保这样的容器镜像以更高的隔离运行的一种方法是容器运行时沙盒。下一节将向您介绍两种实现方式,这两种方式在课程中都有明确提到。

## 可用的容器运行时沙盒实现

在本书中,我们只讨论两种容器运行时沙箱实现,[Kata Containers](https://katacontainers.io)和[gVisor](https://gvisor.dev)。

安装和配置 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](https://oreil.ly/MlSET)描述了使用`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:定量比较”](https://oreil.ly/PBfEn)。

$ 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 的通信,提取敏感信息,然后用于更高级的攻击向量。

![ckss 0505](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 文档](https://oreil.ly/QvtEQ)中有描述。

在大多数情况下,Kubernetes 管理员依赖于 Kubernetes 服务网格来实现 mTLS,而不是手动实现。Kubernetes 服务网格(如 Linkerd 或 Istio)是一种工具,用于为集群添加横切功能,如可观察性和安全性。

另一个选项是使用透明加密来确保流量不会在传输中未加密。一些流行的 CNI 插件,如[Calico](https://oreil.ly/XZSpx)和[Cilium](https://oreil.ly/Qsqq_),已经增加了对[WireGuard](https://www.wireguard.com)的支持。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 作为一种功能。

# 样例练习

这些练习的解决方案可以在 附录 中找到。

1.  创建一个名为 `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` 的文件。检查文件分配的群组。数值是多少?退出容器。

1.  创建一个 Pod 安全性审核(PSA)规则。在名为 `audited` 的命名空间中,创建一个 Pod 安全性标准(PSS),其级别为 `baseline`,该信息应该渲染到控制台。

    尝试在违反 PSS 的命名空间中创建一个 Pod,并在控制台日志中输出消息。您可以提供任何名称、容器映像和安全配置。Pod 是否被创建?需要配置哪些 PSA 级别来防止创建该 Pod?

1.  在集群上安装 Gatekeeper。创建一个 Gatekeeper ConstraintTemplate 对象,定义一个副本集所控制的副本数的最小和最大值。实例化一个使用该 ConstraintTemplate 的约束对象。设置最小副本数为 3,最大副本数为 10。

    创建   创建一个部署对象,将副本数设置为 15。门禁不允许创建该部署、副本集和 Pod,应该渲染一个错误消息。尝试再次创建副本数为 7 的部署对象,验证是否成功创建了所有对象。

1.  使用 `aescbc` 提供者为 etcd 配置加密。创建一个类型为 `Opaque` 的密钥对象。提供键值对 `api-key=YZvkiWUkycvspyGHk3fQRAkt`。使用 etcdctl 查询该 Secret 的值,查看加密值是什么。

1.  导航到从 GitHub 仓库[*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ)checkout 的目录 *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](https://oreil.ly/FiyeH) 和 [VirtualBox](https://oreil.ly/WW8IK)。


# 第六章:供应链安全

较早的章节主要集中在保护 Kubernetes 集群及其组件、用于运行集群节点的操作系统基础设施以及在现有容器镜像上运行工作负载的操作方面。本章退一步,深入探讨了设计、构建和优化容器镜像的过程、最佳实践和工具。

有时,你可能不想创建自己的容器镜像,而是消耗由其他团队或公司生产的现有镜像。在使用它们运行工作负载之前,手动或自动扫描已知漏洞的容器镜像应该成为你的审查过程的一部分。我们将讨论与 CKS 考试相关的一些选项,用于识别、分析和减轻预构建容器镜像的安全风险。

在高层次上,本章涵盖以下概念:

+   减小基础镜像的占用空间

+   保护供应链

+   使用用户工作负载的静态分析

+   扫描已知漏洞的镜像

# 减小基础镜像的占用空间

表面上看,构建容器镜像的过程似乎很简单;然而,魔鬼往往隐藏在细节中。对于新手来说,要避免构建过大、充满漏洞并且不优化容器层缓存的容器镜像可能并不明显。在本章的课程中,我们将在 Docker 容器引擎的帮助下解决所有这些问题。

## 场景:攻击者利用容器漏洞

在定义 Dockerfile 时,你需要做的第一个决定之一是选择一个基础镜像。基础镜像提供操作系统和额外的依赖项,并可能暴露 shell 访问权限。

在像 Docker Hub 这样的公共注册表上,你可以选择的一些基础镜像体积庞大,并且可能包含你不一定需要在其中运行应用程序的功能。操作系统本身以及基础镜像提供的任何依赖项都可能存在漏洞。

在 图 6-1 中,攻击者能够通过访问容器获取关于其详细信息的信息。这些漏洞现在可以被用作更高级攻击的跳板。

![ckss 0601](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0601.png)

###### 图 6-1\. 攻击者利用容器镜像漏洞

建议使用一个功能和依赖项最少的基础镜像。接下来的几节将解释创建更优化的基础镜像的方法,这将使构建速度更快,从容器注册表下载速度更快,并且通过减少臃肿的镜像来减小攻击面。接下来的几节将涉及最重要的技术。你可以在[Docker 文档](https://oreil.ly/43Diy)中找到更详细的 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 镜像](https://oreil.ly/J6Vra)进一步减小容器镜像的大小和攻击面。以下命令下载`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 文档](https://oreil.ly/IRjP3)中找到更多信息。

## 使用多阶段方法构建容器镜像

作为开发者,您可以决定将应用程序代码作为 Dockerfile 指令的一部分进行构建。这个过程可能包括编译代码和构建一个应该成为容器镜像入口点的二进制文件。拥有实施这一过程所需的所有工具和依赖项将自动增加容器镜像的大小,而且您在运行时也不再需要这些依赖项。

在 Docker 中使用[多阶段构建](https://oreil.ly/znQc3)的思想是将构建阶段与运行阶段分离。因此,构建阶段中需要的所有依赖项在完成进程后都会被丢弃,因此不会出现在最终的容器镜像中。这种方法通过去除所有不必要的内容,大大减小了容器镜像的大小。

尽管我们不会详细介绍制作和完全理解多阶段 Dockerfile 的细节,但我想在高层次上展示一下它们的区别。我们将首先展示一个 Dockerfile,它使用编程语言 Go 构建和测试一个简单的程序,如示例 6-1 所示。实质上,我们使用了一个包含 Go 1.19.4 的[基础镜像](https://oreil.ly/nwLtT)。Go 运行时提供了`go`命令,我们将调用它来执行测试并构建应用程序的二进制文件。

##### 示例 6-1\. 使用 Go 基础镜像构建和测试 Go 程序

FROM golang:1.19.4-alpine 1
WORKDIR /app

COPY go.mod .
COPY go.sum .
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go test -v 2
RUN go build -o /go-sample-app . 3
CMD ["/go-sample-app"]


![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO1-1)

使用了一个 Go 基础镜像

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO1-2)

执行针对应用代码的测试。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO1-3)

构建 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 1
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 2
RUN go build -o ./out/go-sample-app . 3

FROM alpine:3.17.0 4
RUN apk add ca-certificates
COPY --from=build /tmp/go-sample-app/out/go-sample-app /app/go-sample-app 5
CMD ["/app/go-sample-app"]


![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO2-1)

在名为`build`的阶段中,使用了一个 Go 基础镜像来构建和测试程序。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO2-2)

执行针对应用代码的测试。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO2-3)

构建 Go 应用程序的二进制文件。

![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO2-4)

使用一个更小的基础镜像来运行容器中的应用程序。

![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO2-5)

复制在`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 仓库](https://oreil.ly/ZbeZl) 中获取更多信息。

Dive

Dive 是一个用于探索嵌入容器镜像中层的工具。它可以帮助您轻松识别不必要的层,进而进行进一步优化。Dive 的代码和文档可在 [GitHub 仓库](https://oreil.ly/UBqqj) 中找到。

这只是容器镜像优化工具的简短列表。在 “工作负载的静态分析” 中,我们将看看其他专注于 Dockerfile 和 Kubernetes 清单的静态分析工具。

# 保护供应链

供应链自动化生产容器镜像并在运行时环境中操作的过程,在这种情况下是 Kubernetes。我们已经提到了一些可以集成到供应链中以支持容器镜像优化方面的工具。在本节中,我们将扩展到针对安全方面的实践。参考书籍 [*Container Security*](https://learning.oreilly.com/library/view/container-security/9781492056690/) 作者 Liz Rice(O’Reilly)以获取更多信息。

## 签署容器镜像

在将容器镜像推送到容器注册表之前,您可以对其进行签名。签名可以通过`docker trust sign`命令实现,该命令会向容器镜像添加一个签名,即所谓的图像摘要。图像摘要是根据容器镜像的内容派生的,通常以 SHA256 的形式表示。在消费容器镜像时,Kubernetes 可以将图像摘要与镜像内容进行比较,以确保其未被篡改。

## 场景:攻击者将恶意代码注入容器镜像

验证图像摘要的 Kubernetes 组件是 kubelet。如果您将 [image pull policy](https://oreil.ly/0wSjy) 配置为 `Always`,即使 kubelet 在之前已经下载和验证过容器镜像,它也会从容器注册表中查询图像摘要。

攻击者可以尝试修改现有容器镜像的内容,注入恶意代码,并将其上传到具有相同标签的容器注册表中,如 图 6-2 所示。然后,运行在容器中的恶意代码可能会将敏感信息发送到攻击者可访问的第三方服务器。

![ckss 0602](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`。

![ckss 0603](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 时,容器运行时引擎将从容器注册表下载声明的容器镜像(如果本地尚不可用)。可以使用[镜像拉取策略](https://oreil.ly/ZBMnH)来进一步调整此运行时行为。

图像名称中的前缀声明了注册表的域名;例如,`gcr.io/google-containers/debian-base:v1.0.1` 指的是 `google-containers/debian-base:v1.0.1` 在[Google Cloud 容器注册表](https://oreil.ly/QFxfY)中,由 `gcr.io` 表示。如果在容器镜像声明中不指定域名,则容器运行时将尝试从 `docker.io`,即[Docker Hub 容器注册表](https://hub.docker.com) 解析它。

## 情景:攻击者上传恶意容器镜像

虽然从公共容器注册表解析容器镜像很方便,但也伴随着风险。任何拥有这些容器注册表登录凭据的人都可以上传新的镜像。通常,使用容器镜像并不需要账户。

如图 6-4 所示(#公共注册表攻击者),攻击者可以使用窃取的凭据上传包含恶意代码的容器镜像到公共注册表。任何引用该注册表中容器镜像的容器都将自动运行恶意代码。

![ckss 0604](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0604.png)

###### 图 6-4\. 攻击者上传恶意容器镜像

在企业级别上,您需要控制信任的容器注册表。建议在公司网络内设置自己的容器注册表,可以完全控制和管理。或者,您可以在云提供商环境中设置一个私有容器注册表,不允许任何其他人访问。

您可以选择其中一个主要的二进制仓库管理器,例如[JFrog Artifactory](https://oreil.ly/jdF_6)。该产品完全支持存储、扫描和提供容器镜像。任何使用容器镜像的消费者应仅允许从您的白名单容器注册表中拉取镜像。所有其他容器注册表都应该被拒绝。

## 使用 OPA GateKeeper 白名单允许的镜像注册表

通过 **OPA Gatekeeper** 控制容器注册表的使用方式。我们讨论了 “理解开放策略代理(OPA)和 Gatekeeper” 的安装过程和功能。在这里,我们将触及约束模板和允许信任容器注册表的约束条件。

示例 6-7 展示了我直接从 OPA Gatekeeper 库获取的 [约束模板](https://oreil.ly/hvgnr)。作为输入属性,约束模板定义了一个字符串数组,表示容器注册表的前缀。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 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 展示了高级别的工作流程。

![ckss 0605](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0605.png)

###### 图 6-5. 拦截特定 Pod API 调用并使用 Webhook 处理它

[ImagePolicyWebhook](https://oreil.ly/JxmUu) 准入控制器插件是我们可以配置用于拦截 Kubernetes API 调用的插件之一。 您可以从其名称推断出插件的功能。 它为 Pod 中定义的所有容器镜像定义策略。 为了与定义的策略比较容器镜像,插件通过 HTTPS 调用到一个服务外部于 Kubernetes 的 webhook。 外部服务然后决定如何验证数据。 在准入控制器插件的上下文中,外部服务也被称为*后端*。

## 实施后端应用

后端应用可以使用您选择的编程语言实现。 它必须满足以下三个要求:

1.  这是一个可以处理 HTTPS 请求的 Web 应用程序。

1.  它可以接受和解析 JSON 请求有效载荷。

1.  它可以发送一个 JSON 响应有效载荷。

实施后端应用不是 CKS 考试的一部分,但您可以在本书的[GitHub 仓库](https://oreil.ly/OF4fF)中找到一个基于 Go 的示例实现。 请注意,该代码不被视为生产就绪。

以下命令演示了应用程序在`https://localhost:8080/validate`上的运行时行为。 您可以在[Kubernetes 文档](https://oreil.ly/8GaQe)中找到示例请求和响应 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 1
    configuration:
    imagePolicy:
    kubeConfigFile: /etc/kubernetes/admission-control/
    imagepolicywebhook.kubeconfig 2
    allowTTL: 50
    denyTTL: 50
    retryBackoff: 500
    defaultAllow: false 3

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO3-1)

提供了 ImagePolicyWebhook 插件的配置。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO3-2)

指向用于配置后端的配置文件。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO3-3)

如果无法访问后端,则拒绝 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 1
    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

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_supply_chain_security_CO4-1)

后端服务的 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 所示。

![ckss 0606](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*](https://oreil.ly/8CR5M)(Addison-Wesley Professional)。

## 使用 Hadolint 分析 Dockerfile

[Haskell Dockerfile Linter](https://oreil.ly/C9bvu),也称为 hadolint,是 Dockerfile 的一个代码检查工具。该工具根据 Docker 文档页面上列出的 [最佳实践](https://oreil.ly/Fwksr) 检查 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](https://kubesec.io) 是分析 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 详情](https://oreil.ly/DDlVa)。该页面列出并按分数排名已知的漏洞(CVE)。自动化工具可以识别嵌入容器镜像中的软件组件,将其与中央漏洞数据库进行比较,并通过其严重性标记问题。

在 CKS 课程中明确提到的具备此能力的开源工具之一是[Trivy](https://oreil.ly/hqPHH)。Trivy 可以以不同的操作模式运行:作为命令行工具、在容器中、作为 GitHub Action 配置在持续集成工作流中、作为 IDE VSCode 的插件以及作为 Kubernetes 操作者。关于可用安装选项和流程的概述,请参阅[Trivy 文档](https://oreil.ly/qB_c8)。在考试期间,您无需安装此工具,它已经预先配置好。您只需要理解如何运行它、如何解释和修复找到的漏洞即可。

假设您已安装了 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`。

![ckss 0607](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](https://kyverno.io)。

对一个容器镜像进行签名,并使用哈希值验证。

构建容器镜像后,请确保也为其创建摘要。将容器镜像及其摘要发布到注册表。练习如何在 Pod 定义中使用摘要,并验证 Kubernetes 在拉取容器镜像时的行为。

理解如何配置 ImagePolicyWebhook 准入控制器插件。

你不需要为 ImagePolicyWebhook 编写后端。这超出了考试范围,并需要掌握一门编程语言。但你确实需要了解如何在 API 服务器配置中启用该插件。即使没有运行的后端应用程序,我建议你练习这个工作流程。

知道如何修复静态分析工具产生的警告。

CKS 课程不指定用于分析 Dockerfile 和 Kubernetes 清单的特定工具。在考试期间,您可能会被要求运行一个特定的命令,该命令将生成一系列错误和/或警告消息。理解如何解释这些消息,并在相关资源文件中修复它们是很重要的。

练习使用 Trivy 来识别和修复安全漏洞。

CKS 的常见问题解答中列出了 Trivy 的文档页面。因此,可以合理地假设 Trivy 可能会在其中的一个问题中出现。您需要了解调用 Trivy 扫描容器镜像的不同方式。生成的报告将清楚地指示需要修复的内容及找到的漏洞的严重程度。考虑到您不能轻易修改容器镜像,您可能会被要求标记运行具有已知漏洞的容器镜像的 Pod。

# 示例练习

这些练习的解决方案可以在附录中找到。

1.  看一下以下的 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`命令。容器镜像的最终大小应更小,但应用程序仍应正常运行。

1.  在你的 Kubernetes 集群中安装 Kyverno。您可以在[文档](https://oreil.ly/yxlLe)中找到安装说明。

    应用描述在[文档页面](https://oreil.ly/Kdj1k)上的“限制镜像注册表”策略。唯一允许的注册表应为`gcr.io`。禁止使用任何其他注册表。

    创建一个定义了容器镜像`gcr.io/google-containers/busybox:1.27.2`的 Pod。创建 Pod 应该失败。使用容器镜像`busybox:1.27.2`创建另一个 Pod。Kyverno 应该允许创建该 Pod。

1.  在 YAML 清单 `pod-validate-image.yaml` 中使用容器镜像 `nginx:1.23.3-alpine` 定义一个 Pod。从 Docker Hub 检索容器镜像的摘要。使用 SHA256 哈希验证容器镜像内容。创建 Pod。Kubernetes 应能够成功拉取容器镜像。

1.  使用 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 的建议。修复所有消息的根本原因,以确保再次执行工具时不会报告任何额外的建议。

1.  转到已检出的 GitHub 仓库 [*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ) 的目录 *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 描述了这种情况。

![ckss 0701](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0701.png)

###### 图 7-1。行为分析工具记录的恶意事件

管理员决定自行安装行为分析工具。该工具将持续监视特定事件并几乎即时记录它们。管理员现在拥有有效的机制来检测入侵并采取行动。

与考试相关的行为分析工具中包括[Falco](https://falco.org),[Tracee](https://oreil.ly/ibXcO)和[Tetragon](https://oreil.ly/q15oU)。在本书中,我们将只关注 Falco,因为它是考试期间可用文档页面链接之一。

## 理解 Falco

Falco 通过观察主机和容器级活动来帮助检测威胁。以下是 Falco 可能监控的一些事件示例:

+   在文件系统中特定位置读取或写入文件

+   打开容器的 shell 二进制文件,例如`/bin/bash`打开 bash shell

+   尝试向不良 URL 进行网络调用

Falco 部署了一组传感器来监听配置的事件和条件。每个传感器由一组规则组成,将事件映射到数据源。当规则匹配特定事件时会产生警报。警报将发送到输出通道以记录事件,例如标准输出、文件或 HTTPS 端点。Falco 允许同时启用多个输出通道。图 7-2 展示了 Falco 的高级架构。

![ckss 0702](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”](https://oreil.ly/PwGWx)是一个免费的视频课程,详细解释了产品的所有细节。要开始学习,您只需注册一个账户。此外,我建议您阅读[*Practical Cloud Native Security with Falco*](https://oreil.ly/0WHIR),这本由 Loris Degioanni 和 Leonardo Grasso(O’Reilly 出版)合著的书籍采用了面向初学者的友好方法来学习 Falco。

## 安装 Falco

Falco 可以作为主机系统上的二进制文件或 Kubernetes 中的 DaemonSet 对象安装。您可以放心地假设 Falco 已经预安装在考试环境中。有关安装过程的更多信息,请查看[Falco 文档的相关部分](https://oreil.ly/MB6wU)。以下步骤简要说明了在 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 配置选项”](https://oreil.ly/sfHW9) 获取所有可用选项的完整参考。

### 默认规则

名为 `falco_rules.yaml` 的文件定义了一组预安装规则。Falco 认为这些规则是默认应用的。其中包括检查当打开容器的 shell 或执行写操作到系统目录时创建警报的规则。您可以在 [“规则示例”页面](https://oreil.ly/fxHQm) 上找到其他示例及其相应的规则定义。

### 自定义规则

名为 `falco_rules.local.yaml` 的文件允许您定义自定义规则并覆盖默认规则。在安装 Falco 时,该文件仅包含注释掉的规则,为您提供添加自己规则的起点。您可以在 Falco 文档中找到 [编写自定义规则的指导](https://oreil.ly/mJnPo)。

### Kubernetes 特定规则

名为 `k8s_audit_rules.yaml` 的文件定义了 [Kubernetes 特定规则](https://oreil.ly/d5FGD),除了记录系统调用事件。典型示例是“当命名空间被删除时记录事件”或“当创建 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 规则,请查看相关的[文档页面](https://oreil.ly/PD1ro)。

### 规则

*规则*是生成警报的条件。它还定义了警报的输出消息。输出消息可以包含硬编码的消息并结合内置变量。警报记录在`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 1
    condition: (evt.type in (execve, execveat) and evt.dir=<)

  • macro: container 1
    condition: (container.id != host)

  • macro: container_entrypoint 1
    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 1
    condition: (never_true)

  • rule: Terminal shell in container 2
    desc: A shell was used as the entrypoint/exec point into a container with an
    attached terminal.
    condition: > 3
    spawned_process and container
    and shell_procs and proc.tty != 0
    and container_entrypoint
    and not user_expected_terminal_shell_in_container_conditions
    output: > 4
    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 5
    tags: [container, shell, mitre_execution] 6


![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO1-1)

定义宏,一种可在多个规则中重复使用的条件,通过名称引用。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO1-5)

指定规则的名称。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO1-6)

由多个宏组成的聚合条件。

![4](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO1-7)

如果发生事件,警报消息应该。消息可能使用内置字段引用运行时值。

![5](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO1-8)

指示规则违反的严重程度。

![6](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO1-9)

将规则集分为相关规则组,以便管理。

有时候您可能希望更改现有的规则。下一节将解释如何最好地覆盖默认规则。

## 覆盖现有规则

不要直接修改 `/etc/falco/falco_rules.yaml` 中的规则定义,我建议您在 `/etc/falco/falco_rules.local.yaml` 中重新定义规则。这样做可以帮助您在想要消除修改或在过程中出现任何错误时回退到原始规则定义。规则定义需要在集群的所有工作节点上进行更改。

在 示例 7-7 中展示的规则定义通过将优先级更改为 `ALERT` 并将输出格式更改为通过 [内置字段](https://oreil.ly/z5oAk) 来包含新格式来覆盖名为 “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 1
    priority: ALERT 2
    tags: [container, shell, mitre_execution]

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO2-1)

简化违规时生成的日志输出。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO2-2)

以 `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,,nginx


除了覆盖现有的 Falco 规则,您还可以在 `/etc/falco/falco_rules.local.yaml` 中定义自己的自定义规则。编写自定义规则不在本书的讨论范围内,但您可以在 Falco 文档中找到大量相关信息。

# 确保容器的不可变性

容器默认是可变的。在容器启动后,您可以打开一个 shell 连接到它,安装现有软件的补丁,修改文件或更改其配置。可变的容器允许攻击者通过下载或安装恶意软件来获取对容器的访问权限。

要对抗这种情况,请确保容器处于不可变状态。这意味着防止对容器文件系统的写操作,甚至禁止对容器进行 shell 访问。如果需要对容器进行任何重大更改,比如更新依赖项或集成新的应用功能,应该发布容器镜像的新标签,而不是手动修改容器本身。然后,您可以将容器镜像的新标签分配给 Pod,而无需直接修改其内部。

## 情景:攻击者安装恶意软件

图 7-3 描述了一个情景,攻击者利用窃取的凭据访问容器。攻击者下载并安装了恶意软件,因为容器允许对根文件系统进行写操作。恶意软件继续监视容器中的活动,例如可以解析应用程序日志以获取敏感信息,然后将信息发送到 Kubernetes 集群外的服务器。因此,数据被用作登录系统其他部分的手段。

![ckss 0703](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 提供。

![ckss 0704](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0704.png)

###### 图 7-4\. 在多个环境中使用相同的容器镜像

避免创建特定于环境的容器镜像简化了创建过程,减少了引入意外安全风险的风险,并使功能测试更加容易。在容器启动后注入运行时值无需更改容器,因此是使其不可变的关键。

当在容器中将 Secret 作为环境变量使用时,请确保避免意外将值记录到标准输出,例如在编写日志消息时作为明文值。任何可以访问容器日志的人都可以解析其中的 Secret 值。作为抽查,识别应用程序代码中使用 Secret 的位置,并评估其暴露风险。

要了解创建、配置和使用 ConfigMaps 和 Secrets 的最新信息,请查阅[Kubernetes 文档](https://oreil.ly/RjxjE)。

## 配置只读容器根文件系统

容器不可变性的另一个方面是防止对容器文件系统的写访问。您可以通过将值`true`分配给属性`spec.containers[].securityContext.readOnlyRootFilesystem`来配置此运行时行为。

仍然有一些应用程序需要写入权限以满足其功能需求。例如,[nginx](https://www.nginx.com) 需要写入到 `/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 服务器。审计日志机制已捕获到感兴趣的事件。管理员可以随时查看这些事件,识别入侵尝试,并采取对策。

![ckss 0705](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_0705.png)

###### 图 7-5\. 通过观察审计日志监控的攻击者

我们只在这里回顾了一个用例,即适用于安全问题的用例。不能低估跟踪公司内部 API 请求的能力。通过审查审计日志,管理员可以为试图创建 Kubernetes 对象的应用程序开发人员提供指导,或者重建可能导致集群行为故障的配置更改。

## 理解审计日志

Kubernetes 可以存储由最终用户触发的对 API 服务器的任何请求或由控制平面本身发出的事件的记录。审计日志中的条目以 [JSON Lines](https://jsonlines.org) 格式存在,可能包括但不限于以下信息:

+   发生了什么事件?

+   是谁触发了这个事件?

+   它是什么时候触发的?

+   哪个 Kubernetes 组件处理了该请求?

事件类型及其对应的请求数据由 *审计策略* 定义。审计策略是一个指定这些规则的 YAML 清单,并且必须提供给 API 服务器进程。

*审计后端* 负责存储根据审计策略定义的记录的审计事件。对于后端,您有两个可配置的选项:

+   一个日志后端,将事件写入文件。

+   Webhook 后端通过 HTTP(S) 将事件发送到外部服务,例如集中式日志记录和监控系统集成的目的。 这样的后端可以帮助调试运行时问题,例如应用程序崩溃。

图 7-6 汇总了配置审计日志所需的所有要素。 下面的章节将解释配置它们的详细信息。

![ckss 0706](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" 1
    rules:
  • level: RequestResponse 2
    resources:
    • group: ""
      resources: ["pods"]
  • level: Metadata 3
    resources:
    • group: ""
      resources: ["pods/log", "pods/status"]

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO3-1)

阻止在 `RequestReceived` 阶段为所有请求生成日志

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO3-2)

记录 `RequestResponse` 级别的 Pod 变更

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO3-3)

记录特定的 Pod 事件,例如日志和状态请求,在 `Metadata` 级别

先前的审计策略并不是很详细,但应该让您对其格式有所了解。 有关更多示例和详细信息,请参阅 [Kubernetes 文档](https://oreil.ly/Zozkf)。

一旦创建了审计策略文件,它就可以被 API 服务器进程消费。 在文件 `/etc/kubernetes/manifests/kube-apiserver.yaml` 中的 API 服务器进程中添加标志 `--audit-policy-file`。 参数分配的值是审计策略文件的完全限定路径。

接下来,我们将详细介绍配置 API 服务器的审计日志记录所需的设置,包括文件日志后端和 Webhook 后端。

## 配置日志后端

要设置基于文件的日志后端,您需要向文件 `/etc/kubernetes/manifests/kube-apiserver.yaml` 添加三个配置项。 以下列表总结了配置内容:

1.  向 API 服务器进程提供两个标志:标志`--audit-policy-file`指向审计策略文件;标志`--audit-log-path`指向日志输出文件。

1.  为审计日志策略文件和日志输出目录添加卷挂载路径。

1.  为审计日志策略文件和日志输出目录在主机路径上添加卷定义。

示例 7-10 显示了 API 服务器配置文件的修改内容。

##### 示例 7-10。配置审计策略文件和审计日志文件

...
spec:
containers:

  • command:
    • kube-apiserver
    • --audit-policy-file=/etc/kubernetes/audit-policy.yaml 1
    • --audit-log-path=/var/log/kubernetes/audit/audit.log 1
      ...
      volumeMounts:
    • mountPath: /etc/kubernetes/audit-policy.yaml 2
      name: audit
      readOnly: true
    • mountPath: /var/log/kubernetes/audit/ 2
      name: audit-log
      readOnly: false
      ...
      volumes:
  • name: audit 3
    hostPath:
    path: /etc/kubernetes/audit-policy.yaml
    type: File
  • name: audit-log 3
    hostPath:
    path: /var/log/kubernetes/audit/
    type: DirectoryOrCreate

![1](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO4-1)

将策略文件和日志文件的位置提供给 API 服务器进程。

![2](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO4-3)

将策略文件和审计日志目录挂载到给定路径。

![3](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/#co_monitoring__logging__and_runtime_security_CO4-5)

定义策略文件和审计日志目录的卷。

通过向 API 服务器进程传递附加标志,可以进一步自定义日志后端的运行时行为。例如,您可以通过提供标志`--audit-log-maxage`指定保留旧审计日志文件的最大天数。请参考[Kubernetes 文档](https://oreil.ly/L-63d)查看完整的标志列表。

现在是产生一些日志条目的时候了。以下`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 文件](https://oreil.ly/bUnmO)中。

在文件`/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 服务器以消耗审计策略文件,提供到后端的引用,并挂载相关的文件系统卷。确保进行所有这些方面的实际操作练习。

# 示例练习

1.  导航到已检出的 GitHub 存储库[*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ)的*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](https://oreil.ly/FiyeH) 和 [VirtualBox](https://oreil.ly/WW8IK)。

1.  转到已检出的 GitHub 仓库 [*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ) 的目录 *app-a/ch07/immutable-container*。使用命令 `kubectl apply -f setup.yaml` 执行操作。

    检查 YAML 清单在 `default` 命名空间中创建的 Pod。对 Pod 进行相关更改,以便其容器被视为不可变。

1.  转到已检出的 GitHub 仓库 [*bmuschko/cks-study-guide*](https://oreil.ly/sImXZ) 的目录 *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](https://oreil.ly/FiyeH) 和 [VirtualBox](https://oreil.ly/WW8IK)。


# 附录:回答审查问题

# 第二章,“集群设置”

1.  创建名为 `deny-egress-external.yaml` 的文件以定义网络策略。网络策略需要将 Pod 选择器设置为 `app=backend` 并定义 `Egress` 策略类型。确保允许协议 UDP 和 TCP 的端口 53。出站策略的命名空间选择器需要使用 `{}` 来选择所有命名空间:

    ```
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: deny-egress-external
    spec:
      podSelector:
        matchLabels:
          app: backend
      policyTypes:
      - Egress
      egress:
      - to:
        - namespaceSelector: {}
        ports:
        - port: 53
          protocol: UDP
        - port: 53
          protocol: TCP
    ```

    运行 `apply` 命令从 YAML 文件实例化网络策略对象:

    ```
    $ kubectl apply -f deny-egress-external.yaml
    ```

1.  符合网络策略的标签选择的 Pod 可以调用集群外的 URL。在这种情况下,标签分配为 `app=frontend`:

    ```
    $ kubectl run web --image=busybox:1.36.0 -l app=frontend --port=80 -it \
      --rm --restart=Never -- wget http://google.com --timeout=5 --tries=1
    Connecting to google.com (142.250.69.238:80)
    Connecting to www.google.com (142.250.72.4:80)
    saving to /'index.xhtml'
    index.xhtml           100% |************| 13987 \
    0:00:00 ETA
    /'index.xhtml' saved
    pod "web" deleted**********
    ```

*****   符合网络策略的标签选择的 Pod 不能调用集群外的 URL。在这种情况下,标签分配为 `app=backend`:

    ```
    $ kubectl run web --image=busybox:1.36.0 -l app=backend --port=80 -it \
      --rm --restart=Never -- wget http://google.com --timeout=5 --tries=1
    wget: download timed out
    pod "web" deleted
    pod default/web terminated (Error)
    ```

    +   首先,请查看 Dashboard 是否已安装。您可以检查 Dashboard 通常创建的命名空间:

    ```
    $ kubectl get ns kubernetes-dashboard
    NAME                   STATUS   AGE
    kubernetes-dashboard   Active   109s
    ```

    如果命名空间不存在,则可以假定尚未安装 Dashboard。使用以下命令安装它:

    ```
    $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/\
    dashboard/v2.6.0/aio/deploy/recommended.yaml
    ```

    创建 ServiceAccount、ClusterRole 和 ClusterRoleBinding。确保 ClusterRole 只允许列出部署对象。以下 YAML 清单已保存在文件 `dashboard-observer-user.yaml` 中:

    ```
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: observer-user
      namespace: kubernetes-dashboard
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      annotations:
        rbac.authorization.kubernetes.io/autoupdate: "true"
      name: cluster-observer
    rules:
    - apiGroups:
      - 'apps'
      resources:
      - 'deployments'
      verbs:
      - list
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: observer-user
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: cluster-observer
    subjects:
    - kind: ServiceAccount
      name: observer-user
      namespace: kubernetes-dashboard
    ```

    使用以下命令创建对象:

    ```
    $ kubectl apply -f dashboard-observer-user.yaml
    ```

    +   运行以下命令为 ServiceAccount 创建一个令牌。选项 `--duration 0s` 确保令牌永不过期。复制在命令的控制台输出中呈现的令牌:

    ```
    $ kubectl create token observer-user -n kubernetes-dashboard \
      --duration 0s
    eyJhbGciOiJSUzI1NiIsImtpZCI6Ik5lNFMxZ1...
    ```

    运行代理命令,并在浏览器中打开链接 [*http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy*](http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy) :

    ```
    $ kubectl proxy
    ```

    选择“Token”身份验证方法,并粘贴您之前复制的令牌。登录到 Dashboard。您应该看到只有部署对象是可列出的(见 图 A-1)。

    所有其他对象将显示“这里没有任何内容。” 图 A-2 显示 Pod 的列表。

    ![ckss aa01](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_aa01.png)

    ###### 图 A-1\. 允许部署的 Dashboard 视图

    ![ckss aa02](https://github.com/OpenDocCN/ibooker-devops-zh/raw/master/docs/cks-stdgd/img/ckss_aa02.png)

    ###### 图 A-2\. 不允许查看 Pod 的 Dashboard 视图

    +   使用以下命令下载 API 服务器二进制文件:

    ```
    $ curl -LO "https://dl.k8s.io/v1.26.1/bin/linux/amd64/kube-apiserver"
    ```

    接下来,下载同一二进制文件的 SHA256 文件,但是不同版本。以下命令下载版本 1.23.1 的文件:

    ```
    $ curl -LO "https://dl.k8s.io/v1.23.1/bin/linux/amd64/\
    kube-apiserver.sha256"
    ```

    与校验和文件比较二进制文件的结果失败,因为版本不匹配:

    ```
    $ echo "$(cat kube-apiserver.sha256)  kube-apiserver" | shasum -a 256 \
      --check
    kube-apiserver: FAILED
    shasum: WARNING: 1 computed checksum did NOT match
    ```****

****# 第三章,“集群加固”

1.  使用 `openssl` 可执行文件创建私钥。提供一个富有表现力的文件名,例如 `jill.key`。 `-subj` 选项提供用户名(CN)和组(O)。以下命令使用用户名 `jill` 和名为 `observer` 的组:

    ```
    $ openssl genrsa -out jill.key 2048
    $ openssl req -new -key jill.key -out jill.csr -subj \
      "/CN=jill/O=observer"
    ```

    使用以下命令检索 CSR 文件内容的 base64 编码值。在下一步创建 CertificateSigningRequest 对象时将需要它:

    ```
    $ cat jill.csr | base64 | tr -d "\n"
    LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tL...
    ```

    以下脚本创建一个 CertificateSigningRequest 对象:

    ```
    $ cat <<EOF | kubectl apply -f -
    apiVersion: certificates.k8s.io/v1
    kind: CertificateSigningRequest
    metadata:
      name: jill
    spec:
      request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tL...
      signerName: kubernetes.io/kube-apiserver-client
      expirationSeconds: 86400
      usages:
      - client auth
    EOF
    ```

    使用`certificate approve`命令批准签名请求并导出已颁发的证书:

    ```
    $ kubectl certificate approve jill
    $ kubectl get csr jill -o jsonpath=*{.status.certificate}*| base64 \
      -d > jill.crt
    ```

    将用户添加到 kubeconfig 文件,并为用户添加上下文。此处使用的集群名称是`minikube`。在您的 Kubernetes 环境中可能不同:

    ```
    $ kubectl config set-credentials jill --client-key=jill.key \
      --client-certificate=jill.crt --embed-certs=true
    $ kubectl config set-context jill --cluster=minikube --user=jill
    ```

1.  创建角色和角色绑定。以下命令将动词`get`、`list`和`watch`分配给`observer`组的`subject`,类型为组,用于 Pods、ConfigMaps 和 Secrets。用户`jill`是该组的一部分:

    ```
    $ kubectl create role observer --verb=create --verb=get --verb=list \
      --verb=watch --resource=pods --resource=configmaps --resource=secrets
    $ kubectl create rolebinding observer-binding --role=observer \
      --group=observer
    ```

1.  切换到用户上下文:

    ```
    $ kubectl config use-context jill
    ```

    我们将选择一个允许的操作,列出 ConfigMap 对象。用户被授权映射此调用:

    ```
    $ kubectl get configmaps
    NAME               DATA   AGE
    kube-root-ca.crt   1      16m
    ```

    列出节点将不被授权。用户没有适当的权限:

    ```
    $ kubectl get nodes
    Error from server (Forbidden): nodes is forbidden: User "jill" cannot \
    list resource "nodes" in API group "" at the cluster scope
    ```

    切换回管理员上下文:

    ```
    $ kubectl config use-context minikube
    ```

1.  创建命名空间`t23`:

    ```
    $ kubectl create namespace t23
    ```

    在命名空间中创建服务账户`api-call`:

    ```
    $ kubectl create serviceaccount api-call -n t23
    ```

    使用名为`pod.yaml`的 YAML 清单文件定义 Pod。文件内容定义一个 Pod,该 Pod 向 API 服务器发出 HTTPS GET 调用,以检索`default`命名空间中的 Service 列表:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: service-list
      namespace: t23
    spec:
      serviceAccountName: api-call
      containers:
      - name: service-list
        image: alpine/curl:3.14
        command: ['sh', '-c', 'while true; do curl -s -k -m 5 \
                  -H "Authorization: Bearer $(cat /var/run/secrets/\
                  kubernetes.io/serviceaccount/token)" https://kubernetes.\
                  default.svc.cluster.local/api/v1/namespaces/default/\
                  services; sleep 10; done']
    ```

    使用以下命令创建 Pod:

    ```
    $ kubectl apply -f pod.yaml
    ```

    检查 Pod 的日志。API 调用未经授权,如下所示的日志输出:

    ```
    $ kubectl logs service-list -n t23
    {
      "kind": "Status",
      "apiVersion": "v1",
      "metadata": {},
      "status": "Failure",
      "message": "services is forbidden: User \"system:serviceaccount:t23 \
                  :api-call\" cannot list resource \"services\" in API \
                  group \"\" in the namespace \"default\"",
      "reason": "Forbidden",
      "details": {
        "kind": "services"
      },
      "code": 403
    }
    ```

1.  在名为`clusterrole.yaml`的文件中创建 YAML 清单,如下所示:

    ```
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: list-services-clusterrole
    rules:
    - apiGroups: [""]
      resources: ["services"]
      verbs: ["list"]
    ```

    在名为`rolebinding.yaml`的文件中引用 ClusterRole。主体应列出命名空间`t23`中的服务账户`api-call`:

    ```
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: serviceaccount-service-rolebinding
    subjects:
    - kind: ServiceAccount
      name: api-call
      namespace: t23
    roleRef:
      kind: ClusterRole
      name: list-services-clusterrole
      apiGroup: rbac.authorization.k8s.io
    ```

    从 YAML 清单创建这两个对象:

    ```
    $ kubectl apply -f clusterrole.yaml
    $ kubectl apply -f rolebinding.yaml
    ```

    容器内运行的 API 调用现在应已获得授权,并允许列出`default`命名空间中的 Service 对象。如下输出所示,该命名空间目前至少托管一个 Service 对象,即`kubernetes.default` Service:

    ```
    $ kubectl logs service-list -n t23
    {
      "kind": "ServiceList",
      "apiVersion": "v1",
      "metadata": {
        "resourceVersion": "1108"
      },
      "items": [
         {
           "metadata": {
             "name": "kubernetes",
             "namespace": "default",
             "uid": "30eb5425-8f60-4bb7-8331-f91fe0999e20",
             "resourceVersion": "199",
             "creationTimestamp": "2022-09-08T18:06:52Z",
             "labels": {
               "component": "apiserver",
               "provider": "kubernetes"
           },
           ...
         }
      ]
    }
    ```

1.  使用以下命令为服务账户创建令牌:

    ```
    $ kubectl create token api-call -n t23
    eyJhbGciOiJSUzI1NiIsImtpZCI6IjBtQkJzVWlsQjl...
    ```

    通过删除并重新创建实时对象更改现有 Pod 定义。添加禁用自动挂载令牌的属性,如下所示:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: service-list
      namespace: t23
    spec:
      serviceAccountName: api-call
      automountServiceAccountToken: false
      containers:
      - name: service-list
        image: alpine/curl:3.14
        command: ['sh', '-c', 'while true; do curl -s -k -m 5 \
                  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ij \
                  BtQkJzVWlsQjl" https://kubernetes.default.svc.cluster. \
                  local/api/v1/namespaces/default/services; sleep 10; \
                  done']
    ```

    API 服务器将允许使用服务账户令牌执行的 HTTPS 请求进行身份验证和授权:

    ```
    $ kubectl logs service-list -n t23
    {
      "kind": "ServiceList",
      "apiVersion": "v1",
      "metadata": {
        "resourceVersion": "81194"
      },
      "items": [
         {
           "metadata": {
             "name": "kubernetes",
             "namespace": "default",
             "uid": "30eb5425-8f60-4bb7-8331-f91fe0999e20",
             "resourceVersion": "199",
             "creationTimestamp": "2022-09-08T18:06:52Z",
             "labels": {
               "component": "apiserver",
               "provider": "kubernetes"
           },
           ...
         }
      ]
    }
    ```

1.  解决此示例练习需要很多手动步骤。以下命令不会呈现它们的输出。

    使用 Vagrant 打开控制平面节点的交互式 Shell:

    ```
    $ vagrant ssh kube-control-plane
    ```

    将`kubeadm`升级到版本 1.26.1 并应用:

    ```
    $ sudo apt-mark unhold kubeadm && sudo apt-get update && sudo apt-get \
      install -y kubeadm=1.26.1-00 && sudo apt-mark hold kubeadm
    $ sudo kubeadm upgrade apply v1.26.1
    ```

    排空节点,升级 kubelet 和`kubectl`,重新启动 kubelet,并解封节点:

    ```
    $ kubectl drain kube-control-plane --ignore-daemonsets
    $ sudo apt-get update && sudo apt-get install -y \
      --allow-change-held-packages kubelet=1.26.1-00 kubectl=1.26.1-00
    $ sudo systemctl daemon-reload
    $ sudo systemctl restart kubelet
    $ kubectl uncordon kube-control-plane
    ```

    节点的版本现在应为 v1.26.1。退出节点:

    ```
    $ kubectl get nodes
    $ exit
    ```

    使用 Vagrant 打开第一个工作节点的交互式 Shell。重复以下所有步骤以操作工作节点:

    ```
    $ vagrant ssh kube-worker-1
    ```

    将`kubeadm`升级到版本 1.26.1 并应用到节点:

    ```
    $ sudo apt-get update && sudo apt-get install -y \
      --allow-change-held-packages kubeadm=1.26.1-00
    $ sudo kubeadm upgrade node
    ```

    通过下列步骤来排空节点、升级 kubelet 和`kubectl`、重启 kubelet 并解除节点的限制:

    ```
    $ kubectl drain kube-worker-1 --ignore-daemonsets
    $ sudo apt-get update && sudo apt-get install -y \
      --allow-change-held-packages kubelet=1.26.1-00 kubectl=1.26.1-00
    $ sudo systemctl daemon-reload
    $ sudo systemctl restart kubelet
    $ kubectl uncordon kube-worker-1
    ```

    节点的版本现在应显示为 v1.26.1。退出节点:

    ```
    $ kubectl get nodes
    $ exit
    ```

# 第四章,“系统加固”

1.  使用以下命令进入工作节点的 Shell:

    ```
    $ vagrant ssh kube-worker-1
    ```

    确定暴露端口 21 的进程。一种方法是使用`lsof`命令。暴露端口的命令是`vsftpd`:

    ```
    $ sudo lsof -i :21
    COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    vsftpd  10178 root    3u  IPv6  56850      0t0  TCP *:ftp (LISTEN)
    ```

    或者,您也可以使用`ss`命令,如下所示:

    ```
    $ sudo ss -at -pn '( dport = :21 or sport = :21 )'
    State   Recv-Q   Send-Q   Local Address:Port \
       Peer Address:Port   Process
    LISTEN  0   32   *:21 \
       *:*   users:(("vsftpd",pid=10178,fd=3))

    ```

    进程`vsftpd`已作为服务启动:

    ```
    $ sudo systemctl status vsftpd
    ● vsftpd.service - vsftpd FTP server
         Loaded: loaded (/lib/systemd/system/vsftpd.service; enabled; \
                 vendor preset: enabled)
         Active: active (running) since Thu 2022-10-06 14:39:12 UTC; \
                 11min ago
       Main PID: 10178 (vsftpd)
          Tasks: 1 (limit: 1131)
         Memory: 604.0K
         CGroup: /system.slice/vsftpd.service
                 └─10178 /usr/sbin/vsftpd /etc/vsftpd.conf

    Oct 06 14:39:12 kube-worker-1 systemd[1]: Starting vsftpd FTP server...
    Oct 06 14:39:12 kube-worker-1 systemd[1]: Started vsftpd FTP server.
    ```

    关闭服务并卸载包:

    ```
    $ sudo systemctl stop vsftpd
    $ sudo systemctl disable vsftpd
    $ sudo apt purge --auto-remove -y vsftpd
    ```

    检查端口时,您将看到它不再列出:

    ```
    $ sudo lsof -i :21
    ```

    退出节点:

    ```
    $ exit
    ```

1.  使用以下命令进入工作节点的 Shell:

    ```
    $ vagrant ssh kube-worker-1
    ```

    使用命令`sudo vim /etc/apparmor.d/network-deny`在`/etc/apparmor.d/network-deny`创建 AppArmor 配置文件。文件内容应如下所示:

    ```
    #include <tunables/global>

    profile network-deny flags=(attach_disconnected) {
      #include <abstractions/base>

      network,
    }
    ```

    运行以下命令强制执行 AppArmor 配置:

    ```
    $ sudo apparmor_parser /etc/apparmor.d/network-deny
    ```

    您不能修改现有的 Pod 对象以添加 AppArmor 的注释。您需要首先删除对象。将 Pod 的定义写入文件:

    ```
    $ kubectl get pod -o yaml > pod.yaml
    $ kubectl delete pod network-call
    ```

    编辑`pod.yaml`文件以添加 AppArmor 注释。对于相关的注释,使用容器名称`network-call`作为键后缀,并使用`localhost/network-deny`作为值。后缀`network-deny`指的是 AppArmor 配置文件的名称。稍作清理后,最终内容可能如下所示:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: network-call
      annotations:
        container.apparmor.security.beta.kubernetes.io/network-call: \
        localhost/network-deny
    spec:
      containers:
      - name: network-call
        image: alpine/curl:3.14
        command: ["sh", "-c", "while true; do ping -c 1 google.com; \
                  sleep 5; done"]
    ```

    从清单中创建 Pod。几秒钟后,Pod 应该转换为“运行”状态:

    ```
    $ kubectl create -f pod.yaml
    $ kubectl get pod network-call
    NAME           READY   STATUS    RESTARTS   AGE
    network-call   1/1     Running   0          27s
    ```

    AppArmor 阻止 Pod 进行网络调用。您可以检查日志以进行验证:

    ```
    $ kubectl logs network-call
    ...
    sh: ping: Permission denied
    sh: sleep: Permission denied
    ```

    退出节点:

    ```
    $ exit
    ```

1.  使用以下命令进入工作节点的 Shell:

    ```
    $ vagrant ssh kube-worker-1
    ```

    为 seccomp 配置创建目标目录:

    ```
    $ sudo mkdir -p /var/lib/kubelet/seccomp/profiles
    ```

    在目录`/var/lib/kubelet/seccomp/profiles`中添加文件`audit.json`,内容如下:

    ```
    {
        "defaultAction": "SCMP_ACT_LOG"
    }
    ```

    您不能修改现有的 Pod 对象以通过安全上下文添加 seccomp 配置。您需要首先删除对象。将 Pod 的定义写入文件:

    ```
    $ kubectl get pod -o yaml > pod.yaml
    $ kubectl delete pod network-call
    ```

    编辑`pod.yaml`文件。将 seccomp 配置指向定义。稍作清理后,最终内容可能如下所示:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: network-call
    spec:
      securityContext:
        seccompProfile:
          type: Localhost
          localhostProfile: profiles/audit.json
      containers:
      - name: network-call
        image: alpine/curl:3.14
        command: ["sh", "-c", "while true; do ping -c 1 google.com; \
                  sleep 5; done"]
        securityContext:
          allowPrivilegeEscalation: false
    ```

    从清单中创建 Pod。几秒钟后,Pod 应该转换为“运行”状态:

    ```
    $ kubectl create -f pod.yaml
    $ kubectl get pod network-call
    NAME           READY   STATUS    RESTARTS   AGE
    network-call   1/1     Running   0          27s
    ```

    你应该能够找到系统调用的日志条目,例如`sleep`命令:

    ```
    $ sudo cat /var/log/syslog
    Oct  6 16:25:06 ubuntu-focal kernel: [ 2114.894122] audit: type=1326 \
    audit(1665073506.099:23761): auid=4294967295 uid=0 gid=0 \
    ses=4294967295 pid=19226 comm="sleep" exe="/bin/busybox" \
    sig=0 arch=c000003e syscall=231 compat=0 ip=0x7fc026adbf0b \
    code=0x7ffc0000
    ```

    退出节点:

    ```
    $ exit
    ```

    在文件`pod.yaml`中定义 Pod:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: sysctl-pod
    spec:
      securityContext:
        sysctls:
        - name: net.core.somaxconn
          value: "1024"
        - name: debug.iotrace
          value: "1"
      containers:
      - name: nginx
        image: nginx:1.23.1
    ```

    创建 Pod,然后检查状态。您将看到状态为“SysctlForbidden”:

    ```
    $ kubectl create -f pod.yaml
    $ kubectl get pods
    NAME         READY   STATUS            RESTARTS   AGE
    sysctl-pod   0/1     SysctlForbidden   0          4s
    ```

    事件日志将告诉您更多的原因:

    ```
    $ kubectl describe pod sysctl-pod
    ...
    Events:
      Type     Reason           Age    From    \
                 Message
      ----     ------           ----   ----    \
                 -------
      Warning  SysctlForbidden  2m48s  kubelet \
                 forbidden sysctl: "net.core.somaxconn" \
                 not allowlisted
    ```

# 第五章,“最小化微服务的漏洞”

1.  使用文件`busybox-security-context.yaml`中的安全设置定义 Pod。您可以找到以下 YAML 清单的内容:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: busybox-security-context
    spec:
      securityContext:
        runAsUser: 1000
        runAsGroup: 3000
        fsGroup: 2000
      volumes:
      - name: vol
        emptyDir: {}
      containers:
      - name: busybox
        image: busybox:1.28
        command: ["sh", "-c", "sleep 1h"]
        volumeMounts:
        - name: vol
          mountPath: /data/test
        securityContext:
          allowPrivilegeEscalation: false
    ```

    使用以下命令创建 Pod:

    ```
    $ kubectl apply -f busybox-security-context.yaml
    $ kubectl get pod busybox-security-context
    NAME                       READY   STATUS    RESTARTS   AGE
    busybox-security-context   1/1     Running   0          54s
    ```

    进入容器并创建文件。您会发现,文件组由安全上下文定义为 2000:

    ```
    $ kubectl exec busybox-security-context -it -- /bin/sh
    / $ cd /data/test
    /data/test $ touch hello.txt
    /data/test $ ls -l
    total 0
    -rw-r--r--    1 1000     2000             0 Nov 21 18:29 hello.txt
    /data/test $ exit
    ```

1.  在文件 `psa-namespace.yaml` 中指定名为 `audited` 的命名空间。使用 `baseline` 级别设置 PSA 标签和 `warn` 模式:

    ```
    apiVersion: v1
    kind: Namespace
    metadata:
      name: audited
      labels:
        pod-security.kubernetes.io/warn: baseline
    ```

    从 YAML 清单创建命名空间:

    ```
    $ kubectl apply -f psa-namespace.yaml
    ```

    您可以在文件 `psa-pod.yaml` 中使用以下 Pod 配置产生错误。YAML 清单设置了属性 `hostNetwork: true`,这不允许 [`baseline` 级别](https://oreil.ly/c8JEW) 使用:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: busybox
      namespace: audited
    spec:
      hostNetwork: true
      containers:
      - name: busybox
        image: busybox:1.28
        command: ["sh", "-c", "sleep 1h"]
    ```

    创建 Pod 会显示警告消息。尽管如此,Pod 已创建。通过配置 PSA 使用 `restricted` 级别可以防止 Pod 的创建:

    ```
    $ kubectl apply -f psa-pod.yaml
    Warning: would violate PodSecurity "baseline:latest": host namespaces \
    (hostNetwork=true)
    pod/busybox created
    $ kubectl get pod busybox -n audited
    NAME      READY   STATUS    RESTARTS   AGE
    busybox   1/1     Running   0          2m21s
    ```

1.  您可以使用以下命令安装 Gatekeeper:

    ```
    $ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/\
    gatekeeper/master/deploy/gatekeeper.yaml
    ```

    Gatekeeper 库描述了用于定义 [副本限制](https://oreil.ly/gyD1-) 的 ConstraintTemplate。检查页面描述的 YAML 清单。使用以下命令应用清单:

    ```
    $ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/\
    gatekeeper-library/master/library/general/replicalimits/template.yaml
    ```

    现在,在名为 `replica-limits-constraint.yaml` 的文件中定义 Constraint 的 YAML 清单:

    ```
    apiVersion: constraints.gatekeeper.sh/v1beta1
    kind: K8sReplicaLimits
    metadata:
      name: replica-limits
    spec:
      match:
        kinds:
          - apiGroups: ["apps"]
            kinds: ["Deployment"]
      parameters:
        ranges:
        - min_replicas: 3
          max_replicas: 10
    ```

    使用以下命令创建 Constraint:

    ```
    $ kubectl apply -f replica-limits-constraint.yaml
    ```

    您可以看到,只有在提供的副本数目在 Constraint 的范围内时,才能创建 Deployment:

    ```
    $ kubectl create deployment nginx --image=nginx:1.23.2 --replicas=15
    error: failed to create deployment: admission webhook \
    "validation.gatekeeper.sh" denied the request: [replica-limits] \
    The provided number of replicas is not allowed for deployment: nginx. \
    Allowed ranges: {"ranges": [{"max_replicas": 10, "min_replicas": 3}]}
    $ kubectl create deployment nginx --image=nginx:1.23.2 --replicas=7
    deployment.apps/nginx created
    ```

1.  配置 etcd 的加密,如 “加密 etcd 数据” 中所述。接下来,使用以下命令创建新的 Secret:

    ```
    $ kubectl create secret generic db-credentials \
      --from-literal=api-key=YZvkiWUkycvspyGHk3fQRAkt
    ```

    您可以使用以下命令检查存储在 etcd 中的 Secret 的加密值:

    ```
    $ 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/db-credentials | hexdump -C
    ```

1.  使用 Vagrant 打开工作节点的交互式 shell:

    ```
    $ vagrant ssh kube-worker-1
    ```

    使用以下 YAML 清单定义 RuntimeClass。内容已存储在文件 `runtime-class.yaml` 中:

    ```
    apiVersion: node.k8s.io/v1
    kind: RuntimeClass
    metadata:
      name: container-runtime-sandbox
    handler: runsc
    ```

    创建 RuntimeClass 对象:

    ```
    $ kubectl apply -f runtime-class.yaml
    ```

    使用 `spec.runtimeClassName` 属性将 RuntimeClass 的名称分配给 Pod。nginx Pod 已在文件 `pod.yaml` 中定义:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      runtimeClassName: container-runtime-sandbox
      containers:
      - name: nginx
        image: nginx:1.23.2
    ```

    创建 Pod 对象。Pod 将转换为状态“Running”:

    ```
    $ kubectl apply -f pod.yaml
    $ kubectl get pod nginx
    NAME    READY   STATUS    RESTARTS   AGE
    nginx   1/1     Running   0          2m21s
    ```

    退出节点:

    ```
    $ exit
    ```

# 第六章,“供应链安全”

1.  使用提供的 Dockerfile 构建的初始容器映像大小为 998MB。您可以使用以下命令生成并运行容器映像。运行快速 `curl` 命令查看应用程序暴露的端点是否可达:

    ```
    $ docker build . -t node-app:0.0.1
    ...
    $ docker images
    REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
    node-app     0.0.1     7ba99d4ba3af   3 seconds ago   998MB
    $ docker run -p 3001:3001 -d node-app:0.0.1
    c0c8a301eeb4ac499c22d10399c424e1063944f18fff70ceb5c49c4723af7969
    $ curl -L http://localhost:3001/
    Hello World
    ```

    您可以进行的更改之一是避免使用大型基础映像。您可以将其替换为 `alpine` 版本的 node 基础映像。还要避免拉取 `latest` 映像。选择您实际希望应用程序运行的 Node.js 版本。以下命令使用带有基础映像 `node:19-alpine` 的 Dockerfile,将容器映像大小减小到 176MB:

    ```
    $ docker build . -t node-app:0.0.1
    ...
    $ docker images
    REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
    node-app     0.0.1     ef2fbec41a75   2 seconds ago    176MB
    ```

1.  您可以使用 Helm 安装 Kyverno,也可以指向项目 GitHub 仓库中可用的 YAML 清单。我们将在这里使用 YAML 清单:

    ```
    $ kubectl create -f https://raw.githubusercontent.com/kyverno/\
    kyverno/main/config/install.yaml
    ```

    设置名为`restrict-image-registries.yaml`的 YAML 清单文件。向文件添加以下内容。该清单代表一个 ClusterPolicy,仅允许使用以`gcr.io/`开头的容器镜像。确保将`spec.validationFailureAction`属性的值设为`Enforce`:

    ```
    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
      name: restrict-image-registries
      annotations:
        policies.kyverno.io/title: Restrict Image Registries
        policies.kyverno.io/category: Best Practices, EKS Best Practices
        policies.kyverno.io/severity: medium
        policies.kyverno.io/minversion: 1.6.0
        policies.kyverno.io/subject: Pod
        policies.kyverno.io/description: >-
          Images from unknown, public registries can be of dubious quality \
          and may not be scanned and secured, representing a high degree of \
          risk. Requiring use of known, approved registries helps reduce \
          threat exposure by ensuring image pulls only come from them. This \
          policy validates that container images only originate from the \
          registry `eu.foo.io` or `bar.io`. Use of this policy requires \
          customization to define your allowable registries.
    spec:
      validationFailureAction: Enforce
      background: true
      rules:
      - name: validate-registries
        match:
          any:
          - resources:
              kinds:
              - Pod
        validate:
          message: "Unknown image registry."
          pattern:
            spec:
              containers:
              - image: "gcr.io/*"
    ```

    使用以下命令应用清单:

    ```
    $ kubectl apply -f restrict-image-registries.yaml
    ```

    运行以下命令验证策略是否已生效。任何不使用前缀`gcr.io/`的容器镜像定义将被拒绝:

    ```
    $ kubectl run nginx --image=nginx:1.23.3
    Error from server: admission webhook "validate.kyverno.svc-fail" \
    denied the request:

    policy Pod/default/nginx for resource violation:

    restrict-image-registries:
      validate-registries: 'validation error: Unknown image registry. \
      rule validate-registries
        failed at path /spec/containers/0/image/'
    $ kubectl run busybox --image=gcr.io/google-containers/busybox:1.27.2
    pod/busybox created
    ```

1.  使用 Docker Hub 的搜索功能找到镜像`nginx:1.23.3-alpine`的 SHA256 哈希。[搜索结果](https://oreil.ly/a4o8E)将引导您到镜像的标签。在页面顶部,您应该找到摘要`sha256:c1b9fe3c0c015486cf1e4a0ecabe78d05864475e279638e9713eb55f013f907f`。在 Pod 定义中使用摘要而不是标签。结果如下所示的 YAML 清单:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      containers:
      - name: nginx
        image: nginx@sha256:c1b9fe3c0c015486cf1e4a0ecabe78d05864475e279638 \
               e9713eb55f013f907f
    ```

    应该正常创建 Pod:

    ```
    $ kubectl apply -f pod-validate-image.yaml
    pod/nginx created
    $ kubectl get pods nginx
    NAME    READY   STATUS    RESTARTS   AGE
    nginx   1/1     Running   0          29s
    ```

    如果以任何形式修改 SHA256 哈希并尝试重新创建 Pod,则 Kubernetes 将不允许您拉取镜像。

1.  在 Docker 容器中运行 Kubesec 将产生一堆建议,如下所示的输出:

    ```
    $ docker run -i kubesec/kubesec:512c5e0 scan /dev/stdin < pod.yaml
    [
      {
        "object": "Pod/hello-world.default",
        "valid": true,
        "message": "Passed with a score of 0 points",
        "score": 0,
        "scoring": {
          "advise": [
            {
              "selector": "containers[] .securityContext .capabilities \
                           .drop | index(\"ALL\")",
              "reason": "Drop all capabilities and add only those \
                         required to reduce syscall attack surface"
            },
            {
              "selector": "containers[] .resources .requests .cpu",
              "reason": "Enforcing CPU requests aids a fair balancing \
                         of resources across the cluster"
            },
            {
              "selector": "containers[] .securityContext .runAsNonRoot \
                           == true",
              "reason": "Force the running image to run as a non-root \
                         user to ensure least privilege"
            },
            {
              "selector": "containers[] .resources .limits .cpu",
              "reason": "Enforcing CPU limits prevents DOS via resource \
                         exhaustion"
            },
            {
              "selector": "containers[] .securityContext .capabilities \
                           .drop",
              "reason": "Reducing kernel capabilities available to a \
                         container limits its attack surface"
            },
            {
              "selector": "containers[] .resources .requests .memory",
              "reason": "Enforcing memory requests aids a fair balancing \
                         of resources across the cluster"
            },
            {
              "selector": "containers[] .resources .limits .memory",
              "reason": "Enforcing memory limits prevents DOS via resource \
                         exhaustion"
            },
            {
              "selector": "containers[] .securityContext \
                           .readOnlyRootFilesystem == true",
              "reason": "An immutable root filesystem can prevent malicious \
                         binaries being added to PATH and increase attack \
                         cost"
            },
            {
              "selector": ".metadata .annotations .\"container.seccomp. \
                           security.alpha.kubernetes.io/pod\"",
              "reason": "Seccomp profiles set minimum privilege and secure \
                         against unknown threats"
            },
            {
              "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[] .securityContext .runAsUser -gt \
                           10000",
              "reason": "Run as a high-UID user to avoid conflicts with \
                         the host's user table"
            },
            {
              "selector": ".spec .serviceAccountName",
              "reason": "Service accounts restrict Kubernetes API access \
                         and should be configured with least privilege"
            }
          ]
        }
      }
    ]
    ```

    修正后的 YAML 清单可能如下所示:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: hello-world
    spec:
      serviceAccountName: default
      containers:
      - name: linux
        image: hello-world:linux
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        securityContext:
          readOnlyRootFilesystem: true
          runAsNonRoot: true
          runAsUser: 20000
          capabilities:
            drop: ["ALL"]
    ```

1.  对现有的`setup.yaml`清单执行`kubectl apply`命令将在命名空间`r61`中创建名为`backend`、`loop`和`logstash`的 Pod:

    ```
    $ kubectl apply -f setup.yaml
    namespace/r61 created
    pod/backend created
    pod/loop created
    pod/logstash created
    ```

    您可以使用以下命令查看它们:

    ```
    $ kubectl get pods -n r61
    NAME       READY   STATUS    RESTARTS   AGE
    backend    1/1     Running   0          115s
    logstash   1/1     Running   0          115s
    loop       1/1     Running   0          115s
    ```

    使用`kubectl describe`命令检查命名空间`r61`中每个 Pod 的镜像。使用的镜像是`bmuschko/nodejs-hello-world:1.0.0`,`alpine:3.13.4`和`elastic/logstash:7.13.3`:

    ```
    $ kubectl describe pod backend -n r61
    ...
    Containers:
      hello:
        Container ID:   docker://eb0bdefc75e635d03b625140d1e \
                        b229ca2db7904e44787882147921c2bd9c365
        Image:          bmuschko/nodejs-hello-world:1.0.0
        ...
    ```

    使用 Trivy 可执行文件检查所有镜像的漏洞:

    ```
    $ trivy image bmuschko/nodejs-hello-world:1.0.0
    $ trivy image alpine:3.13.4
    $ trivy image elastic/logstash:7.13.3
    ```

    如果仔细查看漏洞列表,您会发现所有镜像都包含“CRITICAL”严重性的问题。因此,删除所有 Pod:

    ```
    $ kubectl delete pod backend -n r61
    $ kubectl delete pod logstash -n r61
    $ kubectl delete pod loop -n r61
    ```

# 第七章,“监控、日志和运行时安全”

1.  使用以下命令进入工作节点的 Shell:

    ```
    $ vagrant ssh kube-worker-1
    ```

    检查名为`malicious`的运行中 Pod 的命令和参数。您将看到它试图将消息追加到文件`/etc/threat`:

    ```
    $ kubectl get pod malicious -o jsonpath='{.spec.containers[0].args}'
    ...
    spec:
      containers:
      - args:
        - /bin/sh
        - -c
        - while true; do echo "attacker intrusion" >> /etc/threat; \
          sleep 5; done
    ...

    ```

    Falco 的默认规则之一监视尝试向`/etc`目录写入的文件操作。每次写入尝试都会在标准输出中找到一条消息:

    ```
    $ sudo journalctl -fu falco
    Jan 24 23:40:18 kube-worker-1 falco[8575]: 23:40:18.359740123: Error \
    File below /etc opened for writing (user=<NA> user_loginuid=-1 \
    command=sh -c while true; do echo "attacker intrusion" >> /etc/threat; \
    sleep 5; done pid=9763 parent=<NA> pcmdline=<NA> file=/etc/threat \
    program=sh gparent=<NA> ggparent=<NA> gggparent=<NA> \
    container_id=e72a6dbb63b8 image=docker.io/library/alpine)
    ...
    ```

    通过搜索字符串“etc opened for writing.”在`/etc/falco/falco_rules.yaml`中找到生成消息的规则。规则如下所示:

    ```
    - rule: Write below etc
      desc: an attempt to write to any file below /etc
      condition: write_etc_common
      output: "File below /etc opened for writing (user=%user.name \
               user_loginuid=%user.loginuid command=%proc.cmdline \
               pid=%proc.pid parent=%proc.pname pcmdline=%proc.pcmdline \
               file=%fd.name program=%proc.name gparent=%proc.aname[2] \
               ggparent=%proc.aname[3] gggparent=%proc.aname[4] \
               container_id=%container.id image=%container.image.repository)"
      priority: ERROR
      tags: [filesystem, mitre_persistence]
    ```

    将规则复制到文件`/etc/falco/falco_rules.local.yaml`并修改输出定义,如下所示:

    ```
    - rule: Write below etc
      desc: an attempt to write to any file below /etc
      condition: write_etc_common
      output: "%evt.time,%user.name,%container.id"
      priority: ERROR
      tags: [filesystem, mitre_persistence]
    ```

    重新启动 Falco 服务,并在 Falco 日志中找到更改的输出:

    ```
    $ sudo systemctl restart falco
    $ sudo journalctl -fu falco
    Jan 24 23:48:18 kube-worker-1 falco[17488]: 23:48:18.516903001: \
    Error 23:48:18.516903001,<NA>,e72a6dbb63b8
    ...
    ```

    编辑文件`/etc/falco/falco.yaml`以更改输出通道。禁用标准输出,启用文件输出,并将`file_output`属性指向文件`/var/log/falco.log`。结果配置如下所示:

    ```
    file_output:
      enabled: true
      keep_alive: false
      filename: /var/log/falco.log

    stdout_output:
      enabled: false
    ```

    现在日志文件将附加 Falco 日志:

    ```
    $ sudo tail -f /var/log/falco.log
    00:10:30.425084165: Error 00:10:30.425084165,<NA>,e72a6dbb63b8
    ...
    ```

    退出虚拟机:

    ```
    $ exit
    ```

1.  从`setup.yaml`文件创建名为`hash`的 Pod。其容器中运行的命令会在`/var/config/hash.txt`路径的文件末尾添加一个哈希值,以无限循环方式执行:

    ```
    $ kubectl apply -f setup.yaml
    pod/hash created
    $ kubectl get pod hash
    NAME   READY   STATUS    RESTARTS   AGE
    hash   1/1     Running   0          27s
    $ kubectl exec -it hash -- /bin/sh
    / # ls /var/config/hash.txt
    /var/config/hash.txt
    ```

    要使容器不可变,您需要在现有 Pod 定义中添加配置。您必须将根文件系统设置为只读访问,并将一个卷挂载到路径`/var/config`以允许向名为`hash.txt`的文件写入。生成的 YAML 清单可能如下所示:

    ```
    apiVersion: v1
    kind: Pod
    metadata:
      name: hash
    spec:
      containers:
      - name: hash
        image: alpine:3.17.1
        securityContext:
          readOnlyRootFilesystem: true
        volumeMounts:
        - name: hash-vol
          mountPath: /var/config
        command: ["sh", "-c", "if [ ! -d /var/config ]; then mkdir -p \
                  /var/config; fi; while true; do echo $RANDOM | md5sum \
                  | head -c 20 >> /var/config/hash.txt; sleep 20; done"]
      volumes:
      - name: hash-vol
        emptyDir: {}
    ```

1.  使用以下命令进入控制平面节点的 Shell:

    ```
    $ vagrant ssh kube-control-plane
    ```

    编辑现有的审计策略文件`/etc/kubernetes/audit/rules/audit-policy.yaml`。根据说明添加所需的规则。最终审计策略文件的内容可能如下所示:

    ```
    apiVersion: audit.k8s.io/v1
    kind: Policy
    omitStages:
      - "RequestReceived"
    rules:
      - level: RequestResponse
        resources:
        - group: ""
          resources: ["pods"]
      - level: Metadata
        resources:
        - group: ""
          resources: ["secrets", "configmaps"]
      - level: Request
        resources:
        - group: ""
          resources: ["services"]
    ```

    通过编辑文件`/etc/kubernetes/manifests/kube-apiserver.yaml`,配置 API 服务器以使用审计策略文件。根据请求提供额外选项。所需的相关配置如下:

    ```
    ...
    spec:
      containers:
      - command:
        - kube-apiserver
        - --audit-policy-file=/etc/kubernetes/audit/rules/audit-policy.yaml
        - --audit-log-path=/var/log/kubernetes/audit/logs/apiserver.log
        - --audit-log-maxage=5
        ...
        volumeMounts:
        - mountPath: /etc/kubernetes/audit/rules/audit-policy.yaml
          name: audit
          readOnly: true
        - mountPath: /var/log/kubernetes/audit/logs/
          name: audit-log
          readOnly: false
      ...
      volumes:
      - name: audit
        hostPath:
          path: /etc/kubernetes/audit/rules/audit-policy.yaml
          type: File
      - name: audit-log
        hostPath:
          path: /var/log/kubernetes/audit/logs/
          type: DirectoryOrCreate
    ```

    日志记录的资源之一是`Metadata`级别的 ConfigMap。以下命令创建一个示例 ConfigMap 对象:

    ```
    $ kubectl create configmap db-user --from-literal=username=tom
    configmap/db-user created
    ```

    现在,审计日志文件将包含此事件的条目:

    ```
    $ sudo cat /var/log/kubernetes/audit/logs/apiserver.log
    {"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata", \
    "auditID":"1fbb409a-3815-4da8-8a5e-d71c728b98b1","stage": \
    "ResponseComplete","requestURI":"/api/v1/namespaces/default/configmaps? \
    fieldManager=kubectl-create\u0026fieldValidation=Strict","verb": \
    "create","user":{"username":"kubernetes-admin","groups": \
    ["system:masters","system:authenticated"]},"sourceIPs": \
    ["192.168.56.10"], "userAgent":"kubectl/v1.24.4 (linux/amd64) \
    kubernetes/95ee5ab", "objectRef":{"resource":"configmaps", \
    "namespace":"default", "name":"db-user","apiVersion":"v1"}, \
    "responseStatus":{"metadata": {},"code":201}, \
    "requestReceivedTimestamp":"2023-01-25T18:57:51.367219Z", \
    "stageTimestamp":"2023-01-25T18:57:51.372094Z","annotations": \
    {"authorization.k8s.io/decision":"allow", \
    "authorization.k8s.io/reason":""}}
    ```

    退出虚拟机:

    ```
    $ exit
    ```****
posted @ 2025-11-16 08:57  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报