SELinux-系统管理指南-全-

SELinux 系统管理指南(全)

原文:annas-archive.org/md5/b12493b318ce67f25caf3e5eb21d07f2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

增强安全 LinuxSELinux)是 Linux 最完整的安全解决方案之一,默认情况下大多数主要 Linux 发行版都提供该解决方案,例如 Red Hat Enterprise Linux、CentOS、Fedora 和 Gentoo,同时它也可以轻松地在其他发行版中启用,如 SUSE、Debian 和 Ubuntu。

SELinux 使管理员能够进一步加强其 Linux 系统和应用程序的安全性,使得入侵者和恶意行为者更难滥用系统。

SELinux 系统管理 – 第三版提供了 SELinux 在 Linux 系统中的端到端覆盖,从理解 SELinux 是什么以及它如何作用,到调整 SELinux 控制和它在 Linux 和应用平台中的集成,再到定义和维护自定义策略。

本书适用于谁

本书适用于希望控制其系统安全状态的 Linux 管理员。它包含有关 SELinux 操作和管理程序的最新信息,您将能够通过强制访问控制MAC)进一步加强您的系统——这是一种已经塑造 Linux 安全多年的安全策略。

本书对于 IT 架构师也具有启发性,可以帮助他们理解如何将 SELinux 定位为增强 Linux 系统和组织内 Linux 托管服务的安全性。

读者应具备合理的 Linux 系统维护经验,包括用户管理、软件安装与维护、常规 Linux 安全控制和网络配置。

本书涵盖的内容

第一章SELinux 基本概念,提供了有关 SELinux 技术的基本见解,并使您理解 SELinux 实现之间的差异。

第二章理解 SELinux 决策与日志记录,教您如何分析 SELinux 事件,以及如何配置系统日志以便于 SELinux 故障排除。

第三章管理用户登录,使您能够管理 Linux 用户并将其与正确的 SELinux 上下文关联。

第四章使用文件上下文和进程域,解释了 SELinux 标签如何在系统上暴露,以及如何更改文件和资源的 SELinux 上下文。

第五章控制网络通信,介绍了 SELinux 在网络级别的访问控制保护,从基于套接字的保护措施到使用 SELinux 进行的包过滤。

第六章通过基础设施即代码编排配置 SELinux,展示了如何使用自动化和编排工具在大规模环境中配置 SELinux 设置。

第七章配置特定应用程序的 SELinux 控制,解释了 SELinux 是如何被多个应用程序采纳,以进一步增强它们的安全性。

第八章SEPostgreSQL——使用 SELinux 扩展 PostgreSQL,帮助你了解如何在常规 PostgreSQL 部署中启用 SEPostgreSQL,以及如何在数据库引擎内使用 SELinux 控制。

第九章安全虚拟化,结合 libvirt 和其他虚拟化技术以及 SELinux,进一步保护并隔离虚拟来宾之间的相互影响。

第十章使用 Xen 安全模块与 FLASK,教你如何使用类似 SELinux 的方法,通过 Xen 安全模块来隔离其来宾,以及管理员如何进一步调整和优化隔离。

第十一章增强容器化工作负载的安全性,展示了容器平台如 Docker、podman 和 Kubernetes 如何使用 SELinux 来保护宿主系统,防止潜在不可信的容器,并在容器之间提供隔离。

第十二章调优 SELinux 策略,扩展了 SELinux 布尔值及其对系统的影响,并展示了如何处理不同的 SELinux 策略模块。

第十三章分析策略行为,解释了管理员和分析师如何解读 SELinux 策略,并使用策略分析工具来了解策略的允许行为。

第十四章处理新应用程序,介绍了如何在当前 SELinux 策略不支持的新应用程序上应用 SELinux。

第十五章使用参考策略,解释了如何使用参考策略创建和调整 SELinux 策略。

第十六章使用 SELinux CIL 开发策略,介绍了通用中间语言,并讲解如何运用它来开发自定义策略。

为了充分利用本书

本书专注于 SELinux 技术,SELinux 在许多 Linux 发行版中默认启用。虽然本书使用 CentOS 8 版本作为大部分示例,但任何基于参考策略(所有主要 Linux 发行版都使用这种策略)的 Linux 发行版都能跟随本书的内容。

第五章控制网络通信,有一节讲解了如何使用 SELinux 进行 InfiniBand 基础设施的配置,如果你想严格按照示例操作,需要专门的硬件。然而,本章的大部分内容无需额外的硬件要求即可跟随。

如果你正在使用本书的数字版本,我们建议你自己输入代码,或通过 GitHub 仓库访问代码(下节中会提供链接)。这样可以帮助你避免因复制和粘贴代码而产生的潜在错误。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,链接:github.com/PacktPublishing/SELinux-System-Administration-Third-Edition

如果代码有更新,相关更新会发布在现有的 GitHub 仓库中。

我们还有其他代码包,来自我们丰富的书籍和视频目录,可以在github.com/PacktPublishing/找到。快去看看吧!

《实战中的代码》

本书的《实战中的代码》视频可以在bit.ly/3o4paOb观看。

下载彩色图片

我们还提供了一份 PDF 文件,包含本书中使用的截图/图表的彩色版本。你可以在这里下载:static.packt-cdn.com/downloads/9781800201477_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“在激活模块时,semodule命令会将这些模块复制到一个专门的目录中。”

代码块如下所示:

  chain input {
    type filter hook input priority 0;
    ct state new meta secmark set tcp dport map @secmapping_in
    ct state new ct secmark set meta secmark
    ct state established,related meta secmark set ct secmark
  }

当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会用粗体显示:

(roleattributeset cil_gen_require system_r)
(block pgpool
  (type domain)
  (roletype .system_r domain)
)

任何命令行的输入或输出都如下所示:

$ cat /etc/passwd
cat: /etc/passwd: Permission denied

粗体:表示新术语、重要的词汇或你在屏幕上看到的词汇。例如,菜单或对话框中的词汇会以这种方式出现在文本中。示例:“这可以通过点击开放策略按钮,或通过导航至文件 | 开放策略来实现。”

提示或重要说明

以这种方式显示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提到书名,并通过 customercare@packtpub.com 与我们联系。

勘误:虽然我们已尽一切努力确保内容的准确性,但错误有时会发生。如果你在本书中发现错误,请向我们报告。请访问www.packtpub.com/support/errata,选择你的书籍,点击“勘误提交表单”链接,并填写相关信息。

盗版:如果你在互联网上发现任何形式的非法复制本书内容,我们将非常感激你能提供相关的网址或网站名称。请通过 copyright@packt.com 联系我们,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣撰写或参与编写书籍,请访问authors.packtpub.com

评论

请留下评论。当您阅读并使用本书后,为什么不在您购买本书的网站上留下评论呢?潜在的读者可以看到并参考您的公正意见来做出购买决策,我们在 Packt 能了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。感谢您!

如需了解更多有关 Packt 的信息,请访问packt.com

第一部分:使用 SELinux

在本部分,你将学习 SELinux 是什么,它如何作用于系统,以及如何在系统管理中配置和操作 SELinux 基础知识。

本节包括以下章节:

  • 第一章SELinux 基本概念

  • 第二章理解 SELinux 决策和日志记录

  • 第三章管理用户登录

  • 第四章使用文件上下文和进程域

  • 第五章控制网络通信

  • 第六章通过基础设施即代码编排配置 SELinux

第一章:基本的 SELinux 概念

增强安全的 Linux (SELinux) 为你的 Linux 系统带来额外的安全措施,进一步保护其资源。作为 Linux 内核的一部分,它是一个由主要 Linux 发行版支持的强制访问控制系统。在本书中,我们涵盖了 SELinux 的各个方面,从基本原理到解决 SELinux 问题,配置应用程序以处理 SELinux,甚至编写我们自己的策略。

在我们深入探讨 SELinux 的细节之前,让我们首先介绍一下这项技术的概念:为什么 SELinux 使用标签来识别资源,SELinux 与传统 Linux 访问控制的不同,SELinux 如何执行安全规则,以及 Linux 内核中支持的其他强制访问控制系统。我们还将看到 SELinux 强制执行的访问控制规则是如何通过策略文件提供的。在本章的最后,我们将概述不同 Linux 发行版中 SELinux 实现的差异。

本章将涵盖以下主要主题:

  • 为 Linux 提供更多的安全性

  • 对所有资源和对象进行标签化

  • 定义和分发策略

  • 区分不同的策略

技术要求

查看以下视频,了解代码的实际应用:bit.ly/2FFaUdm

为 Linux 提供更多的安全性

经验丰富的 Linux 管理员和安全工程师已经知道,为了保持系统的安全性,他们需要对系统的用户和进程给予一定的信任。这部分是因为用户可能会尝试利用系统上运行的软件中的漏洞,但这一信任度的主要原因是系统的安全状态依赖于用户的行为。具有访问敏感信息的 Linux 用户可能会轻易将其泄露给公众,操控他们启动的应用程序行为,甚至做出许多其他影响系统安全的行为。常规 Linux 系统上默认的访问控制是自主的;访问控制如何行为由用户决定。

Linux 的/etc/shadow文件,包含本地 Linux 账户的密码和账户信息:

$ ls -l /etc/shadow 
-rw-r-----. 1 root root 1019 Nov 28 20:44 /etc/shadow

如果没有额外的访问控制机制,这个文件对任何由root用户拥有的进程都是可读和可写的,无论该进程在系统上的用途是什么。shadow文件是一个典型的敏感文件,我们不希望它被泄露或以其他方式滥用。然而,一旦某人有了对该文件的访问权限,该用户可以将其复制到其他地方,例如复制到主目录,甚至将其邮件发送到另一台计算机并尝试攻击其中存储的密码哈希值。

另一个需要用户信任的 Linux DAC 示例是数据库服务器的配置。数据库文件本身(希望如此)只对 root 用户的运行时账户可访问。妥善安全的系统将仅向受信任的用户授予对这些文件的访问权限(例如,通过 sudo),允许他们将其有效用户 ID 从个人用户更改为数据库运行时用户,甚至是 root 账户,但仅限于系统管理员事先配置的一组特定命令。这些用户同样可以分析数据库文件,并访问数据库中可能的机密信息,而无需通过数据库管理系统(DBMS)。管理员往往不得不将大量信任寄托在这些用户身上,以保证系统的安全,而不是能够强制执行这些安全控制。

然而,普通用户并不是保障系统安全的唯一原因。许多软件守护进程以 Linux root 用户身份运行,或者在系统上具有重要权限。这些守护进程中的错误可能很容易导致信息泄漏,甚至可能导致可以远程利用的漏洞。备份软件、监控软件、变更管理软件、调度软件等等:它们通常都以在普通 Linux 系统上可能拥有的最高权限账户运行。即使管理员不允许特权用户,其与守护进程的交互仍然会引入潜在的安全风险。因此,用户仍然被信任能够正确地与这些应用程序交互,以确保系统正常运行。通过这种方式,管理员将系统的安全性交给了其(众多)用户的自行判断

这时,SELinux 就应运而生,它为标准 Linux DAC 机制提供了一个额外的访问控制层。SELinux 提供了一种强制访问控制(MAC)系统,不像 DAC 机制那样,它使管理员能够完全控制系统上什么是允许的,什么是不允许的。它通过支持基于策略的方法来确定哪些进程可以做,哪些进程不能做,并通过 Linux 内核强制执行这些策略来实现这一点。

强制意味着操作系统执行访问控制,这些控制规则仅由系统管理员(或安全管理员)启用的策略规则定义。用户和进程没有权限更改安全规则,因此它们无法绕过访问控制;安全性不再交给他们的自行判断。

以关系数据库为例,强制访问控制系统将不再要求管理员信任某些用户,因为它完全控制这些用户可以做什么和不能做什么。正如我们将在 第八章 中看到的,SEPostgreSQL – 通过 SELinux 扩展 PostgreSQL,它可以与 SELinux 子系统交互,使管理员能够对数据访问进行全面控制,甚至在数据库内部也能实现这一点。

这里的强制一词,与之前的自主一词一样,并不是偶然选择用来描述访问控制系统的能力:这两个词在安全研究领域都是已知的术语。许多安全出版物使用这些术语,包括可信计算机系统评估标准TSEC)(csrc.nist.gov/publications/history/dod85.pdf)标准(也称为橙皮书),该标准由美国国防部于 1985 年发布。此出版物已促成了计算机安全认证的通用标准(ISO/IEC 15408),详情请见www.commoncriteriaportal.org/cc/

接下来,我们将描述 Linux 内核如何负责 SELinux 的实现。

介绍 Linux 安全模块(LSM)

再考虑一下shadow文件的示例。一个 MAC 系统可以配置为仅允许有限数量的进程读取和写入该文件。在这种特定配置的系统上,作为root用户登录的用户无法直接访问该文件,甚至无法移动该文件。他们甚至不能更改文件的属性:

# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),
4(adm),6(disk),10(wheel),11(floppy),26(tape),27(video) context=sysadm_u:sysadm_r:sysadm_t:s0-s0:c0.c1023
# cat /etc/shadow
cat: /etc/shadow: Permission denied
# chmod a+r /etc/shadow
chmod: changing permissions of '/etc/shadow': Permission denied

系统通过规则强制执行这些内容,这些规则描述了何时可以读取该文件的内容,或者何时可以更改其属性。在 SELinux 中,这些规则在 SELinux 策略中定义,并在系统启动时加载。正是 Linux 内核本身负责执行这些规则。

强制访问控制系统,如 SELinux,支持通过Linux 安全模块LSM)集成到 Linux 内核中,这是一个在处理用户空间请求之前被调用的 Linux 子系统。此类请求称为系统调用,Linux 支持超过 100 种系统调用。

图 1.1 – LSM 如何集成到 Linux 内核中的高层次概述

图 1.1 – LSM 如何集成到 Linux 内核中的高层次概述

LSM 自 2.6 版本以来已在 Linux 内核中可用,该版本于 2003 年 12 月发布。它是一个框架,在 Linux 内核的多个位置提供钩子,包括系统调用入口点。当这些钩子被触发时,已注册的安全实现,如 SELinux,会自动执行其功能。在 SELinux 中,这些功能会在返回许可/拒绝之前检查策略和其他信息。LSM 本身不提供任何安全功能;相反,它依赖于执行繁重任务的安全实现:该框架是模块化的。

在 LSM 框架内,存在两种类型的安全模块:独占模块和非独占模块。两个独占模块不能同时启用:每个独占的 LSM 模块需要对一些内核对象(通常是与安全上下文相关的对象)进行独占控制,并且无法处理其他也需要这些对象的 LSM 模块。非独占模块没有这个需求,可以随意组合(也叫做 堆叠),无论是否启用了独占 LSM 模块。

堆叠 LSM 模块的一个主要用例是启用系统中运行的容器内的不同安全模型。目前,在 Linux 容器中无法实现不同的安全模块,容器内的安全性会回退到宿主机的安全模块。为了支持这一点,越来越多的独占 LSM 实现(如 SELinux)正在努力将其实现改为非独占,我们可以预期在明年内这一领域会有所进展。

SELinux 是使用 LSM 的一种实现方式。还有其他几种实现存在:

  • AppArmor 是一个强制访问控制系统,专注于应用级别的保护(称为配置文件),主要基于文件系统路径。这使得 AppArmor 对管理员来说易于理解和实现,因为它不像 SELinux 那样需要抽象规则到标签中,因此没有那么复杂。在 标记所有资源和对象 部分中,我们解释了为什么 SELinux 使用标签。在撰写本文时,AppArmor 是一个独占的 LSM 模块,但很可能很快会变为非独占模块。

  • Smack 是一个强制访问控制系统,使用标签来标记进程和资源。这些标签包含由 Smack 解释的安全标识符,用于强制实施访问控制,要求 Smack 中的访问规则比 SELinux 少(与 SELinux 不同,SELinux 不对标签进行解释——除了敏感性之外——因此需要更多的策略规则)。Smack 是一个独占的 LSM 模块。

  • TOMOYO Linux 是一个强制访问控制系统,但其访问控制机制也易于进行系统分析。它会根据应用行为自动构建策略,类似于 AppArmor,它的策略主要使用路径而非标签。TOMOYO Linux(及其分支 AKARI)是一个非独占的 LSM 模块。

  • LoadPin 是一个 LSM 模块,确保 Linux 内核资源(如内核模块和固件)都从一个不可写的单一文件系统加载。LoadPin 是一个非独占的 LSM 模块。

  • ptrace)。Yama 是一个非独占的 LSM 模块。

  • setuid(切换到另一个用户)允许切换到另一个用户。与直接授权使用 setuid 不同,SafeSetId 可以限制哪些用户可以进行此操作。这样,即使工具如 sudo 存在漏洞或配置错误,也能保证其仍然受到限制。SafeSetId 是一个非独占的 LSM 模块。

  • Lockdown 是一个 LSM 模块,保护 Linux 内核内存。它有两种模式:在完整性模式下,它防止从用户空间修改内核对象(如直接内存访问或 PCI 访问);在机密性模式下,它还防止从内核对象提取潜在的机密信息。Lockdown 是一个非独占的 LSM 模块。

  • capability LSM 模块在系统上默认启用,并支持 Linux 能力(授予用户的权限集,当用户被分配某种能力时)。它是一个非独占的 LSM 模块。

要查询系统上活动的 LSM 模块列表,可以读取/sys/kernel/security/lsm

$ cat /sys/kernel/security/lsm
capability,selinux

接下来,我们将解释 SELinux 如何在常规 Linux 访问控制之上工作。

使用 SELinux 扩展常规 DAC

SELinux 不会改变 Linux 的 DAC 实现,也无法覆盖由 Linux DAC 权限做出的拒绝。如果一个普通的系统(没有 SELinux)阻止了某个访问,SELinux 无法覆盖这一决定。这是因为 LSM 钩子是在常规 DAC 权限检查执行之后触发的,这是 LSM 项目的一个设计决策。

例如,如果你需要允许另一个用户访问某个文件,你不能通过添加 SELinux 策略来实现。相反,你需要查看 Linux 的其他功能,例如使用 POSIX 访问控制列表。通过 setfaclgetfacl 命令,用户可以为文件和目录设置额外的权限,允许其他用户或组访问选定的资源。

例如,使用 setfacl 命令授予 admin 用户对文件的读写权限:

$ setfacl -m u:admin:rw /srv/backup/setup.conf

同样,要查看应用于文件的当前 POSIX ACL,可以使用此命令:

$ getfacl /srv/backup/setup.conf
getfacl: Removing leading '/' from absolute path names
# file: srv/backup/setup.conf
# owner: root
# group: root
user::rw-
user::admin:rw-
group::r--
mask::rw-
other::r—

这显示该文件不仅可以由文件所有者写入,还可以由 admin 用户写入。

限制 root 权限

常规 Linux DAC 允许一个全能用户:root。与系统中的其他大多数用户不同,登录的 root 用户拥有完全管理整个系统所需的所有权限,从覆盖访问控制到控制审计、改变用户 ID、管理网络等。这是通过一个叫做 man capabilities 的安全概念支持的。SELinux 也能以细粒度的方式限制对这些能力的访问。

由于 SELinux 细粒度授权的特点,即使是root用户也可以被限制,而不会影响系统的操作。之前访问/etc/shadow的例子只是一个示例,展示了像root这样的强大用户由于 SELinux 的访问控制可能无法执行某些操作。

减少漏洞的影响

如果要强调 SELinux 的一个好处,那就是它能够减少漏洞的影响。但这种漏洞减少往往也被误解。

正确编写的 SELinux 政策将限制应用程序的行为,使其允许的活动减少到最小范围。这个最小特权模型确保不仅能够检测并审计异常的应用程序行为,还能够防止这种行为。许多应用程序的漏洞可以被利用来执行应用程序原本不应执行的任务。当这种情况发生时,SELinux 将加以防止。

然而,有两个关于 SELinux 防止漏洞攻击能力的误解,即政策的影响和漏洞本身。

如果政策没有遵循最小特权模型,那么 SELinux 可能会将这种非标准行为视为正常,并允许这些操作继续进行。对于政策编写者来说,这意味着他们的政策规则必须非常细致。不幸的是,这使得编写政策变得非常耗时:由于 SELinux 已知的有 130 多类和 250 多种权限,政策规则必须考虑到每次交互中所有这些类和权限。

结果是,政策往往变得复杂,且更难维护。一些政策编写者会使政策比实际必要的更加宽松,这可能导致即使某些行为不是应用程序预期的行为,漏洞也能成功利用。一些应用程序政策明确标记为“无限制”(我们在第十四章中讨论,“处理新应用程序”),这表明它们在允许的权限上非常宽松。Fedora、CentOS 和 Red Hat Enterprise Linux 甚至将应用程序的政策起初设为宽松,只有在经过几个版本的发布(和额外的测试)后,才会开始对这些应用程序实施访问控制。

第二个误解是关于漏洞本身。如果应用程序的漏洞允许未经身份验证的用户以普通授权用户的身份使用应用程序服务,那么 SELinux 将无法减少漏洞的影响;它只会注意到应用程序本身的行为,而不是应用程序内部会话的行为。只要应用程序本身的行为符合预期(例如访问自己的文件,而不是乱动其他文件系统),SELinux 将允许这些操作发生。

只有当应用程序开始表现异常时,SELinux 才会阻止漏洞攻击的继续。SELinux 将防止诸如远程命令执行(RCE)等攻击,防止那些不应该执行任意命令的应用程序(如数据库管理系统或 Web 服务器,排除类似 CGI 的功能)被攻击,而会话劫持或 SQL 注入攻击是无法通过 SELinux 政策控制的。

启用 SELinux 支持

在 Linux 系统上启用 SELinux 不仅仅是启用 Linux 内核中的 SELinux LSM 模块。

一个 SELinux 的实现包含以下内容:

  • SELinux 内核子系统,通过 LSM 实现在 Linux 内核中

  • 应用程序用来与 SELinux 交互的库

  • 管理员用来与 SELinux 进行交互的实用程序

  • 定义访问控制本身的策略

这些库和实用程序由 SELinux 用户空间项目捆绑提供(github.com/SELinuxProject/selinux)。除了 SELinux 用户空间项目提供的应用程序和库之外,Linux 系统上的各个组件也会通过 SELinux 特定的代码进行更新,包括 init 系统和一些核心实用程序。

由于 SELinux 不仅仅是一个需要切换的开关,支持 SELinux 的 Linux 发行版通常会预定义并加载 SELinux:Fedora、CentOS 和 Red Hat 企业版 Linux(以及其衍生版本,如 Oracle Linux)是众所周知的例子。其他支持 SELinux 的发行版可能不会自动启用 SELinux,但可以通过安装额外的软件包轻松支持 SELinux(例如 Debian 和 Ubuntu),还有一些发行版有文档说明如何将系统转换为 SELinux(例如 Gentoo 和 Arch Linux)。

在本书中,我们将展示 Gentoo 和 CentOS 8 的示例(CentOS 8 基于 Red Hat 企业版 Linux 发行版的自由软件,并由 Red Hat 赞助)。这两个发行版具有不同的实现细节,这使我们能够展示 SELinux 的全部潜力。为了确保本书中使用的命令可用,可能需要安装一些 SELinux 支持工具。

在 Gentoo Linux 上,至少需要安装以下软件包:

# emerge app-admin/setools sys-apps/policycoreutils

在 CentOS Linux 上,至少需要安装以下软件包:

# yum install setools-console policycoreutils-python-utils

由于软件包可能会随时间变化,查看某个特定命令提供哪个软件包是明智的做法。

重要说明

如果提到的软件包不再存在或未覆盖所有命令,请查阅您发行版的文档,了解需要安装哪些软件包。大多数发行版也允许搜索最合适的软件包,例如 Gentoo 中的 e-file 或 CentOS 或相关发行版中的 yum whatprovides

介绍了 SELinux 的主要功能后,让我们来看看 SELinux 如何了解系统上的内容,以及它使用哪种抽象方法来允许为广泛的用户群体制定策略。

为所有资源和对象打上标签

当 SELinux 必须决定是否允许或拒绝某个特定操作时,它会根据 主体(谁发起了操作)和 客体(操作的目标)之间的上下文做出决定。这些上下文(或上下文的一部分)在 SELinux 强制执行的策略规则中有所提及。

进程的上下文是 SELinux 用来识别进程的。SELinux 并不关心 Linux 进程的所有权,也不关心进程是如何命名的,它的进程 ID 是什么,或者该进程以什么账户运行。它只关心该进程的上下文是什么,这一上下文向用户和管理员展示为一个标签标签上下文通常可以互换使用,尽管它们之间有技术上的区别(一个是另一个的表现形式),但我们不会在此深入讨论。

让我们看一个示例标签——当前用户的上下文:

$ id -Z 
sysadm_u:sysadm_r:sysadm_t:s0-s0:c0.c1023

id命令返回当前用户的信息,显示了使用-Z选项执行的情况(这是一个常见的选项,用于显示从基于 LSM 的安全子系统获取的附加安全信息)。它显示了当前用户的上下文(实际上是id进程在执行时的上下文)。如我们所见,上下文有一个字符串表示,看起来似乎有五个字段(实际上没有,它有四个字段——最后一个字段只是恰好包含了一个冒号字符)。

SELinux 的开发者决定使用标签而不是实际的进程和文件(或其他资源)元数据来进行访问控制。这与像 AppArmor 这样的 MAC 系统不同,后者使用二进制文件的路径(因此是进程名称)和资源的路径来进行权限检查。以下原因促使了 SELinux 采用基于标签的强制访问控制:

  • 使用路径可能对管理员更容易理解,但这并不能让我们将上下文信息保持在资源附近。如果文件或目录移动或重新挂载,或者如果进程在不同的命名空间下查看文件,那么访问控制可能会表现不同,因为它们查看的是路径而非文件。通过基于标签的上下文,系统可以保留这些信息,并持续正确控制资源的访问。

  • 上下文很好地揭示了这个过程的目的。相同的二进制应用程序可以在不同的上下文中启动,具体取决于它是如何启动的。上下文值(例如前面在id -Z输出中看到的那个)正是管理员所需要的。有了它,他们可以知道每个运行实例的权限,但也能从中推断出该进程是如何启动的,以及它的目的是什么。

  • 上下文还对对象本身进行了抽象。我们习惯于讨论进程和文件,但上下文也适用于一些不太具体的资源,如管道(进程间通信)或数据库对象。基于路径的标识只有在你能够写出路径时才有效。

作为一个例子,考虑以下策略声明:

  • 允许httpd进程绑定到 TCP 端口80

  • 允许标记为httpd_t的进程绑定到标记为http_port_t的 TCP 端口。

在第一个示例中,当 Web 服务器进程不使用httpd二进制文件(可能因为已重命名或者不是 Apache 而是另一个 Web 服务器)或者当我们希望在不同端口上具有 HTTP 访问时,我们不能轻松地重用此策略。使用标记方法,二进制文件可以称为apache2MyWebServer.py;只要进程以httpd_t标记,策略就适用。端口定义也是如此:您可以使用http_port_t标记端口8080,从而允许 Web 服务器绑定到该端口,而无需编写另一个策略语句。

解析 SELinux 上下文

要达到上下文,SELinux 使用至少三个,有时是四个值。让我们以 SSH 服务器的上下文为例:

$ ps -eZ | grep sshd
system_u:system_r:sshd_t:s0-s0:c0.c1023 2629 ? 00:00:00 sshd

如我们所见,进程被分配一个包含以下字段的上下文:

  • SELinux 用户system_u

  • SELinux 角色system_r

  • SELinux 类型(当我们查看运行中进程时也称为域)sshd_t

  • 敏感级别s0-s0:c0.c1023

当我们使用 SELinux 时,了解上下文非常重要。在大多数情况下,第三个字段(称为domaintype)最重要,因为超过 99%的 SELinux 策略规则涉及两个类型之间的交互(而不涉及角色、用户或敏感级别)。

SELinux 上下文与 LSM 安全属性对齐,并以标准化的方式向用户空间公开(与多个 LSM 实现兼容),允许最终用户和应用程序轻松查询上下文。这些属性呈现的一个方便获取的位置是在/proc伪文件系统中。

在每个进程的/proc/<pid>位置,我们可以找到一个名为attr的子目录,在其中可以找到以下文件:

$ ls /proc/$$/attr
current  exec  fscreate  keycreate  prev  sockcreate

所有这些文件,如果阅读,要么显示为空,要么显示一个 SELinux 上下文。如果为空,则意味着该应用程序未明确为该特定目的设置上下文,SELinux 上下文将从策略中推断或从其父级继承。

文件的含义如下:

  • current文件显示进程当前的 SELinux 上下文。

  • exec文件显示由此应用程序执行的下一个应用程序执行分配的 SELinux 上下文。通常为空。

  • fscreate文件显示由应用程序编写的下一个文件分配的 SELinux 上下文。通常为空。

  • keycreate文件显示将由此应用程序在内核中缓存的密钥分配的 SELinux 上下文。通常为空。

  • prev文件显示此特定进程的先前 SELinux 上下文。通常是其父应用程序的上下文。

  • sockcreate文件显示将由应用程序创建的下一个套接字分配的 SELinux 上下文。通常为空。

如果一个应用程序有多个子任务,那么相同的信息可以在每个子任务目录中找到,路径为/proc/<pid>/task/<taskid>/attr

通过类型强制执行访问控制

进程的 SELinux 类型(SELinux 上下文的第三部分)(称为领域)是该进程与自己及其他类型(可以是进程、文件、套接字、网络接口等)之间精细粒度访问控制的基础。在大多数 SELinux 文献中,SELinux 的标签基础访问控制机制被微调为 SELinux 是一个类型强制的强制访问控制系统:当某些操作被拒绝时,(类型层面的)精细粒度访问控制的缺失很可能是原因所在。

通过类型强制,SELinux 可以根据应用程序最初是如何执行的来控制其行为:由用户启动的 Web 服务器将与通过 init 系统执行的 Web 服务器使用不同的类型,即使它们的进程二进制文件和路径相同。通过 init 系统启动的 Web 服务器最有可能被信任(因此允许执行 Web 服务器应该做的任何操作),而手动启动的 Web 服务器则不太可能被视为正常行为,因此将具有不同的权限。

重要提示

大多数 SELinux 的在线资源都集中在类型上。尽管 SELinux 类型只是 SELinux 上下文的第三部分,但对于大多数管理员来说,它是最重要的一部分。大多数文档甚至会只谈论像sshd_t这样的类型,而不是完整的 SELinux 上下文。

看一下以下 dbus-daemon 进程:

# ps -eZ | grep dbus-daemon
swift_u:swift_r:swift_dbusd_t:s0-s0:c0.c512 571 ? 00:00:01 dbus-daemon
swift_u:swift_r:swift_dbusd_t:s0-s0:c0.c512 649 ? 00:00:00 dbus-daemon
system_u:system_r:system_dbusd_t:s0-s0:c0.c1023 2498 ? 00:00:00 dbus-daemon

在这个例子中,一个 dbus-daemon 进程是运行的系统 D-Bus 守护进程,使用了恰如其名的 system_dbusd_t 类型,而另外两个则使用了 swift_dbusd_t 类型。尽管它们的二进制文件相同,但它们在系统中的作用不同,因此分配了不同的类型。SELinux 然后使用该类型来管理进程对其他类型的操作,包括 system_dbusd_t 如何与 swift_dbusd_t 进行交互。

SELinux 类型按照约定以 _t 结尾,尽管这并非强制要求。

通过角色授予领域访问权限

SELinux 角色(SELinux 上下文的第二部分)允许 SELinux 支持基于角色的访问控制。尽管类型强制是 SELinux 中最常用(且最知名)的部分,基于角色的访问控制仍然是确保系统安全的重要方法,尤其是防止恶意用户尝试。SELinux 角色定义了当前上下文可以访问哪些类型(领域)。这些类型(领域)又定义了权限。因此,SELinux 角色有助于定义一个用户(有权访问一个或多个角色)能做什么以及不能做什么。

按约定,SELinux 角色定义时以 _r 结尾。在大多数启用 SELinux 的系统中,管理员可以为用户分配以下 SELinux 角色:

  • user_r角色用于受限用户。这个角色只允许具有特定于端用户应用程序的类型的进程。不允许使用特权类型,包括用于切换到另一个 Linux 用户的类型。

  • staff_r角色用于非关键操作。这个角色通常受限于与受限用户相同的应用程序,但具有切换角色的能力。这是操作员拥有的默认角色(以尽可能保持用户在最低特权角色下的状态)。

  • sysadm_r角色用于系统管理员。这个角色非常特权,能够执行各种系统管理任务。但是,某些端用户应用程序类型可能不受支持(特别是如果这些类型用于潜在的易受攻击或不受信任的软件),以保持系统免受感染。

  • secadm_r角色用于安全管理员。这个角色允许更改 SELinux 策略并操作 SELinux 控制。通常在需要系统管理员和系统策略管理之间进行职责分离时使用。

  • system_r角色用于守护进程和后台进程。这个角色非常特权,支持各种守护进程和系统进程类型。然而,不允许使用此角色进行端用户应用程序类型和其他管理类型的操作。

  • unconfined_r角色用于端用户。这个角色允许有限数量的类型,但这些类型非常特权,因为它们允许以几乎无限制的方式运行用户(或另一个未受限制的进程)启动的任何应用程序(不受 SELinux 规则限制)。因此,只有当系统管理员希望保护某些进程(主要是守护进程)而保持系统其余操作几乎不受 SELinux 限制时,才会提供此角色。

其他角色可能存在,例如guest_rxguest_r,具体取决于发行版。建议查阅发行版文档获取更多关于支持角色的信息。最常用的获取可用角色概览的方法是使用seinfo命令:

# seinfo –-role
Roles: 9
   auditadm_r
   object_r
   secadm_r
   …
   user_r

识别了 SELinux 角色后,让我们看看如何将角色分配给用户。

通过用户限制角色

SELinux 用户(SELinux 上下文的第一部分)并非与 Linux(账户)用户相同。与 Linux 用户信息不同的是,SELinux 策略可以(并且通常会)强制保持 SELinux 用户即使 Linux 用户自身已更改也保持不变。由于 SELinux 用户的不可变状态,我们可以实施特定的访问控制,以确保用户无法绕过授予他们的权限集,即使他们获得特权访问也是如此。

这种访问控制的一个示例是某些 Linux 发行版(可选)启用的基于用户的访问控制UBAC)功能,阻止用户在即使那些用户尝试使用 Linux DAC 控制来授予对彼此文件的访问时,访问不同 SELinux 用户的文件。

然而,SELinux 用户最重要的特性是,SELinux 用户定义限制了(Linux)用户可以承担的角色。Linux 用户首先被分配给一个 SELinux 用户,且该用户不需要唯一:多个 Linux 用户可以分配给同一个 SELinux 用户。一旦设置,该用户不能切换到未与该 SELinux 用户关联的 SELinux 角色。

下图展示了 SELinux 的基于角色的访问控制实现:

图 1.2 – 将 Linux 账户映射到 SELinux 用户

图 1.2 – 将 Linux 账户映射到 SELinux 用户

_u 后缀,尽管这不是强制性的。大多数发行版中可用的 SELinux 用户名称是根据它们所代表的角色命名的,但它们的结尾是 _u,而不是 _r。例如,对于 sysadm_r 角色,我们有 sysadm_u SELinux 用户。

通过敏感性控制信息流

SELinux 上下文的第四部分是敏感性,这部分并非总是存在(某些 Linux 发行版默认不启用敏感性标签,但大多数会启用)。标签的这一部分对于 s 和一个类别值(前缀为 c)是必要的。

在许多较大的组织和公司中,文档被标记为内部、机密或严格机密。SELinux 可以为这些资源分配进程的某些安全级别。通过 MLS,我们可以配置 SELinux 以遵循贝尔-拉帕杜拉模型,这是一种以禁止向上读取,禁止向下写入为特征的安全模型:根据进程的安全级别,该进程不能读取任何具有更高机密性级别的内容,也不能写入(或与之通信)任何具有较低机密性级别的资源。SELinux 不使用内部、机密和其他标签,而是使用从零(最低机密性)到系统管理员定义的最高值的数字(这是可配置的,并且在构建 SELinux 策略时设置)。

分类使我们能够为资源分配一个或多个类别,并定义跨类别的访问控制。使用分类的一个功能是支持多租户(例如,为多个客户托管应用程序的系统)在 Linux 系统中的实现。多租户通过为一个租户的进程和资源分配一组类别来提供,而另一个租户的进程和资源则获得不同的类别。当进程没有分配正确的类别时,它无法访问具有其他类别的资源(或其他进程)。

重要说明

在 SELinux 的世界里,有一个不成文的约定,那就是(至少)使用两种类别来区分不同的租户。通过让服务在预定义的类别集中随机选择两个类别来为租户分配,同时确保每个租户都有独特的组合,这些服务实现了适当的隔离。使用两种类别并不是强制性的,但像 sVirt 和 Docker 这样的服务成功地实现了这一方法。

从这个角度来看,类别就像标签,只在进程和目标资源的标签匹配时才允许访问。由于多级安全性并不常用,仅使用类别的好处被保留在所谓的 s0 中。

现在我们知道 SELinux 策略如何使用标签,让我们来看看 SELinux 策略是如何定义和分发的。

定义和分发策略

启用 SELinux 并不会自动开始执行访问控制。如果 SELinux 被启用但找不到策略,它将拒绝启动,因为策略定义了系统的行为(SELinux 应该允许什么)。SELinux 策略通常以编译后的形式分发(就像软件一样),作为策略模块。这些模块随后会被聚合到一个单独的策略存储区,并加载到内存中,以便 SELinux 在系统上执行策略规则。

重要提示

Gentoo,一个基于源代码的元发行版,将 SELinux 策略分发为(源)代码,在安装时编译和构建,就像它处理其他软件一样。

下图显示了 策略规则策略代码)、策略模块策略包(通常是与 策略存储 一一映射)之间的关系:

图 1.3 – 策略规则(代码)、策略模块和策略存储的关系

图 1.3 – 策略规则(代码)、策略模块和策略存储的关系

正如我们所看到的,策略首先被编写,然后编译成模块,之后这些模块被打包并分发。接下来的几个部分将详细描述这些阶段的过程。

编写 SELinux 策略

SELinux 策略编写者可以使用三种可能的语言编写策略规则:

  • 在标准的 SELinux 源格式中 —— 这是一种人类可读且已广泛应用的编写 SELinux 策略的语言

  • 使用参考策略样式,它通过 M4 宏扩展了标准的 SELinux 源格式,以促进策略的开发

  • 使用 SELinux 通用中间语言 (CIL) —— 这是一个计算机可读(并且经过一定努力后可供人类阅读)的 SELinux 策略格式

大多数支持 SELinux 的发行版基于参考策略(github.com/SELinuxProject/refpolicy/)编写其策略,这是一个完全功能的 SELinux 策略集,作为自由软件项目进行管理。这使得发行版可以携带一个功能完整的策略集,而无需自己编写一个。许多项目贡献者是发行版开发者,他们试图将自己发行版的更改推送到参考策略项目中,这些更改会经过同行评审,以确保不会引入可能危及任何平台安全的规则。在没有参考策略项目提供的丰富 M4 宏集的情况下编写策略是困难的,这就是为什么参考策略已经成为政策的事实标准来源。

SELinux CIL 格式相对较新,尽管它已经在广泛使用(SELinux 用户空间在后台将一切转换为 CIL),但策略编写者直接使用它的情况尚不常见。

为了展示这三种语言之间的差异,考虑我们之前讨论过的 Web 服务器规则,方便起见再重复一遍:允许标记为httpd_t的进程绑定到标记为http_port_t的 TCP 端口。

在标准 SELinux 源格式中,我们将其写为如下:

allow httpd_t http_port_t : tcp_socket { name_bind };

使用参考策略风格,这个规则是以下宏调用的一部分:

corenet_tcp_bind_http_port(httpd_t)

在 CIL 语言中,规则表达式如下:

(allow httpd_t http_port_t (tcp_socket (name_bind)))

在大多数表示中,我们可以看到规则的内容:

  • 主体(谁在执行操作);在此情况下,这是标记为httpd_t类型的进程集合。

  • 目标资源或对象(行动的目标);在此情况下,它是标记为http_port_t类型的 TCP 套接字(tcp_socket)。在参考策略风格中,这是通过函数名称隐含的。

  • 行动或权限;在此情况下,它是绑定端口的操作(name_bind)。在参考策略风格中,这是通过函数名称隐含的。

  • 策略将强制执行的结果;在此情况下,它是允许执行该操作(allow)。在参考策略风格中,这是通过函数名称隐含的。

策略通常是为一个应用程序或一组应用程序编写的。因此,前面的示例将成为为 Web 服务器编写的策略的一部分。

策略编写者通常会为每个应用程序或应用程序集创建三个文件:

  • 一个.te文件,包含类型强制规则。

  • 一个.if文件,包含接口和模板定义,允许策略编写者轻松使用新生成的策略规则来增强其他策略。你可以将其与其他编程语言中的头文件进行比较。

  • 一个.fc文件,包含文件上下文表达式。这些是将标签分配给文件系统资源的规则。

完成的策略随后会打包成 SELinux 策略模块。

通过模块分发策略

最初,SELinux 使用单一的、单体的策略方法:所有可能的访问控制规则都保存在一个单一的策略文件中。很快就显而易见,这种方式从长远来看无法管理,于是开发模块化策略的方法应运而生。

在模块化方法中,策略开发者可以为特定应用程序(或应用程序集)、角色等编写独立的策略集。然后,这些策略将被构建并作为策略模块分发。需要为特定应用程序提供访问控制的平台会加载定义该应用程序访问规则的 SELinux 策略模块。

下图展示了策略模块的构建过程。它还展示了即使策略规则本身不是用 CIL 编写的,CIL 也如何发挥作用:

图 1.4 – 从策略规则到策略存储的构建过程

图 1.4 – 从策略规则到策略存储的构建过程

二进制 *.pp 文件(即 SELinux 策略模块)被认为是用高级语言HLL)编写的。但不要以为这意味着它们是人类可读的:这些文件是二进制文件。这里的考虑是,SELinux 希望支持以多种格式编写 SELinux 策略,这些格式被称为高级语言,只要有一个能够将文件转换为 CIL 的解析器。将二进制模块格式(在早期版本的 SELinux 中,这些是加载到内存中的二进制块)标记为高级语言,允许 SELinux 项目以向后兼容的方式引入高级语言和 CIL 之间的区别。

在分发 SELinux 策略模块时,大多数 Linux 发行版会将 *.pp SELinux 策略模块放置在 /usr/share/selinux 中,通常是在一个以策略存储名称命名的子目录中(如 targeted)。在这里,这些模块准备好供管理员激活。

在激活模块时,semodule 命令会将这些模块复制到一个专用目录(/var/lib/selinux/mcs/active/modules)。当所有模块聚集在一个位置时,最终的策略二进制文件会被编译,生成 /etc/selinux/targeted/policy/policy.32(或其他某个编号),并加载到内存中。

在 CentOS 上,SELinux 策略由 selinux-policy-targeted(或 -minimum-mls)包提供。在 Gentoo 上,它们由不同的 sec-policy/selinux-* 包提供(Gentoo 为每个模块使用独立的包,减少了加载到系统上的 SELinux 策略数量)。

在策略存储中捆绑模块

策略存储包含一个完整的策略,并且在任何时候,系统上只能激活一个策略。管理员可以切换策略存储,尽管这通常需要重新启动系统,甚至可能需要重新标记整个系统(重新标记是指重置系统中所有文件和资源的上下文)。

系统上当前的活动策略可以通过以下方式使用 sestatus(SELinux 状态)查询:

# sestatus | grep "Loaded policy name"
Loaded policy name:     mcs

在此示例中,mcs 是当前加载的策略(存储)。SELinux 在下一次重启时使用的策略名称在 /etc/selinux/config 配置文件中定义,作为 SELINUXTYPE 参数。

系统的 init 系统(无论是兼容 SysVinit 系统还是 systemd)通常负责加载 SELinux 策略,实际上是在系统上激活 SELinux 支持。init 系统读取配置文件,找到策略存储并将策略文件加载到内存中。如果 init 系统不支持此功能(即它不支持 SELinux),则应通过 load_policy 命令加载策略。

现在我们对策略开发和分发流程有了更好的了解,接下来看看 Linux 发行版如何区分它们的 SELinux 提供。

区分策略

最常见的 SELinux 策略存储名称是 stricttargetedmcsmls。然而,分配给策略存储的名称并不是固定的,所以这只是一个约定。因此,我们建议查阅发行版文档,以验证正确的策略名称应该是什么。不过,名称通常提供了一些关于通过策略启用的 SELinux 选项的信息。

支持 MLS

可以启用的选项之一是 MLS 支持。如果禁用此选项,SELinux 上下文将不会包含带有敏感信息的第四个字段,从而使进程和文件的上下文如下所示:

staff_u:sysadm_r:sysadm_t

要检查 MLS 是否启用,只需查看进程上下文是否包含第四个字段。另一种方法是检查 sestatus 输出中的 Policy MLS Status 行:

# sestatus | grep MLS
Policy MLS status:       enabled

另一种方法是查看伪文件 /sys/fs/selinux/mls。值 0 表示禁用,而值 1 表示启用:

# cat /sys/fs/selinux/mls
1

启用 MLS 的策略存储通常是 targetedmcsmls,而 strict 通常禁用 MLS。

处理未知权限

权限(如读取、打开和锁定)在 Linux 内核和策略本身中都有定义。然而,有时较新的 Linux 内核支持当前策略尚未理解的权限。

block_suspend 权限(用于阻止系统挂起)为例。如果 Linux 内核支持(并检查)该权限,但已加载的 SELinux 策略尚不理解该权限,那么 SELinux 必须决定如何处理该权限。我们可以配置 SELinux 执行以下操作之一:

  • 允许与未知权限相关的所有操作(allow)。

  • 拒绝与未知权限相关的所有操作(deny)。

  • 当检查到未知权限时,停止并终止系统(reject)。

我们通过deny_unknown值来配置此设置。要查看未知权限的状态,请在sestatus中查找Policy deny_unknown status行:

# sestatus | grep deny_unknown
Policy deny_unknown status:      allowed

管理员可以通过/etc/selinux/semanage.conf文件中的handle-unknown变量(取值为allowdenyreject)来为自己设置此项。

支持 unconfined 域

一个 SELinux 策略可以非常严格,将应用程序的行为限制得尽可能接近实际情况,但它也可以在允许应用程序执行的操作上非常宽松。许多 SELinux 策略中都有 unconfined 域的概念。当启用时,这意味着某些 SELinux 域(进程上下文)可以几乎做任何它们想做的事情(当然,仍然会受到常规 Linux DAC 权限的限制),而只有少数域在其行为上受到真正的限制(约束)。

引入 unconfined 域是为了让 SELinux 在桌面和服务器上保持活跃,这些系统中管理员不希望完全限制整个系统,而仅仅是限制系统上少数几个应用程序。通常,这些实现专注于约束面向网络的服务(如 Web 服务器和数据库管理系统),同时允许最终用户和管理员在不受限制的情况下自由活动。

对于其他 MAC 系统,如 AppArmor,解限是系统设计的固有部分,因为它们只限制特定应用程序或用户的行为。然而,SELinux 旨在成为一个全面的强制访问控制系统,因此即使对于那些不是安全管理员主要关注的应用程序,也需要提供访问控制规则。通过将这些应用程序标记为 unconfined,几乎不会对 SELinux 施加任何限制。

我们可以通过seinfo查看系统是否启用了 unconfined 域,通过查询策略并询问是否定义了unconfined_t SELinux 类型。在支持 unconfined 域的系统上,此类型将是可用的:

# seinfo -t unconfined_t
Types: 1
  unconfined_t

对于不支持 unconfined 域的系统,该类型将不包含在策略中:

# seinfo -t unconfined_t
Types: 0

大多数启用 unconfined 域的发行版将其策略称为targeted,但这种约定并不总是被遵循。因此,最好始终使用seinfo查询策略。CentOS 启用 unconfined 域,而 Gentoo 则通过unconfined USE标志来配置此设置。

限制跨用户共享

当启用 UBAC 时,某些 SELinux 类型将受到额外的约束。这将确保一个 SELinux 用户不能访问另一个用户的文件(或其他特定资源),即使这些用户通过常规 Linux 权限共享他们的数据。UBAC 提供了对资源之间信息流的额外控制,但它远非完美。其本质是将 SELinux 用户相互隔离开来。

重要说明

SELinux 中的约束是一个访问控制规则,它利用上下文的所有部分来做出决策。与仅基于类型的类型强制规则不同,约束可以考虑 SELinux 用户、SELinux 角色或敏感性标签。约束通常只开发一次,之后不会再更改——大多数策略编写者在开发过程中不会触碰约束。

许多 Linux 发行版,包括 CentOS,禁用了 UBAC。Gentoo 允许用户通过 Gentoo ubac USE 标志(默认启用)来决定是否启用 UBAC。

增量的策略版本

在检查sestatus的输出时,我们看到也有对策略版本的引用:

# sestatus | grep version
Max kernel policy version:       32

该版本与策略规则的版本控制无关,而是与当前运行的内核支持的 SELinux 特性有关。在前面的输出中,32 是当前内核支持的最高策略版本。每次向 SELinux 中添加新特性时,版本号都会增加。我们可以在/etc/selinux/targeted/policy中找到策略文件本身(该文件包含系统在启动时加载的所有 SELinux 规则)(其中,targeted 是使用的策略存储区的名称,因此如果系统使用名为mcs的策略存储区,则路径将为/etc/selinux/mcs/policy)。

如果存在多个策略文件,使用seinfo来查找正在使用的策略版本文件:

# seinfo | grep Version
Policy version:                  31 (MLS enabled)

接下来提供了一个策略功能增强列表,以及该功能引入的 Linux 内核版本。许多特性仅对策略开发者相关,但了解特性的发展有助于我们更好地理解 SELinux 的演变:

  • 版本 12 代表 SELinux 的“旧 API”,现已被弃用。

  • 版本 15,在 Linux 2.6.0 中引入,提供了新的 SELinux API。

  • 版本 16,在 Linux 2.6.5 中引入,增加了对条件策略扩展的支持。

  • 版本 17,在 Linux 2.6.6 中引入,增加了对 IPv6 的支持。

  • 版本 18,在 Linux 2.6.8 中引入,增加了对精细化的 netlink 套接字权限的支持。

  • 版本 19,在 Linux 2.6.12 中引入,增加了对 MLS 的支持。

  • 版本 20,在 Linux 2.6.14 中引入,减少了访问向量表的大小。

  • 版本 21,在 Linux 2.6.19 中引入,增加了对 MLS 范围转换的支持。

  • 版本 22,在 Linux 2.6.25 中引入,增加了对策略能力的支持。

  • 版本 23,在 Linux 2.6.26 中引入,增加了对每个域宽松模式的支持。

  • 版本 24,在 Linux 2.6.28 中引入,增加了对显式层次结构(类型边界)的支持。

  • 版本 25,在 Linux 2.6.39 中引入,增加了对基于文件名的转换的支持。

  • 版本 26,在 Linux 3.0 中引入,增加了对非进程类的角色转换支持,以及对角色属性的支持。

  • 版本 27,在 Linux 3.5 中引入,增加了对新创建对象的 SELinux 用户和 SELinux 角色的灵活继承支持。

  • 版本 28,首次在 Linux 3.5 中引入,增加了对新创建对象的 SELinux 类型灵活继承的支持。

  • 版本 29,首次在 Linux 3.14 中引入,增加了对 SELinux 限制内属性的支持。

  • 版本 30,首次在 Linux 4.3 中引入,增加了对扩展权限的支持,首先在 ioctl 控制上实现。它还引入了增强的 SELinux Xen 支持。

  • 版本 31,首次在 Linux 4.13 中引入,增加了对 InfiniBand 访问控制的支持。

  • 版本 32,首次在 Linux 5.5 中引入,增加了对自动推断敏感标签交集的支持,这被称为最小下界,最大上界 (glblub)。

默认情况下,当 SELinux 策略构建时,会使用由 Linux 内核和 libsepol(负责构建 SELinux 策略二进制文件的库)定义的最高支持版本。管理员可以使用 /etc/selinux/semanage.conf 中的 policy-version 参数强制版本降级。

不同的策略内容

除了前一节中描述的策略功能外,策略(和发行版)之间的主要区别是策略内容本身。我们已经讨论过,大多数发行版基于参考策略项目来构建他们的策略。尽管参考策略项目被视为大多数发行版的主策略,但每个发行版都有自己对这一主策略集的偏离。

许多发行版对策略进行了大量增强,但并未直接将策略提交给上游参考策略项目。这样做可能有几个原因:

  • 策略的增强或新增仍不成熟:Fedora、CentOS 和 Red Hat 最初使用的是主动的宽松策略,这意味着策略并未强制执行。相反,SELinux 会记录它本应阻止的操作,并根据这些日志进一步增强策略。这意味着一个策略只有在发布几个版本后才算准备好。

  • 策略的增强或新增过于特定于发行版:如果一个策略集不能在其他发行版中重用,那么一些发行版会选择将这些策略保留给自己,因为将更改推送到上游项目需要付出相当大的努力。

  • 策略的增强或新增没有遵循上游规则和指南:参考策略有一套策略需要遵循的指南。如果一个策略集不符合这些规则,那么参考策略将不接受该贡献。

  • 策略的增强或新增并未实现参考策略项目所期望的相同安全模型:由于 SELinux 是一个非常广泛的强制访问控制系统,因此可以编写完全不同的策略。

  • 发行版没有足够的时间或资源将更改提交到上游。

这意味着 SELinux 策略在不同的发行版之间(甚至同一发行版的不同版本之间)可能会有所不同。

通过这一点,我们可以总结出各个发行版在 SELinux 策略中的一些差异:它们可以选择启用或禁用 MLS 支持,允许或拒绝未知权限,添加发行版提供的未受限域,支持基于用户的访问控制,和/或偏离参考策略项目以适应发行版的原则。

总结

在本章中,我们看到 SELinux 在 Linux 访问控制之上提供了更细粒度的访问控制机制。SELinux 通过 Linux 安全模块实现,并使用标签根据所有权(用户)、角色、类型,甚至资源的安全敏感性和分类来识别其资源和进程。我们介绍了 SELinux 策略在 SELinux 启用系统中的处理方式,并简要讨论了策略编写者如何构建策略。

Linux 发行版实现了 SELinux 策略,这些策略在不同发行版之间可能会有所不同,具体取决于支持的功能,例如敏感度标签、未知权限的默认行为、对限制级别的支持,或施加的特定约束,如 UBAC。然而,大多数策略规则本身是相似的,甚至基于相同的上游参考策略项目。

切换 SELinux 执行模式以及理解 SELinux 在禁止某些访问时创建的日志事件,是我们下一章的主题。在其中,我们还将介绍如何处理常听到的禁用 SELinux 的要求,以及为什么这样做是错误的方向。

问题

  1. DAC 和 MAC 系统之间最重要的区别是什么?

  2. Linux 如何支持不同的 MAC 技术?

  3. SELinux 上下文由哪四个字段构成?

  4. SELinux 如何支持基于角色的访问控制?

  5. 为什么不同的 Linux 发行版没有统一的 SELinux 策略?

第二章:理解 SELinux 决策和日志记录

一旦我们在系统上启用 SELinux,它就会启动其访问控制功能,如上一章所述。一旦启动,管理员需要密切关注其行为,并且通常需要处理如果一个或多个应用程序未按照 SELinux 策略运行时的意外行为。通过 SELinux 日志记录,我们了解 SELinux 如何对系统上的应用程序执行其策略。

管理员需要了解如何在 SELinux 的完全强制模式(类似于基于主机的入侵防御系统)与其宽容日志记录模式之间切换,并使用其各种方法切换 SELinux 的状态(启用或禁用;宽容或强制)。此外,我们还应了解如何禁用 SELinux 对单个域的强制执行,而不是整个系统,并学习如何解释 SELinux 日志事件,这些事件描述了 SELinux 阻止了哪些活动。最后,我们将介绍一些常见的方法,用于在日常操作中分析这些日志事件。

在本章中,我们将涵盖以下主要主题:

  • 开启和关闭 SELinux

  • SELinux 日志记录与审计

  • 获取拒绝请求的帮助

技术要求

查看以下视频,了解代码的实际操作:bit.ly/3dFaUXm

开启和关闭 SELinux

这也许是一个奇怪的开始部分,但禁用 SELinux 是一个常见的请求活动。一些供应商不支持其应用程序在启用 SELinux 的平台上运行,因为这些供应商没有开发 SELinux 策略的专业能力,或者无法培训自己的支持团队来处理 SELinux。

此外,系统管理员通常不愿使用他们不理解或觉得过于复杂的安全控制。幸运的是,SELinux 正在成为多个 Linux 发行版中的事实标准技术,这增加了其在管理员中的曝光度和理解。SELinux 还能够选择性地禁用其访问控制,仅针对系统的一部分,而不是要求我们禁用整个系统。

设置全局 SELinux 状态

SELinux 支持三种主要的状态:disabledpermissiveenforcing。这些状态通过/etc/selinux/config文件中的SELINUX变量进行设置,如以下代码片段所示:

$ grep ^SELINUX= /etc/selinux/config
SELINUX=enforcing

init系统进程加载 SELinux 策略时,SELinux 代码会检查管理员配置的状态。各个状态的描述如下:

  • 如果状态为disabled,则 SELinux 代码会禁用进一步的支持,系统启动时不会激活 SELinux。

  • 如果状态是宽容,则表示 SELinux 处于活动状态,但不会强制执行其政策。相反,SELinux 会报告任何违反政策的行为,但不会阻止这些行为本身。这有时被称为主机入侵检测,因为它仅在报告模式下工作。

  • 如果状态是强制,则表示 SELinux 处于活动状态,并将强制执行其政策。违反规则的行为会被报告并拒绝。这有时被称为主机入侵防护,因为它在执行规则的同时记录其采取的操作。

我们可以使用getenforce命令或sestatus命令获取 SELinux 当前状态的信息,如下所示:

$ sestatus | grep mode
Current mode:	enforcing
$ getenforce
Enforcing

也可以查询/sys/fs/selinux/enforce伪文件来获取类似的信息。如果该文件返回1,则 SELinux 处于强制模式。如果返回0,则处于宽容模式。以下代码片段显示 SELinux 处于强制模式:

$ cat /sys/fs/selinux/enforce
1

当我们修改/etc/selinux/config文件时,需要重启系统以使更改生效。然而,如果我们启动一个不支持 SELinux 的系统(禁用),仅重新启用 SELinux 支持是不够的:管理员还需要确保系统上的所有文件都已重新标记(所有文件的上下文需要被设置)。如果没有 SELinux 支持,Linux 会创建和更新文件,但不会更新或设置这些文件的 SELinux 标签。当系统后来重新启动并启用 SELinux 支持时,除非重新设置标签,否则 SELinux 将无法知道文件的上下文。

文件系统重新标记的内容在第四章使用文件上下文和进程域中有介绍。

在很多情况下,当管理员发现 SELinux 阻止某些任务时,他们常常想禁用 SELinux。这样做至少可以说是草率的,原因如下:

  • SELinux 是一个安全组件——操作系统的一部分。禁用 SELinux 就像禁用防火墙,因为它在阻止某些通信。虽然禁用 SELinux 可能帮助快速让某些功能恢复工作,但这实际上是在移除本来是为了保护你的安全措施。

  • 就像防火墙一样,SELinux 是通过规则进行配置的。如果某个应用程序无法正常工作,我们需要更新该应用程序的规则,就像更新额外的防火墙规则以启用特定的网络流量一样。我们将在第五章控制网络通信中开始更新 SELinux 策略规则。

  • 在最坏的情况下,当我们希望无条件允许应用程序执行的每一个操作时,我们仍然可以保持 SELinux 启用,并将该应用程序运行在一个不受限制的 SELinux 域中,称为宽容域。

各个发行版在将 SELinux 集成到产品中付出了大量的努力,并且提供了优秀的支持渠道,以防万一一切失败时帮助你。

切换到宽容模式或强制模式

大多数发行版提供的 Linux 内核允许通过一个简单的管理命令在强制模式和宽松模式之间切换。这个功能被称为CONFIG_SECURITY_SELINUX_DEVELOP内核配置参数。如果启用该内核参数,Linux 内核也会首先以宽松模式启动,除非设置了特定的启动选项(enforcing=1)。

尽管我们可以认为这种开发模式存在风险(恶意用户只需要将 SELinux 切换到宽松模式就能禁用其访问控制),但切换模式需要强大的管理权限(例如root用户拥有的权限),而大多数应用程序领域并不具备这种权限。

切换宽松模式和强制模式的命令是setenforce命令。它接受一个参数:0(宽松模式)或1(强制模式)。该命令也允许使用permissiveenforcing字符串。

改动会立即生效。例如,我们可以使用以下命令切换到宽松模式:

# setenforce 0

setenforce的效果与将正确的整数值写入/sys/fs/selinux/enforce伪文件的效果相同,如下代码片段所示:

# echo 0 > /sys/fs/selinux/enforce

在修改系统以正确使用 SELinux 时,切换宽松模式和强制模式可能对策略开发人员或系统管理员很有用。我们也可以用它来快速验证应用程序警告或错误是否由于 SELinux 访问控制引起—假设该应用程序没有 SELinux 感知,我们将在理解 SELinux 感知的应用程序部分讨论这个问题。

在生产系统上,可能需要禁用切换到宽松模式的能力。禁用此功能通常需要重新编译 Linux 内核,但 SELinux 策略开发人员也考虑了另一种不允许用户切换 SELinux 状态的方法。用户切换到宽松模式所需的权限是有条件的,系统管理员可以轻松切换此条件来禁用从强制模式切换回宽松模式的操作。这个条件是通过名为secure_mode_policyload的 SELinux 布尔值实现的,默认值为off(意味着允许切换 SELinux 状态)。

onoff(虽然true/false1/0也是有效值),并操作活动的 SELinux 策略的部分。条件的值可以被持久化(意味着它们在重启后依然有效)或仅在当前启动会话期间有效。我们可以通过在setsebool命令中添加-P来使值在重启后持久化,如下所示:

# setsebool -P secure_mode_policyload on

SELinux 布尔值将在第十二章调优 SELinux 策略中详细介绍。

使用secure_mode_policyload SELinux 布尔值允许管理员限制从强制模式切换回宽松模式。这并不会完全禁用 SELinux,只是切换它是否会执行其策略。

从禁用状态切换到运行状态是不被支持的。然而,反向操作是可能的,但只有在以下条件下:如果 Linux 内核是使用 SECURITY_SELINUX_DISABLE 内核配置参数构建的,那么像 init 这样的服务可以有效地在运行时禁用 SELinux,但前提是尚未加载 SELinux 策略。然而,这个功能并不推荐主动使用,它只为那些启动选项难以使用的平台引入。最近的内核将此功能标记为已弃用,因为此类平台数量很少。

使用内核启动参数

使用 setenforce 命令是有意义的,当我们希望在与系统交互时切换到宽松或强制模式。但如果我们需要在系统启动时设置呢?如果由于 SELinux 访问控制系统无法正常启动,我们就无法编辑 /etc/selinux/config 文件。幸运的是,我们还可以通过其他方式更改 SELinux 状态。

解决方案是使用内核启动参数。我们可以使用一个或两个启动参数来启动 Linux 系统,这些参数会优先于 /etc/selinux/config 设置,如下所示:

  • selinux=0:这通知系统完全禁用 SELinux,其效果与在配置文件中设置SELINUX=disabled相同。设置后,不会考虑其他参数(如enforcing)。请记住,启动一个禁用 SELinux 的系统意味着要再次启用它时,我们需要重新标记文件系统上的所有文件和资源。selinux= 参数通过 CONFIG_SECURITY_SELINUX_BOOTPARAM 内核配置得到支持。

  • enforcing=0:这通知系统以宽松模式运行 SELinux,其效果与在配置文件中设置SELINUX=permissive相同。

  • enforcing=1:这通知系统以强制模式运行 SELinux,其效果与在配置文件中设置SELINUX=enforcing相同。

假设有一个使用 GRUB2 作为引导加载程序的 Linux 系统,我们希望将 enforcing=0 添加到启动项中。为此,我们执行以下步骤:

  1. 重启系统,直到 GRUB2 启动屏幕出现。

  2. 使用箭头键导航到需要更改 SELinux 状态的启动项。这通常是默认的启动项,应该已经被选中。

  3. E 键编辑启动项行。在 GRUB2 倒计时归零之前进行此操作;否则,系统将继续启动。

  4. 使用箭头键移动到以options开头的行的末尾。如果没有这样的行,请转到以linuxlinux16linuxefi开头的行的末尾。

  5. enforcing=0 添加到该行的末尾。

  6. Ctrl + XF10 启动该项。

其他引导加载程序有类似的方式来更改启动行,但不会在每次重启时保持更改。有关更多详细信息,请参考您的发行版文档。

除了 SELinux 特定的参数外,还有一些与Linux 安全模块LSM)相关的启动参数,尤其是在将多个 LSM 模块组合到同一系统中时,这些参数非常有用。它们的详细信息如下:

  • lsm.debug启动参数启用 LSM 初始化调试输出,显示实际启用或忽略了哪些 LSM 模块,以及哪些 LSM 模块被认为是排他的。

  • lsm=lsm1,…,lsmN选项选择 LSM 初始化的顺序。例如,要在锁定之前初始化 SELinux,可以使用lsm=selinux,lockdown

  • security=启动参数启用选择活动的主要/独占 LSM 模块。然而,这个参数已经被弃用,推荐使用lsm=参数。

在生产环境中使用 SELinux 时,可能需要妥善保护启动菜单——例如,通过密码保护菜单并定期验证启动菜单文件的完整性。

禁用单个服务的 SELinux 保护

从版本 23(随 Linux 2.6.26 发布)开始,SELinux 还支持在宽松模式和强制模式之间更细粒度的切换方法:使用宽松领域。如前所述,域是 SELinux 用于指派给进程的类型(标签)的术语。通过宽松领域,我们可以将一个或多个领域标记为宽松(因此不受 SELinux 规则的强制执行),即使系统的其他部分仍在强制模式下运行。

要使某个领域变为宽松,可以使用semanage命令,方法如下:

# semanage permissive -a minidlna_t

使用相同的semanage命令,我们可以列出当前定义的宽松领域,如下所示:

# semanage permissive -l
Builtin Permissive Types
Customized Permissive Types
minidlna_t

在之前的示例中,你会注意到也有为内建的宽松类型留有空间。这些是由 Linux 发行版的政策开发者标记为宽松的领域。有些发行版选择首先以宽松模式引入新的应用政策,允许用户在强制执行之前测试这些政策。当这种情况发生时,你可以在Builtin Permissive Types下找到这些宽松领域。

列出自定义宽松类型(那些没有通过发行版标记为宽松的类型)的另一种方法是使用semodule命令。在前一章中,我们在讨论 SELinux 政策模块时简要提到过该命令。我们可以使用它列出名称中包含permissive_的 SELinux 政策模块,因为semanage permissive命令会生成一个小的 SELinux 政策模块,将域标记为宽松,下面的代码片段展示了这一过程:

# semodule -l | grep permissive_
permissive_minidlna_t

要从领域中移除宽松模式,可以将-d参数传递给semanage命令。不过,这只有在系统管理员将该领域标记为宽松时才可行——通过这种方式,无法将发行版提供的宽松领域切换为强制模式。下面的代码片段展示了这一点:

# semanage permissive -d minidlna_t

当一个域被标记为宽松模式时,应用程序的行为应该就像系统上没有启用 SELinux 一样(SELinux 不会强制执行该特定应用程序/域的任何操作),这使得我们更容易发现 SELinux 是否真的引发了权限问题。不过,请注意,其他域(包括与宽松域交互的域)仍然受到 SELinux 访问控制的管理和强制执行。

理解 SELinux 感知应用程序

大多数应用程序本身并不知道它们正在运行在启用了 SELinux 的系统上。没有这一信息,宽松模式实际上意味着应用程序的行为就像最初没有启用 SELinux 一样。然而,一些应用程序积极依赖 SELinux 策略来做出访问控制决策,或与 SELinux 交互以获取更多信息。我们将这些应用程序称为SELinux 感知应用程序,因为它们会根据可用的 SELinux 相关信息改变其行为。

可惜的是,许多这些 SELinux 感知的应用程序并没有正确验证它们是否在宽松模式下运行。因此,在宽松域中运行这些应用程序(或整个系统处于宽松模式)通常不会导致应用程序的行为像 SELinux 没有启用一样。

这类应用程序的示例包括init系统、一些 cron 守护进程以及几个核心 Linux 实用程序(如lsid)。即使 SELinux 没有处于强制模式,它们也可能会根据 SELinux 策略显示权限失败或不同的行为。

我们可以通过检查应用程序是否动态链接libselinux库来判断它是否是 SELinux 感知的。可以使用readelflddobjdump进行此类检查,如下所示:

$ readelf -d /bin/ls | grep selinux
0x0000000000000001 (NEEDED)		Shared library: [libselinux.so.1]
$ ldd /bin/ls | grep selinux
libselinux.so.1 => /lib64/libselinux.so.1 (0x00005d415f3f03f0)
$ objdump -x /bin/ls | grep selinux
NEEDED	libselinux.so.1

知道一个应用程序是否是 SELinux 感知的,有助于故障排除,因为应用程序的行为在 SELinux 被禁用状态和宽松 SELinux 状态下可能仍然不同。

直到现在,我们一直关注启用或禁用 SELinux,从而关注的是细粒度或粗粒度的设置。但一旦启用 SELinux,它与管理员的交互将通过策略执行和日志记录来进行。那么,接下来让我们看看 SELinux 如何处理日志记录。

SELinux 日志记录和审计

SELinux 开发人员明白,像 SELinux 这样的面向安全的子系统只有在具备增强的日志记录甚至调试能力时才能成功。SELinux 所执行的每个动作,作为它实现的 LSM 钩子的一部分,都应该是可审计的。拒绝(SELinux 阻止的操作)应始终记录下来,以便管理员采取相应的措施。SELinux 的调优和更改,例如加载新策略或更改 SELinux 布尔值,应该始终触发一个审计事件。

跟踪审计事件

默认情况下,SELinux 会将其消息发送到 Linux 审计子系统(假设 Linux 内核已配置为启用审计子系统,且通过CONFIG_AUDIT内核配置启用)。在那里,消息将被 Linux 审计守护进程(auditd)接收并记录在/var/log/audit/audit.log文件中。发行版和管理员可以通过配置审计调度进程(audisp)来定义额外的处理规则,该进程负责接收审计事件并将它们分发给一个或多个单独的进程。SELinux 故障排除守护进程(setroubleshootd),一个用于帮助故障排除 SELinux 事件的可选服务,通过此方式获取审计事件。

审计事件流在该图中展示:

图 2.1 – SELinux 生成的审计事件流

图 2.1 – SELinux 生成的审计事件流

启用 SELinux 后,(几乎)每个导致拒绝的权限检查都会被记录。当启用 Linux 审计时,这些拒绝将默认通过审计守护进程记录在audit.log文件中。如果审计守护进程不可用,事件将存储在 Linux 内核消息缓冲区中,我们可以通过dmesg命令查看。内核消息缓冲区中的事件也常常通过系统日志记录器捕获。

如果安装了 SELinux 故障排除守护进程,则审计守护进程除了记录日志外,还会通过审计调度系统将事件分发到sedispatch命令。该命令将进一步处理事件,并通过 D-Bus(Linux 系统上流行的系统总线实现)将其发送到 SELinux 故障排除守护进程。该守护进程将分析事件,并可能向管理员建议一个或多个修复方案。我们将在获取拒绝帮助部分中介绍 SELinux 故障排除守护进程。

每当 SELinux 验证特定的访问时,它并不总是遍历整个策略。相反,它有一个访问向量缓存AVC),在其中存储之前访问尝试的结果。这个缓存确保 SELinux 能够快速响应活动,同时对性能的影响很小。我们注意到这个缓存的缩写作为大多数 SELinux 事件的消息类型,如以下示例开始时所示:

type=AVC msg=audit(03/22/2020 12:15:38.557:2331): avc: denied { read } for pid=12569 comm="dmesg" name="xterm-256color" dev="sdb2" ino=131523 scontext=sysadm_u:sysadm_r:dmesg_t:s0-s0:c0.c1023 tcontext=system_u:object_r:etc_t:s0 tclass=file permissive=0

当 Linux 内核检查权限请求时,这个请求被表示为访问向量,然后会查询缓存以快速找到合适的响应。如果缓存中有正确的访问向量,则决策来自缓存;否则,SELinux 子系统会查阅策略并更新缓存。当然,当加载新的策略或动态调整策略时,SELinux 会使缓存无效。这确保了所有的权限检查都符合活动的策略。

SELinux 的内部工作原理与大多数管理员关系不大,但至少现在我们知道了术语AVC的来源。

调整 AVC

可以通过设置缓存或相关表的大小来稍微调整 AVC。

我们可以通过 /sys/fs/selinux/avc/cache_threshold 伪文件(如果设置了 CONFIG_SECURITY_SELINUX_AVC_STATS 内核配置)来配置缓存大小。例如,要将缓存大小增加到 768 条目(默认值为 512),可以使用以下命令:

# echo 768 > /sys/fs/selinux/avc/cache_threshold

要确认缓存阈值,请读取文件,如下所示:

# cat /sys/fs/selinux/avc/cache_threshold
768

AVC 哈希统计信息可以通过 hash_stats 伪文件访问,如以下代码片段所示:

$ cat /sys/fs/selinux/avc/hash_stats
entries: 506
buckets used: 233/512
longest chain: 5

如果你怀疑系统性能下降是由于 SELinux,建议你查看 hash_stats 中的 longest chain 输出。如果它大于 10,则可能会影响性能,更新缓存大小可能有助于解决问题。

avcstat 命令显示缓存随时间变化的情况(第一个数字是自启动以来的总数)。当缓存未命中次数很高或波动,或者回收次数(淘汰最旧的缓存条目并将其重新用于新条目)波动时,可能需要增加缓存大小。命令如以下代码片段所示:

$ avcstat 5
lookups	hits		misses	allocs	reclaims	frees
58396334	58382324	14010		14010		10736		13511
591		591		0		0		0		0
1657		1653		4		4		0		0

最近的内核还允许通过内核配置参数设置使用的桶数(CONFIG_SECURITY_SELINUX_SIDTAB_HASH_BITS),其缓存统计信息可以通过 /sys/fs/selinux/ss/sidtab_hash_stats 伪文件查看,如以下代码片段所示:

$ cat /sys/fs/selinux/ss/sidtab_hash_stats
entries: 285
buckets use: 55/512
longest chain: 3

另一个性能参数是内部 CONFIG_SECURITY_SELINUX_SID2STR_CACHE_SIZE 设置的大小。

开启更多日志记录

有一个重要的 SELinux 策略指令可以控制哪些操作被(不)审计,那就是 dontaudit。SELinux 策略中的 dontaudit 规则告诉 SELinux 访问拒绝不应该被记录。这是唯一一个 SELinux 不会记录拒绝的例子——SELinux 策略编写者明确禁用了事件的审计。这通常是为了减少日志的杂乱,隐藏那些对系统安全没有影响的外观性拒绝。

seinfo 工具可以告诉我们当前有多少条这些规则,以及其兄弟规则 auditallow(即使政策允许,也记录事件),如以下代码片段所示:

$ seinfo | grep -i audit
Auditallow:	1	Dontaudit:		5559
Auditallowxperm:	0	Dontauditxperm:	   0

幸运的是,我们可以随意禁用这些 dontaudit 规则。通过以下 semodule 命令,这些规则将从活动策略中移除:

# semodule –-disable_dontaudit –-build

这些参数也可以分别缩写为 -D-B。要重新启用 dontaudit 规则,只需像下面这样重新构建策略:

# semodule -B

禁用 dontaudit 规则有时有助于排查那些没有产生有用审计事件的故障。然而,通常情况下,策略编写者标记为外观性的审计事件并不是故障的原因。

配置 Linux 审计

当 Linux 内核审计子系统未配置或 Linux 审计守护程序未运行时,SELinux 将尝试使用审计子系统并退回到常规系统日志记录。

对于 Linux 审计,通常不需要配置任何东西,因为 SELinux AVC 拒绝默认会被记录。您会在审计日志文件(/var/log/audit/audit.log)中找到这些拒绝信息,通常与同一操作相关的系统调用和其他事件消息一起记录,如下面的代码片段所示:

type=PROCTITLE msg=audit(...) : proctitle=ping 8.8.8.8 
type=SYSCALL msg=audit(...) : arch=x86_64 syscall=socket success=no exit=EACCES(Permission denied) a0=inet a1=SOCK_DGRAM a2=icmp a3=0x7fffac013050 items=0 ppid=2685 pid=17292 auid=admin uid=root gid=root euid=root suid=root fsuid=root egid=root sgid=root fsgid=root tty=tty1 ses=1 comm=ping exe=/bin/ping subj=sysadm_u:sysadm_r:ping_t:s0-s0:c0.c1023 key=(null) 
 type=AVC msg=audit(...) : avc:  denied  { create } for  pid=17292 comm=ping scontext=sysadm_u:sysadm_r:ping_t:s0-s0:c0.c1023 tcontext=sysadm_u:sysadm_r:ping_t:s0-s0:c0.c1023 tclass=icmp_socket permissive=0 

要配置审计系统的目标日志文件,请在/etc/audit/auditd.conf中使用log_file参数。

要启用远程审计日志记录(将多个主机的审计事件集中到单个系统上),您可以选择启用 Syslog 转发或启用audisp-remote插件。

使用 Syslog 转发时,配置审计调度守护程序将审计事件发送到本地系统记录器。然后,管理员需要配置本地系统记录器以向远程系统传递事件。

信息提示

使用 Syslog 转发的优点是在服务器上集中其日志事件时不需要额外的软件部署和守护程序。因此,即使在环境中引入了硬化设备,该设置也可以重复使用。当然,还存在其他可以监视日志事件并将其发送到中央服务器的日志管理解决方案。但是,这些解决方案需要更多的配置,并引入额外的软件代理进行安装。

编辑/etc/audit/plugins.d/syslog.conf文件,并将active设置为yes,如下所示:

# vi /etc/audit/plugins.d/syslog.conf
active = yes
direction = out
path = /sbin/audisp-syslog
type = always
args = LOG_INFO
format = string

尽管使用系统记录器来集中审计事件可能不是最佳选择,因为系统记录器通常使用未加密的——甚至未保证——数据传递。通过audisp-remote插件,我们甚至可以使用加密通道发送审计事件,并向远程的auditd服务器提供保证的传递。

首先,在目标(日志)服务器上配置审计守护程序,使其能够通过在端口60上启用审计守护程序来接收来自远程主机的审计日志。我们还将事件格式设置为丰富值,并向事件添加主机名,以便区分来自多个主机的事件,如下所示:

# vi /etc/audit/auditd.conf
tcp_listen_port = 60
log_format = ENRICHED
name_format = HOSTNAME
audisp-remote plugin to connect to the target server's audit daemon, as follows:
# vi /etc/audit/audisp-remote.conf
remote_server = <targethostname>
port = 60

最后,启用audisp-remote插件,如下所示:

# vi /etc/audit/plugins.d/au-remote.conf
active = yes

不要忘记重新启动审计守护程序,以使更改生效。

我们强烈建议您始终使用 Linux 审计子系统。它不仅与故障排除实用工具很好地集成;管理员还可以使用审计工具查询审计日志,甚至生成报告,例如使用aureport,如下面的代码片段所示:

# aureport --avc --start recent
AVC Report
===============================================================
# date time comm subj syscall class permission obj result event
===============================================================
...
7\. 03/21/2020 19:40:55 sudo sysadm_u:sysadm_r:sysadm_sudo_t:s0-s0:c0.c1023 257 dir search sysadm_u:sysadm_r:sysadm_t:s0-s0:c0.c1023 denied 1067
...
10\. 03/21/2020 19:48:19 dmesg sysadm_u:sysadm_r:dmesg_t:s0-s0:c0.c1023 21 file read system_u:object_r:etc_t:s0 denied 1080

Linux 审计系统是 Linux 管理员的重要助手,不仅限于 SELinux 故障排除。除了 Linux 审计系统之外,事件还可以被定向到本地系统日志记录器,下面将进一步解释。

配置本地系统日志记录器

当审计未启用,或 Linux 审计守护进程未运行时,系统日志记录器负责捕获 SELinux 事件。系统日志记录器将通过内核日志设施(kern.*)记录这些事件。大多数系统日志记录器会将这些内核日志事件保存到一个通用日志文件中,例如/var/log/messages

我们可以配置系统日志记录器,将 SELinux AVC 消息定向到它自己的日志文件,例如/var/log/avc.log。例如,对于rsyslog系统日志记录器,我们可以在/etc/rsyslog.d目录下添加一个名为99-selinux.conf的配置条目,内容如下:

# vi /etc/rsyslog.d/99-selinux.conf
:msg, contains, "avc: "	-/var/log/avc.log

重启系统日志记录器后,关于 AVC 的相关信息将会出现在/var/log/avc.log文件中。

当本地系统日志记录器处理 SELinux 日志时,快速获取最新的 AVC 拒绝信息(或其他信息)的一种简单方法是通过dmesg命令,如下所示的代码片段所示:

# dmesg | grep avc | tail

但是要注意,与审计日志不同,许多系统允许普通用户读取dmesg内容。这可能导致一些信息泄漏给不受信任的用户。因此,一些 SELinux 策略不允许普通用户访问内核环形缓冲区(因此也无法使用dmesg),除非user_dmesg SELinux 布尔值设置为on,如下所示的代码片段所示:

# setsebool user_dmesg on

然而,user_dmesg SELinux 布尔值在 CentOS 上不可用。在 CentOS 中,只有标准的非限制用户类型和管理员用户类型可以访问内核环形缓冲区。为了防止其他用户读取这些信息,您需要将这些用户映射到非管理员的 SELinux 用户,例如user_u(x)guest_u,这将在本书后续部分进一步描述。

阅读 SELinux 拒绝信息

每个人在使用 SELinux 系统时,都会多次读取和解释 SELinux 拒绝信息。当 SELinux 禁止访问并且没有dontaudit规则来隐藏它时,SELinux 会记录该信息。如果没有任何记录,可能不是 SELinux 导致了失败。请记住:SELinux 在 Linux自愿访问控制DAC)检查之后执行,因此如果常规权限不允许某项活动,SELinux 就不会被咨询。

当 SELinux 阻止某些访问发生时,SELinux 拒绝信息会被记录。处于强制模式时,应用程序通常会返回权限被拒绝的错误,尽管有时可能会更加模糊。以下代码片段展示了一个例子:

$ ls /proc/1
ls: cannot access '/proc/1': Permission denied
# ls -ldZ /proc/1
dr-xr-xr-x. 9 root system_u:system_r:init_t:s0 0 Mar 21 10:54 /proc/1

那么,拒绝信息是什么样子的呢?以下命令输出展示了来自审计子系统的拒绝信息,我们可以通过ausearch命令进行查询:

# ausearch -m avc -ts recent -i
type=AVC msg=audit(03/22/2020 12:15:38.557:2331): avc: denied { read } for pid=12569 comm="dmesg" name="xterm-256color" dev="sdb2" ino=131523 scontext=sysadm_u:sysadm_r:dmesg_t:s0-s0:c0.c1023 tcontext=system_u:object_r:etc_t:s0 tclass=file permissive=0

让我们将这个拒绝日志分解成各个组成部分。以下列表提供了关于每个部分的更多信息。作为管理员,学会如何阅读拒绝日志非常重要,所以要花足够的时间学习:

  • enforcing 模式。通常是 denied,尽管某些操作会明确标记为审计操作,也会导致 granted。例如:denied

  • { read }

  • pid=12569

  • comm="dmesg"

  • name="xterm-256color"

  • dev="sdb2"

  • ino=131523

  • scontext=sysadm_u:sysadm_r:dmesg_t:s0-s0:c0.c1023

  • tcontext=system_u:object_r:etc_t:s0

  • tclass=file

  • 0,表示 SELinux 处于强制模式;否则,它处于宽容模式(无论是对系统还是对特定域)。例如:permissive=0

我们可以这样解读前面的拒绝:SELinux 拒绝了 dmesg 命令读取名为 "xterm-256color" 的文件。该文件在设备 /dev/sdb2 上的 inode 编号是 131523,并被标记为 etc_tdmesg 命令的 PID 是 12569,并被标记为 dmesg_tdmesg_t 域并未处于宽容模式。

根据操作和目标类别,SELinux 使用不同的字段提供我们解决问题所需的所有信息。考虑以下拒绝:

type=AVC msg=audit(03/22/20 18:12:52.177:2326): avc:  denied  { name_bind } for  pid=15983 comm="nginx" src=89 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:reserved_port_t:s0 tclass=tcp_socket permissive=0

前面的拒绝发生是因为 nginx web 服务器被配置为监听非默认端口(使用 89 而不是默认的 80)。

识别问题是理解操作方式并正确阅读拒绝日志的问题。拒绝日志给了我们足够的信息,可以帮助我们入手,并清楚地了解被拒绝的内容。

管理员可能希望更新 SELinux 策略以允许特定操作(通过向 SELinux 策略中添加 allow 规则,如本书后面所述)。然而,这并不总是正确的做法,因为还有其他选项,通常更好,例如:

  • 为目标资源提供正确的标签(通常当目标是非默认端口、非默认位置等时会出现)

  • 切换布尔值(标志,用于操作 SELinux 策略)以允许额外的权限

  • 为源进程提供正确的标签(通常当操作的应用程序不是通过发行版包管理器安装时会出现)

  • 按预期使用应用程序,而不是通过其他方式使用(因为 SELinux 只允许预期行为),例如通过服务启动守护进程(init 脚本或 systemd 单元),而不是通过命令行操作

如果前面的 nginx 示例是想要的配置(使用非默认端口),那么我们应该将该端口标记为 httpd_t 域,以便绑定到(许多)其他端口。

其他与 SELinux 相关的事件类型

虽然大多数 SELinux 日志事件与 AVC 相关,但它们并不是管理员必须处理的唯一事件类型。大多数审计事件会将 SELinux 信息作为事件的一部分显示,尽管 SELinux 与事件本身关系不大,但一些审计事件类型与 SELinux 直接相关。

提示

所有可能的审计事件的完整列表可以在linux/audit.h头文件中找到,该文件位于/usr/include目录下。

USER_AVC

USER_AVC事件类似于常规的 AVC 审计事件,但源头是一个用户空间对象管理器。这些是使用 SELinux 策略规则的应用程序,但它们自己执行这些规则,而不是通过内核。

以下示例是由 D-Bus 生成的这样的事件:

type=USER_AVC msg=audit(03/22/2020 11:25:56.123:154) : pid=540 uid=dbus auid=unset ses=unset subj=system_u:system_r:system_dbusd_t:s0-s0:c0.c1023 msg='avc:  denied  { acquire_svc } for service=com.redhat.tuned spid=1460 scontext=system_u:system_r:tuned_t:s0 tcontext=system_u:system_r:tunned_t:s0 tclass=dbus permissive=0  exe=/usr/bin/dbus-daemon sauid=dbus hostname=? addr=? terminal=?'

该事件有两个部分。直到msg=字符串之前的所有内容都是关于生成该事件的用户空间对象管理器的信息,是事件的第一部分。实际事件本身(即第二部分)存储在msg=部分,其中包括与常规 AVC 类似的字段。

SELINUX_ERR

当 SELinux 检测到一般的策略违规而不是访问控制违规时,会产生一个SELINUX_ERR事件。仅通过允许操作,SELinux 策略编写者无法解决此问题。这些事件通常指向应用程序和服务的误用,而这些应用程序和服务并非根据策略来完成的,以下代码片段展示了一个示例:

type=PATH msg=audit(03/22/2020 12:25:53.104:2364) : item=0 name=/usr/sbin/rpc.nfsd inode=3019958 dev=08:12 mode=file,755 ouid=root ogid=root rdev=00:00 obj=system_u:object_r:nfsd_exec_t:s0 nametype=NORMAL cap_fp=none cap_fi=none cap_fe=0 cap_fver=0 cap_frootid=0
type=SELINUX_ERR msg=audit(03/22/2020 12:25:53.104:2364) : op=security_compute_sid invalid_context=sysadm_u:sysadm_r:nfsd_t:s0-s0:c0.c1023 scontext=sysadm_u:sysadm_r:sysadm_t:s0-s0:c0.c1023 tcontext=system_u:object_r:nfsd_exec_t:s0 tclass=process 

在上述示例中,一个用户(运行在sysadm_t域中)正在执行rpc.nfsd(标签为nfsd_exec_t),而策略希望过渡到nfsd_t域。然而,这导致了一个完整的上下文sysadm_u:sysadm_r:nfsd_t:s0-s0:c0.c1023,这是一个无效的上下文。sysadm_r SELinux 角色不支持nfsd_t域。

MAC_POLICY_LOAD

每当系统将新的 SELinux 策略加载到内存中时,都会发生MAC_POLICY_LOAD事件。这通常发生在管理员加载新的或更新的 SELinux 策略模块、重新构建策略并禁用dontaudit规则,或切换管理员希望在重启后持续生效的 SELinux 布尔值时。这样的事件如下代码片段所示:

type=MAC_POLICY_LOAD msg=audit(03/22/2020 12:28:17.077:2368) : auid=admin ses=1 lsm=selinux res=yes

当发生MAC_POLICY_LOAD事件时,您可能会注意到随后会有一个USER_MAC_POLICY_LOAD事件。这发生在用户空间对象管理器检测到 SELinux 策略更新并采取行动时。请注意,并非所有用户空间对象管理器都会发送此事件:某些对象管理器会查询实时策略,因此,在加载新策略时不需要采取任何行动。

MAC_CONFIG_CHANGE

当 SELinux 布尔值发生变化但未保持时,会触发MAC_CONFIG_CHANGE事件。这告诉管理员,活动策略已被指示稍微改变其行为,但仍在现有加载策略的范围内。这样的事件如下代码片段所示:

type=MAC_CONFIG_CHANGE msg=audit(03/22/2020 12:29:49.564:2370) : bool=virt_use_nfs val=0 old_val=1 auid=admin ses=1

在上述示例中,virt_use_nfs SELinux 布尔值从 1(开启)更改为 0(关闭)。

MAC_STATUS

当 SELinux 执行状态发生变化时,会出现MAC_STATUS事件。例如,当管理员使用setenforce 0将 SELinux 置于宽容模式时,就会发生以下事件:

type=MAC_STATUS msg=audit(03/22/2020 12:30:45.200:2372) : enforcing=0 old_enforcing=1 auid=admin ses=1 enabled=1 old-enabled=1 lsm=selinux res=yes

MAC_STATUS还用于通知管理员,当 SELinux 状态本身(启用或禁用)发生变化时。

NetLabel 事件

NetLabel 是一个 Linux 内核项目,旨在支持标记的网络数据包,允许诸如 SELinux 上下文之类的安全上下文在主机之间传递。Linux 中 NetLabel 实现支持的协议之一是公共 IP 安全选项CIPSO)标记,我们将在第五章中详细讲解,控制网络通信

以下审计事件与 NetLabel 功能相关:

  • MAC_UNLBL_STCADDMAC_UNLBL_STCDEL事件在添加或删除静态标签时触发。静态标签意味着,如果数据包被接收或发送时没有标签,则会分配这个“默认”静态标签。

  • MAC_MAP_ADDMAC_MAP_DEL事件在将标记协议(如 CIPSO)与其针对 LSM(SELinux)域的参数之间的映射添加或从配置中删除时触发。

  • MAC_CIPSOV4_ADDMAC_CIPSOV4_DEL事件在添加或删除 CIPSO(IPv4)配置时触发。

标记的 IPsec 事件

Linux 支持的另一个标记网络协议是标记的 IPsec,其中IPsec信息协议安全的缩写。通过这个协议,源进程的 SELinux 上下文(通过 IPsec 隧道与目标资源通信)会被 IPsec 守护进程在隧道的两端所知晓。此外,SELinux 将包含关于哪些域可以通过 IPsec 隧道进行通信,哪些域可以在网络上互相通信的规则。

以下审计事件与 IPsec 相关:

  • MAC_IPSEC_ADDSAMAC_IPSEC_DELSA事件在添加或删除安全关联时使用(定义或删除新的 IPsec 隧道)。

  • MAC_IPSEC_ADDSPDMAC_IPSEC_DELSPD事件在添加或删除安全策略定义时使用。安全策略通常描述网络数据包是否需要通过 IPsec 进行处理,如果是的话,通过哪个安全关联进行处理。

  • MAC_IPSEC_EVENT事件是 IPsec 审计消息的通用事件。

本书后续将进一步描述 SELinux 对标记的 IPsec 的支持。

使用 ausearch

ausearch命令是 Linux 审计框架的一部分,是查询存储在系统上的审计事件的常用命令。在我们第一次查看 AVC 拒绝时已简要介绍过它,但简单提及并不能充分体现它的作用。

使用ausearch,我们可以搜索在选定时间段内或之后发生的事件。我们过去使用了-ts recent(时间起始)选项,它显示过去 10 分钟内发生的事件。该参数也可以是时间戳。其他支持的简写值如下:

  • today,表示从当天午夜 1 秒钟开始

  • yesterday,表示从前一天午夜 1 秒钟开始

  • this-weekthis-month,或this-year,意味着从当前周、当前月或当前年第一天的午夜 1 秒开始

  • checkpoint,使用在前一次运行时创建的检查点文件中的时间戳

  • boot,意味着只显示自系统启动以来的事件

  • week-ago,意味着从正好 7 天前的午夜 1 秒开始

使用checkpoint尤其在排查 SELinux 问题时非常有用,因为它允许我们显示自上次调用ausearch命令以来的拒绝事件(及其他 SELinux 事件)。下面的代码片段中进行了说明:

# ausearch --checkpoint /root/ausearch-checkpoint.txt -ts checkpoint

这使得管理员能够进行小范围的调整并重现问题,只查看从那时起发生的事件,而不是一次又一次地查看所有事件。

默认情况下,ausearch命令会显示存储在审计日志中的所有事件。在繁忙的系统中,这可能会导致显示过于冗长的信息,并且可能会出现不需要的事件。幸运的是,用户可以通过ausearch命令限制查询事件的类型。

对于 SELinux 故障排除,使用avc,user_avc,selinux_err可以将事件精确地限制在处理工作所需的范围内,如下面的代码片段所示:

# ausearch -m avc,user_avc,selinux_err -ts recent

如果字段如用户 ID 和时间戳的数字显示过于混乱,那么ausearch可以查找并转换用户 ID 为用户名,时间戳为格式化的时间字段。添加-i选项到ausearch命令,它将解释这些字段并显示已解释的值。

在本节中,我们已了解 SELinux 如何通过日志事件通知系统其操作,以及这些日志事件存储的位置。在下一节中,我们将探讨如何处理这些事件。

获取关于拒绝的帮助

在某些发行版中,提供了额外的支持工具,帮助我们识别拒绝的原因。这些工具具有一些常见错误的知识(例如,设置文件的正确上下文以允许 Web 服务器读取它们)。其他发行版则要求我们凭借经验做出正确的决定,通过发行版的邮件列表、错误跟踪网站以及其他合作渠道(例如Internet Relay ChatIRC))提供支持。

使用 setroubleshoot 进行故障排除

在 CentOS(以及其他Red Hat Enterprise LinuxRHEL)相关发行版,如 Fedora)中,提供了额外的工具,帮助我们排除拒绝问题。这些工具协同工作,捕捉拒绝事件,寻找合理的解决方案,并向管理员报告拒绝及其建议的解决方法。

在图形工作站上使用时,拒绝事件甚至会弹出提示,要求管理员立即查看。安装setroubleshoot包以获得此支持。在没有图形环境的服务器上,管理员可以在系统日志中查看信息,或者甚至可以配置系统通过电子邮件发送 SELinux 拒绝消息。安装setroubleshoot-server包以获得此支持。

在后台,触发审计事件调度程序应用程序(audispd)的是审计守护进程。这个应用程序支持插件功能,SELinux 团队十分感激地实现了这一点。他们构建了一个名为sedispatch的应用程序,它将作为audispd的插件。sedispatch应用程序会检查审计事件是否为 SELinux 拒绝事件,如果是,它会将事件转发到 D-Bus。D-Bus 然后将事件转发给setroubleshootd应用程序(如果该应用程序尚未运行,则启动它),该应用程序分析拒绝事件并为管理员提供反馈。

在工作站上运行时,会触发seapplet,在管理员的工作站上显示弹出窗口。管理员可以选择显示以查看更多详情。不过,管理员无需图形用户界面也能得知 SELinux 问题。你可以在文件系统中找到已分析的反馈,并且在系统日志中可以阅读如何轻松访问这些信息,如下面的代码片段所示:

Mar 22 11:40:35 ppubssa3ed setroubleshoot[1544]: SELinux is preventing /usr/sbin/nginx from name_bind access on the tcp_socket port 89\. For complete SELinux messages run: sealert -l f2914dba-04ef-44ca-9a0b-0f5e62ec72e4

我们可以通过sealert命令(如日志中所提到的)查看完整的解释,如下所示:

# sealert -l f2914dba-04ef-44ca-9a0b-0f5e62ec72e4
SELinux is preventing /usr/sbin/nginx from name_bind access on the tcp_socket port 89.
*****  Plugin bind_ports (99.5 confidence) suggests   ************************
If you want to allow /usr/sbin/nginx to bind to network port 89
Then you need to modify the port type.
Do
# semanage port -a -t PORT_TYPE -p tcp 89
    where PORT_TYPE is one of the following: http_cache_port_t, http_port_t, jboss_management_port_t, jboss_messaging_port_t, ntop_port_t, puppet_port_t.
*****  Plugin catchall (1.49 confidence) suggests   **************************
...

sealert应用程序是一个命令行应用程序,它解析setroubleshoot守护进程存储的信息(存储在/var/lib/setroubleshoot目录下)。

这将为我们提供一组选项来解决拒绝问题。在之前展示的与 Apache 相关的拒绝事件中,sealert给出了一个选项,并附有一定的信心分数。根据问题的不同,这个工具可能会显示多个选项,每个选项都有其自己的信心值(即,sealert对这是正确解决方法的确定程度)。

正如我们从这个例子中看到的,setroubleshoot应用程序本身使用插件来分析拒绝事件。这些插件(通过setroubleshoot-plugins包提供)会查看拒绝事件,检查它们是否与某个特定的、已知的用例匹配(例如,何时更改 SELinux 布尔值或何时目标资源具有错误的上下文),并向setroubleshoot提供反馈,表明插件对此的确定性,以便通过推荐的方法解决此拒绝事件。

当 SELinux 拒绝发生时发送电子邮件

一旦系统进行了微调且拒绝不再频繁发生,管理员可以选择让setroubleshootd在出现新拒绝时发送电子邮件。这真正增强了 SELinux 的主机入侵检测/防止能力,因为管理员不需要不断查看日志来获取信息。然而,请记住,这可能会导致电子邮件的突然激增,从而可能引发拒绝服务DoS)- 类似的行为,如果触发了许多拒绝。管理员应该只有在其邮件基础设施具备速率限制或其他服务质量QoS)控制时才实施此功能。

在文本编辑器中打开/etc/setroubleshoot/setroubleshoot.conf文件,找到[email]部分。更新参数以匹配本地邮件基础设施,如下所示:

# vi /etc/setroubleshoot/setroubleshoot.conf
[email]
recipients_filepath = /var/lib/setroubleshoot/email_alert_recipients
smtp_port = 25
smtp_host = localhost
from_address = selinux@infra.example.com
subject = [infra] SELinux Alert for host infra.example.com

接下来,编辑email_alert_recipients文件(通过recipients_filepath变量引用),并添加需要在 SELinux 警报出现时通知的电子邮件地址。

最后,重新启动 D-Bus 守护进程,如下所示:

# systemctl restart dbus

在非 systemd 系统上工作时,请使用以下命令:

# service dbus restart

需要重新启动 D-Bus,因为 D-Bus 管理setroubleshootd守护进程。

使用 audit2why

如果setroubleshootsealert在 Linux 发行版中不可用,我们仍然可以获得一些关于拒绝的信息。尽管它不像setroubleshoot提供的插件那样详细,但audit2why工具(即audit2allow -w的简称)确实提供了一些拒绝的反馈。遗憾的是,它并不总是能够正确推断。

让我们针对之前使用sealert的相同拒绝尝试一下,如下所示:

# ausearch -m avc -ts recent | audit2why
type=AVC msg=audit(1584880436.644:385): avc:  denied  { name_bind } for  pid=5119 comm="nginx" src=89 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:reserved_port_t:s0 tclass=tcp_socket permissive=0
  Was caused by:
    Missing type enforcement (TE) allow rule.
    You can use audit2allow to generate a loadable module to allow this access.

这里的audit2why工具没有考虑到目标位置的上下文是错误的,并建议更新策略,允许 Web 服务器绑定到unreserved_port_t类型,这与setroubleshoot提供的信息不同,后者更准确,推荐重新标记目标端口。

正如命令输出所提到的,还有一个叫做audit2allow的工具,可以将拒绝转换为 SELinux 策略。我们将在第十二章,“调优 SELinux 策略”中介绍audit2allow

与 systemd-journal 交互

除了用于大多数 SELinux 日志记录和事件的 Linux 审计系统外,我们还可以通过其他日志系统收集信息。例如,systemd 的日志捕获 SELinux 上下文信息并与事件一起存储,允许管理员在查询日志时使用这些信息。

例如,要查看由与system_u:system_r:sssd_t:s0上下文相关的应用程序生成的systemd-journal中的事件,可以使用以下命令:

# journalctl _SELINUX_CONTEXT="system_u:system_r:sssd_t:s0"
-- Logs begin at Sun 2020-03-22 10:43:48 UTC, end at Sun 2020-03-22 12:40:12 UTC. --
Mar 22 10:43:51 ppubssa3ed sssd[545]: Starting up
Mar 22 10:43:51 ppubssa3ed sssd[be[implicit_files]][623]: Starting up
Mar 22 10:43:51 ppubssa3ed sssd[nss][630]: Starting up

因为systemd-journal添加了来源应用程序的 SELinux 上下文,因此恶意应用程序更难生成虚假事件。而常规的系统日志记录器只捕获字符串事件,systemd-journal从系统中检索 SELinux 上下文。使用 SELinux 上下文,可以轻松地跨不同但强相关的应用程序分组事件,并更高保证事件来自特定应用程序。

安装bash-completion包后,我们甚至可以使用它查看systemd-journal日志中存在的 SELinux 上下文,这使得查询日志变得更加容易,如下所示:

# journalctl _SELINUX_CONTEXT=<tab><tab>
kernel
system_u:system_r:auditd_t:s0
system_u:system_r:chronyd_t:s0
...

要查找与 nginx 相关的消息,请使用嵌入的grep过滤器,如下所示:

# journalctl -g nginx
-- Logs begin at Sun 2020-03-22 10:43:48 UTC, end at Sun 2020-03-22 12:52:26 UTC. --
Mar 22 11:40:32 ppubssa3ed systemd[1]: Starting The nginx HTTP and reverse proxy server...
Mar 22 11:40:32 ppubssa3ed nginx[1538]: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
Mar 22 11:40:32 ppubssa3ed nginx[1538]: nginx: [emerg] bind() to 0.0.0.0:89 failed (13: Permission denied)
...
Mar 22 11:40:35 ppubssa3ed setroubleshoot[1544]: SELinux is preventing /usr/sbin/nginx from name_bind access on the tcp_socket port 89\. For complete SELinux messages run: sealert -l f2914dba-04ef-44ca-9a0b-0f5e62ec72e4

嵌入的grep过滤器的好处在于,journalctl仍将显示多行消息,而实际通过grep重定向journalctl输出只会显示符合表达式的各个行。

使用常识

常识并不容易记录,但是当我们对文件标签(及其用途)有一些经验时,阅读拒绝通常会导致正确的解决方案。例如,如果我们收到有关 Web 服务器无法读取其文件的拒绝,并且文件的上下文是(例如)user_home_t,那么这应该是一个警示。最终用户的主文件,例如,使用user_home_t上下文,这对 Web 服务器读取的系统文件不合适。

确保目标资源的上下文正确的一个方法是使用matchpathcon进行验证。该实用程序根据 SELinux 策略返回应有的上下文,如下所示:

$ matchpathcon /srv/www/html/index.html
/srv/www/html/index.html	system_u:object_r:httpd_sys_content_t:s0

对于与文件和目录相关的拒绝,进行此操作可能有助于快速找到合适的解决方案。

此外,许多域都有特定的手册页面,告知读者每个域常用的类型,以及如何更详细地处理该域(例如,可用的布尔值、常见错误等)。这些手册页面以主要服务开头,并以_selinux为后缀,如下所示:

$ man ftpd_selinux

在大多数情况下,处理拒绝的方法最好描述如下:

  • 目标资源标签(如文件标签)是否正确?可以使用matchpathcon进行验证,或与工作(可访问)资源的标签进行比较。

  • 源标签(域)是否符合预期?SSH 守护进程应在sshd_t域中运行,而不是init_t域中。如果不是这种情况,请确保应用程序本身的标签(例如其可执行二进制文件)正确(再次使用matchpathcon进行验证)。

  • 该拒绝是否可能由 SELinux 布尔值覆盖?如果是这样,策略可能已经具备了适当的规则,只需要更改 SELinux 布尔值即可。setroubleshootd会报告这种情况。通常,域的手册页(如httpd_selinux)也会覆盖可用的 SELinux 布尔值。我们将在第十二章《调整 SELinux 策略》中解释如何查询和调整 SELinux 布尔值。

更改文件标签将在第四章《使用文件上下文和进程域》中讨论。

在结束本节之前,常识将是管理 SELinux 拒绝的最有效方法,但前面提到的工具将是开始时的有力助手。

总结

在本章中,我们学习了如何启用和禁用 SELinux,既可以在整个系统级别操作,也可以通过各种方法在单个服务级别进行操作:内核启动选项、SELinux 配置文件或普通命令。其中一个命令是semanage permissive,它可以禁用单个服务的 SELinux 保护。

接下来,我们了解了 SELinux 如何记录事件及其解释方法,这也是管理员在处理 SELinux 时最常见的任务之一。为了帮助我们解释这些事件,我们可以使用setroubleshootsealertaudit2why等工具。我们还简要介绍了与 Linux 审计相关的几个工具,以帮助我们筛选各种事件。

在下一章中,我们将讨论 SELinux 系统中的第一个管理任务:管理用户账户、关联的 SELinux 角色,以及系统资源的安全权限。

问题

  1. 在禁用 SELinux 之前,管理员应该尝试什么?

  2. 管理员默认可以在哪里找到 SELinux 日志?

  3. 我们如何知道一个应用程序是否支持 SELinux?

  4. AVC 的目的是什么?

  5. AVC 事件是 SELinux 的唯一事件类型吗?

第三章:管理用户登录

当我们登录到启用 SELinux 的系统时,我们会收到一个 SELinux 上下文来工作。这个上下文包含了一个 SELinux 用户、一个 SELinux 角色、一个域,并且可选地包含一个敏感性范围。由于 SELinux 用户定义了可以访问的角色和类型,因此管理用户登录和 SELinux 用户是系统中配置终端用户的第一步。

为了启用正确配置的用户,我们将学习定义具有足够权限执行其工作职责的用户,从具有严格 SELinux 保护的普通用户到具有少量 SELinux 保护的完全特权的管理员用户。我们将创建并分配类别和敏感性,同时为用户分配角色,并使用各种工具切换角色。在本章的最后,我们将看到 SELinux 如何与 Linux 认证过程集成。

在本章中,我们将讨论以下主要内容:

  • 面向用户的 SELinux 上下文

  • SELinux 用户和角色

  • 处理 SELinux 角色

  • SELinux 与 PAM

技术要求

查看以下视频,观看代码实际操作:bit.ly/3jbASmr

面向用户的 SELinux 上下文

一旦登录到系统,我们的用户将在某个特定上下文中运行。此用户上下文定义了我们作为用户在系统上的权限和特权。获取当前用户信息的命令id,也支持显示当前的 SELinux 上下文信息:

$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

在具有目标策略类型的 SELinux 系统上,所有用户的登录上下文通常都是unconfined_u(上下文的第一部分)。在更为受限的系统中,用户可以是user_u(普通受限用户)、staff_u(操作员)、sysadm_u(系统管理员)或其他任何 SELinux 用户。

SELinux 用户定义了用户可以切换到的角色。SELinux 角色本身定义了用户可以使用的应用域。默认情况下,系统上提供固定数量的 SELinux 用户,但管理员可以创建额外的 SELinux 用户。管理员的任务还包括将 Linux 登录名分配给 SELinux 用户。

另一方面,SELinux 角色不能通过管理员命令创建,因为 SELinux 角色是 SELinux 策略的一部分。为此,SELinux 策略需要通过额外的规则进行扩展,以创建角色。我们将在第十五章中讨论这个内容,使用参考策略

要查看当前可用的角色,请使用seinfo

# seinfo --role
Roles: 14
  auditadm_r
  dbadm_r
  ...
  xguest_r

SELinux 角色可以是粗粒度的(如sysadm_r)或更具功能导向的(如dbadm_r)。自定义的 SELinux 角色甚至可以非常细粒度,仅授予切换到受限领域的能力。

让我们看看如何创建和管理 SELinux 用户。

SELinux 用户和角色

在启用 SELinux 的环境中,登录二进制文件调用libselinux API 来建立 SELinux 用户和本地用户之间的初始映射。然后,在找到正确的 SELinux 用户后,系统查找该用户应属于的角色和域,并将其设置为用户的上下文。

列出 SELinux 用户映射

登录系统后,我们可以使用id -Z来获取当前的 SELinux 上下文。对于许多用户而言,无论用户名是什么,该上下文将由未受限制的用户(unconfined_u)定义。如果不是这样,它通常是基于sysadm_ustaff_uuser_u中的一个上下文。这是因为大多数 Linux 发行版默认仅提供有限的一组 SELinux 用户,与它们支持的 SELinux 角色对齐。

在登录过程中,处理登录的服务进程将检查本地定义文件,以查找 Linux 账户与 SELinux 用户之间的适当映射。让我们通过semanage login -l查看现有的登录映射。以下输出是 CentOS 系统的默认输出:

# semanage login -l
Login Name		SELinux User	MLS/MCS Range	Service
__default__	unconfined_u	s0-s0:c0.c1023	*
root			unconfined_u	s0-s0:c0.c1023	*

命令的输出显示每行一个登录映射。每个映射包含以下内容:

  • Login Name适用的映射(即用户名)

  • 映射到的SELinux 用户

  • 登录映射到的MLS/MCS 范围

  • 映射适用的Service(用于本地定制,我们将在自定义服务登录部分中讨论)

登录名可以包含一些特殊值,这些值不会直接映射到单一的 Linux 账户:

  • __default__是一个通配规则。如果其他规则都不匹配,则用户将映射到此行标识的 SELinux 用户。在给定的示例中,所有用户都映射到unconfined_u SELinux 用户,这意味着普通的 Linux 用户几乎不受任何限制。当这不是预期的行为时,管理员通常会将普通登录映射到受限的 SELinux 用户,而将管理员登录映射到staff_usysadm_u SELinux 用户。

  • %开头的登录名将映射到组。这允许管理员将一组人员直接映射到 SELinux 用户,而不必单独管理每个映射。

当个人用户映射和组映射都匹配时,个人用户映射优先。存在多个组定义时,SELinux 将使用第一个匹配的组映射(按基础seusers配置文件中列出的顺序)。

重要提示

系统进程(非交互式登录的 Linux 账户)被映射到system_u SELinux 用户。这个 SELinux 用户永远不应分配给最终用户的登录。

在启用了 MLS 或 MCS 的系统中,映射包含有关用户允许的敏感度范围(MLS/MCS 范围)的信息。通过这种方式,我们可以将多个用户映射到相同的受限 SELinux 用户,同时通过允许的敏感度区分这些用户。例如,一个用户可能只允许访问低敏感度区域(s0),而另一个用户则可能还可以访问更高的敏感度(例如,s1)或不同的类别。

将登录映射到 SELinux 用户

让我们通过几个示例来展示如何使这些映射工作。有关更复杂的细节,请参阅SELinux 和 PAM部分。我们假设有一个名为lisa的 Linux 用户,并且我们希望她的账户映射到staff_u SELinux 用户,而所有其他属于users组的用户都映射到user_u SELinux 用户。

我们可以通过semanage login命令来完成这一操作,使用-a(添加)选项:

# semanage login -a -s staff_u lisa
# semanage login -a -s user_u %users

-s参数将 SELinux 用户分配给给定的登录,而-r参数则处理该用户的敏感度(和类别)。例如,让我们使用-m(而不是-a)修改最近创建的基于组的定义,将其映射到staff_u用户,并限制这些用户的敏感度范围为s0-s0,类别为c0c4

# semanage login -m -s staff_u -r "s0-s0:c0.c4" %users

登录映射的敏感度范围不能超过分配给 SELinux 用户的范围。例如,如果staff_u SELinux 用户仅被授予s0-s0:c0.c3的访问权限,则之前的命令将失败,因为它试图分配一个更广泛的访问范围。我们将在创建 SELinux 用户部分讨论如何定义 SELinux 用户及其范围。

semanage login命令更新位于/etc/selinux/targeted中的seusers文件。如果定义了多个组映射,则该文件中映射的顺序决定了哪个映射适用于给定用户。属于多个映射组的用户将根据第一个匹配的映射分配 SELinux 用户。

虽然可以更新seusers文件中条目的顺序,但不建议这样做。每次semanage login修改seusers文件时,它都会重新排序映射。相反,当用户属于多个映射组时,我们建议您创建一个单独的(基于用户的)映射。当我们为包含已有活动映射的用户的组创建组映射时,这也会显示出来:

# semanage login -a -s guest_u %nginx
libsemanage.add_user: User taylor is already mapped to group users, but also belongs to group nginx. Add an explicit mapping for this user to override group mappings.

更改在新登录时生效,因此我们应强制注销这些用户。以下命令会终止lisa用户的所有进程,强制该用户注销:

# pkill -KILL -u lisa

此外,当我们修改用户设置时,还应重置该用户主目录的上下文(在该用户未登录时)。为此,请使用restorecon,如下所示:

# restorecon -RF /home/lisa

上述命令中的-F选项会强制重置,而-R则会递归地执行此操作。

重要说明

运行 restorecon -RF 命令也会重置用户通过 chcon 等工具手动设置的文件上下文。我们建议事先定义 SELinux 用户映射,或者仅通过 chcon -R -u 递归地更改文件的 SELinux 用户。chcon 应用程序和文件上下文将在下一章中讨论。

要删除登录映射,使用 -d(删除)选项。别忘了在之后运行 restorecon 命令:

# semanage login -d lisa
# restorecon -RF /home/lisa

如果该用户在系统中处于活动状态,别忘了强制用户注销。

为服务自定义登录

当通过 semanage login 添加登录映射时,它们适用于所有服务。semanage 中没有选项可以根据服务自定义映射。然而,这并不意味着不可能实现。

SELinux 用户空间工具和库将查阅以下两个配置文件,以了解映射是什么:

  • /etc/selinux/targeted/seusers 文件包含标准的、与服务无关的映射。该文件由 semanage login 管理,不应通过其他方式更新。

  • /etc/selinux/targeted/logins 目录包含自定义映射,每个文件对应一个 Linux 账户。因此,root 用户的自定义映射将位于 /etc/selinux/targeted/logins/root

在自定义映射的文件中,管理员可以为每个服务定义一个不同的 SELinux 用户进行映射。这些服务是用户可以登录的 可插拔认证模块 (PAM) 服务,更多信息可以在 SELinux 和 PAM 部分找到。

例如,要使 root 用户在通过 SSH 登录时映射到 user_u SELinux 用户,而不是默认的 unconfined_u 用户,需要在 root 文件中包含以下内容:

sshd:user_u:s0

查询当前映射时,semanage login 将显示如下自定义内容:

# semanage login -l
...
Local customization in /etc/selinux/targeted/logins
root		user_u		s0		sshd

当然,这种自定义不需要如此剧烈。它也可以用来限制用户的默认 MLS/MCS 范围。例如,要将类别限制为 c0.c8(而不是默认的 c0.c1023 范围),可以使用以下命令:

sshd:unconfined_u:s0-s0:c0.c8

这样的自定义允许我们根据所使用的 PAM 服务灵活地更改访问控制策略。

创建 SELinux 用户

默认情况下,只有少数 SELinux 用户可以映射到登录。如果我们希望对 Linux 账户及其映射进行更多控制,就需要创建额外的 SELinux 用户。

首先,使用 semanage user -l 命令列出当前已知的 SELinux 用户,如下所示:

# semanage user -l
SELinux	Labeling	MLS/		MLS/
User		Prefix	MCS Level	MCS Range		SELinux Roles
guest_u	user		s0		s0			guest_r
root		user		s0		s0-s0:c0.c1023	staff_r ...
...
xguest_u	user		s0		s0			xguest_r

接下来,使用 semanage user 创建一个新的 SELinux 用户,使用 -a(添加)选项。我们需要为 SELinux 提供有关此 SELinux 用户的附加信息,例如以下内容:

  • SELinux 用户的默认敏感度(使用 -L 选项)。这是用户开始时的敏感度。

  • SELinux 用户适用的安全权限(使用 -r 选项)。在定义登录映射时,无法扩展此范围。然而,可以为用户提供更有限的范围,只要它在当前范围内即可。

  • 允许的角色或角色组(使用 -R 选项)适用于 SELinux 用户。

    提示

    前面示例中显示的标签前缀用于动态创建具有特定前缀的 SELinux 策略,例如 <prefix>_home_t 用于用户的主文件。大多数发行版将其留给默认的 user 设置,若要更改,则通过(未记录的)-P 参数来执行 semanage user

在下面的示例中,我们正在配置 SELinux 用户 finance_u

# semanage user -a -L s0 -r "s0-s0:c0.c127" -R user_r finance_u

当命令创建 SELinux 用户时,其信息将成为 SELinux 策略的一部分。从此时起,管理员可以将 Linux 账户映射到该 SELinux 用户。

重要提示

SELinux 角色是通过映射到 Linux 账户的 SELinux 用户来启用的。当管理员希望允许将额外的现有角色添加到 Linux 账户时,管理员可以更新现有的 SELinux 映射以包含新角色,或者创建一个具有新角色访问权限的 SELinux 用户,然后将该 SELinux 用户映射到 Linux 账户。

与登录映射类似,semanage user 还接受 -m 选项来修改现有条目,或使用 -d 来删除条目。例如,以下命令删除 finance_u SELinux 用户:

# semanage user -d finance_u

单独的 SELinux 用户有助于增强审计信息,因为 SELinux 用户在用户会话期间通常不会发生变化,而有效的 Linux 用户 ID 则可能会发生变化。如果用户创建文件或其他资源,这些资源也会在其安全上下文中继承 SELinux 用户部分。

列出可访问的域

在创建 SELinux 用户时,需要提供的参数之一是 SELinux 用户的角色或角色组。大多数角色是显而易见的:dbadm_r 角色适用于数据库管理员,而 webadm_r 角色适用于 Web 应用程序基础设施管理员。如果某个角色不明确,或者管理员不确定某个角色包含哪些访问权限,管理员仍然可以查询 SELinux 策略以获取更多信息。

信息提示

本书将主要关注用于查询和交互式操作活动 SELinux 策略的命令行工具。在第十三章《分析策略行为》中,我们还将介绍图形化工具 apol

如前所述,角色定义了与该角色关联的用户可以访问的域。我们看到 seinfo 可以显示可用的角色,但它还可以做更多事情。它还可以列出某个角色可访问的域,使用 -x 选项:

# seinfo -r dbadm_r -x
Roles: 1
  role dbadm_r types { ... qmail_inject_t user_mail_t ... };

在这个例子中,运行dbadm_r角色作为其安全上下文的一部分的用户将能够过渡到例如qmail_inject_t(用于读取电子邮件消息并将其传递到qmail队列的域)和user_mail_t(用于通用电子邮件发送命令行应用程序的域)域。

通过主导角色提供的信息通常不会引起管理员的关注。角色继承虽然在 SELinux 核心中得到了支持,但在 Linux 发行版的策略中并未使用。它意味着(其他)角色的继承,但它始终只会显示查询的角色。

管理类别

敏感度标签及其关联的类别通过数字值进行标识,这对计算机来说非常方便,但对用户来说却不那么直观。幸运的是,SELinux 工具支持将这些级别和类别翻译成人类可读的值,尽管它们仍然以数字形式存储。因此,几乎所有能够显示上下文的工具都会显示翻译后的内容,而不是以数字值呈现。

翻译通过setrans.conf文件进行管理,该文件位于/etc/selinux/targeted。在这个文件中,我们可以用更易于管理员使用的字符串来命名特定的值(例如,s0:c102)或范围(如s0-s0:c1.c127)。但是,为了进行翻译,mcstransd——MCS 翻译守护进程——需要保持运行。

考虑我们的例子,finance_u SELinux 用户被允许访问c0.c127类别范围。该范围内的两个类别是c102,我们将其标记为Contracts,以及c103,我们将其标记为Salariesc1.c127范围将被标记为FinanceData。下图展示了这些不同类别之间的关系:

图 3.1 – 示例类别和类别范围的关系

图 3.1 – 示例类别和类别范围的关系

为了实现这一点,应该在setrans.conf文件中添加以下内容:

s0:c102=Contracts
s0:c103=Salaries
s0-s0:c1.c127=FinanceData

编辑完setrans.conf文件后,需要重新启动mcstransd应用程序。

这些翻译由 SELinux 工具处理,工具通过位于/var/run/setrans.setrans-unix套接字连接到mcstransd守护进程,从而查询setrans.conf文件。如果守护进程未运行或与守护进程的通信失败,将显示数字敏感度和类别值。

例如,在守护进程运行时,id -Z的输出现在如下所示:

# id -Z
unconfined_u:unconfined_r:unconfined_t:SystemLow-SystemHigh

我们可以使用chcat工具查看可用的敏感度及其对应的人类可读值。以下示例展示了添加与金融相关的翻译后的结果:

$ chcat -L
s0			SystemLow
s0-s0:c0.c1023	SystemLow-SystemHigh
s0:c0.c1023	SystemHigh
s0:c102		Contracts
s0:c103		Salaries
s0-s0:c1.c127	FinanceData

相同的chcat工具也可以用来为用户分配类别。例如,要为lisa Linux 用户授予Salaries类别,我们可以使用以下命令:

# chcat -l -- +Salaries lisa

上述命令为 Linux 用户lisa授予了Salaries类别(c103)。用户映射立即更新了这一信息。同样,我们需要确保lisa用户已经退出,以便更改生效。

通过这一点,我们结束了关于管理 SELinux 用户和登录的章节。我们已经学习了如何将用户与 SELinux 用户对齐,以便他们可以使用正确的上下文登录系统。在下一章节中,我们将介绍 SELinux 角色以及如何将这些角色应用到 SELinux 用户上。

处理 SELinux 角色

我们已经看到 SELinux 用户定义了一个用户可以拥有的角色。但是 SELinux 是如何强制执行用户通过哪个角色登录的呢?而且当用户登录后,如何切换他们的活动角色?

定义允许的 SELinux 上下文

为了选择成功认证的用户分配的上下文,SELinux 引入了默认上下文的概念。根据用户登录时的服务上下文(或用户执行命令时的服务上下文),系统会选择正确的用户上下文。

/etc/selinux/targeted/contexts目录下,存在一个名为default_contexts的文件。文件中的每一行以父进程的 SELinux 上下文信息开始,接着是一个有序列表,列出了基于用户允许的 SELinux 角色可以选择的所有上下文。

考虑以下sshd_t上下文的代码行:

system_r:sshd_t:s0	user_r:user_t:s0 \
                      staff_r:staff_t:s0 \
                      sysadm_r:sysadm_t:s0 \
                      unconfined_r:unconfined_t:s0

这行代码提到,当一个用户通过在sshd_t域中运行的进程登录时,所列出的角色会与该用户的角色进行匹配。用户将过渡到第一个匹配其可以使用的角色的上下文。

例如,假设我们映射到一个 SELinux 用户,该用户可以访问staff_rsysadm_r两个角色。在这种情况下,我们将作为staff_r:staff_t登录,因为这是第一个匹配的角色。

然而,就像用于 Linux 账户映射的seusers文件一样,default_contexts文件是一个默认文件,可以通过特定的自定义设置进行覆盖。这些自定义设置存储在/etc/selinux/targeted/contexts/users子目录中。这些文件以 SELinux 用户的名称命名,并且只有针对该用户有效。这使得我们能够为特定的 SELinux 用户分配不同的上下文,即使他们与其他 SELinux 用户共享相同的角色。由于 SELinux 会逐行检查条目,我们不需要复制整个default_contexts文件的内容。只需列出我们希望看到不同配置的配置行;SELinux 会自动使用其余部分的default_contexts文件。

让我们修改默认上下文,以便staff_u SELinux 用户在通过 SSH 登录时,能够以sysadm_r角色(并使用sysadm_t类型)登录。为此,使用sshd_t行,修改它,并将结果保存为/etc/selinux/targeted/contexts/users/staff_u

system_r:sshd_t:s0	sysadm_r:sysadm_t:s0

特别地,对于 SSH 守护进程,我们还需要启用ssh_sysadm_login布尔值,这是 SELinux 策略开发者为防止用户立即使用高权限账户登录所做的特别防范措施:

# setsebool ssh_sysadm_login on

在这些设置下,我们已将sysadm_r:sysadm_t:s0设置为唯一可能的上下文,确保目标上下文为staff_u:sysadm_r:sysadm_t

使用 getseuser 验证上下文

为了验证我们的更改是否成功,我们可以询问 SELinux 该上下文选择的结果,而无需自己解析文件。我们可以通过getseuser命令实现这一点,该命令接受两个参数:Linux 用户账户和切换用户上下文的进程的上下文。

重要提示

getseuser命令是由 SELinux 用户空间项目提供的辅助工具,但并非所有发行版都提供此工具。你可以在 Debian 和 Gentoo 上找到它,但在 CentOS 或其他 Red Hat Enterprise Linux 衍生发行版上找不到。

下面是一个示例,检查当sshd_t域中运行的进程登录时,sven用户的上下文会是什么:

# getseuser sven system_u:system_r:sshd_t
seuser: user_u, level s0-s0
Context 0	user_u:user_r:user_t:s0

getseuser命令的一个优点是,它询问 SELinux 代码上下文应该是什么,这不仅查看default_contexts和自定义文件,还检查目标上下文是否可以访问,并且没有其他限制阻止切换到此上下文。

使用新角色切换角色

用户在成功认证并登录后,将通过SELinux 用户和角色部分中提到的配置分配上下文。然而,如果 SELinux 用户可以访问多个角色,则 Linux 用户可以使用newrole应用程序从一个角色切换到另一个角色。

考虑一个没有未限制域的 SELinux 系统,并且我们默认以staff_r角色登录。为了执行管理任务,我们需要切换到sysadm_r管理角色,可以通过newrole命令完成。此命令仅在通过/etc/securetty列出的安全终端工作时有效:

$ id -Z
staff_u:staff_r:staff_t:s0
$ newrole -r sysadm_r
Password: (Enter user password)
$ id -Z
staff_u:sysadm_r:sysadm_t:s0

注意 SELinux 用户保持不变,但角色和域已经发生了变化。

newrole命令也可以用于切换到特定的敏感度,示例如下:

$ newrole -l s0-s0:c0.c100

当我们切换到另一个角色或敏感度时,我们实际上是创建了一个具有新角色或敏感度的新会话(新 shell)。该命令不会改变当前会话的上下文,也不会退出当前会话。

我们可以通过退出(使用exitlogoutCtrl + D)返回到我们分配的角色并回到第一个会话。

通过 sudo 管理角色访问

大多数管理员使用sudo进行特权委派:允许用户在比平常更高的权限上下文中运行某些命令。sudo应用还能够切换 SELinux 角色和类型。

我们可以直接将目标角色和类型传递给sudo。例如,当我们编辑 PostgreSQL 配置文件时,我们可以告诉sudo切换到管理角色:

$ sudo -r sysadm_r -t sysadm_t vim /var/lib/pgsql/data/pg_hba.conf

然而,我们也可以通过/etc/sudoers文件配置sudo,允许用户在特定角色和/或类型下运行命令,或者在特定上下文中获得一个 shell。考虑一个同时具有user_rdbadm_r角色的用户(dbadm_r角色是为数据库管理员指定的角色)。在sudoers文件中,以下行允许myuser用户通过sudo运行任何命令,触发时将以dbadm_r角色和dbadm_t域运行:

myuser ALL=(ALL) TYPE=sysadm_t ROLE=sysadm_r ALL

管理员通常更倾向于使用sudo而不是newrole,因为后者不会更改有效的用户 ID,这通常是最终用户在调用更高权限命令时所需要的(无论是针对 root 用户还是特定服务的运行时帐户)。sudo应用还具有出色的日志记录功能,我们甚至可以让命令切换角色,而无需最终用户明确指定目标角色和类型。遗憾的是,它不支持更改敏感度。

使用 runcon 访问其他域

另一个可以切换角色和敏感度的应用是runcon应用。runcon命令对所有用户可用,用于以不同的角色、类型和/或敏感度启动特定的命令。它甚至支持更改 SELinux 用户——前提是 SELinux 策略允许。

runcon命令没有自己的域——它在执行该命令的用户的上下文中运行。因此,用户域本身的权限决定了是否能够更改角色、类型、敏感度,甚至是 SELinux 用户。

大多数情况下,我们会使用runcon来启动具有特定类别的应用程序。这使我们能够利用 SELinux 中的 MCS 方法,而无需要求应用程序启用 MCS:

$ runcon -l Salaries bash
$ id -Z
unconfined_u:unconfined_r:unconfined_t:Salaries

例如,在前面的示例中,我们运行一个带有Salaries类别的 shell 会话(禁止其访问没有设置相同或更少类别的资源)。

切换到系统角色

有时,管理员需要调用一些应用程序,这些应用程序不应该在当前的 SELinux 用户上下文下运行,而是应该作为 system_u SELinux 用户,并拥有 system_r SELinux 角色。SELinux 策略管理员已经认识到这一需求,并允许一个非常有限的领域切换 SELinux 用户到不同的用户——这可能与之前提到的 SELinux 用户不可变性的目的相违背。然而,既然有时候确实需要这样做,SELinux 必须适应这一点。允许切换 SELinux 用户的应用程序之一是 run_init(通过其 run_init_t 域)。

run_init 应用程序主要(几乎是唯一)用于在 Linux 系统上启动后台系统服务。通过使用此应用程序,守护进程不再以用户的 SELinux 上下文运行,而是以系统的上下文运行,这符合 SELinux 策略的要求。

由于这一需求仅在通过服务脚本启动附加服务的系统上出现,使用 systemd 的发行版不需要使用 run_initsystemd 已经以 system_r 角色运行,并负责启动附加服务。因此,不需要角色转换。其他 init 系统,如 Gentoo 的 OpenRC,集成了 run_init,使得管理员通常不需要手动调用 run_init

大多数 SELinux 策略启用基于角色的支持来进行选择性服务管理(针对非 systemd 发行版)。这使得没有完整系统管理权限的用户,仍然能够在 Linux 系统上操作由 SELinux 策略允许的某些服务。这些用户将被授予 system_r 角色,但一旦完成,他们就不需要再调用 run_init 来操作特定的服务了。转换会自动发生,并且仅限于分配给该用户的服务——其他服务无法由这些用户启动。

本节结束了我们关于处理 SELinux 角色的部分。我们已经学习了如何管理 SELinux 角色、切换角色和上下文,以及在特权提升情况下如何定义目标角色和类型。在本章的最后部分,我们将探讨 PAM 如何用于配置系统上的 SELinux 上下文设置。

SELinux 和 PAM

虽然我们讨论了 SELinux 用户和角色的所有信息,但我们并没有涉及应用程序或服务如何创建并分配 SELinux 上下文给用户。正如之前提到的,这一过程是通过 Linux 的 PAM 服务来协调的。

通过 PAM 分配上下文

最终用户通过登录过程(通过 getty 进程触发)、网络服务(例如 OpenSSH 守护进程)或图形登录管理器(如 xdmkdmgdmslim 等)登录到 Linux 系统。

这些服务负责切换我们的有效用户 ID(当然是在身份验证成功后),以确保我们不会作为 root 用户在系统上活动。对于 SELinux 系统,这些进程还需要相应地切换 SELinux 用户(和角色),否则上下文将从服务继承,这对任何交互式会话显然是错误的。

理论上,所有这些应用程序都可以完全支持 SELinux,与 SELinux 用户空间库链接,以获取有关 Linux 映射和 SELinux 用户的信息。开发人员没有选择将所有这些应用程序转换为 SELinux 支持,而是决定通过 Linux 系统提供的 PAM 服务将身份验证提升到一个新水平。

PAM 为处理 Linux(和 Unix)系统上的不同身份验证方法提供了一个非常灵活的接口。前面提到的所有应用程序都使用 PAM 来执行其身份验证步骤。为了使这些应用程序支持 SELinux,我们需要更新它们的 PAM 配置文件,以包含 pam_selinux.so 库。

以下代码列表摘自 CentOS 的 /etc/pam.d/remote 文件,限制为 PAM 会话服务指令。它在身份验证过程中触发 pam_selinux.so 库代码,具体如下:

session	required	pam_selinux.so close
session	required	pam_loginuid.so
session	required	pam_selinux.so open
session	required	pam_namespace.so
session	optional	pam_keyinit.so force revoke
session	include	password-auth
session	include	postlogin

pam_selinux.so 代码支持的参数在 pam_selinux 手册页中有描述。在前面的示例中,close 选项会清除当前上下文(如果有的话),而 open 选项则设置用户的上下文。pam_selinux 模块负责查询 SELinux 配置,并根据守护进程使用的服务名称找到正确的映射和上下文。

在宽容模式下禁止访问

在系统上启用并强制执行 SELinux 可以提高其抵御成功利用和其他恶意活动的能力,特别是当系统用作外壳服务器(或提供其他交互式服务)并且用户受到限制——即被映射到 user_u 或其他受限 SELinux 用户时。

一些管理员可能希望暂时将系统切换到宽容模式。这可能是为了排查问题或支持系统上进行的某些更改。使用宽容模式时,确保互动服务对普通用户不可用是一个好主意。

使用 pam_sepermit,可以在系统上强制执行这一点。PAM 模块将在系统处于宽容模式时拒绝一组已定义用户的访问。默认情况下,这些用户会在 /etc/security/sepermit.conf 中列出,但也可以通过 PAM 配置中的 conf= 选项配置使用其他文件。

sepermit.conf 文件中,有三种方法可以记录哪些用户在系统处于宽容模式时应被拒绝访问:

  • 常规用户名

  • 组名,以 @ 符号为前缀

  • SELinux 用户名,以 % 符号为前缀

在这个文件中,我们将每个用户、组或 SELinux 用户列在一行中。在每个条目后,我们可以(但不必)添加一两个选项:

  • exclusive 意味着即使系统处于宽松模式,系统仍会允许用户处于活动状态,但只能有一个会话处于活动状态。当用户注销时,所有活动进程将被终止。

  • ignore 会在 SELinux 处于强制模式时返回 PAM_IGNORE 作为返回状态,在 SELinux 处于宽松模式时返回 PAM_AUTH_ERR。这允许根据系统的宽松状态为该用户在 PAM 中构建特殊的结构/分支。

要启用 pam_sepermit,只需在身份验证 PAM 服务中启用该模块,如下所示:

auth	required	pam_sepermit.so

当然,切换到宽松模式时不要忘记移除所有活动的用户会话,因为任何正在运行的会话将保持不变。

多实例化目录

我们将要查看的最后一个 PAM 模块是 pam_namespace.so。在深入配置这个模块之前,我们首先来看一下什么是多实例化(polyinstantiation)。

多实例化(Polyinstantiation) 是一种方法,当用户登录到系统时,用户将获得特定于其会话的文件系统资源视图,同时可选择隐藏其他用户的资源。这不同于常规的访问控制,在常规访问控制中,其他资源仍然可见,但可能不可访问。

然而,这种会话特定的视图不仅仅使用常规挂载。该模块使用 Linux 内核的命名空间技术,强制为文件系统提供一个(可能更加有限的)视图,该视图是针对用户会话的,并且是隔离的。其他用户会看到不同的文件系统视图。

让我们举一个常见的例子。假设所有用户(除了 root)都不应该访问其他用户生成的临时文件。使用标准访问控制,这些资源仍然可见(可能不可读,但它们的存在或所在目录会可见)。相反,使用多实例化后,用户只会看到自己的 /tmp/var/tmp 视图。

/etc/security/namespace.conf 中的以下设置将重新映射这两个位置:

/tmp		/tmp/tmp-inst/		level	root
/var/tmp	/var/tmp/tmp-inst/	level	root

在真实的文件系统上,这些位置将被重新映射到 /tmp/tmp-inst/var/tmp/tmp-inst 中的子目录。最终用户无法知道或看到这些重新映射的位置——对他们来说,/tmp/var/tmp 就像他们预期的那样。

创建的子目录的格式(以及多实例化)取决于 namespace.conf 文件中的第三个选项。支持的选项如下:

  • user,它将创建一个以用户名称命名的子目录(如 lisa

  • level,它将创建一个以用户敏感度级别和用户名命名的子目录(如 system_u:object_r:tmp_t:s0-s0:c0.c1023_lisa

  • context,它将创建一个以进程上下文(包括敏感度级别)和用户名命名的子目录(如 system_u:object_r:user_tmp_t:s0_lisa

对于 SELinux 系统,最常见的设置是 level

提示

在默认的namespace.conf文件中,您可能会注意到它也支持用户的家目录。启用levelcontext方法后,它将确保用户具有特定敏感性的家目录。例如,如果系统配置要求用户通过 SSH 登录时拥有较低的敏感性,而通过终端登录时拥有较高的敏感性,则会使用不同的家目录视图。

在前面的示例中,只有 root 用户被排除在这些命名空间更改之外。可以列出其他用户(用逗号分隔),或者为需要启用多实例化的用户提供显式用户列表(如果我们在用户列表前加上~字符)。为了使命名空间更改生效,目标位置需要在系统上以000权限可用:

# mkdir /tmp-inst && chmod 000 /tmp-inst

接下来,在会话服务的 PAM 配置文件中启用pam_namespace.so

session	required	pam_namespace.so

最后,确保 SELinux 允许多实例化目录。在 CentOS 上,这是通过polyinstantiation_enabled SELinux 布尔值来管理的:

# setsebool polyinstantiation_enabled on

其他发行版会通过allow_polyinstantiation SELinux 布尔值来支持它。

通过多实例支持,我们结束了本章的最后一节,学习了 PAM 如何触发系统上的 SELinux 上下文变化。

总结

SELinux 将 Linux 用户映射到 SELinux 用户,并通过 SELinux 用户定义定义用户可以分配的角色。我们学习了如何使用semanage应用程序管理这些映射和 SELinux 用户,以及如何为合适的人授予正确的角色。

我们还看到如何使用相同的命令为用户授予适当的敏感性,并且如何在setrans.conf文件中描述这些级别。我们使用chcat工具执行了大多数与类别相关的管理活动。

在为用户分配角色后,我们看到如何使用newrolesudorunconrun_init在不同角色之间切换。本章最后,我们深入了解了 SELinux 如何集成到 Linux 认证过程,并如何通过几个 SELinux 感知的 PAM 模块进一步调整 Linux 系统。

在下一章中,我们将学习如何管理文件和进程上的标签,并了解如何查询 SELinux 策略规则。

问题

  1. 为什么我们不能直接将 SELinux 角色添加到 Linux 账户?

  2. Linux 账户能否映射到多个 SELinux 用户?

  3. 除了关联有效的 SELinux 角色外,SELinux 用户还有哪些其他优势?

  4. PAM 在处理 Linux 账户和 SELinux 映射时的作用是什么?

第四章:使用文件上下文和进程域

启用 SELinux 的系统强烈依赖于上下文(资源上的)和域(进程上的)的概念。SELinux 强制实施的访问控制使用这些上下文来识别资源,并在策略中定义强制规则。由于其固有的对这些上下文的依赖,本章将详细讨论文件上下文、上下文定义和进程域。

我们将处理文件上下文并了解它们存储的位置,这样你就可以轻松调整系统,以便与 SELinux 最佳配合使用。我们为资源分配上下文,既可以是临时的(用于测试),也可以是永久的,并学习如何利用这些上下文来自动推断进程域。一旦我们知道如何获取进程域信息,我们将查询 SELinux 策略,以了解当前的访问控制。

本章将涵盖以下主要内容:

  • SELinux 文件上下文简介

  • 保留或忽略上下文

  • SELinux 文件上下文表达式

  • 修改文件上下文

  • 进程的上下文

  • 限制转换的范围

  • 类型、权限和约束

技术要求

请观看以下视频,查看代码的实际应用:bit.ly/3m3JzkP

SELinux 文件上下文简介

SELinux 文件上下文是系统管理员在系统中使用 SELinux 时需要处理的最重要的配置。文件的上下文通常通过分配给文件的标签来识别。标签错误的文件是系统管理员的常见头痛来源,大多数常见的 SELinux 问题都通过纠正 SELinux 上下文来解决。

了解 SELinux 上下文的使用方式和位置是理解和解决 SELinux 相关问题的关键。下图展示了上下文如何应用于常规 Linux 资源,以及 LSM 子系统如何使用这些上下文来做出决策:

图 4.1 – 上下文与常规 Linux 信息的区别

图 4.1 – 上下文与常规 Linux 信息的区别

让我们以基于 Web 的部署为例:DokuWiki。DokuWiki 是一个流行的 PHP 维基,它使用文件而不是数据库作为后端系统,且易于安装和管理。作为 Web 托管平台,我们将使用 nginx。

获取上下文信息

假设 DokuWiki 应用将托管在 /srv/web/localhost/htdocs/dokuwiki 位置,并且它会将其维基页面(用户内容)存储在 data/ 子目录中。我们从项目站点下载最新的 DokuWiki tarball,download.dokuwiki.org,并将其解压到此位置:

# mkdir -p /srv/web/localhost/htdocs/
# tar -C /srv/web/localhost/htdocs/ -xvf dokuwiki.tgz
# chown -R nginx:nginx /srv/web/localhost/htdocs/dokuwiki

虽然一些发行版可能提供预打包的 DokuWiki 安装,但我们将使用手动安装的方法来展示本章中的各种文件上下文相关操作。

可以使用ls命令的-Z选项轻松获取文件的上下文。大多数能够提供上下文反馈的工具都会尝试使用-Z选项,正如我们在第一章《基础 SELinux 概念》和第三章《管理用户登录》中所看到的id工具所示。

让我们看看dokuwiki目录本身的当前上下文:

# ls -dZ /srv/web/localhost/htdocs/dokuwiki
undefined_u:object_r:var_t:s0 /srv/web/localhost/htdocs/dokuwiki

这里显示的上下文是var_t。在保留或忽略上下文部分中,我们将把这个上下文改为正确的上下文(因为var_t过于通用,不适合托管网页内容)。

文件和目录上下文在文件系统中作为扩展属性存储,当文件系统支持此功能时。扩展属性(通常缩写为xattr)是与资源的 inode(代表文件、目录或符号链接的文件系统信息块)相关联的键/值组合。每个资源可以有多个扩展属性,但每个唯一的键只有一个值。当我们谈论为文件或目录分配标签(或重新标记文件)时,我们指的是设置或更新这个扩展属性,因为它是 SELinux 用于获取文件 SELinux 上下文的标签。

重要提示

不支持扩展属性的文件系统仍然可以在启用了 SELinux 的系统上使用。然而,整个文件系统(包括所有文件和目录)将显示为单一上下文,无法在文件系统上区分资源。我们将在本章的使用挂载选项设置 SELinux 上下文小节中解释如何在这些文件系统上定义文件上下文。

根据约定,Linux 上的扩展属性使用以下语法:

<namespace>.<attribute>=<value>

扩展属性的命名空间允许进行额外的访问控制或特性。在当前支持的扩展属性命名空间(securitysystemtrusteduser)中,security命名空间对操作该属性施加了特定的限制:如果没有加载安全模块(例如,未启用 SELinux),则只有具有CAP_SYS_ADMIN能力的进程(基本上是 root 或类似权限的进程)才能修改此参数。

我们可以使用getfattr应用程序查询现有的扩展属性,如下例所示:

$ getfattr -m . -d dokuwiki
# file: dokuwiki
security.selinux="unconfined_u:object_r:var_t:s0"

如我们所见,security.selinux扩展属性存储着 SELinux 上下文。这确保了当 SELinux 被禁用时,非管理员用户无法更改文件的 SELinux 上下文,并且当 SELinux 启用时,SELinux 策略控制谁可以操作上下文。

stat应用程序也可以用来显示 SELinux 上下文:

$ stat dokuwiki
  File: dokuwiki
  Size: 211		Blocks: 0		IO Block: 4096	
directory
Device: fd01h/64769d	Inode: 8512888	Links: 8
Access: (0755/drwxr-xr-x)	Uid: (	0/	root) Gid: (	0/	root)
Context: unconfined_u:object_r:var_t:s0
...

从文件或目录获取上下文信息应该和获取常规访问控制信息(读取(r)、写入(w)和执行(x)标志)一样常见。

解释 SELinux 上下文类型

使用 SELinux 一段时间后,使用文件标签为文件分配 SELinux 上下文的动机变得更加清晰。SELinux 上下文根据其目的命名,使管理员更容易看出上下文是否被正确分配。

考虑一个用户文件在其主目录中的上下文(user_home_t)、一个位于 /tmp 目录中的 Java 应用程序目录(java_tmp_t),或 rpcbind 的一个套接字(rpcbind_var_run_t)。这些文件或目录在文件系统中具有截然不同的用途,这一点体现在它们所分配的上下文中。

策略编写者总是尽力一致地命名上下文,使我们更容易理解文件的目的,同时也使得策略几乎是自解释的,管理员可以在不需要额外文档的情况下理解策略的目的。

对于常规文件系统,例如,文件根据其主要位置进行标签,以便它们具有相似的安全属性。例如,我们发现 /bin 文件夹(以及 /usr/bin)中的二进制文件与 bin_t 类型相关联,/boot 中的启动文件与 boot_t 相关联,/usr 中的通用系统资源与 usr_t 相关联。

我们还可以找到更多应用特定的上下文。例如,对于 PostgreSQL 数据库服务器,我们有以下上下文:

  • postgresql_t 上下文是为应用程序本身(进程类型或域)设计的。

  • postgresql_port_t 上下文是为 PostgreSQL 守护进程监听的 TCP 端口设计的。

  • postgresql_server_packet_tpostgresql_client_packet_t 上下文是与接收的网络数据包(对于 postgresql_server_packet_t 类型)或发送到 PostgreSQL 端口的数据包相关的类型。

  • postgresql_exec_t 类型被分配给 postgres 二进制文件。

  • 各种 postgresql_*_t 类型用于与守护进程相关的特定文件系统位置,例如 postgresql_var_run_t(适用于 /var/run 中的资源)、postgresql_etc_t(适用于 /etc 中的资源)、postgresql_log_t(适用于 /var/log 中的资源)和 postgresql_tmp_t(适用于 /tmp 中的资源)。

  • 数据库文件本身的 mysqld_db_t 类型。

基于文件或资源的上下文,管理员可以轻松地检测系统设置中的异常。异常的一个例子是当我们将文件从用户的主目录移动到 Web 服务器位置时。当这种情况发生时,文件保留了 user_home_t 上下文,因为扩展属性随着文件一起移动。由于 Web 服务器进程默认不允许访问 user_home_t,它将无法向用户提供该文件。

让我们看看如何在这种复制或移动操作中正确设置上下文。

保持或忽略上下文

现在我们已经知道文件上下文是作为扩展属性存储的,如何确保文件在写入或修改时获得正确的标签呢?为了在文件系统资源上设置 SELinux 上下文,存在一些准则,从继承规则到显式命令。

继承默认上下文

默认情况下,SELinux 安全子系统使用上下文继承来确定在创建文件(或目录、套接字等)时应该分配哪个上下文。在具有var_t上下文的目录中创建的文件,也将被分配var_t上下文。这意味着文件从父目录继承上下文,而不是从执行进程的上下文继承。

然而,这里有一些例外:

  • 支持 SELinux 的应用程序可以强制文件的上下文不同(当然,前提是 SELinux 策略允许这样做)。由于这是在软件代码内部,这种行为无法进行通用配置。

  • 一个名为restorecond的应用程序可以根据 SELinux 的上下文规则,在不同的路径/文件上强制执行上下文。我们将在SELinux 文件上下文表达式修改文件上下文部分分别介绍这些规则和restorecond应用程序。

  • SELinux 策略允许考虑创建新文件或目录的进程的上下文以及进程正在创建的文件名的过渡规则。

接下来我们将介绍这些过渡规则。

查询过渡规则

类型过渡规则是策略规则,在某些条件下强制使用不同的类型。对于文件上下文,类似的类型过渡规则可以是这样的:如果在httpd_t域中运行的进程在标记为var_log_t的目录中创建一个文件,那么该文件的类型标识符将变为httpd_log_t

基本上,这条规则将httpd_log_t网页服务器日志上下文分配给任何被网页服务器放入日志目录中的文件,而不是使用标准继承时会出现的默认var_log_t上下文。

我们可以使用sesearch查询这些类型过渡规则。sesearch应用程序是查询当前 SELinux 策略的最重要工具之一。对于前面的例子,我们需要目录的(源)域和(目标)上下文:httpd_tvar_log_t。在以下示例中,我们使用sesearch来查找与httpd_t域过渡到var_log_t上下文的类型过渡声明:

$ sesearch -T -s httpd_t -t var_log_t
type_transition httpd_t var_log_t:file httpd_log_t;

type_transition行是一个 SELinux 策略规则,完美地映射到描述中。我们来看一下针对tmp_t类型(分配给用于临时文件的目录,如/tmp/var/tmp)的另一组类型过渡规则:

$ sesearch -T -s httpd_t -t tmp_t
type_transition httpd_t tmp_t:dir httpd_tmp_t;
type_transition httpd_t tmp_t:file httpd_tmp_t;
type_transition httpd_t tmp_t:file krb5_host_rcache_t HTTP_23;
type_transition httpd_t tmp_t:file krb5_host_rcache_t HTTP_48;
type_transition httpd_t tmp_t:lnk_file httpd_tmp_t;
type_transition httpd_t tmp_t:sock_file httpd_tmp_t;

策略告诉我们,如果在标记为tmp_t的目录中创建文件、目录、符号链接或套接字,则该新创建的资源将分配httpd_tmp_t上下文(因此不是默认的继承tmp_t上下文)。除了这些规则外,它还包含了两个命名的文件转换规则,这些规则是更灵活的转换规则。

如果在标记为tmp_t的目录中创建HTTP_23HTTP_48,则不会分配httpd_tmp_t上下文(根据常规类型转换规则本应分配),而是分配krb5_host_rcache_t类型(用于 Kerberos 实现)。

类型转换不仅使我们了解哪些标签(因此也包括 SELinux 上下文)将被分配,还为我们提供了哪些类型与特定域相关的线索。在 web 服务器的示例中,我们通过查询策略发现,其日志文件很可能被标记为httpd_log_t,而临时文件则标记为httpd_tmp_t

复制和移动文件

文件上下文也可以在复制或移动操作过程中与文件一起传输。默认情况下,Linux 会执行以下操作:

  • 在同一文件系统上的移动(mv)操作中保留文件上下文(因为此操作不会触及扩展属性,仅调整文件的元数据)。

  • 在跨文件系统边界的移动操作中忽略当前文件上下文,因为这会创建一个新文件,包括内容和扩展属性。相反,使用继承(或文件转换规则)来定义目标上下文。

  • 在进行复制(cp)操作时忽略文件上下文,而是使用继承(或文件转换规则)来定义目标上下文。

幸运的是,这只是默认行为(基于这些实用程序的扩展属性支持),可以自由操作。

我们可以使用-Z选项告诉mv命令将文件的上下文设置为与目标位置相关联的默认类型。例如,在下一个示例中,两个文件从用户的主目录移动到/srv目录。第一个示例将保留其文件上下文(user_home_tadmin_home_t),而第二个示例将获得与放置在/srv中的用户文件相关的类型(var_t):

# touch test1 test2
# mv test1 /srv
# mv -Z test2 /srv
# ls -Z /srv/test*
staff_u:object_r:admin_home_t:s0 /srv/test1
staff_u:object_r:var_t:s0 /srv/test2

类似地,我们可以通过--preserve=context选项告诉cp命令在复制文件时保留 SELinux 上下文。使用相同的示例,现在我们得到以下结果:

# cp test1 /srv
# cp --preserve=context test2 /srv
# ls -Z /srv/test*
staff_u:object_r:var_t:s0 /srv/test1
staff_u:object_r:admin_home_t:s0 /srv/test2

coreutils包提供的大多数实用程序支持-Z选项:mkdir(创建目录)、mknod(创建设备文件)、mkfifo(创建命名管道)等。

重要说明

如果在使用-Z选项时,mv命令返回failed to set the security context,那么很可能该位置要么没有与之关联的有效上下文,要么文件系统不支持 SELinux 标签。前者适用于将文件移动到/tmp的情况,因为 CentOS SELinux 策略未为/tmp中的文件和目录设置默认上下文。新创建的资源总是需要应用其关联的标签(例如user_tmp_t)。

更进一步,许多这些工具允许用户通过--context选项显式地提供上下文。例如,要创建一个上下文为user_home_t的目录/srv/foo,使用mkdir默认情况下是无法实现的,因为目标上下文会被设置为var_t。通过--context选项,我们可以告诉工具设置特定的上下文:

# mkdir --context=user_u:object_r:user_home_t:s0 /srv/foo
# ls -dZ /srv/foo
user_u:object_r:user_home_t:s0 /srv/foo

对于其他工具,最好查阅手册页面,了解该工具如何处理扩展属性。例如,rsync命令可以通过使用-X--xattrs选项来保留扩展属性。

临时更改文件上下文

我们可以使用chcon工具直接更新文件(或文件们)的上下文。在我们之前的示例中,我们注意到 DokuWiki 文件上的var_t标签。这个标签是针对变量数据的通用类型,而不是用于网页内容的正确上下文。我们可以使用chconhttpd_sys_content_t标签应用到这些文件上,这样就允许网页服务器对这些资源具有读取权限:

# chcon -R -t httpd_sys_content_t /srv/web

chcon提供的另一个功能是告诉它用与另一个文件相同的上下文来标记文件或位置。在下一个示例中,我们使用chcon/srv/web及其资源标记为与/var/www目录相同的上下文:

$ chcon -R --reference /var/www /srv/www

如果我们通过chcon更改文件的上下文并将其设置为与上下文列表中不同的上下文,那么该上下文可能会被恢复:包管理器可能会将文件上下文重置回其预定值,或者系统管理员可能会触发一次完整的文件系统标签重命名操作。

到目前为止,我们只关注了上下文中的类型部分。然而,上下文还包括角色部分和 SELinux 用户部分。如果未启用 UBAC,则 SELinux 用户对任何决策没有影响,重置它几乎没有价值。然而,如果启用了 UBAC,则可能需要重置文件上的 SELinux 用户值。像chcon这样的工具也可以设置 SELinux 用户:

# chcon -u system_u -R /srv/web

文件的角色通常是object_r,因为角色当前只对用户(进程)有意义。

为了能够更改上下文,我们确实需要适当的 SELinux 权限,称为relabelfromrelabelto。这些权限授予域,以指示该域是否可以将标签从一种类型更改为另一种类型。如果我们在审核日志中找到与这些权限相关的拒绝信息,那么这意味着策略禁止该域更改上下文。

将类别放置在文件和目录上

我们主要关注类型的更改,并简要讨论了 SELinux 用户,但另一个重要部分是支持类别和敏感度级别。使用chcon,我们可以如下添加敏感度级别和类别:

# chcon -l s0:c0,c2 doku.php

另一个可以用来分配类别的工具是chcat工具。通过chcat,我们可以分配额外的类别,而无需像chcon那样反复设置它们,甚至可以享受由setrans.conf文件提供的可读性强的类别级别:

# chcat -- +Contracts doku.php

要删除类别,只需使用减号:

# chcat -- -Contracts doku.php

要删除所有类别,请使用-d选项:

# chcat -d doku.php

用户和管理员应当记住,应用程序通常不会自行设置类别,因此需要根据需要手动添加。

在文件上使用多级安全性

当系统使用 MLS 策略时,需要使用chcon工具。语法与类别相同。例如,要在用户主目录的所有文件上设置敏感度 s1和类别集 c2 c4 c10,你可以执行以下操作:

$ chcon -R -l s1:c2,c4.c10 /home/lisa

请记住,执行chcon的用户的上下文和将使用数据的用户的上下文必须能够处理提到的敏感度。

备份和恢复扩展属性

与常规的文件操作工具(如mvcp)一样,备份软件在处理 SELinux 启用的系统时,也需要考虑 SELinux 上下文。备份工具在与 SELinux 启用的系统一起使用时有两个重要的要求:

  • 备份工具必须在能够读取备份范围内所有文件的 SELinux 上下文中运行,当然,还需要能够恢复这些文件。如果备份工具没有特定的 SELinux 策略,那么它可能需要在未受限制或具有高度权限的域中运行才能成功。

  • 备份工具必须能够备份和恢复扩展属性。

一个流行的备份(或归档)工具是tar应用程序,它支持以下 SELinux 上下文:

# tar cjvf dokuwiki-20200405.tar.bz2 /srv/web --selinux

在创建 tar 归档时,添加--selinux以包括 SELinux 上下文(在创建归档和提取文件时都会包括)。

使用挂载选项设置 SELinux 上下文

并非所有文件系统都支持扩展属性。当我们使用不支持扩展属性的文件系统时,文件的 SELinux 上下文要么基于文件系统类型本身(每个文件系统都有自己关联的上下文),要么通过mount选项传递给系统。

在这些情况下,最常用的mount选项是context=选项。当设置时,它将使用指定的上下文作为文件系统中所有资源的上下文。例如,要挂载一个包含 FAT 文件系统的外部 USB 驱动器,并确保最终用户可以写入该驱动器,我们可以使用user_home_t上下文挂载它:

# mount -o context="user_u:object_r:user_home_t:s0" /dev/sdc1 /media/usb

如果文件系统支持扩展属性,但尚未对所有文件进行标记,则可以使用 defcontext= 选项告诉 Linux,如果没有 SELinux 上下文可用,则应使用提供的默认上下文:

# mount -o defcontext="system_u:object_r:var_t:s0" /dev/sdc1 /srv/backups

另一个挂载选项是 fscontext=。它为文件系统类型分配上下文,而不是为文件系统中的文件分配上下文。例如,CD/DVD 文件系统可以是 ISO 9660、Joliet 或 UDF。SELinux 使用此类型定义来映射权限,例如挂载操作和文件创建。通过 fscontext= 选项,可以将文件系统类型设置为不同于默认文件系统类型。

当挂载文件系统时,可以使用的最后一个选项是 rootcontext= 选项。即使文件系统尚未对用户空间可见,此选项也会强制将文件系统的根 inode 设置为给定的上下文。挂载操作期间对该位置的权限检查可能会在该位置没有预期上下文时造成混乱(尤其是当文件系统被挂载到预期位置之外时)。rootcontext= 选项提供了一个可重用的配置选项来设置预期的上下文:

# mount -o rootcontext="system_u:object_r:tmp_t:s0" -t tmpfs none /var/tmp

就是这样——这些就是所有与上下文相关的挂载选项。不过有一点需要注意:context= 选项与 defcontext=fscontext= 选项是互斥的。因此,虽然 defcontext=fscontext= 选项可以一起使用,但不能与 context= 选项一起使用。假设目标文件系统支持扩展属性,则可以使用文件上下文表达式,我们将在下一节中讨论这个内容。

SELinux 文件上下文表达式

当我们认为文件的上下文不正确时,需要纠正上下文。SELinux 提供了几种方法来实现这一点,一些发行版甚至增加了更多的选项。我们可以使用诸如 chconrestorecon(与 semanage 一起使用)、setfilesrlpkg(Gentoo)和 fixfiles 等工具。当然,我们也可以使用 setfattr 命令,但那是设置上下文的最不友好的方法。

让我们看看如何以更可管理的方式设置上下文表达式。

使用上下文表达式

在 SELinux 策略中,保存了一组正则表达式,用于通知 SELinux 工具和库文件应该给文件(或其他文件系统资源)分配什么上下文。尽管此表达式列表不会直接在系统上强制执行,但管理员和 SELinux 工具会使用它来检查上下文是否正确,并将上下文重置为应有的状态。您可以在 /etc/selinux/targeted/contexts/files 目录下的各种 file_contexts.* 文件中找到该列表。

作为管理员,我们可以通过 semanage fcontext 查询此列表,如下所示:

# semanage fcontext -l
SELinux fcontext	type		Context
/			directory	system_u:object_r:root_t:s0
...
/vmlinuz.*		symbolic link	system_u:object_r:boot_t:s0
/xen(/.*)?		all files	system_u:object_r:xen_image_t:s0
...

查询此信息的工具之一是 matchpathcon,我们在第二章 理解 SELinux 决策与日志记录 中介绍过:

# matchpathcon /srv/web/localhost/htdocs/dokuwiki
/srv/web/localhost/htdocs/dokuwiki  system_u:object_r:var_t:s0

然而,并非所有条目都可以通过semanage应用程序查看。与特定用户主目录(例如/home/lisa/.ssh)相关的条目不可见,因为这些条目依赖于 Linux 用户(更重要的是,它相关联的 SELinux 用户)。

但对于所有其他条目,命令的输出包含以下内容:

  • 一个匹配一个或多个路径的正则表达式

  • 规则适用的类别,但翻译成更易读的格式

  • 要分配给与表达式和类别列表匹配的资源的上下文

类别列表使我们能够根据资源类别区分上下文。semanage fcontext的输出使用人类可读的标识符:资源类别可以是常规文件(--)、目录(-d)、套接字(-s)、命名管道(-p)、块设备(-b)、字符设备(-c)或符号链接(-l)。当它说所有文件时,该行在任何类别下都是有效的。

目前,我们还没有定义这样的规则,但在下一节之后,即使定义自定义 SELinux 上下文表达式也将不再是秘密。上下文列表的一个重要属性是 SELinux 如何优先应用它——毕竟,我们可能有两个都匹配某个特定资源或路径的表达式。在 SELinux 中,最具体的规则会获胜。使用的逻辑如下(按顺序):

  1. 如果行 A 有正则表达式,而行 B 没有,则行 B 更具体。

  2. 如果行 A 中第一个正则表达式前的字符数少于行 B 中第一个正则表达式前的字符数,那么行 B 更具体。

  3. 如果行 A 中的字符数少于行 B 中的字符数,则行 B 更具体。

  4. 如果行 A 没有映射到特定的 SELinux 类型(策略编辑器明确告诉 SELinux 不要分配类型),而行 B 有,则行 B 更具体。

然而,这个规则的顺序有一个警告。当通过semanage添加额外的规则时(我们将在下一节中描述),SELinux 的工具会按添加的顺序应用这些规则,而不是按规则的具体性来应用。因此,不是使用最具体的规则,而是使用最近添加的、与路径匹配的规则。

注册文件上下文更改

因为使用chcon更改 SELinux 上下文通常只是一个临时措施,因此强烈建议仅在测试上下文更改的影响时使用chcon。一旦更改被接受,我们需要通过semanage进行注册。例如,要永久将/srv/web(及其所有子目录)标记为httpd_sys_content_t,并将 DokuWiki 的data/conf/文件夹标记为httpd_sys_rw_content_t(以允许 Web 服务器修改这些资源),我们需要执行以下命令:

# semanage fcontext -a -t httpd_sys_content_t "/srv/web(/.*)?"
# semanage fcontext -a -t httpd_sys_rw_content_t "/srv/web/localhost/htdocs/dokuwiki/data(/.*)?"
# semanage fcontext -a -t httpd_sys_rw_content_t "/srv/web/localhost/htdocs/dokuwiki/conf(/.*)?"
# restorecon -Rv /srv/web

我们在这里做的是通过 semanage/srv/web 及其子目录注册为 httpd_sys_content_t,而将两个可写目录注册为 httpd_sys_rw_content_t。然后,我们使用 restorecon(递归)重置 /srv/web 的上下文为在上下文列表中注册的值。这是为大多数资源设置上下文的推荐方法。

这些注册是本地(自定义的)上下文表达式,存储在一个单独的配置文件中(file_contexts.local)。考虑到(本地添加的)表达式的优先级,重要的是将最具体的条目最后添加,否则对于整个目录会应用定义较为宽泛的 httpd_sys_content_t 规则。这不同于(策略添加的)表达式的优先级规则,后者有“最具体的规则优先”的概念。

semanage fcontext 应用程序还可以用于通知 SELinux,文件系统树的某一部分应该像文件系统上另一个位置一样进行标记。这样的 semanage 会应用相同的上下文,就像目标位置是默认位置一样。

通过一个示例,我们来更清晰地展示这一点,让 /srv/web 下的所有内容都像 /var/www 下的文件一样进行标记(包括子目录),因此 /srv/web/icons 会获得与 /var/www/icons 相同的上下文。我们使用 semanage fcontext-e 选项来创建这样的等效规则,如下所示:

# semanage fcontext -a -e /var/www /srv/web
# restorecon -Rv /srv/web

这将创建一个替代条目,使得 /srv/web 下的任何内容都与 /var/www 下的相同位置获得相同标签。

大多数发行版已经配置了一些等效规则,我们可以按以下方式读取:

# cat /etc/selinux/targeted/contexts/files/file_contexts.subs_dist
/run /var/run
...
/sysroot/tmp /tmp

semanage fcontext -l 命令也会在其输出的最后显示这些等效位置。

优化递归上下文操作

restorecon 应用程序根据通过 SELinux 策略和 semanage fcontext 管理的上下文定义,重置文件和其他资源的 SELinux 上下文。当对目录递归应用 restorecon 时,可能需要一段时间。为了在这种情况下提高性能,SELinux 开发者支持跳过 restorecon 操作。

restorecon 命令中加上 -D 选项后,一个额外的扩展属性将写入包含该命令的主要目录,该属性包含当调用该命令时使用的文件上下文定义的哈希值:

# restorecon -RD /home

后续使用 restorecon 命令并加上 -D 选项时,将检查这个哈希值,以判断影响该目录的文件上下文定义是否有修改(使用 semanage fcontext)。如果没有修改,恢复操作将被跳过:

# restorecon -RvD /home
Skipping restorecon as matching digest on: /home

一旦我们更新了影响给定位置的定义,restorecon 将相应地重置上下文:

# semanage fcontext -a -t httpd_user_content_t "/home/[^/]*/cgi-bin(/.*)?"
# restorecon -RvD /home
Relabeled /home/lisa/cgi-bin from staff_u:object_r:user_home_t:s0 to staff_u:object_r:httpd_user_content_t:s0
Updated digest for: /home

restorecon_xattr 命令可以用来管理这些扩展属性(查看或删除)并显示属性是如何形成的:

# restorecon_xattr -v /home
specfiles SHA1 digest: 7ed69be330ad60811481e455ca8e5ab0b1556036
calculated using the following specfile(s):
/etc/selinux/targeted/contexts/files/file_contexts.subs_dist
...
/etc/selinux/targeted/contexts/files/file_contexts.local.bin
/home Digest: 7ed69be330ad60811481e455ca8e5ab0b1556036 Match

引用的 digestsecurity.restorecon_lastsecurity.sehash 扩展属性。较新的用户空间工具使用后者,并将其逻辑应用于每个子目录,而较旧的用户空间实用程序使用前者,并仅在选定的目录上应用其逻辑。

security.restorecon_last 使用的缺点是它不能与子目录一起工作:如果我们对 / 应用递归的 restorecon 操作,那么此工具将忽略 /home 上的摘要。使用 security.sehash 时,对 / 的递归操作将检查 /home 的摘要。

使用可定制类型

某些 SELinux 类型适用于路径无法由管理员准确定义或管理员不希望在触发重新标记操作时重置上下文的文件。为此,SELinux 支持所谓的 restorecon)遇到设置为可定制类型的文件时,它们不会将其上下文恢复为注册的上下文定义。

可定制类型声明在 /etc/selinux/targeted/contexts 内的 customizable_types 文件中。要使 restorecon 重新标记这些文件,管理员需要在工具重置上下文之前传递强制重置选项(-F)。

让我们看看这个 customizable_types 文件的内容:

$ cat /etc/selinux/targeted/contexts/customizable_types
container_file_t
sandbox_file_t
...
httpd_user_content_t
git_session_content_t
home_bin_t
user_tty_device_t

例如,我们可以将主目录中的文件标记为 home_bin_t,这是一种可定制类型,因此,在执行文件系统重新标记操作时,此文件不会重新标记为 user_home_t

$ chcon -t home_bin_t ~/convert.sh

将其他类型标记为可定制类型需要更新 customizable_types 文件,因为没有用户命令可以从此列表中添加或删除类型定义。因为此文件可能在发行版或管理员推出新策略包时被覆盖,所以需要仔细管理。

话虽如此,使用可定制类型也有其优势。作为管理员,我们可能希望创建和支持特定类型,供最终用户使用 chcon 在其主目录中设置单个文件的上下文。通过将这些类型标记为可定制类型,对 /home 的重新标记操作不会重置这些上下文。

当目标类型不是可定制类型时,管理员通常倾向于使用 semanage fcontext 添加表达式,并使用 restorecon 修复文件的上下文。大多数管理员会使用基于目录的标记:这样更容易维护,也更容易向最终用户解释。许多人甚至会将这种方法用于可定制类型:

# semanage fcontext -a -t home_bin_t "/home/[^/]*/bin(/.*)?"

使用此命令,位于 ~/bin 目录中的用户二进制文件和脚本将被标记为 home_bin_t

编译不同的 file_contexts 文件

/etc/selinux/targeted/contexts/files 目录内,可以找到五个不同的 file_contexts 文件:

  • file_contexts 文件本身(没有任何后缀)是由 Linux 发行版提供的 SELinux 策略所提供的基本表达式文件。

  • file_contexts.local 文件包含本地添加的规则(通过之前章节中介绍的 semanage fcontext 命令)。

  • file_contexts.homedirs 文件包含用户主目录的表达式。当通过 semanage loginsemanage user 创建并管理新的用户映射时,文件会根据新情况进行调整。

  • file_contexts.subs_dist 文件包含由发行版的 SELinux 策略提供的等效规则,这些规则告诉 SELinux 将文件系统的一个部分视为与另一个位置具有相同的标签规则。

  • file_contexts.subs 文件包含本地管理的等效规则(通过之前章节中介绍的 semanage fcontext 命令)。

除了这些文件,你还会发现关联的 *.bin 文件(例如 file_contexts.bin 对应 file_contexts 文件,file_contexts.local.bin 对应 file_contexts.local 文件,等等)。这些 *.bin 文件是自动创建的,但如果出现不一致,管理员也可以使用 sefcontext_compile 命令重新生成文件:

# cd /etc/selinux/targeted/contexts/files
# sefcontext_compile file_contexts.local

这些文件包含与主文件相同的信息,但它们是预编译的,能够加速查找。除非工具检测到 *.bin 文件比源文件旧,否则 SELinux 工具将使用这些已编译版本的文件。

交换本地修改

当通过 semanage fcontext 注册本地修改时,它们只适用于单个系统。如果需要将本地定义重新应用到多个系统,管理员可以提取本地修改并将其导入到另一个系统中。

要导出本地修改,请使用 semanage export

# semanage export -f local-mods.conf

包含本地修改的文件(示例中的local-mods.conf)可以根据需要进行调整。这允许管理员移除除他们希望在其他系统上应用的行以外的所有行。

将本地修改存储在文件中后,将该文件传输到其他系统,并导入设置:

# semanage import -f ./local-mods.conf

导入的设置会立即注册。当然,如果文件系统发生更改(semanage fcontext),别忘了对目标目录运行 restorecon

修改文件上下文

我们现在知道如何设置 SELinux 上下文,既可以通过 chcon 等工具直接设置,也可以通过 restorecon 应用程序设置,该应用程序查询 SELinux 上下文列表,以确定文件应该具有的上下文。然而,restorecon 并不是唯一考虑此上下文列表的应用程序。

使用 setfiles、rlpkg 和 fixfiles

setfiles 应用程序是一个较旧的程序,它需要上下文列表文件本身的路径来重置上下文。它通常在其他应用程序的后台使用,因此大多数管理员不再需要直接调用 setfiles

# setfiles /etc/selinux/targeted/contexts/files/file_contexts /srv/web

另一组工具是rlpkg(Gentoo)和fixfiles(CentOS 及相关发行版)应用程序。这两个应用程序有一个很好的功能:它们可以用于重置一个应用程序的文件上下文,而无需手动遍历文件并对其运行restorecon

在下一个示例中,我们使用这些工具恢复由nginx包提供的文件的上下文:

# rlpkg nginx
# fixfiles -R nginx restore

这两个应用程序的另一个特点是,它们可以用于重新标记整个文件系统,而无需执行系统重启,操作方式如下:

# rlpkg -a -r
# fixfiles -f -F relabel

当然,这不像之前的命令那样细粒度。

重新标记整个文件系统

在上一节中列出的rlpkgfixfiles命令并不是在 CentOS(或相关)发行版中进行完整文件系统重新标记的唯一方法。SELinux 提供了另外两种方法,在(重新)启动时请求系统执行完整的文件系统重新标记操作:放置一个触摸文件(系统在启动时读取)或配置引导参数。

触摸文件名为.autorelabel,应放置在根文件系统中。设置后,系统需要重新启动:

# touch /.autorelabel
# reboot

如果我们将autorelabel=1参数添加到引导参数列表中(就像我们之前讨论过的可以设置selinux=enforcing=参数一样),我们也会触发相同的行为。

请求系统执行完整的文件系统重新标记操作将需要一段时间。完成后,系统将再次重启。在重新标记操作完成后,触摸文件将被自动移除。

使用 restorecond 自动设置上下文

上下文也可以由restorecond守护进程应用。该守护进程的目的是将表达式列表规则强制应用到在/etc/selinux/restorecond.conf文件中定义的可配置位置。

以下是一组文件和目录的示例列表,这些位置在restorecond.conf文件中进行了配置,以便每当检测到这些文件和目录中的上下文变化时,restorecond 自动应用 SELinux 上下文:

/etc/services
/etc/resolv.conf
/etc/samba/secrets.tdb
...
/root/.ssh/*

在这种情况下,如果一个进程创建了一个与之前创建的路径匹配的文件,Linux 的 inotify 子系统将通知restorecond。然后,restorecond 将根据表达式列表重新标记该文件,应用正确的标签,无论是哪个进程(以及上下文)创建了该文件。

restorecond的使用主要是出于历史原因,当时 SELinux 不支持命名文件转换。在那时,写入/etc中的resolv.conf无法与写入/etc中的passwd文件区分开来。命名文件转换的引入大大减少了对restorecond的需求。

使用 tmpfiles 在启动时设置 SELinux 上下文

如果 Linux 发行版使用systemd,则可以使用systemd-tmpfiles在启动时自动设置 SELinux 上下文。systemd使用tmpfiles应用程序来自动创建和管理系统上的易失性位置,例如当/runtmpfs挂载的文件系统(内存文件系统)时,管理/run内部的位置。

管理员可以配置tmpfiles以在启动时自动创建文件、目录、设备文件、符号链接等,并重置资源的权限。正是通过这个重置操作,我们可以使用tmpfiles在启动时设置正确的 SELinux 上下文。

第三章《管理用户登录》中,我们讨论了多实例化,其中用户可以在文件系统资源上获得自己的私有视图。我们举的例子使用了名为/tmp/tmp-inst的目录,该目录必须设置为000权限,并且将承载用户特定的/tmp视图。与其每次都创建并设置这个权限,我们可以配置tmpfiles来自动为我们执行此操作,并事先定义正确的 SELinux 上下文:

# semanage fcontext -a -t tmp_t -f d "/tmp/tmp-inst"

/etc/tmpfiles.d中,我们创建一个名为selinux-polyinstantiation.conf的文件,内容如下:

d /tmp/tmp-inst 000 root root

文件名可以自由选择,但请确保使用.conf后缀。每次系统启动时,systemd-tmpfiles会确保以适当的权限创建/tmp/tmp-inst目录。

如果某个位置不需要创建,只需要重置其 SELinux 上下文,则可以在tmpfiles配置中使用z(一个资源)或Z(递归)选项。例如,默认的 SELinux tmpfiles配置selinux-policy.conf就在/usr/lib/tmpfiles.d中使用了该选项:

z /sys/devices/system/cpu/online - - -

使用的-表示告知tmpfiles不要调整权限和所有权,仅重置 SELinux 上下文。

进程的上下文

由于 SELinux 中的所有内容都与上下文相关,甚至进程也会分配一个上下文,也就是所谓的域。让我们看看如何获取这些信息,SELinux 如何从一个域切换到另一个域,并了解如何查询 SELinux 策略以获取更多关于这些切换的信息。

获取进程的上下文

我们看到nginx Web 服务器运行在httpd_t域中,可以通过ps -eZ命令查看,输出如下:

# ps -eZ | grep nginx
system_u:system_r:httpd_t:s0  3744 ?   00:00:00 nginx

还有几种方法可以获取进程的上下文。尽管使用ps命令的方法最为直观,但这些其他方法在脚本化处理或通过监控服务时可能会非常有用。

一种方法是读取我们在第一章《SELinux 基础概念》中已经遇到的/proc/<pid>/attr/current伪文件。它显示一个进程的当前安全上下文:

# pidof nginx
3746 3745 3744
# cat /proc/3744/attr/current
system_u:system_r:httpd_t:s0

要获得更易读的输出,可以使用secon命令来查询给定进程 ID 的上下文:

# secon --pid 3744
user: system_u
role: system_r
type: httpd_t
sensitivity: s0
clearance: s0
mls-range: s0

最后,SELinux 用户空间项目有一个名为getpidcon的辅助工具,libselinux库可以选择提供这个工具。尽管该工具在 CentOS(或相关发行版)上不可用,但其他发行版,如 Gentoo,确实有该工具。该工具需要一个 PID 并返回其上下文:

# getpidcon 679
system_u:system_r:nginx_t:s0

现在,Apache 进程不会自己通知 SELinux 它们需要在httpd_t(或者在 Gentoo 中是nginx_t)域中运行。为此,SELinux 策略中存在转换规则,规定了何时以及如何在特定域中执行进程。

向某个域的转换

就像我们在文件中看到的那样,如果一个进程进行分叉并创建了一个新进程,则该新进程默认会继承父进程的上下文。对于 Web 服务器,主进程在httpd_t域中运行,因此所有启动的工作进程都会继承httpd_t域。

为了区分一个进程的域和另一个进程的域,可以定义域转换。一个execve()函数,最有可能在fork()操作之后执行。

与基于文件的转换一样,域转换可以使用sesearch进行查询。让我们来看看允许转换到httpd_t域的域:

$ sesearch -T -t httpd_exec_t
type_transition certwatch_t httpd_exec_t:process httpd_t;
type_transition cluster_t httpd_exec_t:process httpd_t;
type_transition initrc_t httpd_exec_t:process httpd_t;
...
type_transition system_cronjob_t httpd_exec_t:process httpd_t;

在这种情况下,如果父进程在上述某个域(例如initrc_t域)中运行,并且正在执行标记为httpd_exec_t(分配给httpdnginx二进制文件的标签)的文件,SELinux 将把启动的 Web 服务器的上下文切换到httpd_t

但是,要真正实现这一点,除了域转换外,还需要其他几种权限。以下列表描述了这些不同的权限:

  • 源进程(例如initrc_t)需要被允许转换到httpd_t域,这由进程类上的转换权限来管理:

    $ sesearch -s initrc_t -t httpd_t -c process -p transition -A
    
  • 源进程(例如initrc_t)需要在它正在启动的文件上具有执行权限(httpd_exec_t):

    $ sesearch -s initrc_t -t httpd_exec_t -c file -p execute -A
    
  • 必须将httpd_exec_t类型标识为httpd_t域的入口点。SELinux 使用入口点来确保只有在使用指定的文件上下文运行执行二进制文件或脚本时,域转换才会发生:

    $ sesearch -s httpd_t -t httpd_exec_t -c file -p entrypoint -A
    
  • 目标域必须允许父进程所在角色。在系统守护进程的情况下,角色是system_r

    $ seinfo -r system_r -x | grep httpd_t
    

这些权限的图形表示如下:

图 4.2 – 必要的域转换权限的图形概览

图 4.2 – 必要的域转换权限的图形概览

只有在允许所有这些权限的情况下,域转换才会发生。如果没有,那么应用程序的执行将失败(如果该域在文件上没有executeexecute_no_trans权限),或者它会执行,但仍然在与父进程相同的域中运行。

域转换是一个重要概念,因为它告诉管理员一个应用程序如何进入其特权上下文。为了分析这一点,许多安全管理员会查看一个上下文如何转换到另一个上下文。我们在第十三章分析策略行为中解释了策略分析。

对于策略编写者来说,决定何时创建域转换以及何时保持进程在同一(源)上下文中运行是设计的问题。通常,策略开发者会尽量将父上下文限制在一个范围内,这样每个附加的权限就成为切换到另一个域(该域具有该权限)的考虑因素。基本上,当目标应用程序需要比源域持有的权限更多(或不同)时,策略开发者会触发转换。

这也是为什么在执行用户应用程序时,unconfined_t域的转换次数比受限用户域user_tguest_t少的原因:unconfined_t域已经持有许多权限,因此转换到不同的域几乎没有价值。请注意,这是策略编写者或 Linux 发行版做出的决策,而不是 SELinux 技术本身做出的决策。所有 SELinux 做的就是执行策略规则。

验证目标上下文

在执行应用程序时,SELinux 策略可能会让命令在不同的域中运行。尽管我们可以使用sesearch查询所有规则,但存在一个更简单的命令,它会告诉我们执行命令或脚本时的目标上下文:selinuxexeccon

该命令至少需要一个参数(即将执行的二进制文件或脚本的路径),以及一个可选的第二个参数(源上下文)。如果我们省略第二个参数,工具将使用当前上下文作为源上下文。

例如,要找出当在当前上下文中执行passwd命令时,命令将在哪个域中运行,我们可以使用以下命令:

# selinuxexeccon /usr/bin/passwd
unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023

以下示例展示了当init_t域执行nginx二进制文件时的目标上下文:

# selinuxexeccon /usr/sbin/nginx system_u:system_r:init_t:s0
system_u:system_r:httpd_t:s0

使用selinuxexeccon比单独查询所有适当的权限要快得多。

其他支持的转换

常规域转换是 SELinux 中最常见的转换,但也有其他可能的转换。例如,一些应用程序(如cronlogin)是 SELinux 感知的,并会指定要转换到的域。这些应用程序调用setexeccon()方法(设置执行上下文)来指定目标域,而不使用类型转换规则。然而,其他的权限要求仍然有效。

一些支持 SELinux 的应用程序甚至能够更改其当前上下文(而不仅仅是它们执行的应用程序的上下文)。为了实现这一点,应用程序域需要 dyntransition 权限(这是支持进程级活动的权限之一)。这样的应用程序的一个例子是 OpenSSH,默认情况下,它在 sshd_t 域中运行,但可以切换到 sftpd_t 类型。

查询初始上下文

当 SELinux 尚未为资源分配标签时,它将为该资源分配一个初始上下文(或初始安全标识符SID))。对于某些类别,SELinux 策略将具有一个默认的初始上下文,从中可以进一步启动并分配标签。

可以使用 seinfo 查询各种 SID 的初始上下文:

# seinfo --initalsid -x
Initial SIDs: 27
  sid any_socket system_u:object_r:unlabeled_t:s0
  sid devnull system_u:object_r:null_device_t:s0
...
  sid unlabeled system_u:object_r:unlabeled_t:s0

正如你所见,并不是所有类别都有默认的上下文分配,因为其他类别的上下文是从当前列出的初始 SID 的上下文派生的。

调整内存保护

Linux 系统上的遗留二进制文件可能需要为内存区域设置执行权限,当这些区域用于读取时,即使没有实际使用执行权限。读即执行(read-implies-exec)对于强制访问控制(如 SELinux)来说是个麻烦,因为它们需要在策略中记录适当的权限。如果一个应用程序需要读取访问权限,那么策略是否也必须包含隐含的执行权限?如果策略没有包含执行权限,那么读取操作是否应该失败,因为它隐含了执行权限?

说明性备注

读即执行(read-implies-exec)是对运行旧二进制文件或为其他 Unix 系统编译的二进制文件的遗留支持,其中应用程序没有明确标记其可执行内存为可执行内存,假设所有标记为可读的内存区域都是可执行的。这带来了安全风险,因为恶意行为者可以动态加载可执行代码,而系统无法阻止应用程序执行这些代码。如今,许多操作系统都有明确的内存保护机制,包括防止数据变为可执行代码。遗憾的是,我们往往需要处理遗留情况,因此所有操作系统都有方法选择性地禁用这些内存保护,而在 Linux 中这是通过其 man personality 完成的(更多信息)。

SELinux 开发者通过引入可以调整的内存保护检查,允许管理员选择最合适的权限处理方式。checkreqprot 选项可以设置为 0,以检查内核处理的保护,或设置为 1,以检查应用程序请求的保护。

在旧系统上,此选项将设置为 1 以支持这些遗留二进制文件。然而,最近的发行版适当地构建了它们的应用程序,使用更安全的设置 0,如 sestatus 命令所示:

# sestatus | grep Memory
Memory protection checking:   actual (secure)

你可以通过 /sys/fs/selinux/checkreqprot 来切换此支持:

# echo 1 > /sys/fs/selinux/checkreqprot
# sestatus | grep Memory
Memory protection checking:   requested (insecure)

该参数的默认值在构建 Linux 内核时通过CONFIG_SECURITY_SELINUX_CHECKREQPROT_VALUE内核配置参数进行配置。管理员还可以通过checkreqprot=启动参数启动系统,以设置指定的值。

限制转换范围

出于安全原因,Linux 系统可以在某些情况下减少进程获取提升权限的能力,或提供额外的约束,以减少漏洞被利用的可能性。SELinux 开发人员也遵守这些情况。

在转换时清理环境

当我们执行一个更高权限的命令(无论是setuid应用程序,还是在会话中添加了权限的应用程序)时,GNU C 库glibc)会清理环境。这意味着一组安全敏感的环境变量会被丢弃,以确保攻击者、恶意人员或恶意应用程序无法负面影响会话。

这种安全执行通过LD_PRELOADLD_AUDITLD_DEBUGTMPDIRNLSPATH的移除来控制。

SELinux 还会强制对域转换进行环境清理,确保新执行的域无法访问这些敏感的环境变量。当然,有时转换后的域需要这些变量。并非所有域都能处理清理过的环境,或者使用这些环境变量传递重要信息,因此始终丢弃环境变量可能会导致应用程序域无法使用。

为了允许在不清理环境的情况下进行域转换,可以为域转换授予noatsecure权限。例如,假设我们执行一个 Firefox 插件:

# sesearch -t mozilla_plugin_t -p noatsecure -A
...
allow unconfined_t mozilla_plugin_t:process { ... noatsecure ...};
...

当在unconfined_t域中运行的应用程序执行插件(导致转换到mozilla_plugin_t域)时,必须保留环境变量,否则插件可能无法正常工作。因此,SELinux 策略授予调用 Firefox 插件的域noatsecure权限。

禁用不受限制的转换

Linux 支持的第二个安全约束是使用nosuid选项挂载文件系统。设置该选项后,该文件系统上的任何setuidsetgid二进制文件将不会对执行会话的有效用户或组 ID 产生任何影响。本质上,位于使用nosuid挂载的文件系统上的setuid应用程序将表现得像没有设置setuid位。

为了确保由托管在nosuid挂载文件系统上的应用程序触发的转换不会允许提升的权限,SELinux 策略开发人员必须明确标记一个转换为允许在nosuid挂载文件系统上进行,使用nosuid_transition权限。该权限属于process2类:

$ sesearch -s unconfined_t -p nosuid_transition -A
allow unconfined_t initrc_t:process2 { nnp_transition nosuid_transition };
...

这允许策略开发人员区分常规域转换和nosuid约束的域转换。

注意

SELinux 对可以分配给某个类别的权限数量有一个限制。当权限数量超过 32 时,SELinux 开发者会创建一个不同的类别,权限将继续存在于第二个类别中。目前,权限超过 32 的两个类别是 capability 类别和 process 类别。

然而,这种基于权限的方法可能并不是所有启用 SELinux 的系统上都有实现。只有在定义并将 nnp_nosuid_transition 策略功能设置为 1 时,才会启用:

# cat /sys/fs/selinux/policy_capabilities/nnp_nosuid_transition
1

如果此功能值为0,则 SELinux 将使用一个叫做 nosuid 的挂载文件系统概念。任何具有文件上下文的可执行文件,如果会导致域转换,只有在目标域由父域约束时,才会发生域转换。

注意

nnp_nosuid_transition 功能时,策略开发者通知内核,应该使用 nosuid_transitionnnp_transition 权限检查,而不是有界域,并且该策略通常仅支持这些转换,而不支持有界域。

如果没有约束,那么域转换将不会发生,且会话将保持在当前上下文中(或者如果应用程序不允许在当前上下文中运行,命令将无法执行)。

有界域不仅仅是根据权限实时计算的。SELinux 有一条明确的规则,强制要求目标域由父域约束。即使后来向有界域添加了权限,如果这些权限不属于父域,它们也会被 SELinux 安全子系统拒绝。

使用 seinfo,这些类型的边界可以如下列出:

# seinfo --typebounds

然而,大多数发行版现在不再在其 SELinux 策略中定义有界域,因为新的 nosuid_transition 权限更加灵活。使用有界域要求策略开发者每次需要扩展子域时,都要扩展父域的权限,而当父域是通用的(无论是容器管理平台还是系统服务守护进程)时,这会变得非常麻烦。

使用 Linux 的 NO_NEW_PRIVS

使用挂载了 nosuid 的文件系统是 Linux 的 nnp_transition 权限的特定情况,或者如果没有设置 nnp_nosuid_transition 策略功能,则指向一个有界域。

该参数可以通过应用程序本身使用进程控制函数 prctl() 来设置,但用户也可以影响此值。可以使用 setpriv 命令启动应用程序,并设置 PR_SET_NO_NEW_PRIVS(这是应用程序可以通过 prctl() 函数传递的参数)。

作为示例,在常规用户的主目录中的cgi-bin目录下创建以下简单的基于 Python 的 CGI 脚本:

#!/usr/bin/env python3
import sys, time
import subprocess
import cgi, cgitb
cgitb.enable()
print('Content-Type: text/html;charset=utf-8\n')
PIPE = subprocess.PIPE
STDOUT = subprocess.STDOUT
pd = subprocess.Popen(['ping', '-c', '1', 'localhost'], 
stdout=PIPE, stderr=STDOUT)
while True:
  output = pd.stdout.read(1)
  if output == '' and pd.poll() != None:
    break
  if output != '':
    sys.stdout.write(output.decode('utf-8'))
    sys.stdout.flush()

现在,该 CGI 脚本可用,首先启动一个简单的 CGI 支持网页服务器(我们将选择端口6020,因为非特权用户应该能够将进程绑定到该端口),并连接到它:

$ python3 -m http.server --cgi 6020

在另一个会话中,连接到网页服务器并调用新创建的 Python 脚本(此处命名为test.py):

$ curl http://localhost:6020/cgi-bin/test.py
PING localhost(localhost(::1)) 56 data bytes ...

现在,启动同一个 CGI 支持的网页服务器,但启用 NNP:

$ setpriv --no-new-privs python3 -m http.server --cgi 6020

再次连接到网页服务器并调用test.py CGI 脚本:

$ curl http://localhost:6020/cgi-bin/test.py
ping: socket: Permission denied

由于 Linux 启用了 NNP,ping命令无法获得打开套接字所需的更高权限。

有时,您会在 SELinux 审计日志中注意到execute_no_trans权限的拒绝。这是因为 SELinux 策略不允许应用程序在没有过渡的情况下执行。

类型、权限和约束

现在我们对类型(如进程、文件和其他资源)有了更多的了解,接下来我们将更详细地探讨它们在 SELinux 策略中的应用。

理解类型属性

我们已经讨论了sesearch应用程序及其如何用于查询当前 SELinux 策略。现在让我们看一个具体的进程过渡:

$ sesearch -s initrc_t -t httpd_t -c process -p transition -A
allow initrc_domain daemon:process transition;

尽管我们请求了与initrc_t源域和httpd_t目标相关的规则,但我们得到了关于initrc_domain源域和daemon目标的规则。sesearch在这里做的是展示 SELinux 策略如何通过分配给initrc_thttpd_t类型的属性来允许请求的权限。

initrc_domain,以下类型都被标记为此属性,可以通过seinfo应用程序查看:

$ seinfo -a initrc_domain -x
Type Attributes: 1
  attribute initrc_domain;
    cluster_t;
    ...
    initrc_t;
    ...
    piranha_pulse_t;

如我们所见,initrc_t类型确实是标记为initrc_domain的类型之一。类似地,daemon属性被分配给多个类型(甚至几百个)。因此,前面提到的单一允许规则将超过一千条规则合并为一条。

属性在策略中越来越多地被用作整合和简化策略开发的一种方式。通过seinfo -a,您可以获得当前策略中支持的所有属性的概览。

查询域权限

SELinux 中最常见的规则是allow规则,它告知 SELinux 子系统某个域具有哪些权限。allow规则使用以下语法:

allow <source> <destination> : <class> <permissions>;

<source>字段几乎总是一个域,而<destination>字段可以是任何类型。

<class>字段使我们能够根据资源区分权限,无论它是常规文件、目录、TCP 套接字、能力等。可以通过seinfo -c获得所有支持的类的完整概览。每个类都有一组权限,SELinux 可以控制。例如,sem类(用于信号量访问)与之关联的权限如下:

$ seinfo -c sem -x
Classes: 1
  class sem
inherits ipc

输出中的ipc引用告知我们该类继承了公共ipc类的权限,我们可以通过如下方式查询:

$ seinfo --common=ipc -x
Commons: 1
{
  write
  destroy
  ...
  create
}

<permissions>字段中,大多数规则会使用大括号将一组权限捆绑在一起:

allow user_t etc_t : file { ioctl read getattr lock execute execute_no_trans open };

这种语法允许策略开发人员进行非常精细的权限控制。我们可以使用sesearch命令查询这些规则。向sesearch命令提供的选项越多,我们的搜索参数就越细化。例如,sesearch -A会列出当前生效的所有允许规则。添加源(-s)会将输出筛选为仅显示此域的允许规则。添加目标(-t)则进一步筛选输出。其他可用于通过sesearch筛选允许规则的选项包括类(-c)和权限(-p)选项。

如你现在可能已经猜到,sesearch是一个非常强大的命令,用于查询活动策略,显示与给定选项匹配的 SELinux 策略规则。

了解约束

然而,SELinux 中的allow语句只关注与类型相关的权限。但有时我们需要根据用户或角色信息来限制某些操作。SELinux 通过约束支持这一点。

SELinux 中的约束是应用于一个类及其权限集的规则,必须满足这些规则,SELinux 才会进一步允许请求。考虑以下关于进程转换的约束:

constrain process
  { transition dyntransition noatsecure siginh rlimitinh }
  (
    u1 == u2 or
    (
      t1 == can_change_process_identity and
      t2 == process_user_target
    ) or (
      t1 == cron_source_domain and
      (
        t2 == cron_job_domain or
        u2 == system_u
      )
    ) or (
      t1 == can_system_change and
      u2 == system_u
    ) or (
      t1 == process_uncond_exempt
    )
  );

这个约束表示,如果调用了transitiondyntransition或其他提到的三种进程权限中的任何一个,以下规则至少有一个必须为真:

  • 源(u1)的 SELinux 用户和目标(u2)的 SELinux 用户必须相同。

  • 源(t1)的 SELinux 类型必须设置can_change_process_identity属性,且目标(t2)的 SELinux 类型必须设置process_user_target属性。

  • 源(t1)的 SELinux 类型必须设置cron_source_domain属性,且目标类型(t2)应具有cron_job_domain属性,或者目标 SELinux 用户(u2)应为system_u

  • 源(t1)的 SELinux 类型必须设置can_system_change属性,目标(u2)的 SELinux 用户必须是system_u

  • 源(t1)的 SELinux 类型必须设置process_uncond_exempt属性。

正是通过约束,UBAC 得以实现,如下所示:

u1 == u2
or u1 == system_u
or u2 == system_u
or t1 != ubac_constrained_type
or t2 != ubac_constrained_type

你可以使用seinfo --constrain列出当前启用的约束。对于同一个类和权限集,可以同时激活多个约束。在这种情况下,所有约束都必须为真,权限才能通过。

总结

在本章中,我们学习了如何将文件上下文作为扩展属性存储在文件系统中,以及如何操作文件和其他文件系统资源的上下文。接下来,我们了解了 SELinux 在哪里存储描述将哪个 SELinux 上下文分配给文件的定义。

我们还学习了如何使用semanage工具来操作这些信息,并使用几个利用这些信息来强制资源上下文的工具。

在进程级别上,我们初次接触了 SELinux 策略,识别进程何时在特定的 SELinux 域内启动。我们使用sesearchseinfo应用程序来查询 SELinux 策略。最后,我们看了一些限制应用程序转换范围的 Linux 安全实现,这也影响 SELinux 域转换。

在下一章中,我们将通过 SELinux 与网络相关的功能扩展我们的保护操作系统的知识。

问题

  1. Linux 工具用于显示或明确设置 SELinux 上下文的最常见选项是什么?

  2. 文件或目录的 SELinux 上下文是如何存储在系统中的?

  3. 为什么不建议使用chcon来持久化 SELinux 上下文更改?

  4. 使用semanage fcontext命令定义上下文的顺序重要吗?

  5. 如何在文件系统上重新标记文件?

  6. 在一个域可以转换到另一个域之前,该域需要什么特权?

  7. SELinux 策略如何将多种类型捆绑在一起以便于策略开发?

第五章:控制网络通信

SELinux 的强制访问控制远远超出了文件和进程访问控制的范围。SELinux 提供的一个功能是控制网络通信的能力。默认情况下,一般的网络访问控制使用基于套接字的访问控制机制,但也可以采取更详细的方法。

本章将介绍 SELinux 如何控制网络访问,讲解管理员如何利用iptables进一步加强网络通信安全,并描述如何通过标记的 IPsec 使用 SELinux 策略来实现跨系统安全。最后,我们将介绍 CIPSO 标记及其与 SELinux 的集成。

本章将讨论以下主题:

  • 控制进程通信

  • Linux 防火墙和 SECMARK 支持

  • 保护高速 InfiniBand 网络

  • 理解带标记的网络

  • 使用带标记的 IPsec 与 SELinux

  • 支持 NetLabel 和 SELinux 的 CIPSO

技术要求

本章并非所有部分都适用于所有环境。例如,InfiniBand 支持需要 InfiniBand 硬件,而 NetLabel/CIPSO 支持则需要整个网络都支持 CIPSO(或者在 IPv6 的情况下支持 CALIPSO)协议,才能使主机之间能够相互通信。

查看以下视频,了解代码的实际应用:bit.ly/34bVDdm

控制进程通信

Linux 应用程序可以直接或通过网络进行通信。但从应用程序开发者的角度来看,直接通信和网络通信之间的区别并不总是那么显著。让我们来看一下 Linux 支持的各种通信方式,以及 SELinux 如何与它们对接。

使用共享内存

最不类似网络的方法是使用共享内存。应用程序可以相互共享某些内存部分,并利用这些共享段在两个(或更多)进程之间进行通信。为了管理对共享内存的访问,应用程序开发者可以使用互斥量mutexes)或信号量。信号量是一个原子递增或递减的整数(确保两个应用程序不会在不知情的情况下覆盖彼此的值),而互斥量可以理解为一种特殊的信号量,只取 0 或 1 的值。

在 Linux 中,共享内存访问和控制有两种实现方式:SysV 风格和 POSIX 风格。我们不会过多讨论各自的优缺点,而是重点探讨 SELinux 如何管理这些实现的访问控制。

SELinux 通过特定的类别控制 SysV 风格的原语:sem用于信号量,shm用于共享内存。信号量、互斥量和共享内存段继承创建它们的第一个进程的上下文。

想要控制 SysV 风格原语的管理员可以使用各种 ipc* 命令:ipcs(列出)、ipcrm(删除)和 ipcmk(创建)。

例如,首先列出资源,然后删除已列出的共享内存:

# ipcs
...
------ Shared Memory Segments ------
key		shmid	owner		perms	bytes	nattch	status
0x0052e2c1	0	postgres	600	56	6
# ipcrm -m 0

当使用 POSIX 风格的信号量、互斥量和共享内存段时,SELinux 通过基于文件的访问控制来管理这些操作。POSIX 风格的方法使用位于 /dev/shm 中的常规文件,这使得管理员能够更简单地进行控制和管理。

通过管道进行本地通信

操作系统中第二大类通信方法是使用管道。顾名思义,管道通常是单向通信通道,信息从一个(或多个)发送者流向一个接收者(也有例外,比如 Solaris 管道,它作为双向通道,但 Linux 不支持)。管道的另一个名字是先进先出FIFO)。

在 Linux 中有两种类型的管道:匿名管道(也称为无名管道)和命名管道。它们的区别在于,命名管道使用常规文件系统中的文件作为标识,而匿名管道则通过应用程序构建,没有在常规文件系统中表示。

在这两种情况下,SELinux 会将管道视为 fifo_file 类别的文件。命名管道将与常规文件系统中的路径相关联,并通过 mknodmkfifo 命令创建(或者通过应用程序中的 mkfifo() 函数创建)。然而,匿名管道将作为 pipefs 文件系统的一部分显示。这个文件系统是一个伪文件系统,用户无法访问,但仍然通过 Linux 的虚拟文件系统VFS)抽象表示为一个文件系统。

从 SELinux 策略的角度来看,FIFO 文件是访问控制应用的目标:两个域如果拥有对 FIFO 文件上下文的正确权限集,就能够相互通信。

管理员可以通过诸如 lsof 等工具,或者通过查询 /proc 文件系统(作为 /proc/<pid>/fd 列表的一部分),来查找哪些进程正在通过 FIFOs 与其他进程进行通信。lsof 工具支持 -Z 选项来显示进程的 SELinux 上下文,甚至支持通配符:

# lsof -Z *:postfix_*

在这个例子中,lsof 显示了使用 postfix_* 标签的所有进程的信息。

在 UNIX 域套接字上进行通信

由于管道仅支持单向通信,两个进程之间的任何对话都需要两个管道。此外,使用管道进行真正的客户端/服务器式通信是具有挑战性的。为了实现更复杂的通信流,进程将使用套接字。

大多数管理员都知道,TCP 和 UDP 通信是通过套接字进行的。应用程序可以绑定到套接字并监听传入的通信,或者使用套接字连接到其他远程服务。但即使在单一的 Linux 系统上,套接字也可以用来促进通信流的传递。可以用于进程通信的有两种套接字类型:UNIX 域套接字和 netlink 套接字。ioctl() 系统调用的使用。UNIX 域套接字则更高级,并且管理员可以更直接地访问它们,这也是我们在这里更详细地解释它们的原因。

我们可以像管道一样区分两种 UNIX 域套接字定义:未命名套接字和命名套接字。就像管道一样,它们的区别在于用来识别套接字的路径。命名套接字是在常规文件系统中创建的,而未命名套接字是 sockfs 伪文件系统的一部分。类似地,套接字可以通过诸如 lsof 等工具查询,或者通过 /proc/<pid>/fd 列表查看。

关于 UNIX 域套接字,还有一个区别,即 UNIX 域套接字允许的通信格式。UNIX 域套接字可以创建为 数据报套接字(发送到套接字的数据保持其块大小和格式)或 流套接字(发送到套接字的数据可以以不同大小的块进行读取)。这对 SELinux 策略规则有一定影响。

对于 SELinux,通过 UNIX 域套接字进行通信要求两个域具有适当的通信权限,具体取决于通信的方向,对套接字文件类型的权限(openreadwrite)。

此外,发送(客户端)域需要额外的权限,以便对接收(服务器)域进行访问:

  • 在流套接字情况下,unix_stream_socket 类中的 connectto 权限

  • 在数据报套接字情况下,unix_dgram_socket 类中的 sendto 权限

如你所见,权限取决于通过套接字使用的通信类型。

另一个可以用于进程通信的套接字类型是 netlink。Netlink 套接字是允许用户空间应用程序与内核进程通信并交互的套接字,并且在特殊情况下(当网络管理被 Linux 内核委托给用户空间进程时),它们还可以与另一个用户空间应用程序通信。与常规的 UNIX 域套接字不同,后者的目标上下文与该套接字的拥有者相关联,netlink 套接字始终与 SELinux 上下文本地相关。

换句话说,当一个像 sysadm_t 这样的域想要操作内核的路由信息时,它将通过 netlink 路由套接字与内核进行通信,这个套接字通过 netlink_route_socket 类来标识:

$ sesearch -s sysadm_t -t sysadm_t -c netlink_route_socket -A
allow sysadm_t domain:netlink_route_socket getattr;
allow sysadm_t sysadm_t:netlink_route_socket { append bind ... };

随着应用程序功能的增加,可能会出现某些功能不再被当前 SELinux 策略允许的情况。管理员需要更新 SELinux 策略以允许 netlink 通信。

可以从手册页(man netlink)中的 netlink 信息中概括出支持的 netlink 套接字,从中可以轻松推导出 SELinux 类。例如,NETLINK_XFRM 套接字通过 SELinux 的 netlink_xfrm_socket 类获得支持。

处理 TCP、UDP 和 SCTP 套接字

当我们进一步深入时,我们将查看通过网络进行的套接字通信。在这种情况下,通信不是直接在进程之间进行的(因此在 Linux 术语中是 SELinux 域之间的通信),而是通过 TCP、UDP 和 流控制传输协议 (SCTP) 套接字进行的。

SELinux 也会为这些端口分配类型,这些类型随后会用于套接字通信。对于 SELinux,连接到 DNS 端口的客户端应用程序(TCP 端口 53,在大多数 SELinux 策略中接收 dns_port_t 类型)在 tcp_socket 类中使用 name_connect 权限与端口类型进行通信。SCTP 协议(使用 sctp_socket 类)也使用相同的权限。对于 UDP 服务(因此是 udp_socket 类),name_connect 不被使用。守护进程应用程序使用 name_bind 权限将自己绑定到其关联的端口。

重要说明

SELinux 仅在最近才引入对 SCTP 的支持,并且并非所有 Linux 发行版都已相应更新其策略。要查看是否启用了 SCTP 支持,可以检查 /sys/fs/selinux/policy_capabilities/extended_socket_class 文件的值。值为 1 表示策略已包含 SCTP 支持,而值为 0(或文件不存在)表示系统尚不支持 SCTP。

管理员可以微调为每个 TCP、UDP 或 SCTP 端口分配哪个标签。为此,可以使用 semanage port 命令。例如,要列出当前的端口定义,可以使用以下命令:

# semanage port -l
SELinux Port Type	Proto	Port Number
afs3_callback_port_t	tcp	7001
...
http_port_t		tcp	80, 81, 443, 488, 8008, 8009, ...

在此示例中,我们看到 http_port_t 标签已分配给一组 TCP 端口。可以绑定到 http_port_t 的 Web 服务器域因此可以绑定到任何提到的端口。

为了允许守护进程(如 SSH 服务器)绑定到其他(或附加的)端口,我们需要告诉 SELinux 将该端口映射到适当的标签。例如,要允许 SSH 服务器绑定到端口 10122,我们首先检查该端口是否已经有专用标签。这可以通过 sepolicy 命令来完成:

$ sepolicy network -p 10122
10122: udp unreserved_port_t 1024-32767
10122: tcp unreserved_port_t 1024-32767
10122: sctp unreserved_port_t 1024-32767

unreserved_port_t 标签不是专用标签,因此我们可以将 ssh_port_t 标签分配给它:

# semanage port -a -t ssh_port_t -p tcp 10122

删除端口定义的过程类似:

# semanage port -d -t ssh_port_t -p tcp 10122

当特定端口类型已经分配时,实用程序将显示以下错误:

# semanage port -a -t ssh_port_t -p tcp 80
ValueError: Port tcp/80 already defined

如果是这种情况且无法使用另一个端口,则除了修改 SELinux 策略外别无选择。

列出连接上下文

许多管理员工具可以显示安全上下文信息。与核心工具一样,大多数这些工具使用-Z选项来实现这一功能。例如,要列出当前正在运行的网络绑定服务,可以使用 netstat:

# netstat -naptZ | grep ':80'
tcp  0  0 0.0.0.0:80  0.0.0.0:* LISTEN 17655/nginx: master system_u:system_r:httpd_t:s0

即使是lsof也可以在被请求时显示上下文信息:

# lsof -i :80 -Z
COMMAND PID   SECURITY-CONTEXT             USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx   17655 system_u:system_r:httpd_t:s0 root 8u IPv4 31230  0t0      *:http (LISTEN)

另一个用于查询连接的高级命令是ss命令。仅运行ss命令会显示当前系统的所有连接。当添加-Z时,它还会添加上下文信息。

例如,以下命令查询监听的 TCP 服务:

# ss -ltnZ

也可以调用更高级的查询命令——有关更多信息,请查阅ss手册页。

注意

使用-Z选项来显示 SELinux 上下文信息,或者在用户请求的活动中考虑 SELinux 上下文信息,是应用程序开发人员普遍但非强制的做法。建议检查应用程序的手册页,以确认工具是否以及如何支持 SELinux。例如,要查看ss手册页,可以运行man ss

所有这些交互方式本质上仍然非常原始,最后一组(专注于套接字的部分)与网络关系更密切。可一旦我们查看系统之间的交互时,单凭套接字可能无法提供足够的控制。为了实现更精细的控制,接下来我们将研究防火墙功能及其 SECMARK 支持。

Linux 防火墙和 SECMARK 支持

使用 TCP、UDP 和 SCTP 端口的做法有一些缺点。其中之一是 SELinux 无法了解目标主机,因此无法推测其安全属性。这种方法也无法限制守护进程仅绑定到特定接口:在多网卡情况下,我们可能希望确保守护进程仅绑定到面向内部网络的接口,而不是面向互联网的接口,反之亦然。

过去,SELinux 通过接口节点标签来支持这个绑定问题:一个域可以配置为仅绑定到一个接口,而不绑定到任何其他接口,甚至可以绑定到一个特定的地址(称为节点)。然而,这种支持存在缺陷,且已被大多数情况弃用,转而支持 SECMARK 过滤。

在解释 SECMARK 以及管理员如何控制它之前,首先简要了解一下 Linux 的 netfilter 子系统,它是 Linux 系统上本地防火墙功能的事实标准。

介绍 netfilter

像 LSM 一样,Linux 的 netfilter 子系统在其网络栈处理框架的不同阶段提供了钩子,模块可以在这些阶段中实现。例如,ip_tables(使用iptables命令作为其控制应用程序)是这些模块之一,而ip6_tablesebtables是 netfilter 模块的其他例子。实现 netfilter 钩子的模块必须向 netfilter 框架报告该钩子的优先级。这使得模块的执行顺序可以控制(因为多个调用相同钩子的操作会一起使用)。

ip_tables框架是我们将在本书中详细讨论的框架,因为它支持 SECMARK 方法。这个框架通常被称为iptables,它是其控制应用程序的名称。我们将在本书剩余部分使用这个术语。

iptables提供了多个,这些表是基于功能的网络处理分类。常见的表如下:

  • filter表启用标准的网络过滤功能。

  • nat表用于修改数据包中的路由相关信息,例如源地址和/或目标地址。

  • mangle表用于修改数据包的大部分字段。

  • raw表在管理员希望将某些数据包/流量从 netfilter 的连接跟踪功能中排除时启用。

  • security表允许管理员在常规处理完成后对数据包进行标记。

在每个表中,iptables提供了一组默认的链。这些默认链指定了规则处理的处理流位置(也即在 netfilter 框架中的哪个钩子)。每个链都有一个默认策略——如果链中的规则没有匹配,返回的默认值是什么。在链中,管理员可以添加多个规则以按顺序处理。当规则匹配时,配置的动作将被执行。这个动作可以是允许数据包通过 netfilter 框架中的这个钩子,拒绝数据包,或者执行额外的处理。

常见的链(并非所有表都提供所有链)包括以下几种:

  • PREROUTING链是数据包接收到后的第一个数据包处理步骤。

  • INPUT链用于处理目标是本地系统的数据包。

  • FORWARD链用于处理目标是转发到另一个远程系统的数据包。

  • OUTPUT链用于处理来自本地系统的数据包。

  • POSTROUTING链是数据包发送前的最后一个数据包处理步骤。

简化地说,这些表及其链的实现大致与 netfilter 框架内调用的优先级相关。链容易与 netfilter 框架提供的钩子相关联,而表则告诉 netfilter 哪个链实现应当先执行。

实现安全标记。

通过数据包标记,我们可以使用 iptables(和 ip6tables)的过滤功能为数据包和连接分配标签。其思想是本地防火墙为数据包和连接打标签,然后内核使用 SELinux 授予(或拒绝)应用程序域使用这些带标签的数据包和连接的权限。

此数据包标签称为 manglesecurity。目前只有这些表在其规则集中具有标记数据包和连接操作的功能:

  • mangle 表比大多数其他表具有更高的执行优先级。在这个层次上实施 SECMARK 规则通常是当所有数据包都需要被标记时,即使这些数据包中的许多最终将被丢弃。

  • security 表在 filter 表之后具有执行优先级。这允许先执行常规防火墙规则,仅对常规防火墙允许的数据包打标签。使用 security 表允许 filter 表首先执行自主访问控制规则,并且只有在成功执行 DAC 规则后,SELinux 才执行其强制访问控制逻辑。

一旦 SECMARK 操作触发,它将为数据包或通信分配一个数据包类型。然后 SELinux 策略规则将验证是否允许域接收(recv)或发送给定类型的数据包。例如,Firefox 应用程序(运行在 mozilla_t 域中)将被允许发送和接收 HTTP 客户端数据包:

allow mozilla_t http_client_packet_t : packet { send recv };

SECMARK 相关数据包的另一个支持的权限集是 forward_inforward_out。在 netfilter 中使用转发时会检查这些权限。

需要注意的一点是,一旦定义了 SECMARK 操作,那么最终到达操作系统应用程序的所有数据包都将与一个标签相关联 — 即使内核正在检查的数据包或连接没有 SECMARK 规则。如果发生这种情况,内核会应用默认的 unlabeled_t 标签。一些发行版(如 CentOS)中默认实施的 SELinux 策略允许所有域发送和接收 unlabeled_t 数据包,但并非所有 Linux 发行版都是如此。

为数据包分配标签

当 netfilter 子系统中没有加载任何 SECMARK 相关规则时,SECMARK 就未启用,并且不检查与 SECMARK 权限相关的任何 SELinux 规则。网络数据包没有标签,因此不能应用任何强制控制。当然,仍然适用于常规套接字相关的访问控制 — SECMARK 只是一种额外的控制措施。

一旦启用单个 SECMARK 规则,SELinux 就会对所有数据包执行包标签机制。这意味着现在所有的网络数据包都需要有一个标签(因为 SELinux 只能处理有标签的资源)。数据包的默认标签(初始安全上下文)是 unlabeled_t,这意味着没有匹配此网络数据包的标记规则。

由于现在执行了 SECMARK 规则,SELinux 检查所有与网络数据包交互的域,以查看它们是否被授权发送或接收这些数据包。为了简化管理,一些发行版允许所有域对unlabeled_t数据包授予发送和接收权限。如果没有这些规则,一旦启用单个 SECMARK 规则,所有网络服务将无法正常工作。

要为数据包分配标签,我们需要定义一组与特定网络流匹配的规则,然后调用 SECMARK 逻辑(为数据包或通信标记一个标签)。大多数规则将立即匹配ACCEPT目标,以允许此特定通信到达系统。

让我们实现两个规则:

  • 第一个规则是允许与网站(端口80)的通信,并将相关的网络数据包标记为http_client_packet_t类型(以便 Web 浏览器可以发送和接收这些数据包)。

  • 第二个规则是允许与本地运行的 Web 服务器(端口80)的通信,并将其相关的网络数据包标记为http_server_packet_t类型(以便 Web 服务器可以发送和接收这些数据包)。

对于每一组规则,我们还启用连接跟踪功能,以便相关数据包能够自动正确标记并通过。

使用以下命令处理 Web 服务器流量:

# iptables -t filter -A INPUT -m conntrack --ctstate 
ESTABLISHED,RELATED -j ACCEPT
# iptables -t filter -A INPUT -p tcp -d 192.168.100.15 --dport 
80 -j ACCEPT
# iptables -t security -A INPUT -p tcp --dport 80 -j SECMARK 
--selctx "system_u:object_r:http_server_packet_t:s0"
# iptables -t security -A INPUT -p tcp --dport 80 -j 
CONNSECMARK --save

使用以下命令处理浏览器流量:

# iptables -t filter -A OUTPUT -m conntrack --ctstate 
ESTABLISHED -j ACCEPT
# iptables -t filter -A OUTPUT -p tcp --dport 80 -j ACCEPT
# iptables -t security -A OUTPUT -p tcp --dport 80 -j SECMARK 
--selctx "system_u:object_r:http_client_packet_t:s0"
# iptables -t security -A OUTPUT -p tcp --dport 80 -j 
CONNSECMARK --save

最后,要将连接标签复制到已建立和相关的数据包,请使用以下命令:

# iptables -t security -A INPUT -m state --state 
ESTABLISHED,RELATED -j CONNSECMARK --restore
# iptables -t security -A OUTPUT -m state --state 
ESTABLISHED,RELATED -j CONNSECMARK --restore

即便是这个简单的例子也表明,防火墙规则的定义本身就是一门艺术,而 SECMARK 标签只是其中的一小部分。然而,使用 SECMARK 规则可以允许某些流量,同时确保只有明确的域才能与该流量进行交互。例如,它可以在自助服务终端系统上实现,只允许一个浏览器与互联网通信,而所有其他浏览器和命令都无法通信。将所有与浏览相关的流量标记为特定标签,并只允许该浏览器域在该标签上具有sendrecv权限。

过渡到 nftables

尽管iptables仍然是 Linux 上最广泛使用的防火墙技术之一,但另外两个竞争者(nftablesbpfilter)在流行度上正迅速上升。首先是nftables,它在某些操作上比iptables有一些优势,同时仍然专注于 Linux 内核中的 netfilter 支持:

  • nftables的代码库及其 Linux 内核支持更加精简。

  • 错误报告更为完善。

  • 过滤规则可以逐步更改,而不需要重新加载所有规则。

nftables框架最近已获得 SECMARK 支持,让我们看看如何将http_server_packet_thttp_client_packet_t标签应用于相应的流量。

应用稍大一些的nftables规则最常见的方法是使用配置文件,并设置nft解释器:

#!/usr/sbin/nft -f
flush ruleset
table inet filter {
  secmark http_server {
    "system_u:object_r:http_server_packet_t:s0"
  }
  secmark http_client {
    "system_u:object_r:http_client_packet_t:s0"
  }
  map secmapping_in {
    type inet_service : secmark
    elements = { 80 : "http_server" }
  }
  map secmapping_out {
    type inet_service : secmark
    elements = { 80 : "http_client" }
  }
  chain input {
    type filter hook input priority 0;
    ct state new meta secmark set tcp dport map @secmapping_in
    ct state new ct secmark set meta secmark
    ct state established,related meta secmark set ct secmark
  }
  chain output {
    type filter hook output priority 0;
    ct state new meta secmark set tcp dport map @secmapping_out
    ct state new ct secmark set meta secmark
    ct state established,related meta secmark set ct secmark
  }
}

nftables使用的语法与iptables比较时是可以识别的。脚本首先定义 SECMARK 值。之后,我们创建一个端口(例如示例中的80)与用于 SECMARK 支持的值之间的映射。当然,已经建立的会话也会收到适当的 SECMARK 标签。

如果我们定义多个条目,elements变量使用逗号分隔不同的值:

elements = { 53 : "dns_client" , 80 : "http_client" , 443 : "http_client" }

除了nftables之外,另一个获得关注的防火墙解决方案是 eBPF,接下来我们将介绍它。

评估 eBPF

eBPF(以及bpfilter命令)本质上与iptablesnftables完全不同,因此在介绍 eBPF 的 SELinux 支持细节之前,先了解一下 eBPF 的工作原理。

理解 eBPF 的工作原理

扩展伯克利数据包过滤器eBPF)是一个框架,使用内核中的虚拟机来解释和执行 eBPF 代码,这些代码包括类似于处理器指令集操作的低级指令。由于它是非常低级的,且与处理器无关的语言,它可以用来创建非常快速且高度优化的规则。

BPF 最初用于分析和过滤网络流量(例如,在tcpdump中)。由于其高效性,它很快被发现应用于其他工具,超出了简单的网络过滤和分析功能。随着 BPF 扩展到其他用例,它被称为扩展 BPF,或者 eBPF。

Linux 内核中的 eBPF 框架已成功用于性能监控,其中 eBPF 应用程序接入运行时进程和内核子系统,测量性能并将指标反馈给用户空间应用程序。它当然还支持对(网络)套接字、cgroups、进程调度等的过滤,且支持的功能列表正在迅速增长。

与使用钩子接入系统调用和其他安全敏感操作的 LSM 框架一样,eBPF 也接入了 Linux 内核。它偶尔可以使用现有的钩子(如 Linux 的内核探针kprobes框架),从而受益于这些接口的稳定性。因此,我们可以预期 eBPF 将在 Linux 内核的其他领域进一步扩展其支持。

eBPF 应用程序(eBPF 程序)是在用户空间定义的,然后提交到 Linux 内核。内核验证代码的安全性和一致性,以确保虚拟机不会试图突破它所工作领域的边界。如果通过验证(可能会稍微修改代码,因为 Linux 内核有些操作会修改 eBPF 代码以适应环境或安全规则),eBPF 程序会在 Linux 内核中运行(在其虚拟机内),并执行其预定的任务。

注意

Linux 内核可以将 eBPF 指令编译成本地的、特定于处理器的指令,而不是让虚拟机来解释它们。然而,由于这会带来更高的安全风险,因此将 /proc/sys/net/core/bpf_jit_enable 设置为 1

这些程序可以在内存中加载和保存信息,称为映射。这些 eBPF 映射 可以被用户空间应用程序读取或写入,从而提供与运行中的 eBPF 程序交互的主要接口。这些映射通过文件描述符进行访问,允许进程根据需要传递和克隆这些文件描述符。

各种产品和项目正在使用 eBPF 创建高性能网络功能,例如软件定义网络配置、DDoS 缓解规则、负载均衡器等。与基于 netfilter 的防火墙不同,后者依赖于内核中的庞大代码库,并通过配置进行调优,eBPF 程序是专门为其目的构建的,仅此而已,且只有这些代码在积极运行。

保护 eBPF 程序和映射

对于 eBPF 程序和映射,默认的安全措施非常有限,部分原因是 Linux 内核验证器(在将 eBPF 代码传递给虚拟机之前验证代码)被高度信任,部分原因是只有在相关进程具有 CAP_SYS_ADMIN 能力时,才允许加载 eBPF 代码。由于该能力基本上意味着完全的系统访问,因此没有认为有必要进行额外的安全控制。

自 Linux 内核 4.4 起,一些类型的 eBPF 程序(例如套接字过滤)即使由非特权进程加载也可以运行(但当然,只能针对这些进程有权限访问的套接字)。系统允许加载与 cgroup CAP_NET_ADMIN 能力相关的程序。最近,加载 eBPF 程序的权限已被添加到 CAP_BPFCAP_TRACING 能力中,尽管并非所有 Linux 发行版都已经提供支持这些能力的内核。但希望对 eBPF 进行更精细控制的 Linux 管理员,可以使用 SELinux 来调节和调整 eBPF 的处理。

SELinux 有一个 bpf 类,用于管理基本的 eBPF 操作:prog_loadprog_runmap_createmap_readmap_write。每当一个进程创建一个程序或映射时,该程序或映射将继承该进程的 SELinux 标签。如果这些映射或程序的文件描述符被泄露,恶意应用程序仍然需要拥有对该标签的必要权限,才能利用它。

用户空间操作可以通过 /sys/fs/bpf 虚拟文件系统与 eBPF 框架交互,因此一些 Linux 发行版将特定的 SELinux 标签(bpf_t)与此位置关联。这允许管理员通过 SELinux 策略规则管理对这种类型的访问。

尽管 eBPF 极具扩展性,但目前围绕它的简化框架数量较少,因为它仍处于非常早期的阶段。然而,我们可以预期,随着一个名为 bpfilter 的新工具展示了基于 eBPF 的防火墙功能,更多复杂的支持会很快到来。

使用 bpfilter 过滤流量

bpfilter 应用程序是一种构建新的 eBPF 程序以过滤和处理流量的应用程序。它允许管理员在不理解低级 eBPF 指令的情况下构建防火墙功能,并且最近开始支持 iptables:管理员通过 iptables 创建规则,而 bpfilter 将这些规则转换并转化为 eBPF 程序。

重要提示

虽然 bpfilter 是 Linux 内核树的一部分,但目前应视为一种价值证明,而非生产就绪的防火墙功能。

bpfilter 创建的 eBPF 程序会在 Linux 内核中网络设备驱动程序和 TCP/IP 栈之间的一个层次中钩住,称为 eXpress Data Path (XDP) 层。在这一层,eBPF 程序可以访问完整的网络数据包信息(包括如以太网等链路层协议)。

要使用 bpfilter,需要在 Linux 内核中启用适当的设置,包括 CONFIG_BPFILTERCONFIG_BPFILTER_UMH。后者是 bpfilter 用户模式助手,它将捕获由 iptables 生成的防火墙规则,并将这些规则转换为 eBPF 应用程序。

在加载 bpfilter 用户模式助手之前,我们需要在 SELinux 中允许 execmem 权限:

# setsebool allow_execmem on

接下来,加载 bpfilter 模块,这将使用户模式助手在系统中处于激活状态:

# modprobe bpfilter
# dmesg | tail
...
bpfilter: Loaded bpfilter_umh pid 2109

现在,使用前面列出的命令加载 iptables 防火墙。这些指令会被转换成 eBPF 程序,如 bpftool 所示:

# bpftool p
1: xdp  tag 8ec94a061de28c09 dev ens3
        loaded_at Apr 25/23:19  uid:0
        xlated 533B  jited 943B  memlock 4096B

eBPF 代码本身也可以显示,但目前对于管理员来说几乎难以阅读。

上述所有防火墙功能都与 Linux 内核中支持的 TCP/IP 栈交互。然而,也有一些网络并不依赖于 TCP/IP,例如 InfiniBand。幸运的是,即使在这些更专业的网络环境中,SELinux 也可以用于控制通信流。

保护高速 InfiniBand 网络

InfiniBand 标准是一项相对较新的(在网络历史上)技术,能够实现非常高的吞吐量和非常低的延迟。它通过在网络层(协议)上具有非常低的开销,并且允许用户应用程序直接访问网络层来实现这一点。这种直接访问也对 SELinux 有影响,因为 Linux 内核不再积极参与通过 InfiniBand 链路传输数据。

让我们首先了解 InfiniBand 的样子,然后再看看如何继续对其通信流应用 SELinux 控制。

直接访问内存

InfiniBand 的一个主要前提是允许用户应用程序直接访问网络。InfiniBand 本身是一个流行的远程直接内存访问RDMA)实现,并且得到了供应商的广泛支持。我们发现 RDMA 在高性能集群中被广泛使用。

由于是直接访问,控制措施只能在设置访问方式时实施。如果没有 SELinux,设置和管理 InfiniBand 通信所需要做的仅仅是访问设备文件本身。如果一个进程能够写入 InfiniBand 设备,那么它就能使用 InfiniBand。默认情况下,这些设备只有 root 用户可以访问。

InfiniBand 设备是网络卡或主机通道适配器HCA),并且可以有多个端口。InfiniBand 端口是连接到 InfiniBand 子网的链路或接口。子网是多个机器(端口)连接的高速网络。与常规网络一样,InfiniBand 交换机用于促进子网之间的通信,路由器则用于连接不同的子网。

InfiniBand 子网由子网管理器SM)管理。这是一个协调子网内不同端口以及分区管理的进程。InfiniBand 中的分区是一种区分子网内不同通信的方式,类似于常规网络中的虚拟局域网VLAN)。在分区通信中,子网管理器指定哪些端口可以用于哪个通信分区。

保护 InfiniBand 网络

与常规网络不同,常规网络中防火墙和交换机级别的访问控制是防止未经授权访问的常见手段,而 InfiniBand 的保护措施很少。InfiniBand 在很大程度上假设网络处于一个受信任的环境中。然而,这并不排除我们在 SELinux 中对哪些进程可以访问 InfiniBand 网络进行更严格的控制。

由于通信流本身直接映射到设备的内存中,Linux 内核没有可用于像常规 TCP/UDP 流量那样进行数据包级控制的钩子(例如使用 SECMARK 功能),甚至没有用于套接字的会话级控制。相反,SELinux 集中于以下两个主要控制,如下图所示:

图 5.1 – SELinux InfiniBand 控制

图 5.1 – SELinux InfiniBand 控制

这两个主要控制措施如下:

  • 控制谁可以管理 InfiniBand 子网

  • 控制谁可以访问 InfiniBand 分区

为了正确管理这些控制,semanage 应用程序会将正确的类型分配给适当的 InfiniBand 资源。然而,并非所有 SELinux 策略都包含适当的类型,因此我们还需要将这些类型添加进来。

管理 InfiniBand 子网

我们从管理 InfiniBand 网络开始。在 Linux 中使用 InfiniBand,通常通过opensm应用程序来实现。许多 InfiniBand 适配器有多个端口,允许服务器参与多个 InfiniBand 子网。通过 SELinux,我们可以控制哪个域可以管理子网,通过控制对设备上 InfiniBand 端口的访问。

首先,我们需要为与子网关联的 InfiniBand 端口分配一个标签。为了实现这一点,我们首先需要获取正确的 InfiniBand 设备,创建合适的标签(类型),然后将其分配给端口。

我们可以通过使用ibv_devinfo命令来查询系统中可用的 InfiniBand 设备:

# ibv_devinfo
hca_id: rxe0
  transport:	InfiniBand (0)
  fw_ver:		0.0.0
  ...
  phys_port_cnt:
    port:	1
		state:	PORT_ACTIVE (4)
		...

接下来,我们创建一个类型(标签)并将其分配给端口。此类型仅用于验证opensm应用程序对该端口的访问。我们使用 CIL 语言来实现这一点(我们将在第十六章使用 SELinux CIL 开发策略中详细说明)。创建一个文件,内容如下(我们称之为infiniband_subnet.cil):

(typeattribute ibendport_type)
(type local_ibendport_t)
(typeattributeset ibendport_type local_ibendport_t)
(allow opensm_t local_ibendport_t (infiniband_endport (manage_subnet)))

在前面的代码中,我们通过一个新的类型local_ibendport_t来增强 SELinux 策略,将其分配给ibendport_type属性,然后授予opensm_t域在infiniband_endport类中管理子网的权限。

让我们加载此策略增强:

# semodule -i infiniband_subnet.cil

最后,我们将新创建的类型分配给 InfiniBand 端口:

# semanage ibendport -a -t local_ibendport_t -z rxe0 1

此命令将local_ibendport_t类型分配给rxe0设备的端口号1(通过ibv_devinfo获取)。一旦映射完成,我们也可以使用semanage查询它:

# semanage ibendport -l
SELinux IB End Port Type	IB Device Name	Port Number
local_ibendport_t		rxe0			0x1

如果没有任何映射,命令将不会显示任何输出。

重要提示

目前,大多数 Linux 发行版尚未在 SELinux 策略中加入 InfiniBand 支持,因此我们需要创建自定义标签。我们可以预期,发行版将为 InfiniBand 资源添加默认类型,并且 SELinux 对 InfiniBand 的支持将随着合理的默认设置而扩展。

如果我们在启用了 SELinux 的系统上使用 InfiniBand 且没有任何端口映射,未标记类的初始安全上下文将作为该端口的标签, 即unlabeled_t。然而,不建议使用此标签,因为它通常用于未标记的资源。将任何权限授予unlabeled_t类型应仅限于具有高特权的进程,并且在使用时应仔细考虑,以确保日志解释和 SELinux 策略规则与 InfiniBand 资源的关系是清晰的(通过良好文档化的类型)。

控制对 InfiniBand 分区的访问

前一部分专注于允许管理应用程序opensm管理子网,本节将重点讨论如何限制对 InfiniBand 网络的访问,仅允许正确的域进行访问。如前所述,InfiniBand 子网可以通过使用 InfiniBand 分区进一步划分为多个网络。

最初,这些分区用于在流上实现服务质量QoS)或特定的带宽和性能要求。SM 定义了分区及其属性,应用程序使用分区键P_Key)来通知 InfiniBand 网络哪些通信必须在特定分区中进行。

SELinux 可以通过在 InfiniBand 子网和 P_Key 与 SELinux 类型之间创建映射来管理这些分区。然而,与子网管理一样,我们首先需要找到适当的详细信息并创建一个合适的 SELinux 类型,才能定义映射。

让我们从确定子网和分区的详细信息开始。这两个信息由opensm进行管理。如果您无法访问opensm配置文件,则需要从(InfiniBand)网络管理员处获取这些详细信息。

opensm分区配置(/etc/rdma/partitions.conf)中,子网和前缀可以如下找到:

# grep '=0x' /etc/rdma/partitions.conf
Default=0x7fff, rate=3, mtu=4, scope=2, defmember=full;
Default=0x7fff, ipoib, rate=3, mtu=4, scope=2;
rxe0_1=0x0610, rate=7, mtu=4, scope=2, defmember=full;
rxe0_1=0x0610, ipoib, rate=7, mtu=4, scope=2;

在此示例中,定义了两个分区。第一个是默认分区,需保持(0x7fff)。第二个分区,其键为0x0610,在rxe0设备和端口1上处于活动状态。我们将使用 SELinux 保护的正是这个第二个分区。

让我们创建一个新的类型,并将其分配给此分区。我们再次使用 CIL 格式定义策略增强,并将这些规则存储在名为infiniband_pkey.cil的文件中:

(typeattribute ibpkey_type)
(type local_ibpkey_t)
(typeattributeset ibpkey_type local_ibpkey_t)
(allow unconfined_t local_ibpkey_t (infiniband_pkey (access)))

在此示例中,我们创建了local_ibpkey_t类型,将其分配给ibpkey_type属性,并在infiniband_pkey类中授予了unconfined_t访问权限。

让我们加载策略:

# semodule -i infiniband_pkey.cil

我们现在可以为这个分区创建适当的映射,并将其限制在ff12::子网前缀内:

# semanage ibpkey -a -t local_ibpkey_t -x ff12:: 0x0610
# semanage ibpkey -l
SELinux IB PKey Type	Subnet_Prefix	Pkey Number
local_ibpkey_t		ff12::		0x610

虽然我们可以为每个分区创建单独的类型,但我们也可以使用 SELinux 范围来支持 SELinux 类别:

# semanage ibpkey -a -t local_ibpkey_t -r s0-s0:c0.c4 -x ff12:: 0x0610

使用类别,我们可以根据源域类别授予访问权限,这一点在我们接下来讨论的其他网络保护措施(如标记化网络)中也能受益。

理解标记化网络

进一步微调网络级访问控制的另一种方法是引入标记化网络。通过标记化网络,安全信息会在主机之间传递(与 SECMARK 不同,SECMARK 仅在 netfilter 子系统接收到数据包时才开始,并且其标记不会离开主机)。这也被称为对等标记,因为安全信息在主机(对等体)之间传递。

标记化网络的优点在于安全信息在网络中保持一致,允许在系统之间强制执行端到端的强制访问控制设置,同时保留系统之间通信流的敏感级别。然而,主要的缺点是,这需要一种额外的网络技术(协议),能够管理网络数据包或流的标签。

SELinux 当前支持两种实现方式,作为标签化网络方法的一部分:NetLabel 和标签化 IPsec。使用 NetLabel 时,存在两种实现方式:回退标签和 CIPSO。在这两种情况下,只有源域的敏感度会在通信过程中保留。标签化的 IPsec 支持传输整个安全上下文。

注意

NetLabel 实际上支持启用回环的完整标签支持。在这种情况下,完整标签(而不仅仅是敏感度和类别)会被传递。然而,这仅适用于通过回环接口的通信,因此不会离开当前主机。

很久以前,NetLabel/CIPSO 和标签化 IPsec 的支持合并为一个公共框架,这引入了 SELinux 中的三项额外权限检查:接口检查、节点检查和对等检查。这些权限检查仅在使用标签化流量时激活;如果没有标签化流量,这些检查会被简单忽略。

使用 NetLabel 进行回退标签

NetLabel 项目支持回退标签功能,管理员可以为来自或前往未使用标签化网络的网络位置的流量分配标签。通过使用回退标签,即使没有标签化的 IPsec 或 NetLabel/CIPSO,接下来几节提到的对等控制也能得以应用。

netlabelctl 命令用于控制 NetLabel 配置。让我们为所有来自 192.168.100.1 地址的流量创建一个回退标签分配:

# netlabelctl unlbl add interface:eth0 address:192.168.100.1 label:system_u:object_r:netlabel_peer_t:s0

要列出当前定义,请使用以下命令:

# netlabelctl -p unlbl list
Accept unlabeled packets : on
Configured NetLabel address mappings (1)
 interface: eth0
   address: 192.168.100.1/32
    label: "system_u:object_r:netlabel_peer_t:s0"

有了这个规则,标签化网络将被激活。任何来自 192.168.100.1 地址的流量都会被标记为 netlabel_peer_t:s0 标签,而所有其他流量则会被标记为(默认)unlabeled_t:s0 标签。当然,SELinux 策略必须允许所有域从 unlabeled_t 对等方或 netlabel_peer_t 对等方接收 recv 权限。

回退标签在支持标签化网络环境与非标签化网络混合使用时非常有用,这也是我们在此记录它的原因,先于描述各种标签化网络技术。

基于网络接口限制流量

接口检查的想法是,每个进入系统的包都会通过接口上的 ingress 检查,而每个离开系统的包都会通过 egress 检查。ingressegress 是涉及的 SELinux 权限,而接口则被赋予一个安全上下文。

可以使用 semanage 工具授予接口标签,特别适用于在 MLS 情况下为接口分配敏感级别,尽管也可以为接口分配不同的标签(但这需要对正在运行的 SELinux 策略进行更多调整,以保证系统能够正常工作):

# semanage interface -a -t netif_t -r s1-s1:c0.c128 eth0

与其他 semanage 命令类似,我们可以按如下方式查看当前的映射:

# semanage interface -l
SELinux Interface	Context
eth0				system_u:object_r:netif_t:s1-s1:c0.c128

请记住,对于传入通信,执行域是对等域。对于标记的 IPsec,这将是发起连接的客户端域,而在 NetLabel/CIPSO 中,这是关联的对等标签(例如 netlabel_peer_t)。

默认情况下,接口被标记为 netif_t,且没有敏感度约束。然而,这不会在 semanage interface -l 输出中显示,因为其默认输出为空。

接受来自选定主机的对等通信

SELinux 节点表示数据发送到(sendto)或接收自(recvfrom)的特定主机(或主机网络),并通过 SELinux 节点类进行处理。就像接口一样,这些可以通过 semanage 工具列出和定义。在以下示例中,我们将 10.0.0.0/8 网络标记为 node_t 类型,并为其关联一组类别:

# semanage node -a -t node_t -p ipv4 -M 255.255.255.255 -r s0-s0:c0.c128 192.168.100.1

我们还可以列出当前的定义:

# semanage node -l

类似于网络接口流,传入通信的执行域是对等标签。

默认情况下,节点被标记为node_t,且没有类别约束。然而,在semanage node -l的输出中,这不会显示,因为其默认输出为空。

验证对等流

最终检查是peer类检查。对于标记的 IPsec,这是发送数据的套接字的标签(例如mozilla_t)。然而,对于 NetLabel/CIPSO,对等标签将是静态的,基于源,因为 CIPSO 只能传递敏感度级别。NetLabel 中常见的标签是netlabel_peer_t

与接口和节点检查不同,对等检查将对等域作为目标,而不是源。

重要说明

在所有标记的网络使用案例中,拒绝列表中的进程与审计日志中显示的拒绝无关。这是因为拒绝是从内核子系统内部触发的,而不是通过用户进程调用触发的。因此,内核中断了一个不相关的进程以准备并记录拒绝,且该进程名称在拒绝事件中被使用。

最后,查看以下图表,它提供了这些各种控制及其适用级别的概览:

图 5.2 – 各种与网络相关的 SELinux 控制的示意图

图 5.2 – 各种与网络相关的 SELinux 控制的示意图

顶级控制在域级别处理(如httpd_t),而底层控制则在对等级别处理(如 netlabel_peer_t)。

使用旧式控制

大多数 Linux 发行版启用 network_peer_control 功能。这是 SELinux 子系统中的一种增强功能,利用前述的对等类来验证对等流。

然而,SELinux 策略可以选择回到之前的方式,在这种方式下,同伴之间的流量不再通过同伴类进行控制,而是使用 tcp_socket 类进行通信。在这种情况下,tcp_socket 类将会用于 peer 域,并且还会使用 recvfrom 权限(除了已有的 tcp_socket 权限之外)。

当前 network_peer_control 能力的值可以通过 SELinux 文件系统查询:

# cat /sys/fs/selinux/policy_capabilities/network_peer_controls
1

如果该值为 0,则前面提到的同伴控制将通过 tcp_socket 类而非同伴类进行处理。

SELinux 中的默认标签化网络控制不会传递任何进程上下文,而使用 NetLabel 的回退标签化通常用于系统既参与标签化网络又参与非标签化网络的环境。然而,还有一种更常见的网络实现,它不仅支持标签化网络,还能传递域上下文,并且不需要专门的环境:标签化 IPsec。

使用带标签的 IPsec 与 SELinux

尽管设置和维护一个 IPsec 配置远超本书的范围,但让我们看一个简单的 IPsec 示例,展示如何在系统上启用标签化 IPsec。记住,之前提到的接口、节点和同伴级别的标签化网络控制,都是在我们使用标签化 IPsec 时自动启用的。

在 IPsec 配置中,有三个重要的概念需要注意:

  • 安全策略数据库SPD)包含了内核在何时应通过 IP 策略处理通信的规则和信息(从而通过安全关联处理通信)。

  • 安全关联SA)是两个主机之间的单向通道,包含该通道的所有安全信息。当使用标签化 IPsec 时,它还包含了导致安全关联形成的客户端的上下文信息。

  • 安全关联数据库SAD)包含了各个安全关联。

在标签化 IPsec 配置中,安全关联不再仅仅通过源和目标地址进行索引,还包括源上下文。因此,参与标签化 IPsec 配置的 Linux 系统,在主机之间的单一通信流中,很容易就会有数十个 SA,因为每个 SA 现在还代表一个客户端域。

标签化 IPsec 通过 SELinux 引入了一些额外的访问控制:

  • SPD 中的每个条目都有一个上下文。想要获取 SA 的域需要拥有对该上下文的 polmatch 权限(这是 association 类的一部分)。此外,发起 SA 的域需要拥有对目标域的 setcontext 权限(也是 association 类的一部分)。

  • 只有授权的域才能修改 SPD,修改操作也受到 setcontext 权限的管理,但现在也要考虑 SPD 上下文条目。这一权限通常授予 IPsec 工具,例如 Libreswan 的 pluto(ipsec_t)。

  • 参与 IPsec 通信的域必须拥有与自身关联的 sendto 权限,并拥有与 peer 域关联的 recvfrom 权限。接收域还需要拥有来自 peer 类的 recv 权限,该权限与 peer 域相关联。

因此,尽管标记化 IPsec 无法决定 mozilla_t 是否能与 httpd_t 通信(因为 mozilla_t 只需要能够发送到其自身关联),但它可以控制 httpd_t 是否允许或拒绝来自 mozilla_t 的传入通信(因为它需要在 mozilla_t 关联上拥有 recvfrom 权限)。下图展示了这一复杂的权限博弈:

图 5.3 – 标记化 IPsec 的 SELinux 控制示例

图 5.3 – 标记化 IPsec 的 SELinux 控制示例

在接下来的示例中,我们将使用 Libreswan 工具设置两个主机之间的简单 IPsec 隧道。

设置常规 IPsec

配置 Libreswan 主要是配置 Libreswan 的主配置文件 (ipsec.conf)。大多数发行版会使用一个 include 目录(如 /etc/ipsec.d),管理员或应用程序可以将特定连接的设置放入该目录。通常,include 目录用于实际的 IPsec 配置,而一般的 ipsec.conf 文件则用于配置 Libreswan 的行为。

要创建主机到主机的连接,我们首先在两台主机上定义共享密钥。我们将连接命名为 rem1-rem2(因为这两台主机的主机名),因此共享密钥将存储为 /etc/ipsec.d/rem1-rem2.secrets

192.168.100.4 192.168.100.5 : PSK "somesharedkey"

接下来,我们在 /etc/ipsec.d/rem1-rem2.conf 中定义 VPN 连接,如下所示:

conn rem1-rem2
	left=192.168.100.4
	right=192.168.100.5
	auto=start
	authby=secret
	#labeled-ipsec=yes
	#policy-label=system_u:object_r:ipsec_spd_t:s0

启用标记化 IPsec 的设置暂时被注释掉,先测试不带此功能的 IPsec 连接。

在两台系统上启动 IPsec 服务:

# systemctl start ipsec

验证连接是否正常工作,例如,可以通过 tcpdump 检查网络流量,或通过 ip xfrm state 检查连接状态。

启用标记化 IPsec

要在 Libreswan 中使用标记化 IPsec,请取消注释 /etc/ipsec.d/rem1-rem2.conf 中的 labeled-ipsecpolicy-label 指令。重启 ipsec 服务,并重新尝试连接。

当一个应用程序尝试通过 IPsec 与远程域进行通信时,pluto(或任何其他支持标记 IPsec 的 Internet Key Exchange version 2IKEv2)客户端)将与对方交换必要的信息(包括上下文)。然后,双方将更新 SPD,并将相同的 安全策略信息SPI)与之关联。从那时起,发送方将把商定的 SPI 信息添加到 IPsec 数据包中,以便远程端可以立即重新关联正确的上下文。

这里的巨大优势是客户端和服务器的上下文,包括敏感度和类别,是同步的(它们并没有在每个数据包中实际发送,但在建立安全关联时会初步交换)。

在某些专业或高度安全的环境中,标记网络在网络内部得到支持。最常用的标记技术是 CIPSO,接下来我们将介绍其 SELinux 支持。

支持 NetLabel 和 SELinux 的 CIPSO

NetLabel/CIPSO 标签并在网络中传输敏感度。与标记的 IPsec 不同,其他上下文信息不会被发送或同步。因此,当我们考虑两个点之间的通信流时,它们将具有默认的、共同的 SELinux 类型(而不是与源或目标关联的 SELinux 类型),但将根据远程端的敏感度标签拥有敏感度标签。

NetLabel 配置的一部分是映射定义,它告知系统哪些通信流(来自选定接口,甚至是配置的 IP 地址)属于某个 Interpretation DomainDOI)。CIPSO 标准将 DOI 定义为一组对 CIPSO 标签进行类似解读的系统,或者在我们的情况下,使用相同的 SELinux 策略和敏感度标签配置。

一旦这些映射建立,NetLabel/CIPSO 将在主机之间传递敏感度信息(和类别)。我们在通信流中看到的上下文将是 netlabel_peer_t,这是分配给 NetLabel/CIPSO 源流量的默认上下文。

通过这种方法,我们可以启动具有敏感度范围的守护进程,从而仅接受具有正确安全许可的用户或客户端的连接,即使是在远程的 NetLabel/CIPSO 启用系统上。

配置 CIPSO 映射

拥有一个良好的 CIPSO 启用网络的初步要求是要对将使用的 DOI 及其后果有共同的理解。标记网络可以为特定目的使用不同的 DOI。

除了 DOI,我们还需要关注如何通过启用 CIPSO 的网络传递类别和敏感度。CIPSO 标签控制此设置,NetLabel 支持以下三个值:

  • 使用tags:1时,类别在 CIPSO 包中以位图的方式提供。这是最常见的方式,但将支持的类别数量限制为 240(从 0 到 239)。

  • 使用tags:2时,类别会被单独列举。这允许更广泛的类别范围(最多 65,543 个),但最多仅支持 15 个列举类别。当你有许多类别但每个范围只需要支持少数类别时,建议使用tags:2

  • 使用tags:5时,类别可以通过范围方式列出(最低和最高),最多可以有七对这样的低/高范围。

请注意,CIPSO 标签结果是在后台处理的:系统管理员只需配置 NetLabel 映射以使用所选标签值。

假设我们有两个启用了 CIPSO 的网络,10.1.0.0/16doi:1相关联,10.2.0.0/16doi:2相关联。两者都使用标签值1。首先,我们启用 CIPSO 并允许其传递 CIPSO 标记的包,DOI 设置为12。我们不进行任何翻译(因此,CIPSO 包上设置的类别和敏感度将由 SELinux 使用):

# netlabelctl cipsov4 add pass doi:1 tags:1
# netlabelctl cipsov4 add pass doi:2 tags:1

如果需要进行翻译(比如我们使用敏感度s0-s3,而 CIPSO 网络使用敏感度100-103),命令会像这样:

# netlabelctl cipsov4 add std doi:1 tags:1 
levels:0=100,1=101,2=102

接下来,我们实现映射规则,告诉 NetLabel 配置将哪个网络流量与doi:1doi:2相关联:

# netlabelctl map del default
# netlabelctl map add default address:10.1.0.0/16 protocol:cipsov4,1
# netlabelctl map add default address:10.2.0.0/16 protocol:cipsov4,2

要列出当前的映射,使用list选项:

# netlabelctl map list -p
Configured NetLabel domain mappings (2)
 domain: DEFAULT (IPv4)
   address: 10.1.0.0/16
    protocol: CIPSO, DOI = 1
 domain: DEFAULT (IPv4)
   address: 10.2.0.0/16
    protocol: CIPSO, DOI = 2

就是这样。我们移除了初始的默认映射(因为那样会阻止新默认映射的添加),然后配置了 NetLabel 来为给定网络标记合适的 CIPSO 配置流量。

添加特定领域的映射

NetLabel 还可以配置为确保给定的 SELinux 域使用定义良好的 DOI,而不是之前配置的默认 DOI。例如,要让 SSH 守护进程(运行在sshd_t域中)的网络流量被标记为 CIPSO doi:3,我们可以使用以下配置:

# netlabelctl cipsov4 add pass doi:3 tags:1
# netlabelctl map add domain:sshd_t protocol:cipsov4,3

映射规则甚至可以更加选择性。我们可以告诉 NetLabel,对于来自某个网络的 SSH 流量使用doi:2,对于来自另一个网络的 SSH 流量使用doi:3,甚至在来自任何其他网络时使用未标记的网络流量:

# netlabelctl map del domain:sshd_t protocol:cipsov4,3
# netlabelctl map add domain:sshd_t address:10.1.0.0/16 protocol:cipsov4,1
# netlabelctl map add domain:sshd_t address:10.4.0.0/16 protocol:cipsov4,3
# netlabelctl map add domain:sshd_t address:0.0.0.0/0 protocol:unlbl

NetLabel 框架会优先匹配最具体的规则,因此,只有在没有其他规则匹配时,0.0.0.0/0 才会被匹配。

使用本地 CIPSO 定义

如前所述,NetLabel 默认只传递敏感度和类别。然而,当使用本地(通过回环接口)CIPSO 时,可以使用完整的标签控制。当启用时,对默认的netlabel_peer_t类型将不应用对等控制,而是使用客户端或服务器域。

要使用本地 CIPSO 定义,首先声明本地使用的 DOI:

# netlabelctl cipsov4 add local doi:5

接下来,让本地通信使用定义的 DOI(在我们的示例中为5):

# netlabelctl map add default address:127.0.0.1 protocol:cipsov4,5

启用此功能后,本地通信将与doi:5关联,并使用本地映射,将完整标签传递给强制访问控制系统(SELinux)。

支持 IPv6 CALIPSO

CIPSO 是 IPv4 协议,但 IPv6 也有类似的框架,称为calipso而非cipsov4

与 CIPSO 在 NetLabel 中的实现相比,CALIPSO 有一些小的差异:

  • 仅支持一种标签类型(与 CIPSO 的三种标签类型不同)。因此,CALIPSO 管理员无需在任何地方指定tags:#

  • CALIPSO 仅使用直通模式。不支持翻译。

  • 当前,NetLabel CALIPSO 实现不支持本地模式,其中完整标签会被传递。

除了这些差异,CALIPSO 的使用与 CIPSO 非常相似。

概述

SELinux 默认使用基于通信原语的文件表示或所用套接字的访问控制。在 InfiniBand 网络中,访问控制仅限于访问 InfiniBand 端口和分区。对于 TCP、UDP 和 SCTP 端口,管理员可以通过semanage命令处理控制,而不需要更新 SELinux 策略。一旦进入基于网络的通信领域,可以通过 Linux netfilter 支持、更高级的通信控制来实现,通过 SECMARK 标签和对等标签进行控制。

在 SECMARK 标签的情况下,使用本地防火墙规则将上下文映射到数据包,然后通过 SELinux 策略进行管理。在对等标签的情况下,应用程序上下文本身(标记为 IPsec)或其敏感性级别(netfilter/CIPSO)标识访问控制所应用的资源。这允许通过 SELinux 策略实现几乎是应用到应用的网络流量控制。

我们了解到,最常见的防火墙框架(iptablesnftables)已经支持 SECMARK,而较新的基于 eBPF 的bpfilter应用程序尚未获得此支持。

在下一章中,我们将探讨如何利用常见的基础设施即代码框架来处理服务器环境中的各种 SELinux 控制。

问题

  1. 如何将 SELinux 类型映射到 TCP 端口?

  2. SECMARK 标签是否会改变网络数据包在传输过程中?

  3. 用于 InfiniBand 支持的semanage子命令有哪些?

  4. 是否需要专用设备来支持标记的 IPsec?

第六章:通过基础设施即代码编排配置 SELinux

随着大型分布式应用平台、云服务的出现,以及虚拟化基础设施的广泛采用,系统管理员正通过基础设施即代码(Infrastructure-as-Code)框架来积极管理他们的系统:这些框架使用类似源代码的信息来管理系统,包括编排和配置工具。

在本章中,管理员将学习如何分发和加载自定义 SELinux 策略模块,设置文件上下文定义并将其应用到系统,设置系统或 SELinux 域的宽松状态,配置系统上的 SELinux 设置,以及如何在工具不支持的情况下自定义 SELinux 操作。我们将使用四个流行的自动化框架来进行应用:Ansible、Chef、Puppet 和 SaltStack。

本章将涵盖以下主题:

  • 介绍目标设置和策略

  • 使用 Ansible 进行 SELinux 系统管理

  • 利用 SaltStack 配置 SELinux

  • 使用 Puppet 自动化系统管理

  • 使用 Chef 进行系统自动化

技术要求

本章的代码文件可以在我们的 Git 仓库中找到:github.com/PacktPublishing/SELinux-System-Administration-Third-Edition

查看以下视频,了解代码的实际操作:bit.ly/2T4Fksv

介绍目标设置和策略

在开始使用这四个自动化框架之前,我们需要明确我们希望实现的目标。毕竟,要真正比较自动化框架,我们需要每次使用相同的测试来测试每个框架。

操作的幂等性

每当我们创建一个带有中央存储库的远程管理环境时,需要考虑运行远程管理活动对系统的影响。一个非常常见的最佳实践,所有这些框架都强烈采纳的,就是幂等性。

幂等任务是指如果系统的状态已经是所需的状态,那么执行该任务不会修改系统。换句话说,重复执行一个任务不会影响系统或正在运行的进程,前提是无需进行更改。例如,考虑加载 SELinux 模块:如果模块已经加载,则不应重新加载该模块。如果模块尚未加载,则会加载适当的模块。

虽然自动化框架支持的大多数操作都是幂等的,但如果框架不支持我们需要的功能,我们需要自己创建自定义操作。例如,如果框架不支持加载 SELinux 模块,我们就需要编写自己的代码来完成此操作。

大多数编排框架会将非幂等任务封装在更为幂等的定义中。例如,如果配置文件的更改需要系统重启,那么封装后的定义可能是在文件更改后重启。引擎可以检查文件的状态(更改时间)和系统的状态(重启时间),从而推断是否需要重启,即使系统重启本身是一个非幂等的任务。

策略和状态管理

我们希望通过自动化框架支持的第一个场景是确保 SELinux 处于活动(强制)模式,并且正确的 SELinux 策略已加载,这是机器的包管理系统通常执行的任务。虽然让包管理系统处理这一任务比较方便,但它只能使用发行版特定的默认策略。具有不同安全需求的系统的系统管理员在使用默认策略时会受到限制,因此他们需要创建自定义策略和策略处理程序。因此,我们将研究如何分发和加载自定义策略。

我们将在示例中使用的自定义策略是 CIL 策略,这是一种非常新的策略,自动化框架通常不直接支持它。然而,它为我们提供了一个很好的重复性场景,允许我们在自动化框架中创建自定义规则。我们将策略本身存储在一个名为test.cil的文件中,文件内容如下:

(auditallow staff_sudo_t sysadm_t (process (transition)))

这个简单的策略将启用日志记录任何从staff_sudo_t域到sysadm_t域的过渡,并且可以通过sudo快速进行测试。在我们的示例中,它的唯一作用是快速验证策略是否已正确加载。

在状态管理方面,我们将确保系统处于强制模式,但将zoneminder_t SELinux 域标记为宽松模式。

SELinux 配置设置

我们希望采取的第二组操作是根据之前章节中讨论的各种 SELinux 设置来配置系统。我们之前通过semanage命令看到过大多数这些设置,我们将学习各种自动化框架如何支持这些设置,并且支持到什么程度。

我们不会逐一讲解每个设置,而是集中讨论每个自动化框架中支持的配置集。如果某个框架不支持某个特定配置(例如semanage ibpkey配置,这个配置相对较新),我们需要为此创建自定义操作。在这种情况下,我们将演示如何处理这一问题,因为这种处理方法在其他场景中也会重复出现,且类似。

设置文件上下文

我们希望看到的第三组操作是自动化框架如何支持将文件上下文应用于资源。这可以是应用semanage fcontext配置,之后进行恢复操作(例如使用restorecon),也可以是验证框架是否支持直接应用上下文。

直接应用上下文允许管理员直接使用框架,而无需费力地创建和重新应用文件上下文定义(这可能会带来一定的性能开销)。然而,只有在自动化框架是唯一可以进行系统更改的方法时,才应考虑这样做。在任何其他情况下,缺失的文件上下文定义可能会导致管理员将上下文重置为错误的状态。

从错误中恢复

在本章中,我们深入探讨了允许在多个系统上管理 SELinux 的各种框架。本章的目的是介绍这些框架的核心支持功能,而不是详细解释框架本身或框架本身的安全配置。我们不建议在没有测试的情况下立即将其应用于生产系统,务必确保做好备份!

话虽如此,本章中应用的许多设置如果出现问题是可以轻松纠正的。我们参考第二章理解 SELinux 决策和日志记录,在需要时选择性地将 SELinux 设置为宽容模式。

此外,每个框架都可以轻松暂停,允许管理员在不受框架立即覆盖更改影响的情况下修正问题。

比较框架

我们进一步讨论的每个框架都有自己对基础设施自动化和配置的处理方式。本书的目的不是详细探讨每个框架的细节,而是专注于它们的核心支持功能,以及它们如何处理 SELinux。我们还将抽象化如何处理不同的 Linux 发行版,并且所有示例都基于 CentOS。

此外,这些框架不断改进和发展。在本章中,我们仅探讨它们的常见用法,而不是它们如何在特定部署中发挥作用。例如,如果一个框架默认使用基于代理的架构,但也支持基于 SSH 的连接,我们只会考虑基于代理的架构,因为这是这些框架的默认设置,我们希望专注于 SELinux 配置支持功能。

但不要让这阻止你进一步实验这些框架,并根据自己的喜好进行调整!话说回来,让我们深入了解第一个引擎——Ansible。

使用 Ansible 进行 SELinux 系统管理

我们将首先考虑的编排和自动化工具是 Ansible,它是一个非常流行的开源解决方案,用于远程管理系统。Ansible 得到了 Red Hat 的商业支持,但并不限制其支持 Red Hat 或甚至 Linux 系统。其他环境,如 Windows 环境甚至网络设置,也有显著的基于 Ansible 的支持。

Ansible 的工作原理

Ansible 通常使用一个托管配置并解析设置的中央服务器。然后,Ansible 运行时通过 SSH 连接到远程系统,将必要的数据发送到临时位置,并在本地执行步骤。

使用 SSH 作为主要连接方式有显著的优势:管理员知道该协议是如何工作的以及如何进行配置和控制。此外,Ansible 不需要在目标机器上进行额外的部署,除了 Python 和 libselinux 的 Python 绑定(这些通常在启用了 SELinux 的机器上默认安装)。

Ansible 通过其模块知道如何访问各种资源。Ansible 模块包含 Ansible 用于正确执行任务的逻辑。模块代码会分发到目标机器,并在远程系统上执行。

管理员配置系统时使用的定义存储在 Ansible 剧本中。剧本定义了系统应如何配置,Ansible 将读取和解释剧本,以了解它必须在每个系统上执行什么。

为了便于在更大的环境中管理 Ansible 剧本,Ansible 使用 Ansible 角色 来捆绑一致的定义。管理员可以在剧本中为系统分配角色,以自动提升这些系统的状态。例如,可以创建一个角色来创建一个正确配置的 Web 服务器、数据库等。

在本章中,我们将创建一个名为 packt_selinux 的角色,并将其应用于远程系统。在该角色中,我们将展示如何使用 Ansible 配置和执行各种 SELinux 任务。

安装和配置 Ansible

要安装和设置 Ansible,大多数 Linux 发行版都提供对该框架的开箱即用支持。在 CentOS 上,可以执行以下步骤。其他发行版的用户可以轻松推导出适用于其平台的步骤:

  1. 你需要启用 企业 Linux 的附加包EPEL),然后就可以轻松安装 Ansible。请在主节点(你希望管理其他系统的节点)上执行以下命令:

    # yum install epel-release
    # yum install ansible
    
  2. 安装完成后,创建一对 SSH 密钥,用于主系统和目标系统之间的通信。使用 ssh-keygen 命令在主系统上创建密钥对,然后将公钥(~/.ssh/id_rsa.pub)复制到远程系统,保存为 ~/.ssh/authorized_keys

    # ssh-keygen
    # scp ~/.ssh/id_rsa.pub rem1:/root/.ssh/authorized_keys
    
  3. 测试远程连接是否正常工作,例如,通过远程执行 id 命令:

    # ssh rem1 id
    
  4. 如果测试成功,我们可以配置 Ansible,将此远程系统视为它将管理的节点之一。为此,请编辑 /etc/ansible/hosts 并将主机名添加到列表中:

    # cat /etc/ansible/hosts
    rem1
    
  5. 为了查看 Ansible 是否能够正确管理远程系统,我们可以让它收集有关远程系统的所有信息。Ansible 中的事实代表了发现的远程系统设置,可用于稍后调整 playbook 和角色。例如,Ansible 发现的发行版信息可以用来选择安装时使用的包名称:

    all, reflecting all entries in /etc/ansible/hosts) to execute the tasks in the setup module. 
    

最后一项任务的输出是一组大量发现的事实,显示我们连接成功,并且 Ansible 已经准备好管理远程系统。

创建和测试 Ansible 角色

为了允许在多个系统间共享可重用的配置,Ansible 建议使用其 Ansible 角色。我们将创建一个名为packt_selinux的角色,让它创建一个自定义目录,然后将这个角色分配给远程系统:

  1. 使用ansible-galaxy创建一个空的、但已准备好使用的角色:

    packt_selinux/tasks/main.yml, which will host all the settings and definitions we want to apply when we assign the packt_selinux role to a system. The other directories are, for our brief introduction to Ansible, less relevant, but play an important role in making sufficiently modular roles.
    
  2. 编辑main.yml文件,并创建一个自定义目录。文件的内容应如下所示:

    ---
    - name: Create /usr/share/selinux/custom directory
       file:
            path: /usr/share/selinux/custom
            owner: root
            group: root
            mode: '0755'
            state: directory
    

    在后续步骤中,该文件将通过更多的块进行扩展。每个块都以一个名称开始来标识该块,然后是状态定义。在当前块中,我们使用了 Ansible 的file模块来断言在给定参数下文件或目录是可用的。

  3. 将角色分配给远程系统并应用 playbook。我们通过首先创建一个/etc/ansible/site.yml文件,并添加以下内容来实现:

    ---
    - hosts: all 
      roles:
            - packt_selinux
    
  4. 运行此 playbook,将我们角色中定义的设置应用于远程系统:

    # ansible-playbook /etc/ansible/site.yml
    

    Ansible 将显示其进度,以及执行更改的任务。在我们的案例中,更改意味着目录已被创建。

现在我们已经测试了我们的角色并将其分配给远程系统,接下来要做的就是逐步更新角色,直到它包含我们所需的所有逻辑。不需要其他配置,且在每次更改后,我们可以从主服务器重新运行ansible-playbook命令。

使用 Ansible 为文件系统资源分配 SELinux 上下文

在当前角色中,我们在/usr/share/selinux内创建了一个自定义目录。这个父目录已设置为usr_t SELinux 类型,因此新创建的子目录也会继承该类型。然而,该目录的 SELinux 用户会有所不同,因为 Ansible 在远程登录到系统后创建了这个目录。在默认的 CentOS 配置中,这意味着目标目录的上下文将具有unconfined_u作为其 SELinux 用户组件。

更新main.yml中的定义,并显式设置 SELinux 用户和类型:

---
- name: Create /usr/share/selinux/custom directory
  file:
        path: /usr/share/selinux/custom
        owner: root
        group: root
        mode: '0755'
        state: directory
        setype: 'usr_t'
        seuser: 'system_u'

应用更改后(使用ansible-playbook),更新后的定义将为该目录正确设置 SELinux 用户和 SELinux 类型。

在这种情况下,我们为文件定义添加了两个参数:setypeseuser。Ansible 文件模块支持以下与 SELinux 相关的参数:

  • seuser 是资源的 SELinux 用户。对于系统资源,设置为 system_u,如示例中所使用的。

  • serole 是资源的 SELinux 角色。通常不使用此参数,因为系统上的角色继承通常会导致资源被标记为 object_r 角色,这在大多数情况下是正确的。

  • setype 是资源的 SELinux 类型,是文件模块中最常用的 SELinux 参数。

  • selevel 是资源的 SELinux 敏感度级别,默认值为 s0

正如我们从示例中已经学到的,如果继承的上下文是正确的,你不需要声明类型。

使用 Ansible 加载自定义 SELinux 策略

Ansible 当前的版本不支持加载自定义的 SELinux 模块。虽然在 Ansible galaxy(一个贡献者可以添加更多模块的生态系统)中可以找到自定义模块,但我们来看一下如何将自定义策略分发到 Ansible 控制的系统上,并在模块尚未加载时加载它。

虽然我们可以开始创建自定义模块,但我们将使用现有角色中的任务组合来实现这一目标。我们将按顺序尝试完成以下任务:

  1. 将名为 test.cil 的自定义策略上传到远程系统。

  2. 检查此自定义策略是否已经加载。

  3. 加载自定义策略,但仅在前面的检查失败时。

这三个任务通过三个模块处理:copy 模块、shell 模块和 command 模块。我们将在不同的步骤中使用每个模块:

  1. 在本章前面提到的自定义策略,通过将 test.cil 文件放入 packt_selinux 角色的 files/ 文件夹中来创建。

  2. 在角色的 main.yml 文件中创建一个新的代码块,内容如下:

    - name: Upload test.cil file to /usr/share/selinux/custom
      copy:
            src: test.cil
            dest: /usr/share/selinux/custom/test.cil
            owner: root
            group: root
            mode: '0644'
    

    这将确保 test.cil 文件(当前在主机上)被分发到目标节点,存放在我们之前创建的目录中。

  3. 接下来,我们检查策略是否已经加载。为此,我们使用 shell 模块,并稍后使用失败或成功的状态。因此,我们将返回值存储在 test_is_loaded 变量中,并明确告诉 Ansible 忽略失败,因为我们将其作为检查,而不是状态定义:

    - name: Check if test SELinux module is loaded
      shell: /usr/sbin/semodule -l | grep -q ^test$
      register: test_is_loaded
      ignore_errors: True
    
  4. command 模块加载策略文件,且仅在前一个任务失败时加载:

    - name: Load test.cil if not loaded yet
      command: /usr/sbin/semodule -i /usr/share/selinux/custom/test.cil
      when: test_is_loaded is failed
    

这种方法展示了如何利用我们对 SELinux 的了解来定义和设置状态。这个方法也可以应用于其他 SELinux 设置,例如,在定义或调整设置之前,通过验证列表输出(例如,使用 semanage)来进行检查。

使用 Ansible 开箱即用的 SELinux 支持

Ansible 提供了许多模块,原生支持多种与 SELinux 相关的设置,以下是我们简要介绍的:

  • selinux 模块可用于设置或更改 SELinux 状态(强制或宽容),以及选择适当的 SELinux 策略类型(例如,targeted):

    - name: Set SELinux to enforcing mode
      selinux:
              policy: targeted
              state: enforcing
    
  • 使用seboolean模块,可以随意调整 SELinux 布尔值:

    - name: Set httpd_builtin_scripting to true
      seboolean:
              name: httpd_builtin_scripting
              state: yes
    
  • sefcontext模块允许我们更改 SELinux 文件上下文定义:

    - name: Set the context for /srv/web
      sefcontext:
              target: '/srv/web(/.*)?'
              setype: httpd_sys_content_t
              state: present
    
  • 使用selinux_permissive,我们可以选择性地将某些 SELinux 策略域标记为宽松模式:

    - name: Set zoneminder_t as permissive domain
      selinux_permissive:
              name: zoneminder_t
              permissive: true
    
  • selogin模块可以用来将登录映射到 SELinux 用户,就像semanage login一样:

    - name: Map taylor's login to the unconfined_u user
      selogin:
              login: taylor
              seuser: unconfined_u
              state: present
    
  • seport可以用来创建 SELinux 端口映射:

    - name: Set port 10122 to ssh_port_t
      seport:
              ports: 10122
              proto: tcp
              setype: ssh_port_t
              state: present
    

其他 SELinux 设置可能通过自定义模块得到支持,但通过前面介绍的方法,管理员已经可以开始在其环境中的所有系统上配置 SELinux。

使用 SaltStack 配置 SELinux

我们要考虑的第二个编排和自动化框架是 SaltStack,它得到了 SaltStack 公司商业支持。SaltStack 使用类似于 Ansible 的声明式语言,并且也是用 Python 编写的。在本章中,我们将使用开源的 SaltStack 框架,但也有 SaltStack 的企业版,它在开源版本的基础上增加了更多的功能。

SaltStack 的工作原理

SaltStack,通常也被称为 Salt,是一个自动化框架,使用代理/服务器模型进行集成。与 Ansible 不同,SaltStack 通常要求在目标节点(称为minions)上安装代理,并激活 minion 守护进程以实现与主服务器的通信。此通信是加密的,minion 认证使用公钥验证,必须在主服务器上进行批准,以确保没有恶意 minion 参与 SaltStack 环境。

尽管 SaltStack 也支持无代理安装,但我们将重点介绍基于代理的部署。在这种配置下,minions 会定期与主服务器检查是否需要应用任何更新。但管理员无需等到 minion 拉取最新的更新:你还可以从主服务器触发更新,从而将更改推送到节点。

minion 应该处于的目标状态被写入以.sls为后缀的文件中。这些 Salt State 文件可以引用其他状态文件,从而实现模块化设计并在多台机器上重用。

如果我们需要更复杂的编码,SaltStack 支持创建和分发模块,称为Salt 执行模块。然而,与 Ansible 的 Galaxy 不同,目前没有社区仓库来寻找更多的执行模块。

安装和配置 SaltStack

SaltStack 的安装在不同的 Linux 发行版中类似。让我们看看在 CentOS 机器上是如何安装的:

  1. 我们首先需要启用包含 SaltStack 软件的仓库。该项目通过 RPM 文件来维护仓库定义,这些文件可以立即安装:

    # yum install https://repo.saltstack.com/py3/redhat/salt-py3-repo-latest.el8.noarch.rpm
    
  2. 一旦我们在所有系统上启用了软件仓库,就可以在主服务器上安装salt-master,并在远程系统上安装salt-minion

    master ~# yum install salt-master
    remote ~# yum install salt-minion
    
  3. 在启动系统上的守护进程之前,我们首先更新 minion 配置以指向主节点。默认情况下,minion 会尝试连接到主机名为 salt 的主节点,但可以通过编辑 /etc/salt/minion 并设置正确的主机名来轻松更改:

    remote ~# vim /etc/salt/minion
    master: ppubssa3ed
    
  4. 配置好 minion 后,我们现在可以启动 SaltStack 的主节点(salt-master)和 minion(salt-minion)守护进程:

    master ~# systemctl start salt-master
    remote ~# systemctl start salt-minion
    
  5. Minion 将连接到主节点并展示其公钥。要列出当前连接的代理,可以使用 salt-key -L

    master ~# salt-key -L
    Accepted Keys:
    Denied Keys:
    Unaccepted Keys:
    rem1.internal.genfic.local
    Rejected Keys:
    master ~# salt-key -a rem1.internal.genfic.local
    The following keys are going to be accepted:
    Unaccepted Keys:
    rem1.internal.genfic.local
    Proceed? [n/Y] y
    Key for minion rem1.internal.genfic.local accepted.
    
  6. 一旦我们接受了密钥,主节点就会知道并控制该 minion。让我们看看是否能够与远程系统正确交互:

    master ~# salt '*' service.get_all
    

    这个命令将列出 minion 上的所有系统服务。

salt 命令是用于从主节点查询和与远程 minion 交互的主要命令。如果上一个命令成功返回了所有系统服务,那么 SaltStack 就已经正确配置并准备好管理远程系统。

创建并测试我们的 SELinux 状态与 SaltStack

让我们创建一个名为 packt_selinux 的 SELinux 状态,并将其应用到远程 minion 上:

  1. 我们首先需要创建顶级文件。此文件是 SaltStack 的主配置文件,通过它来配置整个环境:

    master ~# mkdir /srv/salt
    master ~# vim /srv/salt/top.sls
    base:
      '*':
        - packt_selinux
    
  2. 接下来,我们为 packt_selinux 创建状态定义:

    init.sls file is the main state file for this packt_selinux state. So, when SaltStack reads the top.sls file, it sees a reference to the packt_selinux state and then searches for the init.sls file inside this state.
    
  3. 将本章前面定义的 SELinux test.cil 模块放置在 /srv/salt/packt_selinux 目录下,这也是我们在状态定义中引用的位置。放置后,我们可以将此状态应用到环境中:

    master ~# salt '*' state.apply
    

salt 命令的 state.apply 子命令用于将状态应用到整个环境。每次修改状态定义时,可以使用此命令强制更新 minions。否则,minions 将默认每 60 分钟更新一次其状态。这些计划的状态更新称为 mine 更新,并在 /etc/salt/minion 中的代理上进行配置。

使用 SaltStack 为文件系统资源分配 SELinux 上下文

在写作时,SaltStack 尚未在稳定版本中完全支持在资源中处理 SELinux 类型。然而,SaltStack 支持运行命令,但只有在某个测试成功(或失败)之后才能执行。

更新 init.sls 文件,并在其中添加以下代码:

{%- set path = '/usr/share/selinux/custom/test.cil' %}
{%- set context = 'system_u:object_r:usr_t:s0' %}
set {{ path }} context:
  cmd.run:
    - name: chcon {{ context}} {{ path }}
    - unless: test $(stat -c %C {{ path }}) == {{ context }}
path and context) so that we do not need to iterate the path and context multiple times, and then use these variables in a cmd.run call.

cmd.run 方法允许我们通过本书前面所见的命令轻松创建自定义 SELinux 支持。unless 检查包含一个测试,以判断是否需要执行该命令,从而允许我们创建幂等的状态定义。

使用 SaltStack 加载自定义 SELinux 策略

让我们在远程系统上加载我们的自定义 SELinux 模块。SaltStack 支持通过 selinux.module 状态加载 SELinux 模块:

load test.cil:
  selinux.module:
    - name: test
    - source: /usr/share/selinux/custom/test.cil
    - install: True
    - unless: "semodule -l | grep -q ^test$"

与前一部分一样,我们需要添加一个 unless 语句,否则每次应用状态时,SaltStack 都会尝试重复加载 SELinux 模块。

使用 SaltStack 内置的 SELinux 支持

SaltStack 对 SELinux 的原生支持正在逐步扩展,但仍有很大的改进空间:

  • 使用selinux.boolean,可以在目标机器上设置 SELinux 布尔值:

    httpd_builtin_scription:
      selinux.boolean:
        - value: True
    
  • 文件上下文(由semanage fcontext管理)可以使用selinux.fcontext_policy_present状态进行定义:

    "/srv/web(/.*)?":
      selinux.fcontext_policy_present:
        - sel_type: httpd_sys_content_t
    
  • 若要删除定义,请使用selinux.fcontext_policy_absent定义。

  • 使用selinux.mode,我们可以将系统置于强制模式或宽容模式:

    enforcing:
      selinux.mode
    
  • 端口映射通过selinux.port_policy_present状态来处理:

    tcp/10122:
      selinux.port_policy_present:
        - sel_type: ssh_port_t
    

使用前面提到的cmd.run方法,我们可以以可重复的方式将 SELinux 配置更新应用于不支持的设置的系统。

使用 Puppet 自动化系统管理

Puppet 是我们将要检查的第三个自动化框架。它是我们列表中最古老的一个,首次发布于 2005 年,通常被视为其他自动化框架的基准。它得到了 Puppet 公司(也常被称为 Puppet Labs)的商业支持。

Puppet 如何工作

与 SaltStack 类似,Puppet使用基于代理/服务器的模型,并通过公钥认证代理,确保环境中没有恶意代理活跃。

Puppet 主节点可以访问Puppet 清单,这是 Puppet 希望实现的状态声明。这些清单使用一种受到 Ruby 启发的特定语言,并可以引用由模块提供的类,以确保在环境中具有可重用性。

Puppet 模块因此是 Puppet 的工作马,而 Puppet 拥有一个重要的社区,称为 Puppet Forge,允许您下载和安装社区创建的模块,以更轻松地管理您的环境。

Puppet 代理会定期连接到主节点,向主节点报告远程机器的当前详细信息。这些当前详细信息被称为事实,Puppet 可以利用这些信息动态处理环境中的变化。主节点随后将目标状态编译成所谓的目录并将该目录发送到代理。代理然后应用此目录并报告结果。

安装和配置 Puppet

Puppet 公司提供多个 Linux 发行版的集成包。以下说明重点介绍与 RPM 兼容的发行版,但其他平台也有非常相似的说明:

  1. Puppet 公司通过 RPM 文件提供存储库定义。在建立存储库后,您可以安装puppetserverpdk(在主节点上)以及puppet-agent(在远程系统上),使软件随时可用:

    # yum install https://yum.puppet.com/puppet6-release-el-8.noarch.rpm
    master ~# yum install puppetserver pdk
    remote ~# yum install puppet-agent
    
  2. 配置主节点,使其证书命名正确。编辑/etc/puppetlabs/puppet中的puppet.conf文件,并在[master]部分内更新或添加以下设置:

    master ~# vim /etc/puppetlabs/puppet/puppet.conf
    certname = ppubssa3ed.internal.genfic.local
    server = ppubssa3ed.internal.genfic.local
    environment = production
    
  3. 启动 Puppet 服务器,以便客户端可以开始连接到它:

    master ~# systemctl start puppetserver
    
  4. 在远程系统上,编辑相同的配置文件,并在[main]部分更新或添加以下设置:

    remote ~# vim /etc/puppetlabs/puppet/puppet.conf
    [main]
    certname = rem1.internal.genfic.local
    server = ppubssa3ed.internal.genfic.local
    environment = production
    runinterval = 1h
    
  5. 接下来,启动 Puppet 代理:

    remote ~# systemctl start puppet
    
  6. 在主节点上,我们现在可以查询待处理的证书请求。它应该显示我们最近启动的代理的请求:

    master ~# /opt/puppetlabs/bin/puppetserver ca list
    Requested Certificates:
      rem1.internal.genfic.local   (SHA256) ...
    
  7. 我们可以通过以下方式接受此请求(签署证书):

    master ~# /opt/puppetlabs/bin/puppetserver ca sign --certname rem1.internal.genfic.local
    Successfully signed certificate request for rem1.internal.genfic.local
    
  8. 为了验证连接是否正常工作,请登录到远程机器并触发代理应用(当前为空的)目录:

    remote ~# /opt/puppetlabs/bin/puppet agent --test
    

与 SaltStack 不同,Puppet 依赖代理频繁轮询服务器,而不是将更改推送到代理。在我们之前做的配置中,我们将代理配置为每小时检查一次。通过puppet agent --test命令,我们可以立即指示代理运行状态检查。

使用 Puppet 创建和测试 SELinux 类

让我们创建packt_selinux类,通过它我们将配置远程机器的 SELinux 设置:

  1. 调用/etc/puppetlabs/code/modules目录:

    master ~# cd /etc/puppetlabs/code/modules
    master ~# pdk new module packt_selinux --skip-interview
    

    结果是一个空模块,包含许多默认文件和目录。我们将主要与模块的清单文件一起工作。

  2. packt_selinux/manifests目录中,创建一个名为init.pp的新文件,内容如下:

    class packt_selinux {
      file { "/usr/share/selinux/custom":
        ensure => directory,
        mode => "0755",
      }
    }
    
  3. 接下来,在/etc/puppetlabs/code/environments/production/manifests位置创建一个名为site.pp的文件,内容如下:

    node 'rem1.internal.genfic.local' {
      include packt_selinux
    }
    

    site.pp文件为 Puppet 提供了顶级层次结构,用于将其环境与适当的定义关联。在此示例中,主机名为rem1.internal.genfic.local的节点通过引用我们之前创建的packt_selinux模块进行配置。

    packt_selinux模块内部,我们创建了packt_selinux类,该类目前由一个指令组成,用于创建/usr/share/selinux/custom

  4. 设置这些定义后,让远程代理更新其状态:

    remote ~# puppet agent -t
    

    在产品环境中,通常会定期安排此命令执行,或者持续运行 Puppet 代理作为守护进程。

在将类正确分配给节点后,我们可以通过更多的 SELinux 细节扩展我们的配置。

使用 Puppet 为文件系统资源分配 SELinux 上下文

让我们通过以下代码段增强当前的类定义:

file { 'selinux_custom_module_test':
  path => "/usr/share/selinux/custom/test.cil",
  ensure => file,
  owner => "root",
  group => "root",
  source => "puppet:///modules/packt_selinux/test.cil",
  require => File["/usr/share/selinux/custom"],
  seltype => "usr_t",
}

为了使此块正常工作,我们需要将test.cil SELinux 模块放置在packt_selinux模块位置中的files/文件夹内。此块将使 Puppet 将文件上传到该目录,并设置依赖关系,要求该目录必须存在。require语句指向先前定义的块。

我们还看到 Puppet 对 SELinux 类型定义提供了开箱即用的支持。文件类具有几个支持 SELinux 的参数,可以使用:

  • seluser定义了资源的 SELinux 用户。

  • selrole定义了资源的 SELinux 角色。

  • seltype定义了资源的 SELinux 类型。

  • selrange定义了资源的 SELinux 敏感度范围。

  • selinux_ignore_defaults告诉 Puppet 忽略默认的 SELinux 上下文(如从 SELinux 策略查询到的)。

我们之前的示例实际上是多余的,因为 Puppet 会主动查询 SELinux 策略以发现正确的资源上下文并应用。设置selinux_ignore_defaultstrue时,Puppet 将不会查询并调整上下文,这在测试没有正确上下文定义的新配置时非常有用。

使用 Puppet 加载自定义 SELinux 策略

Puppet 确实支持加载和管理 SELinux 模块。然而,它的支持目前仅限于传统的 SELinux 策略模块,而不包括基于 CIL 的模块。

所以,让我们在模块定义中创建另一个块,加载test.cil文件,但仅在没有已加载测试 SELinux 模块的情况下:

exec { '/usr/sbin/semodule -i /usr/share/selinux/custom/test.cil':
  require => File['selinux_custom_module_test'],
  unless => '/usr/sbin/semodule -l | grep -q ^test$',
}

这种方法允许我们在本地 Puppet 支持不足的情况下,创建自定义的 SELinux 配置调整。

使用 Puppet 的开箱即用的 SELinux 支持

Puppet 有一些开箱即用的 SELinux 相关类,并通过 Puppet Forge 提供更多支持,Puppet Forge 是一个由社区贡献模块组成的生态系统。我们推荐的一个模块是puppet-selinux模块,Puppet(公司)在 Puppet Forge 上维护该模块(因此更有可能在 Puppet 的后续版本中保持支持)。

安装新模块非常简单,可以使用puppet module命令:

master ~# /opt/puppetlabs/bin/puppet module install puppet-selinux

然后我们可以在清单中引用通过此模块提供的selinux类:

  • 可以直接使用selinux类来设置系统的强制(或宽松)状态:

    class { selinux:
      mode => 'enforcing',
      type => 'targeted',
    }
    
  • (本地)selboolean类可以用于设置 SELinux 布尔值:

    selboolean { 'httpd_builtin_scripting':
      value => off,
    }
    
  • SELinux 文件上下文可以使用selinux::fcontext类定义:

    selinux::fcontext { '/srv/web(/.*)?':
      seltype => 'httpd_sys_content_t',
    }
    
  • 文件上下文的等效定义由selinux::fcontext::equivalence处理,方式如下:

    selinux::fcontext::equivalence { '/srv/www':
      ensure => 'present',
      target => '/srv/web',
    }
    
  • 自定义端口映射由selinux::port处理:

    selinux::port { 'set_ssh_custom_port':
      ensure => 'present',
      seltype => 'ssh_port_t',
      protocol => 'tcp',
      port => 10122,
    }
    
  • 可以使用selinux::permissive将单独的 SELinux 域设置为宽松模式:

    selinux::permissive { 'zoneminder_t':
      ensure => 'present',
    }
    
  • 如果存在标准的 SELinux 模块,则使用selmodule可以加载它。在这种情况下,它将在selmoduledir引用的目录中查找以块名称命名的 SELinux 模块:

    selmodule { 'vlock':
      ensure => 'present',
      selmoduledir => '/usr/share/selinux/custom',
    }
    

尽管在 Puppet Forge 上可能会有其他支持 SELinux 的模块,但请确保验证这些模块是否成熟且足够稳定。如果它们的支持不确定,您可能需要采用之前使用过的exec方式,如在使用 Puppet 加载自定义 SELinux 策略一节中所示。

使用 Chef 进行系统自动化

我们将要探讨的最后一个自动化框架是 Chef。Chef 相比于前面提到的框架,更偏向于手动操作和开发导向,但它仍然非常强大。它有类似名称的公司 Chef 提供商业支持。

Chef 的工作原理

Chef 对自动化有着稍微更为广泛的方法,并且需要更多的工作来启动和运行。然而,一旦设置完成,它提供了一个非常灵活和可编程的环境,可以在其中完成基础设施自动化。

Chef 架构中有三种类型的系统:

  • Chef 服务器 充当中央枢纽,用于维护自动化代码,并与远程系统交互以应用更改。

  • Chef 工作站 是管理员和工程师开发 Chef recipes(代码)和 cookbooks 并与 Chef 服务器交互的端点。每个 Chef 环境可以有多个 Chef 工作站。

  • Chef 客户端 是在由 Chef 环境管理的远程系统(节点)上运行的代理。

开发者在 recipes 中创建自动化代码,这些代码类似于任务。多个 recipes 被打包在一个 cookbook 中,并上传到 Chef 服务器,然后可以将这些 recipes 应用于一个或多个节点。可以将 cookbooks 与之前的自动化框架中的模块进行比较。

Chef 客户端和服务器使用基于公钥的身份验证和加密进行它们的交互。客户端采取主动措施,连接服务器以下载最新的 cookbooks 和其他资源,然后计算并应用最新的更改,并将这些更改的反馈发送回服务器。

安装和配置 Chef

完整的 Chef 安装需要管理员安装几个组件。Chef 工作站和 Chef 服务器需要管理员安装,而 Chef 代理将由 Chef 后续安装。

安装 Chef 工作站

要安装和使用 Chef,请先下载 Chef 工作站。所有 Chef 软件都可以从 downloads.chef.io 下载。对于 CentOS,Chef 工作站以 RPM 的形式提供,可以使用 yum 进行安装。

但是,与常见的打包软件不同,Chef 工作站的依赖关系没有显式列出为 RPM 依赖关系,导致软件在没有必要的库的情况下安装。安装结束时,RPM 文件将执行一个后安装脚本,检查依赖关系并报告缺少的库:

master ~# yum install chef-workstation-0.17.5-1.el7.x86_64.rpm

目前,依赖关系需要安装以下 CentOS 软件包:

master ~# yum install libX11-xcb libXcomposite libXcursor libXdamage nss gdk-pixbuf2 gtk3 libXScrnSaver alsa-lib git

安装完成后,作为常规非根用户运行 chef -v 来验证是否满足所有依赖:

master ~$ chef -v

命令应输出包含的 Chef 组件的版本。

安装和配置 Chef 服务器核心

第二个安装是 Chef 服务器核心。此软件再次以 RPM 的形式提供:

  1. 使用 yum 安装 Chef 服务器核心:

    master ~# yum install chef-server-core-13.2.0-1.el7.x86_64.rpm
    

    安装完成后,我们需要为我们的环境配置它。

  2. 创建一个名为 /var/opt/chef 的目录。我们将使用这个目录来存储用于身份验证 Chef 服务器的加密密钥:

    master ~# mkdir /var/opt/chef
    
  3. 接下来,使用 chef-server-ctl 配置 Chef 服务器:

    master ~# chef-server-ctl reconfigure
    

    这将在当前系统上设置 Chef 服务器。此设置可能需要一些时间才能完成,但完成后,我们可以继续在 Chef 中创建用户帐户。

  4. 让我们在此系统上为用户lisa创建一个名为chefadmin的帐户,并为其设置自定义密码:

    master ~# chef-server-ctl user-create chefadmin Lisa McCarthy lisa@ppubssa3ed.internal.genfic.local pw4chef --filename /var/opt/chef/chefadmin.pem
    
  5. 在 Chef 配置中创建一个组织单元,并将其与新创建的用户关联:

    master ~# chef-server-ctl org-create ppubssa3ed "Packt Pub SSA 3rd Edition" --association_user chefadmin --filename /var/opt/chef/ppubssa3ed-validator.pem
    

完成这些后,服务器管理已经完成,我们可以开始创建我们的开发环境。

准备开发环境

如前所述,Chef 比以前的自动化框架更偏向开发。将与 Chef 交互的用户(使用 Chef 工作站)需要首先建立开发环境:

  1. 我们之前为用户lisa创建了一个名为chefadmin的帐户。现在,以用户lisa身份登录并在用户的主目录中创建开发环境:

    master ~$ mkdir chef
    master ~$ cd chef
    master ~$ git init
    
  2. 我们创建一个支持 Git 的环境,因为 Chef 工具需要它。如果你还没有激活 Git 配置,你可能需要添加你的电子邮件和名字:

    master ~$ git config --global user.email "lisa@ppubssa3ed.internal.genfic.local"
    master ~$ git config --global user.name "Lisa McCarthy"
    
  3. 接下来,在此环境中创建 Chef 刀具配置文件.chef/knife.rb(在我们的示例中是~/chef/.chef/knife.rb):

    chefadmin.pem in our example) and adjust the configuration accordingly.
    
  4. 下载 Chef 服务器使用的证书(这些证书是自签名证书),然后检查 SSL 连接:

    master ~$ knife ssl fetch
    master ~$ knife ssl check
    
  5. 如果检查成功,我们可以提交更改:

    master ~$ git add -A
    master ~$ git commit -m 'Chef configuration baseline'
    

我们现在已经准备好开始我们的食谱和食谱开发了。

创建 SELinux 食谱

我们将要开发的食谱将包含各种 SELinux 配置项,然后将这些配置项分配给远程节点:

  1. 让我们从创建一个名为packt_selinux的食谱开始:

    metadata.rb and recipes/default.rb. The metadata.rb file contains information about the cookbook and, while it is not necessary for our example, it is sensible to edit and update this file immediately. Later, we will adjust this file to include dependency information toward other cookbooks.
    
  2. recipes/default.rb文件包含我们希望应用于远程系统的实际逻辑。让我们为/usr/share/selinux/custom目录创建一个定义:

    master ~$ vim recipes/default.rb
    directory '/usr/share/selinux/custom' do
      owner 'root'
      group 'root'
      mode '0755'
      action :create
    end
    
  3. 现在将食谱上传到 Chef 服务器:

    master ~$ knife cookbook upload packt_selinux
    
  4. 我们可以使用list子命令查询 Chef 服务器上可用的食谱:

    master ~$ knife cookbook list
    packt_selinux   0.1.0
    
  5. 有了食谱可用后,让我们启动目标节点。引导过程只需要执行一次,但必须由 Chef 认证用户触发:

    master ~$ knife bootstrap rem1 --ssh-user root --node-name rem1
    
  6. 这确保了 Chef 服务器知道远程系统。我们可以使用knife node list查询节点,并使用show子命令获取有关节点的更多详细信息:

    master ~$ knife node show rem1
    
  7. 使用run_list add子命令将packt_selinux食谱分配给节点:

    chef-client binary executes either regularly (through a cron job or similar) or starts as a daemon.
    
  8. 对于我们的目的,我们将在远程系统上触发chef-client命令,以下载并应用最新的更改:

    chef-client should show how it found and applied the changes listed in the recipe.
    

如果此命令成功返回,那么 Chef 已经准备好使用我们开发的食谱来管理远程系统。

使用 Chef 为文件系统资源分配 SELinux 上下文

Chef 对 SELinux 上下文的原生支持有限。当被指示在节点上创建或修改文件时,它会根据节点上当前的文件上下文定义重新标记这些文件。然而,我们可以订阅在食谱中定义的事件,并在这些事件发生时触发适当的操作。例如,要显式设置目录的上下文,我们可以创建类似这样的内容:

execute 'set_selinux_custom_context' do
  command '/usr/bin/chcon -t usr_t /usr/share/selinux/custom'
  action :nothing
  subscribes :run, 'directory[/usr/share/selinux/custom]', :immediately
end

在将其添加到recipes/default.rb文件后,我们首先需要将更新后的 cookbook 上传到服务器:

master ~$ knife cookbook upload packt_selinux

之后,我们可以在远程节点上重新运行chef-client以应用此更新的食谱。如果目录之前已经创建,则食谱不会更改任何内容,因为订阅不会被触发。

使用 Chef 加载自定义 SELinux 策略

让我们更新我们的食谱,以包括加载自定义策略的逻辑。我们将在食谱中使用两个块,一个用于将test.cil文件上传到节点,另一个用于加载它,但仅在之前未加载时:

cookbook_file '/usr/share/selinux/custom/test.cil' do
  source 'test.cil'
  owner 'root'
  group 'root'
  mode '0755'
  action :create
end
bash 'load_test_cil' do
  code '/usr/sbin/semodule -i /usr/share/selinux/custom/test.cil'
  not_if '/usr/sbin/semodule -l | grep -q ^test$'
  only_if { ::File.exists?('/usr/share/selinux/custom/test.cil') }
end

test.cil文件放入packt_selinux cookbook 目录下名为files的文件夹中,然后上传更新后的 cookbook 并使用chef-client重新应用更改。

使用 Chef 的开箱即用 SELinux 支持

虽然 Chef 本身对 SELinux 的原生支持有限,但在 Chef 超市(Chef 社区管理和分发自定义 cookbook 的地方)在线提供了许多 cookbook。Chef(公司)本身维护selinux cookbook,它允许管理系统的 SELinux 状态,而selinux_policy cookbook 处理一些其他 SELinux 设置。

让我们下载并安装selinuxselinux_policy cookbooks:

master ~$ knife supermarket install selinux_policy
master ~$ knife supermarket install selinux
master ~$ knife cookbook upload selinux_policy
master ~$ knife cookbook upload selinux

接下来,调整我们自己的 cookbook 中的metadata.rb文件,添加对新添加的 cookbook 的依赖:

depends 'selinux_policy'
depends 'selinux'

现在,我们可以使用一些预定义的食谱来处理 SELinux 配置设置:

  • 使用selinux_state,我们可以将系统置于强制模式或宽容模式:

    selinux_state "SELinux enforcing" do
      action :enforcing
    end
    
  • selinux_policy_boolean食谱可以配置 SELinux 布尔值:

    selinux_policy_boolean 'httpd_builtin_scripting' do
      value false
    end
    
  • 使用selinux_policy_port,可以定义自定义的 SELinux 端口映射:

    selinux_policy_port '10122' do
      protocol 'tcp'
      secontext 'ssh_port_t'
    end
    
  • 可以使用selinux_policy_fcontext设置文件上下文定义:

    selinux_policy_fcontext '/srv/web(/.*)?' do
      secontext 'httpd_sys_content_t'
    end
    
  • 可以使用selinux_policy_permissive食谱将 SELinux 域设置为宽容模式:

    selinux_policy_permissive 'zoneminder_t' do
    end
    

在远程系统上调用chef-client之前,别忘了上传更改后的 cookbook。

总结

像 Ansible、SaltStack、Puppet 和 Chef 这样的自动化框架可以轻松用于管理多个系统上的 SELinux 设置。虽然并非所有框架都能原生处理 SELinux 设置,但通过使用社区提供的模块或创建自定义规则来检查和更新设置,可以轻松解决这个问题。在本章中,我们已经看到如何通过安装基于 CIL 的自定义 SELinux 策略来实现这一点。

我们了解到这些框架都有各自的方法。例如,Ansible 不使用远程系统上的任何软件安装,并使用 SSH 与目标系统通信。其他框架都使用代理/服务器模型,但在配置设置(Puppet 和 SaltStack 之间的语法明显不同)或设计(Chef 使用开发人员拥有其开发环境的工作站)上有各自的观点。所有这些框架都很容易安装和配置,并且可以处理大多数 SELinux 设置而不会出现任何问题。所有工具都有一种模块化定义的方式,因此可以轻松地应用到更多的系统中。

现在我们知道如何一致地应用 SELinux 设置,让我们看看还有哪些 SELinux 控制存在,但现在通过用户空间应用程序特定的支持。

问题

  1. 哪一款工具原生支持在资源上设置 SELinux 上下文?

  2. 这些编排工具如何允许超出原生支持的可重用定制?

  3. 列出的编排工具之间有哪些明显的区别?

第二部分:SELinux 感知平台

一些应用程序和平台具有明确的 SELinux 支持,用于进一步加强安全控制。在这一部分,将解释最常见的平台 SELinux 控制。

本节包括以下章节:

  • 第七章配置特定应用的 SELinux 控制

  • 第八章SEPostgreSQL – 用 SELinux 扩展 PostgreSQL

  • 第九章安全虚拟化

  • 第十章将 Xen 安全模块与 FLASK 一起使用

  • 第十一章增强容器化工作负载的安全性

第七章:配置特定应用的 SELinux 控制

一些 Linux 服务和应用程序启用了额外的 SELinux 控制,除了内核强制的 SELinux 策略外。它们允许管理员通过应用程序本身进一步操作和执行策略规则——隔离用户、减少数据泄露风险,并缓解恶意行为的影响。

本章中,我们将探讨几个支持 SELinux 的应用程序,如 systemd 服务,以及它们如何允许管理员设置和指定目标域和资源标签。我们还将讨论 D-Bus 服务,D-Bus 服务允许 SELinux 策略控制服务绑定和 D-Bus 内部的消息通信。接下来,我们将介绍启用 PAM 的服务,允许用户通过这些服务登录。

最后,我们将以 mod_selinux 作为本章的结束,它是一个 Apache 模块,允许对 Web 服务器行为进行 SELinux 特定的调整。这种方法展示了没有本地 SELinux 支持的应用程序如何通过扩展来满足管理员的需求。

本章将涵盖以下主题:

  • 调整 systemd 服务、日志记录和设备管理

  • 通过 D-Bus 进行通信

  • 配置 PAM 服务

  • 使用 mod_selinux 与 Apache 配合

技术要求

查看以下视频,了解代码的实际应用:bit.ly/37jYtze

调整 systemd 服务、日志记录和设备管理

init 系统,负责处理服务和启动操作。

在开发阶段,systemd 向其组件库中添加了几个其他组件:

  • D-Bus 提供了一个系统和会话总线服务,允许通过 D-Bus 进行应用程序间通信,并与 systemd 合并。

  • systemd 还集成了 udev,它提供了一个灵活的设备节点管理应用程序。

  • 向 systemd 添加了登录功能,实现了对用户会话的精细控制。

  • journald 守护进程加入了 systemd 家族,提供了一种新的系统和服务日志记录方法,替代了部分标准系统日志记录器的功能。

  • timerd 守护进程支持基于时间的任务执行,替代了部分标准 cron 守护进程的功能。

  • 网络配置可以通过 systemd-networkd 进行管理。

将多个系统服务吸收进一个单一应用程序套件的持续方法未曾被忽视,也不是没有争议的。一些发行版甚至拒绝将 systemd 作为默认的 init 系统。

systemd 项目为大多数服务提供了 SELinux 支持。像 systemd 这样的应用程序,不仅具备 SELinux 意识,而且还在特定的 SELinux 类和权限上执行访问控制(而不是依赖于 Linux 内核),被称为 用户空间对象管理器

图 7.1 — 内核强制、标准 SELinux 和用户空间管理 SELinux 之间的区别

图 7.1 — 内核强制、标准 SELinux 和用户空间管理 SELinux 之间的区别

如果一个应用程序实施了针对特定类和权限的访问控制,那么它也将拥有自己的 AVC(有关 AVC 的更多信息,请参见第二章理解 SELinux 决策和日志记录)。由这些应用程序生成的日志事件将被标识为USER_AVC事件,而不是(内核管理的)AVC事件。systemd 应用程序支持特定于 systemd 的类,正如我们在管理单元操作访问部分中所看到的那样。但在深入探讨这些具体细节之前,让我们先了解一下 systemd 是什么以及它对 SELinux 的支持。

systemd 中的服务支持

大多数人所知道的 systemd 主要功能是它对系统服务的支持。与传统的兼容 SysV 的init系统不同,systemd 不使用脚本来管理服务。相反,它使用声明性的方法来管理各种服务,记录所需的状态和配置参数,同时使用自己的逻辑确保在正确的时间和顺序启动正确的服务。

理解单元文件

systemd 使用单元文件来声明服务应如何运行。这些单元文件使用 INI 风格的语法,支持每个文件中的节和键/值对。一个服务可以有多个单元文件,它们会对该服务产生整体影响。重要的是要记住,同一个服务的不同单元文件是相关的:

  • *.service单元文件定义了如何启动系统服务,它的依赖关系是什么,systemd 应该如何处理突然的故障等。

  • *.socket单元文件定义了应创建哪些套接字以及应分配给它的权限。systemd 使用它来处理那些可以按需启动而不是在启动时直接启动的服务。

  • *.timer单元文件定义了服务应在何时或以何种频率启动。那些不一定以守护进程方式运行,但需要在定义的间隔内执行某些逻辑的服务,可以使用这些定时器文件来确保定期执行。这些设置与经典但仍广泛使用的 crontab 相似,我们在PAM 服务Cron小节中简要提到过。

也存在其他单元文件,尽管它们更像是通用的系统配置(如切片定义和自动挂载设置),与运行时服务的关系较少。

系统单元文件可以放置在以下三个位置之一:

  • 默认情况下,单元文件由系统的包管理器安装在/usr/lib/systemd/system目录中。

  • 在运行时,更新可以放置在/run/systemd/system目录中,这将覆盖默认位置的单元文件。然而,这个位置是临时的,在重启后不会保留。

  • 系统管理员可以通过将单元文件放置在 /etc/systemd/system 中来覆盖这两个位置的配置。这些单元文件会覆盖先前的定义,因此无需删除先前位置的单元文件。

例如,查看 /usr/lib/systemd/system 中的默认 Nginx 服务单元文件 nginx.service

[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
PrivateTmp=true
[Install]
WantedBy=multi-user.target

这个单元文件声明了启动 Nginx 的命令,并通知 systemd 服务应该在成功达到 networkremote-fsnss-lookup 目标之后启动(这是启动过程中的一个里程碑,允许正确处理依赖关系)。该单元文件还声明它是 multi-user 目标的依赖项(这相当于使用 SysV 风格的 init 服务时的默认运行级别),意味着该服务应在系统启动时启动。

为服务设置 SELinux 上下文

当 systemd 启动一个服务时,它会执行在服务单元文件中的 ExecStart= 配置项中定义的命令。默认情况下,系统会根据 SELinux 策略执行标准的域转换。

然而,软件包开发人员和系统管理员可以更新服务单元文件,以使服务在显式提到的 SELinux 域中启动。为了实现这一点,可以通过在单元文件的 [Service] 部分添加 SELinuxContext= 配置项来扩展该部分。

例如,为了确保 Nginx 使用 httpd_t:s0:c0.c128 上下文启动,你可以使用以下方式:

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
SELinuxContext=system_u:system_r:httpd_t:s0:c0.c128
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
PrivateTmp=true

当然,也可以使用此方法让服务在不同的上下文中运行,这在为守护进程开发自定义策略时非常有用。然而,请记住,SELinux 策略规则仍然适用:例如,你不能要求 systemd 启动 Nginx,并将其放在 dnsmasq_t 域中,而不更新 SELinux 策略,使得 httpd_exec_thttpd_t 域的入口点)也成为 dnsmasq_t 域的入口点。

当你请求 systemd 显式使用 SELinux 上下文时,systemd 会尝试在所有与执行相关的任务中使用此上下文:ExecStartPreExecStartExecStartPostExecStopPreExecStopExecStopPostExecReload。由于这些任务通常没有标记正确的入口点标签,这些命令可能会失败。在这种情况下,可以在命令前加上 +,以便 SELinux 上下文定义不适用于它们:

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=+/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
SELinuxContext=system_u:system_r:httpd_t:s0:c0.c128
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
PrivateTmp=true

在开发和更改单元文件时,修改的设置可能不会立即应用到系统中。在修改单元文件后,运行 systemctl daemon-reload 将确保 systemd 读取系统中的最新更改。

使用临时服务

systemd 还可以用来启动应用程序,使其像服务一样在 systemd 的控制下运行。这些应用程序被称为 临时服务,因为它们没有通常声明 systemd 应如何行为的单元文件。

瞬态服务是通过 systemd-run 应用程序启动的。为了展示这一点,让我们创建一个简单的 Python 脚本(一个计算 Pi 到 10,000 位的小脚本):

from decimal import Decimal, getcontext
getcontext().prec=10000
with open('/tmp/pi.out', 'w') as f:
  print(sum(1/Decimal(16)**k * (
    Decimal(4)/(8*k+1)-
    Decimal(2)/(8*k+4)-
    Decimal(1)/(8*k+5)-
    Decimal(1)/(8*k+6)) for k in range(10000)), file=f)

由于这个过程需要一些时间,我们可以选择在 systemd 控制下运行此 Python 脚本:

# systemd-run python3.6 /tmp/pi.py
Running as unit: run-rf9ce45c...f343.service

由于瞬态服务没有单元文件来管理,因此必须通过命令行来更改 SELinux 上下文。当然,只有当策略中定义的标准域转换未能产生所需行为时,才需要这样做:

# systemd-run -p SELinuxContext=guest_u:guest_r:guest_t:s0 python3.6 /tmp/pi.py

systemd-run 应用程序通过 --property(或 -p)选项支持此功能,通过该选项可以添加单元文件属性。在前面的示例中,我们使用此选项通过 SELinuxContext 属性在 guest_t 域中运行脚本,这类似于我们在单元文件中定义该属性的方式。

需要 SELinux 的服务

有些服务只有在启用或禁用 SELinux 时才应运行。在 systemd 中,可以通过其条件参数来定义此行为。

一个服务单元文件可以包含几个条件,这些条件需要在 systemd 考虑执行该服务之前验证。这些条件可以指向系统类型(虚拟化或非虚拟化)、内核命令行参数、是否存在的文件等等。我们关心的是 ConditionSecurity,它表示给定安全系统的状态——在我们的案例中是 SELinux。

例如,查看 /usr/lib/systemd/system 中的 selinux-autorelabel.service 单元文件:

[Unit]
Description=Relabel all filesystems
DefaultDependencies=no
Conflicts=shutdown.target
After=sysinit.target
Before=shutdown.target
ConditionSecurity=selinux
[Service]
ExecStart=/usr/libexec/selinux/selinux-autorelabel
Type=oneshot
TimeoutSec=0
RemainAfterExit=yes
StandardOutput=journal+console

同样,Linux 发行版提供了 selinux-autorelabel-mark.service 文件。该服务确保,如果系统启动时 SELinux 未激活(且 /.autorelabel 文件尚不存在),则 systemd 会创建一个空的 /.autorelabel 文件。此文件确保在系统以 SELinux 支持重新启动时,执行重新标记操作。

服务启动期间的重新标记文件

许多服务需要的一项操作是准备服务特定的运行时目录,例如 Apache 服务的 /run/httpd。systemd 通过 tmpfiles.d 支持此功能。我们已经在 第四章《使用文件上下文和进程域》中简要介绍了 tmpfiles。在 tmpfiles 中,我们可以定义在这些文件未放置在(持久化的)文件系统中时,请求立即(在启动时)提供或更新的文件和位置。

例如,提供 Apache 守护进程的包会在系统上安装以下定义作为 /usr/lib/tmpfiles.d/httpd.conf

d /run/httpd	710 root apache
d /run/httpd/htcacheclean	700 apache

与 systemd 单元文件类似,包含这些设置的文件应该声明在以下三个位置之一。每个位置都覆盖前一个位置的设置:

  • 默认的、软件包提供的位置是 /usr/lib/tmpfiles.d

  • 运行时声明可以放在 /run/tmpfiles.d 中。

  • 本地系统管理员提供的声明放在 /etc/tmpfiles.d 中。

这些定义可以比仅创建目录更具体。通过 tmpfiles.d 应用程序,可以设置定义来创建文件、预先清空目录、创建子卷、管理特殊文件(如符号链接或块设备)、设置扩展属性等。

其特点之一是设置文件模式和所有权,并恢复文件(z)或递归地针对目录(Z)的 SELinux 上下文。这可以用于更改具有正确上下文定义的文件的上下文,但其上下文未正确分配。

例如,查看 /usr/lib/tmpfiles.d 中的 selinux-policy.conf 文件中的定义:

z /sys/devices/system/cpu/online - - -
Z /sys/class/net - - -
z /sys/kernel/uevent_helper - - -
w /sys/fs/selinux/checkreqprot - - - - 0

我们需要重新标记 /sys 中的文件,因为该位置默认标记为 sysfs_t,并且在运行时更改上下文不会在重启后保留其状态。然而,其中一些文件应该有不同的标签——例如 /sys/devices/system/cpu/online 文件需要 cpu_online_t 标签:

# matchpathcon /sys/devices/system/cpu/online
/sys/devices/system/cpu/online  system_u:object_r:cpu_online_t:s0

该定义确保在启动时重新标记此(伪)文件,以便所有依赖于标有 cpu_online_t 的文件的其他进程可以顺利继续工作。

定义的其他参数在前一个示例中明确标记为破折号,意味着不需要配置其他参数。它们可以用来设置模式、用户标识符 (UID)、组标识符 (GID)、年龄以及与规则相关的参数。

使用 zZ 状态的这些其他参数的示例配置是 systemd.conf 文件:

# grep ^[zZ] /usr/lib/tmpfiles.d/systemd.conf
z /run/log/journal 2755 root systemd-journal - -
Z /run/log/journal/%m ~2750 root systemd-journal - -
z /var/log/journal 2755 root systemd-journal - -
z /var/log/journal/%m 2755 root systemd-journal - -
z /var/log/journal/%m/system.journal 0640 root systemd-journal - -

有关定义格式的更多信息,请参见 man tmpfiles.d

使用基于套接字的激活

系统守护进程还支持基于套接字的激活。当配置完成时,systemd 会创建守护进程通常监听的套接字,并在第一次使用该套接字时启动守护进程。这使得系统可以快速启动(因为许多守护进程不需要立即启动),同时确保所有必需的套接字都可用。

当客户端仅向套接字写入信息时(例如 /dev/log 套接字),客户端甚至不需要等待守护进程激活。数据会存储在缓冲区中,直到守护进程能够读取它。只有当缓冲区满时,操作才会阻塞,直到守护进程刷新缓冲区。

查看位于 /usr/lib/systemd/system 中的 systemd-journald.socket 单元文件:

[Unit]
Description=Journal socket
Documentation=man:systemd-journal.service(8) man:journald.conf(8)
DefaultDependencies=no
Before=sockets.target
IgnoreOnIsolate=yes
[Socket]
ListenStream=/run/systemd/journal/stdout
ListenDatagram=/run/systemd/journal/socket
SocketMode=0666
PassCredentials=yes
PassSecurity=yes
ReceiveBuffer=8M
Service=systemd-journald.service

当客户端使用上述套接字之一时,systemd 将启动 systemd-journald.service 单元来处理客户端交互。只要这些套接字未被使用,服务将不会启动。

[Socket] 部分中,可以定义一个 SELinux 特定的条目:SELinuxContextFromNet=true。当一个单元文件设置了这个条目时,systemd 会从客户端上下文(连接到套接字的应用程序)中获取 MLS/MCS 信息,并将其附加到服务的上下文中。这种敏感性继承可以用来防止在通过套接字进行通信时发生信息泄露。

管理单元操作访问

到目前为止,我们已经查看了与 systemd 的 SELinux 支持相关的配置设置。systemd 还使用 SELinux 来控制对通过单元文件定义的服务的访问。当用户想对某个单元执行操作(例如启动服务或检查运行中的服务状态)时,systemd 会查询 SELinux 策略,以查看是否允许此操作。

systemd 守护进程使用服务类来验证客户端域对请求操作的权限。例如,为了验证用户上下文 sysadm_t 是否可以查看与 sshd.service 单元文件相关的服务状态,它会检查该文件的上下文(为 sshd_unit_file_t),然后验证是否授予了状态权限:

# sesearch -s sysadm_t -t sshd_unit_file_t -c service -p status -A

其他支持的权限有 disableenablereloadstartstop。当权限未被授予时,审计日志中会显示 USER_AVC 拒绝消息(而非 AVC 消息),因为该消息不是由 Linux 内核生成的,而是由 systemd 生成的。因此,虽然规则本身是 SELinux 策略的一部分,但访问控制是由 systemd 强制执行的。

systemd 或通过 systemd 查询的客户端可能还会提供额外的错误信息,表明 SELinux 策略阻止了该操作。例如,如果我们尝试通过 D-Bus 查询 systemd(我们将在 D-Bus 通信 部分介绍),并且从一个非特权用户域发起查询,那么我们会得到以下错误:

Error: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: SELinux policy denies access

为了便于故障排除,systemd 还具有一个广泛的日志记录组件,叫做 systemd-journald,我们将在接下来介绍。

使用 systemd 进行日志记录

systemd 不仅负责服务管理,还承担着其他几个任务。其中一个任务是日志管理,传统上通过系统日志记录器来实现。

虽然 systemd 仍然支持使用传统的系统日志记录器,但现在建议使用 systemd-journald。日志守护进程的一个优势是,它不局限于文本的单行日志消息。守护进程现在可以使用二进制数据以及多行消息作为日志记录的一部分。

日志守护进程还会注册关于发送进程的信息,并与日志消息本身一起记录。这些附加信息包含了所有者数据(进程所有者),包括发送进程的 SELinux 上下文。

检索与 SELinux 相关的信息

传统的接收 SELinux 相关信息的方法(排除我们之前讨论的审计事件)是通过 grep 在日志信息中查找。使用日志守护进程,我们可以通过以下方式实现:

# journalctl -b | grep -i selinux

传递给日志控制应用程序的 -b 选项通知日志守护进程,我们只关心来自特定引导的日志消息。

根据 SELinux 上下文查询日志

日志守护进程的一个独特功能是使用与日志消息相关的信息作为查询的一部分,针对日志数据库进行查询。例如,我们可以要求日志守护进程只显示那些来自于运行在 udev_t 上下文中的守护进程或应用程序的消息:

# journalctl _SELINUX_CONTEXT=system_u:system_r:init_t:s0

可用的上下文可以通过系统上的 Bash 自动补全支持获取。在输入 _SELINUX_CONTEXT= 后,按 Tab 键两次即可查看可能的值。

使用 setroubleshoot 集成与日志

SELinux 故障排除守护进程也与 systemd-journald 集成。来自 setroubleshootd 的任何警报也可以通过日志守护进程获取。

这有助于管理员在调查问题时能快速发现 SELinux 拒绝。例如,当 Nginx Web 服务器无法正常工作,且这是由于 SELinux 策略导致时,快速检查服务的状态会显示 SELinux 策略正在阻止某些操作:

# systemctl status nginx

要获取更多关于消息的信息,可以使用 journalctl

# journalctl -xe

如你所见,systemd-journald 已经捕获了与服务相关的环境信息,这些信息能够为解决潜在问题提供宝贵的指导。

另一个具有 SELinux 配置功能的 systemd 服务是设备守护进程。

处理设备文件

Linux 有着悠久的设备管理历史。最初,管理员需要确保设备节点已经存在于文件系统中(/dev 是持久化文件系统的一部分)。随着时间的推移,Linux 逐渐采用了更动态的设备管理方式。

现在,设备文件通过伪文件系统(devtmpfs)和一个名为 udev 的用户空间设备管理器进行管理。这个设备管理器也被并入到 systemd 中,成为 systemd-udevd

设备管理器通过内核套接字监听内核事件。这些事件向设备管理器报告检测到的或已插入的设备(或此类设备的移除),并允许设备管理器采取适当的行动。对于 udev,这些操作在 udev 规则中定义。

使用 udev 规则

配置 udev 子系统主要通过 udev 规则完成。这些规则是包含匹配部分和操作部分的单行指令。

匹配部分包含对 udev 从 Linux 内核接收到的事件进行的验证。此验证使用从事件中获取的键/值对,包含以下可能的键:

  • 内核提供的设备名称(KERNEL

  • 设备子系统(SUBSYSTEM

  • 内核驱动程序(DRIVER

  • 特定属性(ATTR

  • 活跃环境变量(ENV

  • 指示设备是否被检测到或移除的操作类型(ACTION

虽然可能有更多的匹配键,但上述列表是最常用的。

Linux 内核还会向设备管理器通报设备的层级结构。这使得可以基于某些因素定义规则,例如 USB 设备是通过哪个 USB 控制器插入的。除了设备本身的信息,内核还会通过类似的键值对提供层级相关的信息。然而,这些键值对使用复数形式的键定义:SUBSYSTEMS 代替 SUBSYSTEMDRIVERS 代替 DRIVER,等等。

例如,要匹配供应商 ID 为 05a9 和产品 ID 为 4519 的 USB 网络摄像头,匹配相关的键值对可能如下所示:

KERNEL=="video[0-9]*", SUBSYSTEM=="video4linux", SUBSYSTEMS=="usb", ATTR{idVendor}=="05a9", ATTR{idProduct}=="4519"

udev 规则的第二部分是要执行的操作。最常见的操作是创建指向已创建设备文件的符号链接,确保即使从内核角度看设备名称发生变化,应用程序仍然可以通过相同的符号链接访问相同的设备。例如,我们可以通过 SYMLINK+="webcam1" 扩展前面的例子,使 /dev/webcam1 指向此新检测到的设备。

当然,udev 应用程序支持比仅仅定义符号链接更多的操作。它可以在设备上设置所有权(OWNER)或组成员身份(GROUP),控制谁可以访问设备。udev 还可以设置环境变量(ENV),甚至在匹配的设备插入或从系统中拔出时执行命令(RUN)。为了确保命令仅在设备被添加时执行,我们需要添加一个如 ACTION=="add"ACTION 设置。

重要提示

udev 可以将 ENV 作为匹配键和操作键进行解释。它们的区别在于执行的操作(单个等号 = 或双等号 ==)。ENV{envvar}=="value" 是匹配操作(检查变量是否与给定的 value 匹配),而 ENV{envvar}="value" 是操作(将变量设置为 value)。

udev 规则默认存储在 /usr/lib/udev/rules.d 位置。发行版和应用程序/驱动程序会将其默认规则存储在此位置。额外的规则或规则覆盖可以放在 /etc/udev/rules.d 中。

需要记住的是,即使 udev 已经遇到匹配的规则,它仍会继续处理规则。这可以通过 OPTIONS 操作在每条规则的基础上进行更改,例如 OPTIONS+="last_rule",它告诉 udev 停止进一步处理该事件的规则。

在设备节点上设置 SELinux 标签

udev 支持的其中一个操作是为设备节点分配 SELinux 标签。我们可以使用 SECLABEL{selinux} 操作来实现这一点:

KERNEL=="fd0", ..., SECLABEL{selinux}="system_u:object_r:my_device_t:s0"

请注意,这个操作仅设置设备节点上的上下文。如果规则还设置了符号链接,那么符号链接本身将继承默认的 device_t 上下文。

给设备节点放置 SELinux 标签通常是与其他安全相关权限一起进行的,因此该规则通常会执行附加的操作,例如设置目标所有者(OWNER)、组(GROUP)和权限集(MODE)。毕竟,SELinux 安全控制仅在常规的自由访问控制检查通过后才会生效,所以不要忘记确保用户在 SELinux 控制之外也能访问设备节点。

到目前为止,我们看到的所有设置都与 systemd 服务管理和系统支持有关。systemd 生态系统中的另一个组件是 D-Bus,它更多的是促进不同应用程序之间通过可编程通信总线进行通信和交互,而非系统管理。

通过 D-Bus 进行通信

D-Bus 守护进程提供了一个应用程序之间的进程间通信通道。与传统的 IPC 方法不同,D-Bus 是一个更高级别的通信通道,提供的不仅仅是简单的信号传递或内存共享。想要通过 D-Bus 进行通信的应用程序可以链接到许多与 D-Bus 兼容的库,如 libdbus、sd-bus(systemd 的一部分)、GDBus 和 QtDBus 应用程序提供的库。

D-Bus 守护进程是 systemd 应用程序套件的一部分。

了解 D-Bus

Linux 通常支持两种 D-Bus 类型——系统范围的和特定会话的 D-Bus 实例:

  • 系统范围的 D-Bus 是用于系统通信的主要实例。许多服务或守护进程会将自己与系统 D-Bus 关联,以便其他应用程序通过 D-Bus 与它们进行通信。

  • 特定会话的 D-Bus 是为每个登录用户运行的实例。它通常被图形化应用程序用来在用户会话内相互通信。

这两个 D-Bus 实例都是通过 dbus-daemon 应用程序提供的。系统范围的 D-Bus 会以 --system 选项运行,而特定会话的实例会以 --session 选项运行。

应用程序通过命名空间向 D-Bus 注册自己。通常,命名空间使用项目的域名。例如,systemd 声明了 org.freedesktop.systemd1 命名空间,而 D-Bus 的命名空间是 org.freedesktop.DBus

当前关联的应用程序可以通过 Python 简单查询:

# python3.6
>>> import dbus
>>> for service in dbus.SystemBus().list_names():
...   print(service)
org.freedesktop.DBus
org.freedesktop.login1
org.freedesktop.systemd1
org.freedesktop.PolicyKit1
com.redhat.tuned
:1.10
:1.11
org.freedesktop.NetworkManager
...

每个应用程序然后在总线上提供对象,其他对象(其他应用程序)可以访问这些对象——当然,前提是它们具有相应的权限。这些对象通过类似路径的语法表示,通常也会使用项目的域名作为前缀。

例如,要列出当前与org.freedesktop.systemd1关联的对象,我们可以使用gdbus命令。为了方便使用,我们首先启用自动完成支持,然后可以使用Tab键轻松添加适当的值:

# source /usr/share/bash-completion/completions/gdbus
# gdbus call --system --dest <TAB><TAB>
# gdbus call --system --dest org.freedesktop.systemd1 --object-path /org/freedesktop/systemd1<TAB><TAB>
Display all 220 possibilities? (y or no)
/org/freedesktop/systemd1
/org/freedesktop/systemd1/job
/org/freedesktop/systemd1/unit
...

应用程序可以触发这些对象上的方法,或通过这些方法向绑定到这些对象的应用程序发送消息。

例如,要通过 D-Bus 获取sshd.service单元的状态,我们在org.freedesktop.systemd1对象上的/org/freedesktop/systemd1路径调用org.freedesktop.systemd1.Manager.GetUnitFileState方法,并带有sshd.service参数,像这样:

# gdbus call --system \
  --dest org.freedesktop.systemd1 \
  --object-path /org/freedesktop/systemd1 \
  --method org.freedesktop.systemd1.Manager.GetUnitFileState \
  sshd.service
('enabled',)

我们接下来将学习,这些调用也可以通过 SELinux 策略进行控制。

通过 SELinux 控制服务获取

D-Bus 应用程序(如 systemd)将查询 SELinux 策略,以验证是否允许操作。再次强调,实施策略的是 D-Bus 应用程序本身,而不是 Linux 内核子系统。

管理员可以在 D-Bus 中启用的第一个控制是确保只有确立良好的域才能在 D-Bus 中获取指定的对象。如果没有此控制,恶意代码例如可能会注册自己为org.freedesktop.login1,并在总线上作为系统守护进程运行。其他应用程序可能会错误地向应用程序发送敏感信息。

应用程序将此策略信息存储在位于/usr/share/dbus-1/system.d的文件中。例如,登录服务(存储为org.freedesktop.login1.conf)安装了以下策略片段:

<busconfig>
  <policy user="root">
    <allow own="org.freedesktop.login1"/>
    <allow send_destination="org.freedesktop.login1"/>
    <allow receive_sender="org.freedesktop.login1"/>
  </policy>
  <policy context="default">
    <deny send_destination="org.freedesktop.login1"/>
    <allow
       send_destination="org.freedesktop.login1"
       send_interface="org.freedesktop.DBus.Introspectable"/>
    ...
  </policy>
</busconfig>

由于登录守护程序在systemd_logind_t域中运行,我们可以按以下方式增强此配置:

<busconfig>
  <selinux>
    <associate
      own="org.freedesktop.login1"
      context="system_u:system_r:systemd_logind_t:s0" />
  </selinux>
  ...
</busconfig>

配置完毕后,D-Bus 将检查应用程序(我们假定其在systemd_logind_t上下文中运行)是否具有acquire_svc权限(dbus类)对systemd_logind_t上下文。默认情况下,SELinux 策略不具有此权限,因此注册失败:

# systemctl restart dbus-org.freedesktop.login1
Job for systemd-logind.service failed because a timeout was exceeded.
See "systemctl status systemd-logind.service" and "journalctl -xe" for details.
# ausearch -m user_avc -ts recent

当我们添加以下 SELinux 策略规则时,systemd-logind的注册将如预期般成功:

(allow systemd_logind_t systemd_logind_t (dbus (acquire_svc)))

加载此策略(例如test.cil)并再次尝试restart操作:

# semodule -i test.cil
# systemctl restart dbus-org.freedesktop.login1

通过限制哪些域可以获取特定服务,我们确保仅使用受信任的应用程序。即使非受信任的应用程序获得了 root 权限(这是 D-Bus 对登录服务进行的另一个检查,如第一个busconfig片段所示),它们通常也不会在该应用程序的域中运行。

管理员可以增强此 D-Bus 配置,而无需更改现有的配置文件。例如,先前提到的 SELinux 管理的busconfig片段完全可以保存为不同的文件。

管理消息流

D-Bus 验证的第二个控制是哪些应用程序可以相互通信。这不能通过服务配置进行配置,而是一个纯粹的 SELinux 策略控制。

每当源应用程序调用目标应用程序的方法时,D-Bus 会验证源应用程序和目标应用程序之间的 send_msg 权限。

例如,在用户域(sysadm_t)和服务域(systemd_logind_t)之间通过 D-Bus 进行的通信将检查以下权限:

allow sysadm_t systemd_logind_t : dbus send_msg;
allow systemd_logind_t sysadm_t : dbus send_msg;

如果没有授予这些权限,D-Bus 将不允许通信发生。如果在任何时候,无法获取应用程序上下文,则将使用总线守护进程上下文。

失败将作为 USER_AVC 条目记录在审计日志中。如果通信应该被允许,我们可以像这样创建一个简单的 SELinux 策略文件来解决这个问题:

(allow sysadm_t systemd_logind_t (dbus (send_msg)))
(allow systemd_logind_t sysadm_t (dbus (send_msg)))

将这些规则存储在以 .cil 结尾的文件中(例如,local_logind_systemd.cil),并使用 semodule 加载它:

# semodule -i local_logind_systemd.cil

让我们考虑一些其他具有 SELinux 支持的应用程序,这些支持并不一定是内置的,而是通过系统内的 SELinux 策略和 PAM 集成实现的。

配置 PAM 服务

systemd 和 D-Bus 是支持 SELinux 的应用程序,内置了明确的 SELinux 支持。Linux 系统中还有其他一些服务与 SELinux 配合良好,尽管它们本身并不支持 SELinux。许多这些服务通过 PAM 集成与 SELinux 相关联。

我们在第三章管理用户登录中讨论了 PAM 集成。在本节中,我们将介绍三个使用 PAM 的示例服务,以及如何进一步微调 SELinux 以支持这些服务。

Cockpit

Cockpit 是一个简单的基于浏览器的管理应用程序,它允许管理员轻松查看系统资源(监控)并与系统交互。它还允许用户通过浏览器登录系统。

正是这个基于浏览器的终端我们需要配置:通过调整 SELinux 用户的目标 SELinux 角色,我们可以有选择地将用户放入特定角色。这实际上定义了用户可以通过此基于浏览器的会话完成的任务。

安装 Cockpit

Cockpit 应用程序在 CentOS 仓库中随时可用,因此安装起来非常简单:

# yum install cockpit

虽然应用程序无需额外配置,但如果需要调整,您需要自己创建配置文件 /etc/cockpit/cockpit.conf,因为该应用程序不会创建默认配置文件。在此配置文件中,您可以配置 TLS 设置,或者普遍禁用加密通信。

为了演示运行,让我们禁用加密通信(但如果你打算在生产环境中使用 Cockpit,除了保持加密开启外,还应该确保只有受信任的主机能连接,可能还需要使用ClientCertAuthentication指令来要求客户端证书认证):

[WebService]
AllowUnencrypted=true

设置好这些之后,我们可以继续配置 SELinux 以支持 Cockpit。

限制用户登录

通过这些步骤,我们将把更为受限的user_r角色添加到staff_u SELinux 用户,并确保所有映射到staff_u SELinux 用户的登录都通过user_r角色登录到 Cockpit。如果他们通过其他服务登录,则会继续使用默认的staff_r角色。

注意

使用user_r角色而不是(更加受限的)guest_r角色,是为了让 Cockpit 应用程序能够正常工作。应用程序将在用户的权限下运行服务,如果我们使用guest_t用户域,权限将不足以支撑 Cockpit 的正常运行。

让我们首先添加user_r角色,以便稍后将用户放入正确的上下文中:

# semanage user -m -R "staff_r sysadm_r system_r user_r" staff_u

接下来,我们希望更新 SELinux 配置,以确保任何由staff_u映射的用户登录 Cockpit 时都将使用user_r角色。Cockpit 应用程序的登录是通过在cockpit_session_t上下文中运行的服务进行的,我们通过先检查进程的上下文,然后登录 Cockpit 并再次检查进程的上下文来得知这一点。在这里,我们注意到一个新进程(cockpit-session)以cockpit_session_t上下文运行:

# ps -eZ | grep cockpit
system_u:system_r:cockpit_ws_t:s0	 ... cockpit-ws
system_u:system_r:cockpit_session_t:s0 ... cockpit-session localhost

现在有了这些信息,我们可以按照如下方式编辑/etc/selinux/targeted/contexts/users/staff_u文件:

system_r:local_login_t:s0   staff_r:staff_t:s0 sysadm_r:sysadm_t:s0
system_r:remote_login_t:s0  staff_r:staff_t:s0
system_r:sshd_t:s0          staff_r:staff_t:s0 sysadm_r:sysadm_t:s0
system_r:cockpit_session_t:s0   user_r:user_t:s0
system_r:crond_t:s0         staff_r:staff_t:s0 staff_r:cronjob_t:s0

通过调整cockpit_session_t上下文中列出的角色顺序(或者将其限制为仅使用user_r角色),我们可以确保允许以user_r角色运行的用户(就像我们之前配置的staff_u用户)通过user_r角色进行操作。由于这个角色比默认的staff_t用户域更为受限,因此通过 Cockpit 进行的登录会更加隔离。

这种方法适用于所有支持 PAM 的服务,因为它仅依赖于服务 PAM 配置中的pam_selinux.so调用。对于某些服务,SELinux 策略管理员还会加入更多的调整,例如在 cron 和 SSH 中,我们接下来会讨论。

Cron

系统中的 Cron 服务允许你按预定计划运行任务或命令。一些 Cron 应用程序明确支持 SELinux(如 fcron),使其能够计算作业应运行的目标上下文。即使是没有任何 SELinux 逻辑的 Cron 系统,也可以进行精细调优。

切换用户特定与通用上下文

通过 SELinux 策略支持的常见设置是切换用户任务是否在用户的默认上下文中运行(例如,员工用户的 staff_t),或在默认的受限 cron 上下文中运行(cronjob_t)。这两种方法各有优缺点。

当我们配置系统使得用户作业在用户的默认上下文中运行时,用户将知道他们作业的权限。访客用户有访客权限,员工用户有员工权限,依此类推。这是最常见的配置,CentOS 上的默认 cron 系统使用包含用户任务的文件上下文(位于 /var/spool/cron)来推断目标运行时上下文。

通过在更受限制的上下文中运行用户任务,如cronjob_t,所有用户的 cron 作业将以相同的权限运行,管理员可以轻松微调所有用户作业的权限。这还允许管理员为 cron 作业授予特定权限,同时保持用户上下文不包含这些权限。

让我们设置一个简单的任务,每分钟执行一次,即休眠 59 秒。作为普通用户,创建一个文件(假设是 lisa.cron),内容如下:

* * * * * sleep 59

此文件使用常见的 cron 语法,其中适用以下规则:

  1. 第一个字段表示分钟。

  2. 第二个字段表示小时。

  3. 第三个字段表示日期。

  4. 第四个字段表示月份。

  5. 第五个字段表示星期几。

  6. 该行的其余部分是要执行的命令。

各个字段可以使用表达式来简化时间定义。例如,要每 15 分钟执行一次,可以在第一个字段中使用 */15。如果你只想在 8 点和 18 点执行,可以在第二个字段中使用 8,18。另一个例子是,如果你只想在工作日执行,可以在第五个字段中使用 1-5(在 cron 中,星期天的有效值是 0 和 7)。

通过使用 crontab 命令加载它,文件会被检查错误,如果没有错误,它将安全地放置在 /var/spool/cron 中(crontab 命令是一个 setuid 命令,能够修改 /var/spool/cron,即使常规用户无法访问此位置):

$ crontab ./lisa.cron

从这里,cron 守护进程会拾取这个文件,1 分钟后,我们将看到命令在后台激活:

$ ps -efZ | grep sleep
staff_u:staff_r:staff_t:s0 ...  sleep 59

从输出中可以看到,命令正在 staff_t 上下文中运行。要将其更改为 cronjob_t 类型,而不是像我们在 Cockpit 应用程序中那样编辑 SELinux 上下文定义文件,可以使用 cron_userdomain_transition SELinux 布尔值:

# setsebool cron_userdomain_transition off

该布尔值会更改活动的 SELinux 策略行为,使得从 cron 系统执行的任何用户任务都在 cronjob_t 域中执行。您可能需要重置 crontab 定义(这取决于使用的 cron 系统),但之后,我们将看到作业在 cronjob_t 域中运行:

$ ps -efZ | grep sleep
staff_u:staff_r:cronjob_t:s0 ...  sleep 59

使用 SELinux 布尔值来允许管理员根据需要区分系统行为是常见的做法。对于 SSH 守护进程,SELinux 策略管理员定义了类似的规则。

OpenSSH

OpenSSH 守护进程是最常见的安全 Shell 守护进程。它允许用户通过终端远程访问系统,还可以安全地传输文件、建立应用程序通信隧道等。

通过 SSH 登录时,PAM 控制会生效,但 SELinux 策略也嵌入了特定的 SSH 控制,并可以通过 SELinux 布尔值进行控制。

直接以 sysadm_t 登录

第一个需要评估的更改是允许直接以sysadm_r角色登录。默认情况下,映射到staff_u SELinux 用户的用户使用(限制更多的)staff_r角色登录,然后需要显式切换角色以获得更特权的sysadm_r角色。

我们需要进行的第一个更改是编辑/etc/selinux/targeted/contexts/users/staff_u文件,并调整sshd_t上下文中列出的角色顺序:

system_r:local_login_t:s0   staff_r:staff_t:s0 sysadm_r:sysadm_t:s0
system_r:remote_login_t:s0  staff_r:staff_t:s0
system_r:sshd_t:s0          sysadm_r:sysadm_t:s0 staff_r:staff_t:s0 
system_r:cockpit_session_t:s0   user_r:user_t:s0
system_r:crond_t:s0         staff_r:staff_t:s0 staff_r:cronjob_t:s0

然而,这还不够。SELinux 策略管理员已禁用通过 SSH 直接登录到sysadm_r角色,迫使用户显式切换角色(从而重新认证)。这种做法是因为 SSH 通常是一个公开可访问且难以控制的服务(与如 Web 服务器等服务不同,后者可以通过反向代理和 Web 应用防火墙进行保护)。

将 SELinux 的ssh_sysadm_login布尔值更改为true以启用所需的行为:

# setsebool ssh_sysadm_login true

该布尔值更改 SELinux 策略行为,允许通过 SSH 守护进程登录到sysadm_r角色。

Chroot Linux 用户

SSH 支持的另一个功能是强制选定用户的登录进行 chroot 操作。chroot(即改变根目录的合成词)是一种进程隔离方法,进程不再看到整个文件系统,而只能看到其中的一部分。

信息性说明

现在,chroot 环境是隔离进程的一种简便方法,但 chroot 本身仍然受到 Linux 的自由访问控制的管理,逃脱 chroot 环境并非不可能。建议使用 SELinux 进一步限制进程,但这超出了本节的范围。有关内容,请参见第十四章处理新应用程序

在我们配置 SSH 以 chroot 某些用户之前,我们需要创建一个正确运行的环境:一旦我们更改了进程的根目录,进程希望读取或执行的所有命令和库都需要在此 chroot 环境中可用。

让我们首先创建一个 chroot 环境。一个有用的工具是 Jailkit,它有助于创建正确的文件夹结构和文件。Jailkit 默认不在常规仓库中提供,但可以轻松安装,并且只需要一个有效的编译器和 Python 环境。

我们首先安装必要的依赖项:

# yum install gcc python36-devel

接下来,我们下载 Jailkit 源代码并进行构建。由于 CentOS 默认没有链接的 Python 二进制文件(因为它需要使用 python3 作为运行时),我们需要告诉构建脚本如何处理 Python。我们通过声明 PYTHONINTERPRETER 环境变量来实现:

# wget https://olivier.sessink.nl/jailkit/jailkit-2.21.tar.bz2
# tar xvf jailkit-2.21.tar.bz2
# cd jailkit-2.21
# export PYTHONINTERPRETER=/usr/bin/python3
# ./configure
# make
# make install

安装完成后,您可能需要移除 Jailkit 配置文件中重复的 includesections 调用(如果您没有移除,接下来的 jk_init 命令会提示您)。/etc/jailkit/jk_init.ini 中的 openvpn 部分应如下所示:

[openvpn]
comment = jail for the openvpn daemon
paths = /usr/sbin/openvpn
users = root,nobody
groups = root,nobody
devices = /dev/urandom, /dev/random, /dev/net/tun
includesections = netbasics, uidbasics
need_logsocket = 1

配置更新后,我们现在可以创建 chroot 环境。让我们创建 /srv/chroot 目录,并使用 jk_init 命令将必要的文件、目录、设备节点等添加到该目录中:

# mkdir /srv/chroot
# jk_init -v -j /srv/chroot extshellplusnet

我们希望确保此位置内资源的 SELinux 上下文与根位置相等,因此让我们创建一个文件上下文等效定义:

# semanage fcontext -a -e / /srv/chroot
# restorecon -RvF /srv/chroot

设置好 chroot 环境后,我们现在可以更新 SSH 配置,以便将用户限制在 chroot 中:

Match User lisa
  X11Forwarding no
  AllowTcpForwarding no
  ChrootDirectory /srv/chroot

虽然并非所有系统都适用(因为这取决于发行版),我们可能需要告诉 SELinux 策略,用户域的用户可以进行 chroot 操作。此权限(sys_chroot)通常在默认情况下未启用:

# setsebool selinuxuser_use_ssh_chroot true

设置完成后,重启 SSH 守护进程并查看 chroot 是否成功:

# systemctl restart ssh

Chroot 环境不仅仅对 SSH 访问有意义;其他守护进程也可能支持 chroot 环境,以进一步保护系统资源。在过去,chroot 支持是进一步强化系统的一种常见方式。然而,命名空间和资源隔离的支持已经大大超越了 chroot 监狱的需求。这些新特性也推动了容器化生态系统的发展,我们将在第十一章中讨论,增强容器化工作负载的安全性

对于诸如 Cockpit、cron 和 OpenSSH 等应用程序,SELinux 的支持通常通过 SELinux 策略提供,并使用 PAM 集成将 SELinux 控制链接到应用程序中。然而,也可以显式地在未专门支持 SELinux 的应用程序中构建 SELinux 支持,只要这些应用程序支持通过模块化设计动态添加逻辑。作为此例,我们接下来将讨论 Apache 和 mod_selinux Apache 模块。

使用 mod_selinux 配合 Apache

应用程序通常是基于 Web 的,它们将界面暴露为常见的网站或简单的 Web 服务,并在 Web 服务器中执行大部分逻辑,或者在 Web 服务器与用户交互的后台服务中执行逻辑。

基于 Web 的应用程序具有巨大的优势,即最终用户通常不需要在其设备上安装任何额外的应用程序或客户端,无论是工作站、笔记本电脑、手机、手表还是智能电视。

然而,与之前讨论的服务不同,Apache 并没有通过系统上的 PAM 登录运行单独的用户会话。相反,用户请求是由 Web 服务器线程和进程本身处理的,这使得简单的基于 SELinux 的控制变得有些困难。

介绍 mod_selinux

Apache 支持模块:可以动态加载的代码,增强 Web 服务器的功能,而无需重新编译 Web 服务器的代码。正是这种模块化使 Apache 变得非常流行,正如我们通过它对 PHP 等功能的支持所看到的,它将动态 Web 应用程序引入到一个曾经仅用于提供静态内容的服务器平台中。

mod_selinux使用相同的模块化支持,使得 Apache Web 服务器可以变得对 SELinux 更加敏感。一旦我们启用mod_selinux,就可以配置 Apache 切换 SELinux 敏感性,甚至为运行的代码切换 SELinux 域,从而进一步隔离 Web 服务器的行为,并允许 SELinux 策略控制 Web 服务器能够执行的操作。mod_selinux还支持用户映射,使 Apache Web 服务器能够在不同的域中运行特定的用户会话。

在构建mod_selinux模块之前,我们先在系统上安装必要的依赖项:

# yum install gcc git httpd httpd-devel redhat-rpm-config libselinux-devel

安装完依赖项后,我们可以下载并构建mod_selinux代码。该代码可以从 GitHub 的 Kaigai 的mod_selinux仓库中获取:

# git clone https://github.com/kaigai/mod_selinux
# cd mod_selinux
# apxs -c -i mod_selinux.c

apxs命令是-c)和安装(-imod_selinux模块的命令。不过,我们还没有在 Apache 配置中激活它,我们通过在/etc/httpd/conf.modules.d目录下创建一个新的模块配置文件99-selinux.conf(你可以选择任何你喜欢的名称,但确保它以.conf后缀结尾)来实现这一点:

LoadModule selinux_module modules/mod_selinux.so

现在,虽然我们已经安装了该模块,但它还不能使用,因为我们还没有加载适用于它的 SELinux 策略。

mod_selinux仓库包含了必要的 SELinux 策略代码。然而,它与 Linux 发行版使用的较新 SELinux 策略并不完全兼容。我们需要编辑mod_selinux.if文件,并删除所有关于httpd_user_script_ro_thttpd_user_script_rw_thttpd_user_script_ra_t的引用,因为这些类型在当前的 SELinux 策略中已经不再出现:

# sed -i '/script_r/d' mod_selinux.if

第二个更改——目前是外观上的——是将函数调用从miscfiles_read_certs重命名为miscfiles_read_generic_certs。这些是参考策略中使用的函数,参考策略是编写 SELinux 策略的一种不同方式——仍然是最常见的方式(我们将在第十五章使用参考策略中详细介绍)。虽然在写作时这两个函数都被支持,但miscfiles_read_certs函数不再推荐使用,并且很快会被废弃:

# sed -i 's/miscfiles_read_certs/miscfiles_read_generic_certs/g' mod_selinux.if

一旦我们调整了策略,就可以构建并加载它。由于该策略是使用参考策略样式开发的,安装时首先需要构建模块,然后再加载它(与我们到目前为止使用的直接可加载的 CIL 示例不同)。

# make -f /usr/share/selinux/devel/Makefile mod_selinux.pp
# semodule -i mod_selinux.pp

在加载 SELinux 模块并安装 mod_selinux Apache 模块后,我们可以开始使用 SELinux 特定的控制配置 Apache 守护进程。

配置一般的 Apache SELinux 敏感度

mod_selinux 支持的最简单配置设置是配置 Apache 以特定的 SELinux 敏感度运行。假设我们希望 Apache 以 s0-s0:c0.c100 敏感度运行,那么我们需要调整 Apache 配置并使用 selinuxServerDomain 指令。

假设我们想调整默认欢迎页面的敏感度,可以编辑 /etc/httpd/conf.d/welcome.conf 并添加以下代码片段:

<IfModule mod_selinux.c>
  selinuxServerDomain *:s0-s0:c0.c100
</IfModule>

如果 Apache Web 服务器使用虚拟主机定义(允许一个 Web 服务器定义管理多个网站,基于客户端用于访问 Web 内容的主机名),则需要使用 selinuxDomainVal 指令,而不是 selinuxServerDomain 指令。

例如,假设 Web 服务器管理两个虚拟主机,一个用于 apps.genfic.local 域,另一个用于 intranet.genfic.local,那么我们可以像这样为每个虚拟主机分配各自的敏感度设置:

<VirtualHost *:80>
  DocumentRoot /srv/web/apps/htdocs
  ServerName apps.genfic.local
  selinuxDomainVal *:s0:c1,c2
</VirtualHost>
<VirtualHost *:80>
  DocumentRoot /srv/web/intranet/htdocs
  ServerName intranet.genfic.local
  selinuxDomainVal *:s0:c3,c4
</VirtualHost>

重启 Apache Web 服务器并验证该设置是否已生效:

# systemctl restart httpd
# ps -efZ | grep httpd_t
system_u:system_r:httpd_t:s0-s0:c0.c100 ... /usr/sbin/httpd

如您所见,Web 服务器现在正在以给定的敏感度运行。不过,重要的警告是:mod_selinux 代码不支持 mcstransd,即我们在 第三章 中介绍的翻译守护进程,管理用户登录,因此不能使用诸如 SystemLow-SystemHigh 之类的易读敏感度定义。

将最终用户映射到特定域

要将用户映射到特定域(当用户登录到 Web 应用时),我们需要创建一个用户映射文件。然后,在 Web 服务器配置中使用 selinuxDomainMap 指令引用该映射文件。

首先,我们在 /etc/httpd/conf.d 下创建映射文件,命名为 mod_selinux.map,并使用以下内容:

test	user_webapp_t:s0:c0.c100
*	user_webapp_t:s0:c0,c1
__anonymous__	anon_webapp_t:s0

该映射文件包含三个映射:

  • 第一个映射适用于名为 test 的用户,映射到 user_webapp_t 域,并具有 s0:c0.c100 敏感度。

  • 第二个映射适用于任何成功通过身份验证的用户,映射到 user_webapp_t 域,并具有 s0:c0,c1 敏感度。

  • 第三个映射适用于未通过身份验证的用户,映射到 anon_webapp_t 域。

然后,我们可以通过调整先前创建的代码片段来引用此映射:

<IfModule mod_selinux.c>
  selinuxServerDomain *:s0-s0:c0.c100
  selinuxDomainMap /etc/httpd/conf.d/mod_selinux.map
</IfModule>

重启 Web 服务器以应用更改。

基于源更改域

mod_selinux模块还支持根据我们在其他地方配置中定义的环境变量设置服务器域值。例如,我们可以在某个条件触发时,首先在环境变量中声明该值,然后告诉mod_selinux使用该环境变量的值来设置服务器域。

让我们通过一个例子来更直观地理解这一点。假设网站同时管理本地(内部)用户和远程办公用户的 Web 应用程序。假设这些用户通过不同的源 IP 地址访问 Web 服务器,我们可以通过源 IP 地址来区分两者,并分配不同的 SELinux 敏感度值。

我们可以在 Apache 配置中使用SetEnvIf指令来实现这一点,该指令声明一个环境变量,但仅当请求符合特定条件时才会声明。我们使用的条件是Remote_Addr指令,它会检查源 IP 地址是否与后面的表达式匹配。

假设本地用户来自10.10.0.0/16,远程用户来自负载均衡器或反向代理,IP 地址为10.121.12.15,那么我们可以如下区分:

SetEnvIf Remote_Addr "10.10.[0-9]+.[0-9]+$" SENSITIVITY=*:s0:c0.c80
SetEnvIf Remote_Addr "10.121.12.15" SENSITIVITY=*:s0:c90
selinuxDomainEnv SENSITIVITY

可以混合使用多个mod_selinux指令。该模块会使用第一个成功的声明,因此你可以首先使用用户映射,如果该映射没有命中(因为该用户没有在映射中声明),则使用环境变量,如果仍然失败,则回退到默认设置。

为了实现这种回退定义,我们只需要按顺序声明mod_selinux指令,像这样:

selinuxDomainMap /etc/httpd/conf.d/mod_selinux.map
selinuxDomainEnv SENSITIVITY
selinuxDomainVal *:s0:c0,c1

通过这些声明,你可以利用 SELinux 域和敏感度对 Web 服务器的安全性进行微调。虽然这永远不能替代应用程序内部的安全措施,但它提供了额外的隔离,以防未经授权或恶意用户利用应用程序中的漏洞。

总结

在本章中,我们首先介绍了 systemd,并重点讲解了 systemd 所提供的服务管理功能。我们学习了如何使用自定义 SELinux 上下文启动服务,以及如何在启动时正确标记附加文件。除了服务管理外,通过 systemd 的单元文件,本章还涵盖了瞬时服务以及如何立即关联正确的 SELinux 上下文。

还简要提到了一些其他 systemd 的功能和服务。我们看到 SELinux 上下文如何作为 systemd 日志的一部分进行注册,以及如何使用该上下文查询事件。我们简要了解了 udev 及其规则如何帮助管理员管理设备。它的一个操作是设置设备节点的 SELinux 上下文。

接着,我们探讨了 D-Bus,了解 SELinux 如何用来控制应用程序与服务的关联,以及 D-Bus 如何使用 send_msg 权限来验证其通道之间的通信。

在讲解 D-Bus 后,我们查看了几个使用 PAM 启动用户上下文的服务,并深入探讨了具体示例,如 SSH,了解 SELinux 策略开发者如何进一步优化这些服务的支持。

我们最后查看了 mod_selinux,这是一个为 Apache 提供 SELinux 支持的动态模块,尽管 Apache 本身并未包含任何 SELinux 特性,它依然可以在 Apache 配置中启用 SELinux 支持。

在下一章,我们将探讨另一个支持 SELinux 的应用程序 SEPostgreSQL,它通过 SELinux 为流行且强大的 PostgreSQL 数据库扩展了强制访问控制支持。

问题

  1. 为什么不应该直接更新 /usr/lib/systemd/system 中的单元文件?

  2. 哪个应用程序可以在启动时重置文件的 SELinux 上下文?

  3. 如何获取与给定 SELinux 上下文相关的所有 journald 日志事件?

  4. 如何为由 udev 创建的设备节点设置 SELinux 标签?

  5. SELinux 控制是否总是适用于 D-Bus 关联?

  6. Apache 如何在没有包含任何 SELinux 代码的情况下,仍然能够识别 SELinux?

第八章:SEPostgreSQL——通过 SELinux 扩展 PostgreSQL

在上一章中,我们讨论了几个示例 SELinux 感知应用程序:这些应用程序能够识别并与 SELinux 子系统交互,以进一步增强应用程序上下文中的安全性。部分应用程序使用现有的策略构件,例如 Apache 的mod_selinux,而其他应用程序则通过自定义类来增强策略,从而更精细地调整其行为(如 D-Bus 和acquire_svc权限)。

通过安全增强型 PostgreSQLSEPostgreSQL),我们可以得到一个更复杂的 SELinux 感知应用程序示例,该程序使用多个额外的 SELinux 类,并对其内部数据库对象进行标签化,以进一步强化安全规则。在本章中,我们将学习如何在 PostgreSQL 中应用标签,调试其执行规则,将正确的标签与 PostgreSQL 资源关联,并展示这种基于标签的安全方法如何增强关系数据库中的特定安全实践。

在本章中,我们将涵盖以下主要主题:

  • 介绍 PostgreSQL 和 sepgsql

  • 理解 SELinux 的数据库特定对象类和权限

  • 使用 MCS 和 MLS

  • 将 SEPostgreSQL 集成到网络中

技术要求

请查看以下视频,了解代码的实际应用:bit.ly/3dDcg4Z

介绍 PostgreSQL 和 sepgsql

PostgreSQL 是一个流行的、功能丰富且成熟的关系数据库管理系统。与 Apache 类似,它也通过可加载模块支持功能的模块化扩展。我们将研究的模块称为sepgsql,它为 PostgreSQL 提供了 SELinux 支持,以实现额外的访问控制,提供基于 SELinux 策略规则的细粒度数据流控制。

请注意,sepgsql并没有在 PostgreSQL 中实现完整的强制访问控制系统,因为并非所有的 PostgreSQL 语句都会导致策略检查。虽然它增强了 PostgreSQL 数据库的安全性,但该模块也有一些限制,详见其在线文档,网址为www.postgresql.org/docs/10/sepgsql.html(如有需要,请根据版本号调整 URL;此 URL 中的参考文档适用于 PostgreSQL 10,这是 CentOS 8 中使用的版本,也是本章所使用的版本)。

使用 sepgsql 重新配置 PostgreSQL

在我们安装sepgsql之前,必须确保有一个可用的 PostgreSQL 系统。大多数 Linux 发行版都有现成的教程,指导如何部署 PostgreSQL,这通常涉及创建与之关联的数据库。

在本章中,我们假设数据库本身位于/var/lib/pgsql/data,这是基于 CentOS 的 PostgreSQL 安装的默认位置。PostgreSQL 的配置文件也位于此位置。

要安装 sepgsql,应执行以下步骤:

  1. 让我们首先通过以(默认)postgres 超级用户身份登录,并列出当前可用的数据库,来查看数据库是否正常工作:

    /var/lib/pgsql/data/log to get more information. This log file is the default log file for all PostgreSQL-related activities, as we will see when troubleshooting its SELinux support in the *Troubleshooting sepgsql* section.
    
  2. 假设 PostgreSQL 正常工作,接下来我们将配置它使用 sepgsql 模块。该模块是 PostgreSQL 中的一个贡献模块,由 PostgreSQL 社区维护。在 CentOS 中,sepgsql 模块属于 postgresql-contrib 包,如果系统中尚未安装,可以通过 yum install postgresql-contrib 命令轻松安装。

  3. 编辑 /var/lib/pgsql/data 目录下的 postgresql.conf 文件,并查找 shared_preload_libraries 语句。默认情况下,它会被注释掉,因此需要取消注释,并在其中添加 sepgsql

    shared_preload_libraries = 'sepgsql' # (change requires restart)
    
  4. 如前所述,修改此参数需要重新启动数据库。我们稍后会进行,但首先,我们将关闭数据库,因为接下来的操作需要在离线模式下进行:

    # systemctl stop postgresql
    
  5. 接下来,我们需要重新配置所有数据库并启用与 sepgsql 相关的函数。我们将在 使用 sepgsql 特定函数 部分讲解这些函数。为了启用这些函数,我们需要再次成为 postgres 超级用户,并且对每个可用的数据库加载特定的 SQL 文件:

    \l command, which we used earlier to check whether the database is functioning properly.
    
  6. 让我们通过启动 PostgreSQL 数据库、登录到 PostgreSQL 并请求当前上下文来验证 sepgsql 是否正常工作:

    sepgsql function sepgsql_getcon(), which retrieves the current context for the session.
    

让我们进一步配置数据库,创建一个测试账户,用于验证 sepgsql 控制:

创建测试账户

为了验证 sepgsql 控制是否生效,我们应该创建一个非 postgres 超级用户的测试账户,并且创建一个本地用户,以便将其映射到不同的 SELinux 上下文中。由于 SELinux 上下文会决定会话所关联的权限,我们希望能够展示不同上下文之间的影响。

首先,在 PostgreSQL 中(使用 postgres 超级用户),创建一个名为 testuser 的测试账户,并允许该账户使用指定密码进行身份验证:

postgres=# CREATE USER testuser PASSWORD 'somepassword';

我们还需要配置数据库以允许基于密码的身份验证(因为默认的 PostgreSQL 配置会使用系统信任或其他身份验证方式)。为此,在 /var/lib/pgsql/data 目录中编辑 pg_hba.conf 文件,并进行如下设置:

local		all	postgres					peer
local		all	testuser					md5
host		all	testuser		127.0.0.1/32	md5
host		all	testuser		192.168.100.0/24	md5

pg_hba.conf 文件管理 PostgreSQL 的基于主机的身份验证规则。我们更新该文件,允许 testuser 账户(使用 md5 作为标识符)进行基于密码的身份验证,同时允许 postgres 超级用户继续使用对等信任进行身份验证。

在这些更改生效后,PostgreSQL 允许 testuser 账户进行基于密码的身份验证,无论是当用户通过本地基于套接字的方式发起通信,还是通过网络通信方式进行连接。

我们还需要告诉 SELinux 策略,允许普通用户连接到 PostgreSQL 服务:

# setsebool -P selinuxuser_postgresql_connect_enabled on

虽然这足以访问 PostgreSQL 服务,但不足以允许常规用户域(user_t)与sepgsql交互。为了实现这一点,我们需要调整 SELinux 策略,使得user_t域也与sepgsql_client_type属性相关联,并且user_r角色可以激活与sepgsql相关的类型。

我们通过一个小的 CIL 策略来实现此操作,如下所示:

(typeattributeset cil_gen_require sepgsql_client_type)
(typeattributeset cil_gen_require user_t)
(typeattributeset cil_gen_require sepgsql_trusted_proc_t)
(typeattributeset cil_gen_require sepgsql_ranged_proc_t)
(typeattributeset sepgsql_client_type (user_t))
(roleattributeset cil_gen_require user_r)
(roletype user_r sepgsql_trusted_proc_t)
(roletype user_r sepgsql_ranged_proc_t)

也可以通过参考策略风格模块来完成此操作,如下所示:

policy_module(local_sepgsql, 1.0)
gen_require(`
	role user_r;
	type user_t;
')
postgresql_role(user_r, user_t)

假设我们继续使用基于 CIL 的策略,让我们将文件(即local_sepgsql.cil)加载为 SELinux 策略模块:

# semodule -i local_sepgsql.cil

在更改pg_hba.conf文件后,别忘了重启 PostgreSQL 服务。

在 PostgreSQL 内部调整 sepgsql

sepgsql模块引入了两个配置参数,可以用于调整 PostgreSQL 内部的sepgsql

  • sepgsql.permissive参数告诉 PostgreSQL 在 PostgreSQL 内部不强制执行 SELinux 策略规则。这类似于 SELinux 在系统上的宽容状态,但仅涵盖 PostgreSQL 内部的sepgsql相关功能。

  • sepgsql.debug_audit参数告诉 PostgreSQL 始终记录与 SELinux 相关的决策,即使它们是允许处理某个语句。这类似于系统上 SELinux 的auditallow语句。

然而,非常重要的一点是要理解,正如在第七章中解释的那样,sepgsql是一个用户空间对象管理器,配置应用程序特定的 SELinux 控制:Linux 内核中的 SELinux 子系统并不用于强制执行访问控制,只有sepgsql才负责。SELinux 子系统的唯一作用是允许 PostgreSQL 查询当前的 SELinux 策略或获取当前的 SELinux 上下文信息。

因此,之前的配置参数大多独立于系统的配置工作。虽然 SELinux 必须在系统上激活,但它不需要处于强制模式才能使sepgsql在 PostgreSQL 内部强制执行规则,系统处于宽容模式也不会使sepgsql的强制执行变为宽容模式。

sepgsql.debug_audit参数与系统策略有一定关系。我们可以在 SELinux 策略中添加auditallow语句,以强制记录即使是被允许的事件。sepgsql.debug_audit参数的作用是强制记录所有事件,这对排查sepgsql问题非常有用,接下来我们将看到这一点。

排查 sepgsql 问题

让我们为单个会话启用调试语句,并再次调用sepgsql_getcon函数:

# su postgres -c "/usr/bin/psql postgres"
postgres=# SET sepgsql.debug_audit = true;
SET
postgres=# SELECT sepgsql_getcon();
...

如果你想为整个系统启用此配置,可以将配置放入postgresql.conf文件中:

sepgsql.debug_audit = true

在 PostgreSQL 日志中,我们将注意到以下信息:

STATEMENT:  SET sepgsql.debug_audit = true
STATEMENT:  SELECT sepgsql_getcon();
LOG:  SELinux: allowed { execute } \
  scontext=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 \
  tcontext=system_u:object_r:sepgsql_proc_exec_t:s0 \
  tclass=db_procedure name="pg_catalog.sepgsql_getcon()"

前两行记录了我们在会话中执行的语句,而第三行是与执行 sepgsql_getcon 相关的 SELinux 日志事件。

该事件告诉我们,unconfined_t 域(源上下文)尝试(并成功)执行数据库过程(如 db_procedure 类所示),该过程标记为 sepgsql_proc_exec_t 类型。数据库中的功能是 pg_catalog 模式中的 sepgsql_getcon 函数。

如果发生拒绝,这将导致日志中出现类似的事件,并且拒绝触发的最终用户将看到错误信息,PostgreSQL 会显示类似以下的错误消息:

ERROR:  SELinux: security policy violation

与例如 D-Bus 执行的审计日志(会在常规审计日志中产生 USER_AVC 事件)不同,sepgsql 将遵循 PostgreSQL 数据库本身的日志配置,因此在尝试故障排除 sepgsql 时,请密切关注该日志文件(或 PostgreSQL 中配置的其他日志目标)。

在这个简单的示例中,你可能已经注意到事件引用了一个数据库特定的类(db_procedure)。在接下来的部分,我们将深入探讨与 sepgsql 相关的各种类、权限和类型,并因此支持 SELinux 策略。

理解 SELinux 的数据库特定对象类和权限

sepgsql 模块使用多个数据库特定的 SELinux 类来精细调控策略和访问控制。可以通过 /sys/fs/selinux/classseinfo 命令列出支持的类:

# seinfo --class | grep db_
db_blob
db_column
db_database
db_language
db_procedure
db_schema
db_sequence
db_table
db_tuple
db_view

这些类具有明显的关系数据库含义:db_database 用于与数据库相关的权限,db_table 用于表权限,db_procedure 用于数据库过程,依此类推。虽然并非所有类都仍然被 sepgsql 支持(db_database 类不再直接支持),但大多数类仍然在 PostgreSQL 数据库中具有其常规映射。

让我们看看 sepgsql 支持哪些权限,以及如何利用这些权限在数据库中精细调控访问控制。

理解 sepgsql 权限

sepgsql 强制执行的访问控制是在 PostgreSQL 已经支持的自主访问控制基础上实现的。与其使用当前在数据库中操作的角色或用户的权限,sepgsql 模块将使用与会话关联的上下文。

由于我们可以为使用相同数据库角色认证的会话使用不同的 SELinux 上下文,我们可以在数据库中创建不同的访问控制,而无需将其与用户帐户本身关联。例如,我们可以根据数据库会话的初始化方式进行区分:远程会话可能与本地启动的会话具有不同的上下文,或者即使在数据库中共享相同帐户,不同的 Linux 用户也可能具有独特的授权。

重要说明

由于远程连接需要对等方上下文是可访问的,sepgsql要求使用带标签的 IPSec,或者我们需要通过 NetLabel 和 CIPSO 引入回退标签,正如在第五章中看到的,控制网络通信。我们将在解释各种权限映射后,在将 SEPostgreSQL 集成到网络中部分中建立这样的映射。

一旦登录,对表的查询将触发针对 SELinux 策略的几项检查:

  • 对表执行任何SELECTINSERTUPDATEDELETE语句都会导致对db_table类中的selectinsertupdatedelete权限进行权限检查。

  • WHERE子句列出一个或多个不同的表时,还会检查这些不同表的select权限。

  • 此外,还会检查每个引用列的列级权限,并且这会与db_column类中的权限进行比较。再次强调,对select权限的检查验证了读取访问权限,而updateinsert权限反映了在值发生变化时需要检查的控制。

有关支持的权限的更详细概述,可以参考 PostgreSQL 的sepgsql文档。

使用默认支持的类型

默认的 SELinux 策略有多种类型可以在sepgsql设置中直接使用。大多数 SEPostgreSQL 配置不会偏离这些默认类型,而是依赖于我们在第三章中提到的基于类别和敏感度的控制,管理用户登录

要查看这些默认类型是什么,它们的用途以及如何在 PostgreSQL 中分配这些标签,我们先从创建一个名为db_test的新数据库开始:

# su postgres -c "/usr/bin/psql postgres"
postgres=# CREATE DATABASE db_test;
CREATE DATABASE

接下来,我们连接到这个新创建的数据库,并创建一个简单的表,命名为tb_users,它有以下列:

  • 用户的 ID,命名为uid

  • 用户的姓名,命名为name

  • 用户的电子邮件地址,命名为mail

  • 用户的邮寄地址,命名为address

  • 用户的密码盐和哈希,分别命名为saltphash

    重要提示

    所使用的示例仅仅是一个示范,旨在展示如何处理 SELinux 标签和sepgsql。适当的数据库设计以及处理密码哈希和其他敏感数据的最佳实践远远超出了本书的范围!

正如你所想,我们将进一步保护这些列:虽然密码哈希显然应该被视为非常敏感的信息,但我们还应该确保妥善保护邮件和地址字段,因为这些是个人身份信息PII),在世界许多地方受特定隐私法律的管辖:

postgres=# \c db_test;
db_test=# CREATE TABLE tb_users(uid int primary key, name text, mail text, address text, salt text, phash text);

现在,这个表关联的标签是什么?为此,我们需要查询 PostgreSQL 的内部表/视图,特别是pg_seclabels

db_test=# SELECT objname,provider,label FROM pg_seclabels WHERE objname='tb_users';
 objname  | provider | label
----------+----------+----------------------------------------
 tb_users | selinux  | unconfined_u:object_r:sepgsql_table_t:s0

如你所见,表已获得 sepgsql_table_t 类型和默认敏感度(s0)。

sepgsql_table_t 是表的默认类型。我们通常会看到该类型用于一般的表支持和列。除了 sepgsql_table_t 类型外,策略还有一些其他表和列相关的类型,管理员可以用来区分 sepgsql 强制执行的控制:

  • sepgsql_fixed_table_t 类型可用于只能追加(插入)但不能更新的表或列。这可以用于与日志相关的表或审计事件,我们希望通过 sepgsql 控制进一步加强这一点(超出可以用于此的数据库内控件)。

  • sepgsql_ro_table_t 类型可用于只能读取(只读)的表或列。

  • sepgsql_secret_table_t 类型可用于常规用户或会话无法访问的表或列,只能由管理员访问。这通常用于只能通过受保护和/或特权程序访问的表或列。

  • unpriv_sepgsql_table_t 类型类似于 sepgsql_table_t 类型,但特定于由管理员或未受限用户管理的表或列,这些表或列无法被受限用户访问。

  • 另一方面,user_sepgsql_table_t 类型专门为受限用户管理的表或列而构建。这使得管理员能够区分用户特定的表和一般表。

让我们授予 testuser 账户对该表和数据库的(完全)访问权限,并向表中添加一些数据:

db_test=# GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO testuser;
db_test=# INSERT INTO tb_users VALUES (1, 'Sven Vermeulen', 'some@example.com', 'Some Place 10001, Somewhere', 'abc123', 'f5ba94...3');
db_test=# INSERT INTO tb_users VALUES (2, 'Lisa McCarthy', 'lisa@example.com', 'Lisa Place 15, Someplace', 'def456', 'ba53f2...0');

如果我们通过测试用户查询数据,我们可以看到所有已添加到表中的数据:

db_test=> SELECT * FROM tb_users;

让我们将 phash 列的类型更改为 sepgsql_secret_table_t

db_test# SECURITY LABEL ON COLUMN tb_users.phash IS 'system_u:object_r:sepgsql_secret_table_t:s0';

然而,仅此一点并不能防止 testuser 用户访问数据。这取决于 testuser 如何登录到数据库——会话是从哪个上下文启动的。如果我们从未受限域启动会话,那么该会话仍然允许访问数据。我们不如从常规用户会话(user_t)登录,并再次尝试访问数据:

db_test=> SELECT * FROM tb_users;
ERROR:  SELinux: security policy violation

即使用户在数据库中拥有所有权限,我们仍然注意到策略阻止了访问。然而,我们可以查询那些未标记为 sepgsql_secret_table_t 的列:

db_test=> SELECT uid, name, mail, address, salt from tb_users;

由于 phash 列现在被标记为 sepgsql_secret_table_t,我们仍然希望常规数据库用户能够查询哈希值是否与数据库中的哈希值匹配,或者设置新的哈希值。这使得数据库用户可以在不轻易泄漏密码哈希的情况下管理账户。我们通过函数实现这一点,接下来我们将描述这些函数。

创建受信任的过程

PostgreSQL 支持函数和过程,以更结构化和可管理的方式促进数据库内或数据上的操作的隔离或组合。过程允许在数据库中进行事务更新,但自身不返回值。函数返回值,但不允许进行事务更新。在我们的示例中,我们将创建两个函数,一个用于将哈希与存储的哈希进行比较(但不将存储的哈希显示给数据库用户),另一个用于更新存储的哈希。

信息提示

虽然我们应该为第二个函数使用过程,但并非所有今天使用的 PostgreSQL 版本都支持它们。支持过程的功能仅从 PostgreSQL 版本 11 开始引入,而我们的示例使用 PostgreSQL 10.6,因为那是 CentOS 8 支持的当前版本。

首先,我们来创建这两个函数:

postgres=# CREATE FUNCTION compare_hash(fuid int, fphash text) RETURNS boolean AS 'SELECT phash = regexp_replace(fphash, ''[^a-f0-9]*'', '''', ''g'') FROM tb_users WHERE uid = fuid' LANGUAGE sql;
postgres=# CREATE FUNCTION set_hash(fuid int, fphash text) RETURNS int AS 'UPDATE tb_users SET phash = regexp_replace(fphash, ''[^a-f0-9]*'', '''', ''g'') WHERE uid = fuid RETURNING uid' LANGUAGE sql;

我们在函数中引入了正则表达式来清理输入,因为我们稍后会将这些函数标记为受信任的,我们不希望这些函数成为 SQL 注入等攻击的跳板。

一旦定义了这些函数,授权用户可以使用它们来访问更多受保护的数据。当然,我们需要正确标记这些函数。在默认的 SELinux 策略中,以下类型可用于处理过程和函数:

  • sepgsql_proc_exec_t 是分配给常规函数或过程的类型。执行后,过程将在用户的当前上下文中运行,因此不会发生转换。

  • sepgsql_trusted_proc_exec_t 是分配给受信任的过程或函数的类型。执行后,这些函数将在 sepgsql_trusted_proc_t 域内运行,该域具有访问更多特权类型的权限,例如 sepgsql_secret_table_t

  • sepgsql_ranged_proc_exec_t 是分配给受信任的过程或函数的类型,但它有一个额外的权限:允许范围过程改变当前的敏感性。范围过程权限对于可以访问当前上下文无法访问的类别标签列的函数或过程很有用。执行后,这些函数和过程将在 sepgsql_ranged_proc_t 域内运行。

  • 用户管理的过程可以标记为 unpriv_sepgsql_proc_exec_t(对于未受限用户)和 user_sepgsql_proc_t(对于受限用户)。这些过程和函数将继续在用户域内运行。

要获取当前分配给函数的标签,可以使用 LIKE 语句,因为函数在定义时(在 objname 列中)包含了变量,因此它们并不总是那么显而易见,无法立即选择:

db_test=# SELECT objname,provider,label FROM pb_seclabels WHERE objname LIKE 'compare_hash%';

让我们将这些函数标记为受信任的:

db_test=# SECURITY LABEL ON FUNCTION compare_hash(fuid integer, fphash text) IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0';
db_test=# SECURITY LABEL ON FUNCTION set_hash(fuid integer, fphash text) IS 'system_u:object_r:sepgsql_trusted_proc_exec_t:s0';

有了这些标签,数据库用户即使无法访问 phash 列本身,也可以执行适当的检查和更改:

db_test=> SELECT compare_hash(1, 'abc123');
f
db_test=> SELECT set_hash(1, 'abc123');
1
db_test=> SELECT compare_hash(1, 'abc123');
t

当然,防止未授权用户访问敏感数据并不是 PostgreSQL 在没有sepgsql的情况下无法做到的事。PostgreSQL 可以将过程和函数标记为以函数或过程所有者的权限而非执行会话的权限运行。sepgsql提供的是另一种实现此功能的方式,或者通过其他安全模型提供数据保护。

例如,在我们的示例中,testuser账户的数据库内权限仍然适用,我们并没有为testuser账户授予其他权限或将其权限提升到更高的级别——相反,我们是使用 SELinux 标签和上下文信息来进一步过滤权限。

使用 sepgsql 特定的函数

sepgsql PostgreSQL 模块添加了一些可以用来与数据库内标签进行交互的函数:

  • 使用sepgsql_getcon(),我们可以获取当前会话的上下文。

  • 使用sepgsql_setcon(),我们可以更改当前会话的上下文,前提是当前上下文具有执行此操作的权限。

  • 使用sepgsql_restorecon(),当前数据库中的所有对象会重新标记回默认设置。该函数支持一个参数,可以是NULL,也可以是定义新默认值的文件引用。

  • 使用sepgsql_mcstrans_in()sepgsql_mcstrans_out(),我们可以与mcstrans守护进程(如果它正在运行)进行交互,将人类可读的敏感度范围转换为原始(_in())或反之(_out())。

这些函数在维护标签或定义依赖于上下文信息的逻辑函数时非常有用。

使用 MCS 和 MLS

启用sepgsql模块的最常见用例是使用多类别支持MCS)和多级安全性MLS)支持,结合 SELinux 来细化资源访问控制。

基于类别限制对列的访问

假设我们使用从c900c909的类别编号范围来处理特定的 PII 数据集,并通过直接授予访问权限或使用特定的 SELinux 上下文来授权用户访问这些类别。

在数据库中,我们可以使用该范围内的类别编号来标记敏感的 PII 数据:

db_test=# SECURITY LABEL ON COLUMN tb_users.mail IS 'system_u:object_r:sepgsql_table_t:s0:c903';
db_test=# SECURITY LABEL ON COLUMN tb_users.address IS 'system_u:object_r:sepgsql_table_t:s0:c903';

应用标签后,没有访问该类别权限的用户将无法访问数据:

db_test=> SELECT sepgsql_getcon();
user_u:user_r:user_t:s0-s0:c0.c100
db_test=> SELECT uid,name,mail,address FROM tb_users;
ERROR:  SELinux: security policy violation;

当为用户正确设置类别范围时,访问数据将被授权:

db_test=> SELECT sepgsql_getcon();
user_u:user_r:user_t:s0-s0:c0.c100,c900.c904
db_test=> SELECT uid,name,mail,address FROM tb_users;

但是需要理解的是,大多数域都允许切换其类别集,只要它仍然在允许的范围内:

# semanage login -l
Login Name     SELinux User    MLS/MCS Range    ...
...
taylor         user_u          s0-s0:c0.c100,c900.c903 ...

这意味着,即使该用户的会话启动时使用了更有限的类别集(例如,使用runcon命令),用户仍然可以再次调用runcon以扩展类别范围,或者使用sepgsql_setcon()函数:

db_test=> SELECT sepgsql_getcon();
user_u:user_r:user_t:s0-s0:c0.c100;
db_test=> SELECT sepgsql_setcon('user_u:user_r:user_t:s0-s0:c0.c100,c900.c903');
db_test=> SELECT sepgsql_getcon();
user_u:user_r:user_t:s0-s0:c0.c100,c900.c903

为了解决这个问题,我们需要使目标域受到 MCS 约束。

限制用户域以操作敏感度范围

SELinux 策略始终允许减少类别范围,因此,最初包含 c900 类别的范围可以始终切换到一个排除该类别的范围。在 SELinux 中,授予域减少类别范围特权的规则使用了支配规则,这些规则本质上是运行数学集合表达式的算法:如果目标集合完全包含在源集合中,SELinux 将允许类别范围的转换。

然而,该策略也允许扩展类别范围(如果该范围仍然在 SELinux 配置中为用户定义的允许范围内),除非该域本身被标记为 MCS 限制。默认的 MCS 限制域通常是那些用于沙箱使用或虚拟化的域,正如我们将在 第九章 安全虚拟化 中看到的那样。

然而,我们可以轻松添加更多的域。例如,要将用户域标记为 MCS 限制,只需加载以下 CIL 策略:

(typeattributeset cil_gen_require mcs_constrained_type)
(typeattributeset cil_gen_require user_t)
(typeattributeset mcs_constrained_type (user_t))

这将防止 user_t 域再次扩展其类别范围。

将 SEPostgreSQL 集成到网络中

当我们在 PostgreSQL 中使用 sepgsql 模块时,所有数据库会话都需要与之关联一个安全上下文。对于本地通信(使用 Unix 域套接字)来说,这个上下文是容易获取的,但网络会话(最常见的类型)并不会自动设置上下文。

如果系统没有参与标记的网络设置,正如我们在 第五章 控制网络通信 中看到的那样,与数据库的交互将失败:

$ psql -U testuser -h ppubssa3ed db_test
psql: FATAL:  SELinux: unable to get peer label: Protocol not available

为了解决这个问题,推荐的方法是开始使用标记的 IPSec。不过,我们也可以使用 NetLabel 在需要时引入回退标记。

为远程会话创建回退标签

通过 Linux 的 NetLabel 和 CIPSO 支持(如在 第五章 控制网络通信 中看到的那样),我们可以同时引入回退标签(基于源地址关联标签),并且为本地通信使用完整的标签。

通过完整的本地标签支持,NetLabel 可以将源上下文传递给目标,如果所有的通信仅在回环设备上进行(因为此类通信不会离开系统,允许 NetLabel 跟踪并支持端到端的流动,并向接收服务提供上下文信息)。

让我们为本地标签创建 CIPSO 定义:

# netlabelctl cipsov4 add local doi:2

我们现在为来自网络的通信(通过 eth0 接口和 192.168.100.1/24 网络)创建一个默认上下文。正是这个上下文我们会在通过网络连接到 PostgreSQL 服务器时看到:

# netlabelctl unlbl add interface:eth0 address:192.168.100.0/24 label:user_u:user_r:user_t:s0

我们现在可以删除默认映射规则,并为不同的通信类型添加映射规则:

# netlabelctl map del default
# netlabelctl map add default address:0.0.0.0/0 protocol:unlbl
# netlabelctl map add default address:::/0 protocol:unlbl
# netlabelctl map add default address:127.0.0.1 protocol:cipsov4,2

我们创建的映射将允许一切通信不带标签(但请记住,我们已为来自192.168.100.0/24的通信定义了特定标签),并在本地主机上进行基于回环的完整标签设置。

调整 SELinux 策略

除了标签配置外,我们可能还需要进一步微调 PostgreSQL 的 SELinux 策略。这里有几个 SELinux 布尔值值得一提:

  • postgresql_selinux_transmit_client_label SELinux 布尔值(默认禁用)允许postgresql_t域设置其自己的会话上下文。当 PostgreSQL 服务器与其他远程数据库建立连接时,可能需要设置自己的会话上下文(例如,使用 PostgreSQL 的外部数据包装器FDW)支持)。启用后,客户端上下文也会传递给远程数据库。

  • postgresql_selinux_unconfined_dbadm SELinux 布尔值(默认启用)授予任何未限制的用户域在sepgsql中的数据库管理员权限。

  • postgresql_selinux_users_ddl SELinux 布尔值(默认启用)允许非特权用户使用user_sepgsql_table_t

  • selinuxuser_postgresql_connect_enabled SELinux 布尔值(默认禁用)允许用户域通过 Unix 域套接字连接到 PostgreSQL 守护进程。

别忘了使布尔值更改持久化(使用setsebool -P),否则系统重启后设置将恢复为默认值。

总结

PostgreSQL 数据库可以通过sepgsql模块扩展 SELinux 支持。该模块为数据库中的各种对象添加了标签支持,并检查会话上下文与目标标签之间的访问权限。为了获取会话上下文,sepgsql依赖于基于套接字的通信或带标签的网络通信。

在本章中,我们学习了如何启用sepgsql模块以及如何排除可能的策略问题。然后,我们在示例数据库中使用了各种默认类型,并用这些类型展示了sepgsql中的访问控制如何工作。接着,我们利用 SELinux 的 MCS 支持进一步处理基于类别的访问控制。最后,我们在使用回退标签支持的网络中集成了 PostgreSQL。

在下一章中,我们将探讨 Linux 中的安全虚拟化,并看看 SELinux 如何帮助隔离虚拟客体。

问题

  1. SEPostgreSQL 是默认 PostgreSQL 技术的一部分吗?

  2. sepgsql能够正常使用之前,还需要启用哪些其他设置?

  3. 如何设置或查询数据库对象上的标签?

  4. 为什么sepgsql决策事件没有出现在系统审计日志中?

第九章:安全虚拟化

越来越多的系统工具内置了对 SELinux 的支持,或使用 SELinux 的功能来进一步加固自身的服务提供。当我们讨论虚拟化时,libvirt 是虚拟化管理工具的领军者,使用 QEMU基于内核的虚拟机 (KVM) 管理程序。

在本章中,管理员将学习什么是 安全虚拟化sVirt),以及它是如何通过 libvirt 工具套件应用的,SELinux 域如何设置,以及 sVirt 如何使用 SELinux 分类将客户机相互隔离。我们将研究 SELinux 如何帮助降低虚拟化风险,并理解如何调整 SELinux 策略以支持虚拟化服务。

本章将涵盖以下主要内容:

  • 理解 SELinux 安全虚拟化

  • 使用 SELinux 支持增强 libvirt

  • 使用 Vagrant 与 libvirt

技术要求

查看以下视频,了解代码的实际应用:bit.ly/2T805Ug

虽然在旧系统上运行本章示例是可行的,但我们建议使用支持硬件虚拟化的更现代的系统。这将确保在练习过程中获得更高的性能,因为完全仿真可能会严重拖慢进度,尤其是在旧系统上。

要验证系统是否支持硬件虚拟化(从而能够使用基于 Linux KVM 的虚拟化),以下命令应该有输出:

# grep -E 'svm|vmx' /proc/cpuinfo
flags   : fpu vme de ... vmx ...

如果没有输出,则表示系统不支持硬件辅助虚拟化。

理解 SELinux 安全虚拟化

虚拟化是许多基础设施服务设计中的核心概念。自 1970 年代初期作为隔离工作负载和抽象硬件依赖关系的手段出现以来,虚拟化实现已取得了巨大的发展。今天,当我们看基础设施服务提供时,我们很快意识到,如果没有依赖虚拟化的好处和优点,许多云服务提供商将无法运营。

虚拟化提供的特性之一是隔离,而 SELinux 可以很好地支持并增强这一特性。

介绍虚拟化

当我们讨论虚拟化时,我们关注的是它提供的抽象层,用以隐藏某些资源视图(如硬件或处理能力)。虚拟化有助于提高硬件使用效率(从而带来更好的成本控制)、集中化的资源和系统视图、公司可处理的操作系统数量的灵活性、资源分配的标准化,甚至改进的安全服务。

存在几种虚拟化类型:

  • 全系统仿真:通过软件完全仿真硬件。QEMU 是一款开源仿真软件,能够处理全系统仿真,使管理员和开发人员能够在其系统不兼容的情况下运行具有不同处理器架构的虚拟平台。

  • 本地虚拟化:在这种情况下,硬件的主要部分在各个实例之间共享,客户机可以在其上不加修改地运行。Linux 的KVM就是这种类型的虚拟化,它也通过 QEMU 获得支持。

  • 准虚拟化:客户机操作系统使用虚拟化层提供的特定 API(在这些虚拟化层上,无法托管未经修改的操作系统)。Xen 的初始版本只支持准虚拟化。使用 KVM 与 VirtIO 驱动程序是另一个更具模块化的示例。

  • 操作系统级虚拟化或容器化:客户机使用主机操作系统(内核),但看不见主机上运行的进程和其他资源。Docker 容器或 LXC 容器就是操作系统级虚拟化的例子。

  • 应用虚拟化:应用程序在专用软件运行时环境中运行。这里一个常见的例子是支持 Java 应用程序,它们运行在Java 虚拟机JVM)上。

许多虚拟化平台支持几种虚拟化类型。QEMU 根据其配置,可以从全仿真到准虚拟化不等。

当我们处理虚拟化层时,以下术语会频繁出现:

  • 主机是运行虚拟化软件的(本地)操作系统或服务器。

  • 客户机是运行在主机上的虚拟化服务(通常是操作系统或容器)。

  • 虚拟机监控器是管理虚拟化平台的硬件抽象和资源共享能力的专业虚拟化软件。它负责创建和运行虚拟机。

  • 镜像是代表分配给客户机的文件系统、磁盘或其他介质的一组文件。

  • 虚拟机是客户机运行的抽象硬件或资源集合。

在本章中,我们将使用这些术语,并且在第十章《使用 Xen 安全模块与 FLASK》和第十一章《增强容器化工作负载的安全性》中也会使用这些术语,因为这些章节同样涉及到特定的虚拟化实现以及 SELinux 如何在其中主动提供额外的安全控制。

在我们开始配置和调整虚拟化服务之前,先来看看 SELinux 为虚拟化环境提供了什么。

审视虚拟化的风险

然而,虚拟化带来了一些风险。如果我们向架构师或其他关注风险的人询问虚拟化的风险,他们会谈到虚拟机扩展、与安全或不安全 API 相关的挑战、虚拟化服务的更高复杂性等问题。

虚拟化本身的挑战超出了本章的范围,但有一些值得注意的风险直接与 SELinux 的关注领域相关。如果我们能够将 SELinux 与虚拟化层结合使用,就能更积极地缓解这些风险:

  • 第一个风险是虚拟机中的数据敏感性。当多个虚拟机共同托管时,可能存在一个来宾能够通过虚拟化软件的漏洞、虚拟化管理程序的网络功能或通过侧信道攻击等方式访问另一虚拟机上的敏感数据的风险。

    使用 SELinux,可以通过敏感度范围来控制数据的敏感性。不同的来宾可以在不同的敏感度范围内运行,即使在虚拟化层上也能保证数据的敏感性。

  • 另一个风险是离线来宾镜像的安全性。在这种情况下,管理员或配置错误的虚拟机可能会获得对另一个来宾镜像的访问权限。SELinux 可以通过正确标记的来宾镜像来防止这种情况,并确保离线虚拟机的镜像与在线虚拟机的镜像在类型上有所不同。

  • 虚拟机也可以耗尽系统资源。在 Linux 系统中,许多资源可以通过控制组cgroups)子系统进行控制。由于该子系统是通过系统调用和常规文件 API 来管理的,因此可以使用 SELinux 进一步控制对该功能的访问,确保例如 libvirt 维护的控制组仅由 libvirt 控制。

  • 突破攻击,即利用虚拟化管理程序中的漏洞试图访问宿主操作系统的攻击,可以通过 SELinux 的类型强制得到缓解,因为即使是虚拟化管理程序也不需要对宿主的所有内容拥有完全的管理权限。

  • SELinux 还可以用于授权访问虚拟化管理程序,确保只有正确的团队(通过基于角色的访问控制)能够控制虚拟化管理程序及其定义。

  • 最后,SELinux 还提供了更好的来宾隔离,这不仅仅是针对来宾镜像的访问。得益于 SELinux 的 MCS 实现,来宾可以以强制性的方法彼此隔离。通过类型强制,来宾的允许行为可以被定义和控制。这是托管服务提供商使用的关键能力,因为它们允许运行(对它们来说)不可信的来宾虚拟机。

然而,SELinux 并不是虚拟化提供商的完整安全解决方案。SELinux 的一个主要设计限制是,如果系统本身不是 SELinux 感知的,它就不是动态的。当我们为虚拟机分配一个类型时,这个类型通常是固定的,不会改变。虚拟机的行为特性将根据其运行的软件而有所不同。

运行 web 服务器的虚拟机与运行数据库或邮件网关的虚拟机具有不同的行为特性。虽然 SELinux 策略管理员可以为每个虚拟机创建新的域,但这样做效率低下。因此,大多数 SELinux 策略将只提供几个可以被虚拟机使用的、具有广泛特性的域。

使用 libvirt 时,这些域是 sVirt 解决方案的一部分。

重用现有的虚拟化域

当 Red Hat 引入其虚拟化解决方案时,也加入了 SELinux 支持,并将最终的技术命名为 sVirt,源自 secure virtualization(安全虚拟化)。由于“安全虚拟化”这一术语在市场上并不独特,我们主要使用术语 sVirt 来指代 SELinux 在虚拟化管理解决方案中的集成,例如 libvirt。

使用 sVirt,开源社区为通过 SELinux 增强虚拟化和容器化的安全性提供了一种可重用的方法。它通过以下域和类型来实现这一点,这些域和类型可以在任何底层虚拟化平台上使用:

  • libvirtd 等虚拟机监控软件本身使用 virtd_t 域。

  • 不需要与主机系统交互,并且除了与通用虚拟机相关的资源之外不需要任何其他资源的来宾(虚拟机)通常会使用 svirt_t 域。该域是最为隔离的来宾域,适用于完全虚拟化解决方案。

  • 需要与主机进行更多交互的来宾,例如使用 QEMU 网络功能和共享服务的,将使用 svirt_qemu_net_t 域。

  • 使用 KVM 网络功能和共享服务的来宾将使用 svirt_kvm_net_t 域。该域在权限上与 svirt_qemu_net_t 相似,但经过优化,适用于 KVM。

  • 容器化的来宾,如我们在 第十一章 中看到的,增强容器化工作负载的安全性,将使用 svirt_lxc_net_t 域,其特权经过优化,适用于操作系统级虚拟化。

  • 需要更灵活内存访问(例如执行可写内存段和内存堆栈)的来宾将使用 svirt_tcg_t 域。这种灵活的内存访问在完全虚拟化的来宾中很常见,因为它们的仿真/虚拟化需要使用 Tiny Code Generator (TCG),因此得名。

  • 包含来宾数据的镜像文件将标记为 svirt_image_t 类型。

  • 当前未使用的镜像文件将使用默认的 virt_image_t 类型。

  • 以只读方式使用的镜像文件将被分配 virt_content_t 类型。

为了在域允许做的事情上提供一些灵活性,额外的 SELinux 布尔值被启用,我们接下来将介绍这些内容。

微调虚拟化支持的 SELinux 策略

在切换 SELinux 布尔值以控制虚拟化域的限制时要小心。这些布尔值会影响主机级别的 SELinux 策略,不能用来更改单个来宾的访问控制或权限。因此,当我们更改 SELinux 布尔值的值时,这个变化会影响该主机上所有来宾的权限。

让我们看看虚拟化环境中各种 SELinux 布尔值的作用:

  • 如果启用 staff_use_svirt 布尔值,则允许 staff_t 用户域与虚拟机进行交互和管理,因为默认情况下这仅允许未受限用户。

  • 如果启用 unprivuser_use_svirt 布尔值,则允许无特权的用户域(例如 user_t)与虚拟机进行交互和管理。

  • 使用 virt_read_qemu_ga_datavirt_rw_qemu_ga_data 布尔值,QEMU 来宾代理(它是一个可选的代理,运行在来宾中,帮助进行如备份过程中的文件系统冻结等操作)可以读取或管理标记为 virt_qemu_ga_data_t 类型的数据。然而,该类型默认未使用,这些 SELinux 布尔值默认是禁用的。

  • virt_sandbox_share_apache_content 布尔值允许来宾域共享 web 内容。这通常用于容器,但如果虚拟机管理程序支持将主机文件系统映射到来宾,也可以在来宾中使用。

  • 启用 virt_sandbox_use_audit 后,这个布尔值允许来宾域将审计消息发送到主机的审计服务。

  • virt_sandbox_use_fusefs 布尔值授予来宾域挂载并与 virt_use_fusefs 布尔值交互的特权,后者允许来宾读取这些文件系统上的文件。

  • 如果 virt_sandbox_use_netlink 布尔值处于活动状态,则来宾域可以使用 Netlink 系统调用操作主机内的网络堆栈。

  • 使用 virt_transition_userdomain,容器可以过渡到用户域(包括未受限用户域 unconfined_t)。

  • 当启用 virt_use_execmem 时,来宾可以使用可执行内存。

  • virt_use_glusterdvirt_use_nfsvirt_use_samba 布尔值允许来宾使用挂载在主机上的网络文件系统,分别通过 GlusterFS、NFS 和 Samba 提供。请注意,这不涉及来宾内部的挂载,例如连接到 NFS 服务器的来宾。这些布尔值处理通过主机上的文件系统挂载进行的交互。

  • 设备访问也通过一些 SELinux 布尔值进行管理,例如 virt_use_comm 布尔值,用于与串行和并行通信端口交互;virt_use_pcscd,允许来宾访问智能卡;以及 virt_use_usb,授予访问 USB 设备的权限。

  • virt_use_rawip 布尔值允许来宾使用并与原始 IP 套接字进行交互,从而绕过常规网络栈中的一些处理逻辑,进行网络交互。

  • 使用 virt_use_sanlock,来宾可以与 sanlock 服务进行交互,这是一个用于共享存储的锁管理器。

  • virt_use_xserver 设置为 true 时,来宾可以使用主机上的 X 服务器。

如果需要允许某个来宾或一小部分来宾执行安全敏感操作,建议将这些来宾运行在一个隔离的主机上,在该主机上允许这些操作,同时将其他来宾运行在不允许这些特定操作的主机上。

管理员还可以为特定的来宾使用不同的 SELinux 域,微调单个虚拟机的访问控制。如何分配特定域当然取决于底层技术。在 通过 SELinux 支持增强 libvirt 这一部分中,我们将介绍如何在基于 libvirt 的虚拟化中实现这一点。

理解 sVirt 使用 MCS

SELinux 域和所提到的类型不足以实现来宾之间的适当限制和隔离。sVirt 通过广泛使用 SELinux 的 多类别安全MCS)增加了另一层安全性。

在 SELinux 中,一些域被标记为 MCS 约束类型。当是这种情况时,该域将无法访问没有相同分类集(或更多分类集)资源,因为它无法扩展其自身的活动分类集——这一点我们在 第八章SEPostgreSQL – 使用 SELinux 扩展 PostgreSQL 中看到过。

sVirt 实现确保之前提到的虚拟化域都标记为 MCS 约束类型。可以通过询问系统哪些类型设置了 mcs_constrained_type 属性来确认这一点:

# seinfo -amcs_constrained_type -x
Type Attributes: 1
  attribute mcs_constrained_type
    container_t
    netlabel_peer_t
    openshift_app_t
    openshift_t
    sandbox_min_t
    sandbox_net_t
    sandbox_t
    sandbox_web_t
    sandbox_x_t
    svirt_kvm_net_t
    svirt_qemu_net_t
    svirt_t
    svirt_tcg_t

通过 MCS 限制,sVirt 实现了来宾之间的正确隔离。每个运行中的虚拟机(通常运行为 svirt_t)将被分配两个(随机的)SELinux 分类。虚拟机需要使用的镜像将被分配相同的两个 SELinux 分类。

每当虚拟机试图访问错误的镜像时,MCS 分类的差异将导致 SELinux 拒绝访问。同样,如果一台虚拟机试图连接或攻击另一台虚拟机,MCS 保护将再次阻止这些行为的发生。

sVirt 选择两个类别,即使只有少数类别可用,也能允许大量来宾运行。假设虚拟机监控程序正在使用 c10.c99 类别范围运行。这意味着虚拟机监控程序最多只能选择 90 个类别。如果每个来宾只分配一个类别,则虚拟机监控程序最多可以支持 90 个来宾,才会允许多个来宾之间相互交互(当然,假设恶意行为者发现了允许这种交互的漏洞,虚拟机监控程序软件通常会禁止这种访问)。然而,如果使用两个类别,则同时运行的来宾数量将变为 4,005(这是通过公式 n(n-1)/2* 得到的,表示从 90 个类别中选择的唯一对数)。

让我们来看看 libvirt 的 SELinux 支持是怎样的。

增强 libvirt 以支持 SELinux

libvirt 项目提供了一个虚拟化抽象层,管理员可以通过它管理虚拟机,而无需直接了解或精通底层的虚拟化平台。因此,管理员可以使用 libvirt 提供的工具来管理在 QEMU、QEMU/KVM、Xen 等上运行的虚拟机。

要使用 sVirt 方法,可以构建带有 SELinux 支持的 libvirt。当这种情况发生时,且来宾(从安全角度)由 SELinux 管理,则 sVirt 域和类型将由 libvirt 使用/强制执行。libvirt 代码还会执行类别选择以强制实施来宾隔离,并确保图像文件被分配正确的标签(正在使用的图像文件应该与不活跃的图像文件分配不同的标签)。

区分共享资源和专用资源

不同的图像标签允许不同的使用场景。用于托管主要操作系统(来宾系统)的图像通常会接收 svirt_image_t 标签,并与来宾运行时本身(以 svirt_t 运行)共享相同的类别对进行重新分类。此图像是可由来宾写入的。

当我们考虑一个需要多个来宾读取或写入的图像时,libvirt 可以选择不为该文件分配任何类别。没有类别时,MCS 约束不适用(实际上它们仍然适用,但任何类别集都会主导空类别集,因此对这些正确标记的文件的操作是允许的)。

需要为来宾以只读方式挂载的图像(如可启动媒体)会被分配 virt_content_t 类型。如果是专用的,则还可以分配类别。对于共享读访问,无需分配类别。

请注意,这些标签差异主要适用于虚拟化技术,而不适用于容器技术。

评估 libvirt 架构

libvirt 项目有多个客户端与 libvirtd 守护进程交互。该守护进程负责管理本地虚拟化管理程序软件(无论是 QEMU/KVM、Xen 还是其他任何虚拟化软件),甚至能够管理远程虚拟化管理程序。后一种功能通常用于提供必要 API 来管理主机上虚拟资源的专有虚拟化管理程序:

图 9.1 – libvirt 高层架构

图 9.1 – libvirt 高层架构

由于 libvirt 项目的跨平台和跨虚拟化管理程序特性,sVirt 是一个很好的匹配。为了确保环境的安全,使用的是通用(但受限的)域,而不是特定于虚拟化管理程序的域。

为 sVirt 配置 libvirt

大多数支持 libvirt 的 SELinux 系统将自动启用 SELinux 支持。如果不是这样,但 SELinux 支持是可能的,那么只需配置 libvirt 以允许 SELinux 安全模型。我们在 libvirt 中按每个虚拟化管理程序的基础映射 SELinux 安全模型。

与 sVirt 相关的配置参数通常是按每个虚拟化管理程序的基础定义的。例如,对于基于 QEMU 的虚拟化驱动程序,我们需要编辑 /etc/libvirt/qemu.conf 文件。让我们看看与安全虚拟化相关的各种参数:

  • 第一个参数,定义 sVirt 是否启用的是 security_``driver 参数。虽然 libvirt 会在检测到 SELinux 激活时默认启用 SELinux,但我们可以通过设置 selinux 值显式标记 sVirt 支持为启用:

    security_driver = "selinux"
    security_driver = "none"
    
  • libvirt 中与 sVirt 相关的第二个设置是 security_default_confined。该变量定义了虚拟机是否默认受限(因此与 sVirt 保护相关)或不受限。默认值为 1,表示默认启用限制。要禁用它,您需要将其设置为 0

    security_default_confined = 0
    
  • libvirt 软件的用户也可以请求创建一个未受限制的虚拟机(libvirt 默认允许此操作)。如果我们将 security_require_confined 设置为 1,则不能创建未受限制的虚拟机:

    security_require_confined = 1
    

当平台上有活动的虚拟机时,我们可以确认 sVirt 正在运行,因为我们可以检查其进程的标签,验证它是否确实获得了两个随机类别。

让我们创建这样一个虚拟机,使用常规的 QEMU 虚拟化管理程序。我们使用 Alpine Linux ISO 来启动虚拟机,但这只是一个示例——您可以用任何您想要的 ISO 替换它:

# virt-install --virt-type=qemu --name test \
  --ram 128 --vcpus=1 --graphics none \
  --os-variant=alpinelinux3.8 \
  --cdrom=/var/lib/libvirt/boot/alpine-extended-x86_64.iso \
  --disk path=/var/lib/libvirt/images/test.qcow2,size=1,format=qcow2

提到的位置很重要,因为它们将确保文件被正确标记:

  • /var/lib/libvirt/boot(和 /var/lib/libvirt/isos)中,应放置只读内容,这将导致文件自动被标记为 virt_content_t

  • /var/lib/libvirt/images中,我们创建实际的虚拟机镜像。当虚拟机关闭时,镜像将标记为virt_image_t,但一旦启动,标签会根据与域相关的类别进行调整。

该命令将创建一个名为test的虚拟机,分配 128 MB 内存和 1 个虚拟 CPU。不会启用任何特定的图形支持,这意味着虚拟机的标准控制台或屏幕将不会与任何图形服务(如虚拟网络计算VNC))相关联,而是依赖于虚拟机内部的串行控制台定义。此外,我们还让虚拟机使用一个 1 GB 的小磁盘,并采用QEMU 写时复制QCOW2)格式。

一旦我们创建了虚拟机并启动它,我们可以轻松检查其标签:

# ps -efZ | grep test
system_u:system_r:svirt_tcg_t:s0:c533,c565 /usr/bin/qemu-system-x86_64 -name guest=test,...

要列出当前定义的虚拟机,可以使用virsh命令:

# virsh list --all
 Id   Name     State
------------------------
 1    test     running

--all参数将确保即使是已经定义但当前未运行的虚拟机也会被列出。

重要提示

在 libvirt 中,虚拟机实际上被称为。由于 SELinux(因此本书)在提到进程上下文时也常使用一词,因此我们在提到 libvirt 的域时,将使用虚拟机这一术语,以避免可能的混淆。

virsh命令是与 libvirt 交互的主要入口。例如,要向虚拟机发送关机信号,可以使用shutdown参数,而destroy参数则会强制虚拟机关机。最后,要删除定义,可以使用undefine

如前面示例所示,我们定义的虚拟机正在运行 svirt_tcg_t域。让我们看看如何调整 libvirt 为虚拟机使用的标签。

更改虚拟机的 SELinux 标签

一旦定义了虚拟机,libvirt 允许管理员通过编辑表示虚拟机的 XML 文件来修改其参数。在这个 XML 文件中,SELinux 标签也有一个位置。

要查看当前定义,可以使用virshdumpxml参数:

# virsh dumpxml test

在 XML 的末尾,会显示安全标签。对于 SELinux,它可能是这样的:

<seclabel type='dynamic' model='selinux' relabel='yes'>
  <label>system_u:system_r:svirt_tcg_t:s0:c533,c565</label>
  <imagelabel>system_u:object_r:svirt_image_t:s0:c533,c565</imagelabel>
</seclabel>

如果我们想修改这些设置,可以使用virshedit参数:

# virsh edit test

这将打开本地编辑器中的 XML 文件。然而,一旦我们完成这一步,我们会注意到seclabel条目并没有出现。这是因为默认行为是使用动态标签(因此type='dynamic')和默认标签,这不需要任何默认定义。

让我们改用静态定义,并让虚拟机使用c123,c124类别对运行。在显示的 XML 中,在结尾(但仍然在<domain>...</domain>定义内),放入以下 XML 片段:

<seclabel type='static' model='selinux' relabel='yes'>
  <label>system_u:system_r:svirt_tcg_t:s0:c123,c124</label>
</seclabel>

以不同类型运行来宾当然也可以用类似的方式进行,改变 svirt_tcg_t 为其他类型。然而,记住并非所有类型都能使用。例如,默认的 svirt_t 域不能与 QEMU 的完全系统虚拟化一起使用(因为如果 QEMU 不能使用 KVM,它会使用 TCG)。

重要提示

libvirt 使用的默认类型声明在 /etc/selinux/targeted/contexts 中,分别在 virtual_domain_contextvirtual_image_context 文件中。然而,不建议更改这些文件,因为它们会在 SELinux 策略更新时被发行版覆盖。

relabel 语句请求 libvirt 根据来宾当前分配的标签(relabel='yes')或不进行重新标记(relabel='no')来重新标记所有资源。对于动态类别分配,这将始终是 yes,而对于静态定义,两个值都是可能的。

当然,如果我们需要,我们也可以使用带有自定义类型定义的动态类别分配。为此,我们声明 type='dynamic',但在 <baselabel> 实体中显式定义一个标签,如下所示:

<seclabel type='dynamic' model='selinux'>
  <baselabel>system_u:system_r:svirt_t:s0</baselabel>
</seclabel>

这将使来宾使用动态关联的类别对运行,同时使用自定义标签,而不是默认选择的标签。

自定义资源标签

如果来宾定义启用了重新标记(无论是由于使用了动态类别分配,还是管理员明确请求),则来宾使用的资源将相应地进行重新标记。

管理员可以通过我们之前使用的相同接口自定义 libvirt 的标记行为:来宾定义文件。例如,如果我们不希望 libvirt 重新标记表示来宾磁盘的 test.qcow2 文件,我们可以像这样将其添加到 XML 中:

<disk type='file' device='disk'>
  <driver name='qemu' type='qcow2'/>
  <source file='/var/lib/libvirt/images/test.qcow2'>
    <seclabel relabel='no'/>
  </source>
  <target dev='hda' bus='ide'/>
  <address type='drive' controller='0' bus='0'
           target='0' unit='0'/>
</disk>

这在你希望允许不同来宾之间共享一些资源,但又不希望所有来宾都能读取这些资源时非常有用。在这种情况下,我们可以将文件本身标记为(例如)svirt_image_t:s0:c123,并确保具有类别对的来宾始终包含类别 c123

控制可用的类别

当 libvirt 选择随机类别时,它会根据自己的类别范围进行选择。默认情况下,MCS 系统将这个范围设置为 c0.c1023。要更改类别范围,我们需要确保以正确的上下文启动 libvirt 守护进程(libvirtd)。

在 systemd 中,我们在 第七章配置特定应用的 SELinux 控制 中看到,这可以通过编辑服务单元文件并定义正确的 SELinuxContext 变量来实现。让我们也将其应用于 libvirtd

  1. 首先,将系统提供的 libvirtd.service 文件复制到 /etc/systemd/system

    # cp /usr/lib/systemd/system/libvirtd.service /etc/systemd/system
    
  2. 编辑 libvirtd.service 文件并添加以下定义:

    SELinuxContext=system_u:system_r:virtd_t:s0-s0:c800.c899
    
  3. 重新加载 systemd 的守护进程定义,以便它能够获取新的 libvirtd.service 文件:

    # systemctl daemon-reload
    
  4. 重启 libvirtd 守护进程:

    # systemctl stop libvirtd
    # systemctl start libvirtd
    
  5. 现在我们可以重新启动我们的虚拟机并验证每个虚拟机现在是否在libvirtd守护进程定义的范围内运行。

    # virsh start test
    # ps -efZ | grep virt
    system_u:system_r:virtd_t:s0-s0:c800.c899 /usr/sbin/libvirtd
    system_u:system_r:svirt_t:s0:c846,c891 /usr/bin/qemu-system-x86_64 -name guest=test...
    

    如我们所见,libvirt 选择的类别现在都在定义的范围内。

不使用 systemd 的系统可以编辑 SysV 风格的init脚本,并使用runcon

runcon -l s0-s0:c800.c899 /usr/sbin/libvirtd \
  --config /etc/libvirt/libvirtd.conf --listen

每次启动新虚拟机时,libvirt 的代码会随机选择两个类别。然后,服务将检查这些类别是否属于其自身的范围,以及该类别对是否已被使用。如果这些检查中的任何一个失败,libvirt 将随机选择一对新的类别,直到找到符合要求的空闲类别对。

更改存储池位置

libvirt 的一个非常常见的配置更改是将其重新配置为使用不同的存储池位置。这对 SELinux 也有轻微影响,因为我们没有为新位置定义正确的文件上下文。

让我们来看一下如何创建一个新的存储池位置并更改 SELinux 配置:

  1. 列出当前的存储池,以确保新池的名称未被占用:

    # virsh pool-list --all
     Name                   State     Autostart
    -----------------------------------------------
     boot                   active    yes
     images                 active    yes
     root                   active    yes
    
  2. 创建目标位置:

    # mkdir /srv/images
    
  3. 使用pool-define-as创建新的存储池。在以下命令中,我们将池命名为large_images

    # virsh pool-define-as large_images dir - - - - "/srv/images"
    Pool large_images defined
    
  4. 配置 SELinux 以正确标记存储池:

    # semanage fcontext -a -t virt_image_t "/srv/images(/.*)?" 
    
  5. 重新标记目录结构:

    # restorecon -R /srv/images
    
  6. 让 libvirt 填充目录结构:

    # virsh pool-build large_images
    
  7. 启动存储池:

    # virsh pool-start large_images
    
  8. 打开自动启动,以便当 libvirtd 启动时,存储池也可以立即使用:

    # virsh pool-autostart large_images
    
  9. 我们可以使用pool-info命令来验证一切是否正常运作:

    # virsh pool-info large_images
    

    输出将显示新位置的当前和可用容量。

如果我们将存储池托管在 NFS 挂载的位置上,那么我们还需要启用virt_use_nfs SELinux 布尔值。

现在我们已经完全掌握了如何配置 libvirt 和 SELinux,接下来让我们看看如何将流行的 Vagrant 工具与 libvirt 配合使用。

使用 Vagrant 与 libvirt

Vagrant 是一个快速启动和管理虚拟机的框架,在开发社区中非常流行。虽然 Vagrant 默认使用 Oracle VirtualBox 作为虚拟机管理程序,但我们可以安装 libvirt 插件来将 Vagrant 与 libvirt 一起使用,从而受益于 SELinux 提供的 sVirt 安全性。

部署 Vagrant 和 libvirt 插件

Vagrant 应用程序可以通过一个单独的 RPM 文件进行安装。您可以在 www.vagrantup.com/downloads.html 找到最新版本并安装。例如,对于 CentOS 系统,您可以直接使用 yum

# yum install https://releases.hashicorp.com/vagrant/2.2.9/vagrant_2.2.9_x86_64.rpm

要安装 libvirt 插件,我们首先需要确保依赖项已正确安装。在线文档 github.com/vagrant-libvirt/vagrant-libvirt 给出了需要安装的包的概述。不要忘记这一步,因为插件安装过程中出现的依赖项失败并不总是显而易见的。

安装好依赖后,使用vagrant本身下载并安装插件:

# vagrant plugin install vagrant-libvirt

安装完插件后,我们可以继续设置 Vagrant box。

安装一个兼容 libvirt 的 box

Vagrant 使用box:这些是为快速安装 Vagrant 而准备的镜像。并不是所有的 Vagrant box 都与 libvirt 提供者兼容。幸运的是,Vagrant Cloud 网站(app.vagrantup.com/boxes/search?provider=libvirt)允许你快速找到兼容的 box。

假设我们想要使用一个名为fedora/32-cloud-base的 Fedora 镜像,那么我们可以按如下方式进行配置:

  1. 创建一个新的目录,我们将在其中定义 box 配置,并进入该位置:

    # mkdir vagrant
    # cd vagrant
    
  2. 初始化 Vagrant box,使用fedora/32-cloud-base的 box 定义:

    Vagrantfile that can be used to further configure the box.
    
  3. 编辑Vagrantfile并添加以下代码:

    config.vm.provider :libvirt do |libvirt|
      libvirt.storage_pool_name = "images"
      libvirt.driver = "qemu" # or kvm
    end
    

    这将配置 libvirt 提供者使用images目录作为默认存储池,并在 libvirt 中使用 QEMU 驱动。

  4. 仍然在Vagrantfile中,添加以下代码来为 box 指定一个合适的名称:

    config.vm.define :test do |test|
      test.vm.box = "fedora/32-cloud-base"
    end
    

    这里选择的名称是test,这将导致一个名为vagrant_test的虚拟机。

  5. 要启动测试虚拟机,像这样运行vagrant up命令:

    vagrant up every time with the --provider=libvirt parameter, we can also declare the VAGRANT_DEFAULT_PROVIDER="libvirt" environment variable and drop the command-line argument, as the environment variable will then be used.
    

一旦虚拟机启动并运行,你可以使用vagrant ssh连接到它。虽然你可以使用virsh命令操作虚拟机,但你也可以使用vagrant halt来关闭虚拟机,或者使用vagrant destroy,然后运行vagrant box remove来彻底从系统中移除该 box。

配置 Vagrant box

一旦一个 box 被部署,它就可以通过 libvirt 作为标准虚拟机使用。这意味着我们之前看到的通过修改标签或使用 SELinux 布尔值调整 SELinux 控制的操作仍然适用。

让我们首先验证一下 Vagrant 是否确实在使用 libvirt 来启动自己的 box:

# virsh list --all
 Id    Name            State
-----------------------------
 1     vagrant_test    running

的确,虚拟机已可用,名为vagrant_test。我们也可以使用virsh edit命令来修改其配置:

# virsh edit vagrant_test

只要 Vagrant box 没有被销毁,libvirt 中的设置将会持续存在。

总结

虚拟化是一项强大的技术,其安全性可以通过 SELinux 得到增强。通过 sVirt,开源社区提供了一种强有力的方法来隔离虚拟机,确保虚拟机只能访问它们应该访问的资源。

在这一章中,我们探讨了虚拟化及其相关的风险,并讨论了如何通过 SELinux 提供的控制手段(如类型强制和 MCS 隔离)来缓解这些风险。

接着,我们介绍了 libvirt 如何在 Linux 平台上支持多种虚拟化技术,以及它如何包含一项名为 sVirt 的技术,该技术实现了 SELinux 集成,提供了客户机隔离和访问控制。我们看到管理员如何在 libvirt 内操作 sVirt 逻辑,例如通过不同的域标签或类别集。我们以如何将 Vagrant 与 libvirt 一起使用的信息结束了本章内容。

在下一章,我们将介绍另一种虚拟化解决方案,称为 Xen,它采用了一种基于 SELinux 的技术来加强安全性。

问题

  1. sVirt 有什么独特之处是常规 SELinux 配置无法做到的?

  2. SELinux 在虚拟化中解决的两个主要风险是什么?

  3. virt_image_tsvirt_image_tvirt_content_t 之间有什么区别?

  4. 如何使用 libvirt 更改客户机标签?

  5. 我们如何使用 Vagrant 并仍然受益于 sVirt?

第十章:使用 Xen 安全模块与 FLASK

第九章 安全虚拟化 中,我们看到 libvirt 能够基于 SELinux 域和类别分配,对几个支持的虚拟机监控器应用 sVirt 保护措施。Xen 作为另一种流行的开源虚拟机监控器,也受 libvirt 支持,但更常见的做法是单独使用 Xen,而不依赖 libvirt。

Xen 本身有一个名为 Xen 安全模块XSM)的安全框架,类似于 Linux 安全模块LSM),以及一个名为 XSM-FLASK 的访问控制系统,这是基于 SELinux 的安全框架。我们将了解 Xen 如何使用 XSM,如何构建支持 XSM 的 Xen,最后,如何将策略应用于 Xen 域。

在本章中,我们将讨论以下主要主题:

  • 理解 Xen 和 XSM

  • 运行支持 XSM 的 Xen

  • 应用自定义 XSM 策略

技术要求

查看以下视频,了解代码演示:bit.ly/3kcCePl

理解 Xen 和 XSM

Xen 项目是一个由 Linux 基金会维护的项目,负责维护 Xen 虚拟机监控器(hypervisor)。虽然 Xen 项目管理多个与安全性和虚拟化相关的软件,但我们的重点是 Xen 虚拟机监控器。

介绍 Xen 虚拟机监控器

Xen 虚拟机监控器直接运行在硬件之上,位于各种虚拟机和硬件之间。与在 Linux 中作为进程运行以提供虚拟化功能的 QEMU 或 KVM 不同,Xen 工作得更加独立。因此,管理员不会将运行的实例视为独立的进程。相反,他们需要依赖 Xen 命令和 API 来获取更多信息并与 Xen 虚拟机监控器交互。

重要提示

与 libvirt 类似,Xen 虚拟机监控器使用 domain 这个术语来指代其虚拟机。由于我们在 SELinux 中经常使用 domain 来表示正在运行的进程的 SELinux 类型,因此也表示正在运行的虚拟机的 SELinux 类型,我们将在可能的情况下使用 guest(来宾)一词。然而,也会有一些与 Xen 相关的术语,我们需要保留 domain 术语。

Xen 总是至少定义了一个虚拟来宾,称为 xend。管理员通过 dom0 来创建和操作运行在 Xen 中的虚拟来宾。这些常规来宾是非特权的,因此简称为 domU——非特权域

当管理员启动 Xen 主机时,他们会启动 Xen 的 dom0 实例,然后通过该实例与 Xen 进行进一步的交互。Linux 内核已经支持在 dom0domU 中运行很长时间了(自 Linux 内核 3.0 起,包括完整的支持,如后端驱动程序)。

让我们使用现有的 Linux 部署来安装 Xen,并使用这个现有的部署作为 Xen 的 dom0 来宾。

安装 Xen

虽然许多 Linux 发行版开箱即用提供 Xen 支持,但这些发行版很可能不支持 XSM(我们将在启用 XSM 的 Xen 运行部分启用它)。因此,在不急于调整预构建的 Xen 环境的情况下,我们希望直接从 Xen 项目发布的源代码开始构建它。

在我们开始使用 Xen 之前,更不用说启用它的 XSM 支持,我们首先需要确保正在运行一个启用 Xen 的 Linux 内核。

使用启用 Xen 的 Linux 内核

系统上的 Linux 内核必须支持(至少)在 dom0 客户机中运行。没有此支持,dom0 客户机不仅无法与 Xen 虚拟机监控程序交互,还无法启动 Xen 虚拟机监控程序本身(Xen 启用内核需要在启动自己作为 dom0 客户机之前引导 Xen 虚拟机监控程序)。

如果你构建自己的 Linux 内核,必须根据wiki.xenproject.org/wiki/Mainline_Linux_Kernel_Configs中的文档配置内核。一些 Linux 发行版提供了更深入的构建说明(如 Gentoo 的wiki.gentoo.org/wiki/Xen)。然而,在 CentOS 中,当前的最后版本缺少开箱即用的 Xen 支持(因为 CentOS 更专注于 libvirt 及其相关技术的虚拟化支持)。

幸运的是,社区提供了维护良好的 Linux 内核版本,这些版本包括 Xen 支持,通过kernel-ml包提供。让我们安装这个内核包:

  1. 启用企业 Linux 仓库ELRepo),该仓库引入了其他几个社区驱动的仓库:

    # yum install elrepo-release
    
  2. 安装kernel-ml包,它将安装最新的 Linux 内核,并带有包括 Xen 支持的配置。我们同时启用elrepo-kernel仓库,该仓库提供了这个包:

    # yum install --enablerepo=elrepo-kernel kernel-ml
    
  3. 通常,Linux 启动加载程序会重新配置以包含这些新内核。如果没有,或者你想确保内核被正确检测,可以使用以下命令重新生成统一引导加载程序GRUB2)配置文件:

    # grub2-mkconfig -o /boot/grub2/grub.cfg
    

    当然,如果你的系统使用的是不同的启动加载程序,则需要遵循不同的说明。请参考你的 Linux 发行版文档,了解如何配置启动加载程序。

  4. 使用新安装的内核重启系统:

    # reboot
    

如果一切顺利,你现在应该在运行一个兼容 Xen 的内核。当然,这并不意味着 Xen 已启用,而只是表示如果需要,内核能够支持 Xen。接下来,让我们继续构建 Xen 虚拟机监控程序及相关工具。

从源代码构建 Xen

Xen 虚拟机监控程序和工具依赖于各种程序和库,而在从源代码构建 Xen 时,并非所有工具和库都能正确检测为依赖项。

让我们首先安装这些依赖项:

  1. 启用PowerTools仓库:

    # dnf config-manger --set-enabled PowerTools
    
  2. 安装 CentOS 仓库支持的依赖项:

    # yum install gcc xz-devel python36-devel acpica-tools uuid-devel ncurses-devel glib2-devel pixman-devel yajl yajl-devel zlib-devel transfig pandoc perl-Pod-Html git glibc-devel.i686 patch libuuid-devel
    
  3. 安装 dev86 包。写这篇文档时,CentOS 8 中还没有该包,因此我们使用 CentOS 7 中的版本:

    # yum install https://download-ib01.fedoraproject.org/pub/epel/7/x86_64/Packages/d/dev86-0.16.21-2.el7.x86_64.rpm
    

现在已安装好依赖项,让我们下载最新的 Xen 并进行构建:

  1. 访问 xenproject.org/downloads/ 并转到最新的 Xen Project 发行版。

  2. 在页面底部,下载最新的归档文件。

  3. 在系统上解压下载的归档文件:

    $ tar xvf xen-4.13.1.tar.gz
    
  4. 进入解压后的归档文件所在的目录:

    $ cd xen-4.13.1
    
  5. 配置本地系统的源。在此阶段,不需要传递特定的参数:

    $ ./configure
    
  6. 构建 Xen 虚拟机监控器及相关工具:

    $ make world
    
  7. 在系统上安装 Xen 虚拟机监控器和工具:

    # make install
    
  8. 重新配置引导加载程序。此操作应自动检测 Xen 二进制文件并添加必要的引导加载项:

    # grub2-mkconfig -o /boot/grub2/grub.cfg
    
  9. 配置系统以支持安装在 /usr/local/lib 中的库:

    # echo "/usr/local/lib" > /etc/ld.so.conf.d/local-xen.conf
    # ldconfig
    
  10. /usr/local 中的子目录创建等价规则,以便正确应用 SELinux 文件上下文:

    # semanage fcontext -a -e /usr/local/bin /usr/bin
    # semanage fcontext -a -e /usr/local/sbin /usr/sbin
    
  11. 重新标记 /usr/local 中的文件:

    # restorecon -RvF /usr/local
    
  12. 这些步骤的结果是 Xen 已经准备好在系统上启动。然而,默认情况下,引导加载程序不会使用启用 Xen 的内核,因此在重启时,选择正确的条目非常重要。该条目的标题将包含 with Xen hypervisor

    # reboot
    
  13. 在重启进入启用 Xen 的系统后,我们需要做的就是启动 Xen 守护进程:

    # systemctl start xencommons
    # systemctl start xendomains
    # systemctl start xendriverdomain
    # systemctl start xen-watchdog
    
  14. 为了验证一切按预期工作,列出当前正在运行的来宾:

    Domain-0, which is the guest you just executed the xl list command in.
    
  15. 通过确保之前启动的守护进程在启动时重新启动,来完成安装:

    # systemctl enable xencommons
    # systemctl enable xendomains
    # systemctl enable xendriverdomain
    # systemctl enable xen-watchdog
    

在我们继续进行 XSM 操作之前,让我们先在 Xen 中创建一个来宾(作为 domU),这样我们可以在稍后的 使用 XSM 标签 部分将策略与之关联。

创建一个无特权的来宾

当 Xen 虚拟机监控器启用时,我们与 Xen 交互的操作系统被称为 dom0,它是 Xen 支持的(唯一的)特权来宾。其他来宾是无特权的,我们希望通过 XSM 进一步隔离和保护这些来宾之间的交互和它们采取的操作。

首先,我们创建一个简单的无特权来宾,与特权的 dom0 来宾一起运行。在这个例子中,我们使用 Alpine Linux,但您可以轻松地将其替换为其他发行版或操作系统。这个示例将使用 ParaVirtualizedPV)来宾方式,但 Xen 也支持 硬件虚拟化机HVM)来宾:

  1. 下载 Alpine Linux 发行版的 ISO 镜像,因为该发行版在低内存消耗和较低的(虚拟)磁盘大小要求方面更为优化。当然,如果您的系统能够处理,您也可以选择其他发行版。我们选择从 www.alpinelinux.org/downloads/ 下载适用于虚拟系统的优化版,并将 ISO 存储在系统的 /srv/data 中。

  2. 将 ISO 挂载到系统上,以便在接下来的步骤中创建一个没有特权的客体时使用它的可引导内核:

    # mount -o loop -t iso9660 /srv/data/alpine-virt-3.8.0-x86_64.iso /media/cdrom
    
  3. 创建一个映像文件,该文件将用作虚拟客体的启动磁盘:

    # dd if=/dev/zero of=/srv/data/a1.img bs=1M count=3000
    
  4. 接下来,为虚拟客体创建一个配置文件。我们将文件命名为 a1.cfg 并将其放置在 /etc/xen 目录下:

    # Alpine Linux PV DomU
    # Kernel paths for install
    kernel = "/media/cdrom/boot/vmlinuz-virt"
    ramdisk = "/media/cdrom/boot/initramfs-virt"
    extra = "modules=loop,squashfs console=hvc0"
    # Path to HDD and ISO file
    disk = [
      'format=raw, vdev=xvda, access=w, target=/srv/data/a1.img',
      'format=raw, vdev=xvdc, access=r, devtype=cdrom, target=/srv/data/alpine-virt-3.8.0-x86_64.iso'
    ]
    # DomU settings
    memory = 512
    name = "alpine-a1"
    vcpus = 1
    maxvcpus = 1
    
  5. 使用 xl create 命令启动虚拟客体:

    -c option will immediately show the console to interact with, allowing you to initiate and complete the installation of the operating system in the guest. 
    
  6. 当客体需要重启时,使用 shutdown 命令,并编辑配置文件。删除与 ISO 相关的行,防止客体再次启动到安装环境。

  7. 要再次启动客体,请再次使用 xl create 命令。如果客体安装完成且不再需要访问控制台,请删除 -c 选项:

    # xl create -f /etc/xen/xa1.cfg
    
  8. 我们可以通过 xl list 确认虚拟客体是否正在运行:

    # xl list
    Name           ID     Mem VCPUs       State      Time(s)
    Domain-0        0    7836     4      r-----         99.4
    alpina-a1       1     128     1      -b----          2.5
    

在 Xen 中,客体通过 create 子命令启动,并通过 shutdown(优雅关机)或 destroy 子命令关闭。

完成这些步骤后,我们现在拥有一个正在运行的 Xen 安装和一个运行中的客体。接下来,是时候了解 Xen 在安全方面能为我们提供什么了。

了解 Xen 安全模块

第一章基础的 SELinux 概念 中,我们了解到 SELinux 是通过一个名为 Linux 安全模块LSM)的 Linux 子系统来实现的。Xen 借用了这一思想,并采用了类似的安全措施。

通过 Xen 安全模块XSM),Xen 使得可以定义和控制 Xen 客体之间、以及 Xen 客体和 Xen 虚拟机监控程序之间的行为。与 Linux 内核不同,虽然 Linux 内核中有多个强制访问控制框架可以插入到 LSM 子系统中,但 Xen 当前仅提供一个可用的 XSM 模块,名为 XSM-FLASK

FLASK 代表 Flux Advanced Security Kernel,它是 SELinux 用于自己访问控制表达式的安全架构和方法。通过 XSM-FLASK,开发人员和管理员可以执行以下操作:

  • 在客体之间定义权限和细粒度的访问控制

  • 为原本没有特权的客体定义有限的特权提升

  • 在策略级别上控制客体的直接硬件和设备访问

  • 限制并审计由特权客体执行的活动

虽然 XSM-FLASK 使用类似 SELinux 的命名约定(甚至使用 SELinux 构建工具来构建策略),但与 XSM-FLASK 相关的设置是独立于 SELinux 的。如果 dom0 启用了 SELinux(没有理由不启用),其策略与 XSM-FLASK 策略无关。

XSM-FLASK 使用的标签也不会显示在来宾内部运行的常规 Linux 命令中(因此也包括 dom0)。由于运行中的来宾不会作为系统中的进程显示,它们根本没有 SELinux 标签,只有 XSM-FLASK 标签(如果启用)。因此,Xen 无法从 sVirt 方法中受益,如第九章《安全虚拟化》中所述。

运行启用 XSM 的 Xen

从常规的 Xen 部署切换到启用 XSM 的 Xen 部署,只需重新构建 Xen 并启用 XSM 支持,然后重启系统。Xen 自带一个现成的策略,可以直接应用,我们将在 XSM 过程中使用它。

重新构建带有 XSM 支持的 Xen

让我们在系统上重新构建带有 XSM 支持的 Xen 虚拟机监控程序和工具:

  1. 通过在build目录(我们的示例是xen-4.13.1)内运行make clean命令清理之前的构建:

    $ make clean
    
  2. build目录内,进入xen目录:

    $ cd xen
    
  3. 使用make menuconfig启动 Xen 配置:

    $ make menuconfig
    
  4. 进入 XSM 设置并启用与 XSM 相关的参数:

    Common Features --->
      [*] Xen Security Modules support
      [*]   FLux Advanced Security Kernel support
    [*]     Compile Xen with a built-in FLAS security 
              policy
      [*]   SILO support
    Default XSM implementation (FLux Advanced 
            Security Kernel)
    
  5. 返回主构建目录(我们的示例中是xen-4.13.1):

    $ cd ..
    
  6. 重新构建 Xen 虚拟机监控程序和工具:

    $ ./configure
    $ make world
    
  7. 将更新的 Xen 构建安装到系统中:

    /boot (named xenpolicy-4.13.1).
    
  8. 使用新的 Xen 构建重新配置引导加载程序,确保 XSM 策略也与之一起加载:

    # grub2-mkconfig -o /boot/grub2/grub.cfg
    
  9. 重启系统:

    # reboot
    
  10. 一旦重新启动,我们可以通过查询 Xen 以获取与运行中的来宾相关的标签,来验证 XSM 策略是否已加载并生效:

    # xl list -Z
    Name               ID   ...     Security Label
    Domain-0            0   ...   system_u:system_r:dom0_t
    alpina-a1           1   ...   system_u:system_r:domU_t
    

如果xl list命令在给定-Z参数时列出安全标签,那么 Xen 正在以 XSM 策略运行。我们来看这些标签的使用位置。

使用 XSM 标签

当 Xen 以 XSM 支持启动并且默认策略已启用时,来宾可以使用以下类型:

  • dom0_t 类型保留给特权来宾使用。

  • domU_t 类型是用于非特权来宾的默认类型。

  • isolated_domU_t 类型用于分配给不应与其他非特权来宾交互的非特权来宾,仅能与特权 dom0 进行交互。

  • prot_domU_t 类型用于那些如果 XSM 策略布尔值prot_doms_locked被设置时将被阻止启动的来宾。

  • nomigrate_t 类型应用于不允许从一个 Xen 主机迁移到另一个 Xen 主机的来宾。内部机制防止 dom0 来宾在启动后访问该来宾的内存。

XSM 策略中还有一些其他类型不可用于常规来宾:

  • dm_dom_t 类型分配给设备模型来宾。这是一个特殊的特权来宾,表示为 HVM 类型来宾虚拟化的硬件,而不危及 dom0。

  • xenstore_t 类型分配给xenstore存根来宾。这是一个特殊的特权来宾,支持非特权来宾访问其虚拟化资源,而不危及 dom0。

  • nic_dev_t类型分配给可以在直通模式下使用的硬件设备(这意味着 domU 客户机可以直接与这些硬件设备交互,而无需通过特权客户机)。

这些占位域(在 Xen 中被称为占位域stubdoms)是 Xen 进一步增强安全性的方式,因为无法被阻止的特权操作与 dom0 的隔离性增强。如果在任何时候这些特权服务中的安全漏洞被利用,它们不一定会影响 dom0,并且通过适当的 XSM 策略,甚至可以完全缓解。

将其中一个标签分配给客户机,只需编辑客户机在/etc/xen中的配置文件,并添加seclabel配置参数:

seclabel = 'system_u:system_r:isolated_domU_t'

一旦配置并重启(使用xl create),新标签将在查询运行中的客户机时可见:

# xl list -Z
Name               ID   ...   Security Label
Domain-0            0   ... system_u:system_r:dom0_t
alpina-a1           1   ... system_u:system_r:isolated_domU_t

为客户机应用正确的标签是最常见的用例(因为它有效地处理了我们期望从 XSM 实现中获得的访问控制和保护措施),但也支持其他操作。

操作 XSM

与 SELinux 一样,可以执行几项活动来进一步操作 XSM 子系统或当前活动策略。

定义状态,范围从禁用到强制执行

当 Xen 启动时,我们可以添加一个名为flask的内核参数,可以设置为以下值之一:

  • flask=enforcing时,确保 XSM 处于活动状态,强制执行其客体与资源之间的策略,并且强制执行是即时的(没有延迟激活)。

  • flask=permissive时,XSM 将加载策略,但不会强制执行策略中设定的规则。这显然是为了开发目的,行为类似于 SELinux 的宽容模式。

  • flask=late时,XSM 在加载策略之前不会强制执行任何访问控制,加载策略后才会开始强制执行。这允许管理员启动时启用 XSM,但只有在管理员认为准备好时,才加载并应用策略。

  • flask=disabled时,XSM 不会强制执行任何访问控制,也不会加载策略。

该参数可以在启动时直接设置(从引导加载程序)或通过系统中的引导加载程序配置进行设置。例如,使用 GRUB2 时,我们可以编辑/etc/default/grub并添加或修改以下参数:

GRUB_CMDLINE_XEN_DEFAULT="flask=enforcing"

别忘了重新生成 GRUB2 配置文件:

# grub2-mkconfig -o /boot/grub2/grub.cfg

与 SELinux 一样,我们也可以通过命令行操作 XSM 的状态。使用xl getenforce,我们可以查询当前状态:

# xl getenforce
Enforcing

可以使用xl setenforce命令切换到另一种状态:

# xl setenforce permissive

这些命令与 dom0 中的 SELinux 配置无关:将 Xen 从宽容模式切换到强制模式或反之,特定于 Xen,不会影响 dom0 中的 SELinux 设置。

查询 XSM 日志

像 SELinux 一样,XSM 也使用 AVC 日志记录来向管理员提供关于它所做决定的反馈。使用xl dmesg,我们可以查询这些日志信息(以及其他 Xen 输出日志):

# xl dmesg
...
(XEN) avc: granted { setenforce } for domid=0 scontext=system_u:system_r:dom0_t tcontext=system_u:system_r:security_t tclass=security

并非所有已授权的操作都会被记录,但被拒绝的操作总会导致 AVC 条目的生成。这些 AVC 条目本身格式与 SELinux 的 AVC 条目完全相同,使得管理员可以使用 SELinux 工具,如audit2allow,来生成 XSM 策略。

使用 XSM 布尔值

Xen 启用的默认策略有两个布尔值可以切换:

  • guest_writeconsole布尔值,默认为 1(开启),允许来宾访问并写入 Xen 控制台。

  • prot_doms_locked布尔值,默认为 0(关闭),如果启用,将禁止prot_domU_t来宾启动。

虽然xl命令没有可用的子命令来查询和设置 XSM 布尔值,但系统中安装了另外两个命令来完成此任务——flask-get-boolflask-set-bool

  • 使用flask-get-bool,我们可以查询布尔值的当前状态,或者列出所有布尔值及其当前值:

    # flask-get-bool -a
    guest_writeconsole: 1
    prot_doms_locked: 0
    
  • flask-set-bool命令用于切换布尔值:

    # flask-set-bool prot_doms_locked 1
    

这与 SELinux 的getseboolsetsebool命令非常相似。

查询 XSM 策略

XSM 策略文件(xenpolicy-4.13.1)与 SELinux 策略文件非常相似。因此,我们可以使用 SELinux 工具查询此文件并了解更多关于策略的信息:

  • 使用seinfo,我们可以查询有关策略的统计信息,查看支持的类、已启用的约束以及更多内容。唯一会失败的查询是列出策略中支持的类型:

    $ seinfo --all ./xenpolicy-4.13.1
    
  • 使用sesearch,我们可以查询 XSM 策略规则本身,例如列出所有允许规则:

    $ sesearch -A ./xenpolicy-4.13.1
    

当我们在第十三章中讨论分析 SELinux 策略时,分析策略行为,我们将熟悉其他可以用于分析 XSM 策略文件的工具。

标记硬件资源

使用flask-label-pci命令,管理员可以为指定的 PCI 设备打上给定的标签。这种方法允许管理员为无特权的来宾标记某些设备,以便进行直通访问。

例如,要将地址为3:2:0的 PCI 设备标记为nic_dev_t类型,可以使用以下命令:

# flask-label-pci 0000:03:02.0 system_u:object_r:nic_dev_t

如你从名称中可能猜到的,这种类型最初是为网络设备的直通访问而定义的,但也可以用于其他 PCI 硬件。

应用自定义 XSM 策略

Xen 还允许管理员构建和使用他们自己的自定义策略。

Xen 的默认策略位于 Xen 构建目录中的tools/flask/policy目录下。例如,dom0 来宾的策略规则位于modules/dom0.te中。

重要说明

调整 Xen XSM 策略超出了本章的范围。你可以在 第十五章 使用参考策略 中找到有关如何使用参考策略风格方法创建 SELinux 策略的说明。Xen XSM 策略基于这种风格。

构建自定义策略的方法是更新这些文件(更新前请先备份),然后重新构建策略本身:

$ make

策略构建的结果是一个新的 xenpolicy-4.13.1 文件。这个文件可以通过 xl loadpolicy 命令直接加载:

# xl loadpolicy /path/to/xenpolicy-4.13.1

这个命令类似于 flask-loadpolicy 命令:

# flask-loadpolicy /path/to/xenpolicy-4.13.1

如果在测试后,策略被认为已经准备好用于持续使用,可以将其复制到 /boot 目录,这样它将在下次启动时自动加载。

总结

Xen 虚拟机监控器与 QEMU 和 KVM 虚拟机监控器有很大不同,后者在 libvirt 中更常用。Xen 的 SELinux 支持与 sVirt 也不同,因为 SELinux 子系统只能在 Xen 客户操作系统内部启用,且 SELinux 无法看到其他客户操作系统。

Xen 通过实现自己的 SELinux 副本作为 XSM-FLASK 解决了这个问题,并在其工具中集成了对 XSM-FLASK 标签的适当支持。在本章中,我们学习了如何为 Xen 客户操作系统应用我们自己的类型,切换 XSM 状态,切换 XSM 布尔值,甚至如何构建并加载我们自己的 XSM-FLASK 策略。

在下一章,我们将讨论容器工作负载以及 SELinux 如何帮助管理员进一步强化和保护容器运行时。我们将看到如何将 sVirt 应用于容器运行时,以及工具如何处理 SELinux 支持。

问题

  1. 为什么常规的 SELinux 子系统不管理 Xen 客户操作系统?

  2. Xen 客户操作系统如何分配标签?

  3. 处理 XSM 标签的常见 Xen 命令有哪些?

  4. 管理员如何加载自定义策略进行测试?

第十一章:增强容器化工作负载的安全性

容器平台和管理框架为管理员和开发者提供了应用层抽象。轻量级容器框架允许快速开发和部署新应用,而较重的容器平台则可以实现资源的最优消耗和高度弹性的托管平台。

SELinux 在许多框架和平台中发挥着重要作用,确保不受信任的容器无法逃脱或与它们不应交互的资源进行交互。在本章中,我们将探讨 SELinux 如何得到支持,从systemd-nspawnpodman(以及 Docker),最后是在更大的环境中与 Kubernetes 结合使用。我们还将学习如何使用udica工具为容器创建自定义的 SELinux 域。

在本章中,我们将涵盖以下主要内容:

  • 使用 SELinux 与 systemd 的容器支持

  • 配置 podman

  • 利用 Kubernetes 的 SELinux 支持

技术要求

查看以下视频,观看代码实际运行: bit.ly/34aHOfl

使用 SELinux 与 systemd 的容器支持

第七章配置特定应用的 SELinux 控制中,我们介绍了 systemd 作为一个 SELinux 感知的应用套件,能够以可配置的 SELinux 上下文启动不同的服务。除了服务支持外,systemd 还拥有其他不少功能。其中之一就是systemd-nspawn

使用systemd-nspawn,systemd 提供容器功能,允许管理员以集成的方式与 systemd 管理的容器进行交互,几乎就像这些容器本身就是服务一样。它使用与 Linux Containers 项目(现代容器框架的前身)和 Docker 相同的原语,基于命名空间(因此有nspawn中的n)。

信息性说明

Linux Containers 项目有一个名为LXC的产品,它结合了 Linux 内核中的多个隔离和资源管理服务,如控制组cgroups)和命名空间隔离。cgroups 允许对 CPU、内存和 I/O 的资源消耗进行限制或调节,而命名空间则允许隐藏信息并限制对系统资源的访问视图。Docker 的早期版本是建立在 LXC 之上的,尽管 Docker 后来直接使用了 Linux 服务本身,而不再使用 LXC。

在 SELinux 方面,容器内运行的软件可能无法正确地查看 SELinux 的状态(这取决于容器配置),因为容器与主机本身是隔离的。SELinux 目前尚不支持命名空间,无法让容器或其他隔离进程拥有自己的 SELinux 视图,因此,如果容器能够查看 SELinux 状态,它绝不应该被允许修改它。

现在,与可以使用我们在第九章中看到的 sVirt 方法的 Docker、podman和 Kubernetes 不同,安全虚拟化systemd-nspawn方法不支持此技术。

说明性备注

systemd-nspawn命令可能没有默认安装。在 CentOS、Debian 及相关发行版中,提供此工具的软件包名为systemd-container。其他发行版如 Gentoo 和 Arch Linux 则将其作为默认的 systemd 安装的一部分。

让我们看看systemd-nspawn是如何工作的,以及它的 SELinux 支持情况。

初始化 systemd 容器

要创建一个 systemd 容器,我们需要在文件系统中创建一个存放其文件的位置,然后使用正确的参数调用systemd-nspawn。为了准备文件系统,我们可以下载预构建的容器镜像,或自行创建一个。让我们使用 Jailkit 软件,正如在第七章中使用的那样,配置应用程序特定的 SELinux 控制,并从中构建一个容器:

  1. 首先,创建容器运行时将托管的目录:

    # mkdir /srv/ctr
    
  2. 编辑/etc/jailkit/jk_init.ini文件,并包含以下部分:

    [nginx]
    comment = nginx runtime
    paths = /usr/sbin/nginx, /etc/nginx, /var/log/nginx, /var/lib/nginx, /usr/share/nginx, /usr/lib64/nginx, /usr/lib64/perl5/vendor_perl
    users = root,nginx
    groups = root,nginx
    includesections = netbasics, uidbasics, perl
    

    本节告诉 Jailkit 应该将哪些内容复制到目录中,并支持哪些用户。

  3. 执行jk_init命令以填充目录:

    # jk_init -v -j /srv/ctr/nginx nginx
    
  4. 最后,使用systemd-nspawn启动容器:

    # systemd-nspawn -D /srv/ctr/nginx /usr/sbin/nginx \
      -g "daemon off;"
    

由于 Nginx 默认会尝试作为守护进程运行,因此容器会立即停止,因为它没有活动进程。通过使用daemon off选项启动,nginx将保持在前台,容器可以继续工作。

使用特定的 SELinux 上下文

当我们直接启动容器时,该容器将以用户的 SELinux 上下文运行。然而,我们可以通过命令行参数传递容器的目标上下文:

  • --selinux-context=选项(简写为-Z)允许管理员为容器的运行时进程定义 SELinux 上下文。

  • --selinux-apifs-context=选项(简写为-L)允许管理员为容器的文件和文件系统定义 SELinux 上下文。

然而,必须仔细选择可用的 SELinux 类型。容器内运行的进程不能执行任何类型的转换,因此通常无法使用常规的 SELinux 域。以我们的 Nginx 示例为例,httpd_t域不能用于该容器。

我们可以使用发行版为容器工作负载提供的 SELinux 类型。最近的 CentOS 版本将使用如container_t(以前称为svirt_lxc_net_t)这样的域,以及面向文件的 SELinux 类型container_file_t。虽然这个域不具备容器所需的所有可能权限,但它为容器提供了一个良好的基准。

让我们为我们的容器使用这种类型:

  1. 首先,我们需要通过一些附加权限扩展container_t权限,以支持nginx守护进程。创建一个包含以下内容的 CIL 策略文件:

    (typeattributeset cil_gen_require container_t)
    (typeattributeset cil_gen_require container_file_t)
    (typeattributeset cil_gen_require http_port_t)
    (typeattributeset cil_gen_require node_t)
    (allow container_t container_file_t (chr_file (read open getattr ioctl write)))
    (allow container_t self (tcp_socket (create setopt bind listen accept read write)))
    (allow container_t http_port_t (tcp_socket (name_bind)))
    (allow container_t node_t (tcp_socket (node_bind)))
    (allow container_t self (capability (net_bind_service setgid setuid)))
    
  2. 将此文件作为新的 SELinux 模块加载:

    # semodule -i custom_container.cil
    
  3. 使用container_file_t SELinux 类型重新标记容器的文件:

    # chcon -R -t container_file_t /srv/ctr/nginx
    
  4. 使用适当的标签启动容器:

    # systemd-nspawn -D /srv/ctr/nginx \
    -Z system_u:system_r:container_t:s0 \
    -L system_u:object_r:container_file_t:s0 \
    /usr/sbin/nginx -g "daemon off;"
    

每当容器启动时,它都会保持附加到当前会话。当然,我们可以创建服务文件来在后台启动容器,或者使用screentmux等会话管理服务。然而,更加用户友好的方法是使用machinectl

使用 machinectl 简化容器管理

machinectl命令允许管理员通过 systemd 更轻松地管理容器甚至虚拟机。对于容器,machinectl将使用systemd-nspawn

让我们使用这个machinectl命令来下载、启动和停止一个容器:

  1. 首先,使用pull-tar参数下载一个准备好的容器镜像,并将其准备好在系统上使用:

    # machinectl pull-tar https://nspawn.org/storage/archlinux/archlinux/tar/image.tar.xz archlinux
    # machinectl import-tar archlinux.tar.xz
    
  2. 使用list-images参数列出可用的镜像:

    # machinectl list-images
    
  3. 现在我们可以克隆这个镜像并启动容器:

    # machinectl clone archlinux test
    # machinectl start test
    
  4. 要访问容器环境,请使用shell参数:

    # machinectl shell test
    
  5. 我们可以使用poweroff参数关闭容器:

    # machinectl poweroff test
    

当我们使用machinectl时,容器将运行在unconfined_service_t SELinux 域中。目前无法覆盖此设置。幸运的是,我们还有其他工具可用于简化容器管理,这些工具在内置 SELinux 支持方面更强大,例如 Docker 和podman

配置 podman

podman工具是 CentOS 8 及其他源自 Red Hat Enterprise Linux 的发行版的默认容器管理工具。其他发行版,如 Gentoo,也可以通过安装libpod轻松获取podman

选择 podman 而非 Docker

当我们将podman与 Docker 进行比较时,如果只是用它来进行基本的容器管理操作,可能看不出太大区别。命令非常相似,podman甚至具有一个 Docker 兼容层,这使得习惯使用 Docker 的管理员也能方便地使用podman

但是,从底层来看,存在许多差异。首先,podman是一个无守护进程的容器管理系统,允许最终用户轻松地在其受限空间内运行容器。libpod项目还采用了不同的设计原则,并支持不同的容器运行时,支持基于开放容器倡议OCI)定义的容器运行时接口(CRI-O)。

让我们使用podman在系统上部署一个 PostgreSQL 容器:

  1. 首先,我们需要找到合适的容器。我们可以使用podman search命令来查找:

    # podman search postgresql
    
  2. 在列出的各种 PostgreSQL 容器中,我们选择了 Bitnami 版本:

    /var/lib/containers/storage. 
    
  3. 现在我们可以启动一个容器,为 PostgreSQL 超级用户(postgres)设置密码,并确保 PostgreSQL 端口(5432)对系统可用:

    # podman run -dit --name postgresql-test \
      -e POSTGRESQL_PASSWORD="pgsqlpass" \
      -p 5432:5432 postgresql
    

    此命令将基于我们刚刚下载的容器基础创建一个容器定义,并在系统上启动它。

  4. 我们可以使用psql来验证数据库是否运行:

    # psql -U postgres -h localhost
    
  5. 当我们完成容器操作时,可以停止它(使用podman stop),这样会保留当前的容器信息,允许我们稍后重新启动它(使用podman start)或完全从系统中删除容器:

    # podman rm postgresql-test
    
  6. 删除容器会移除容器运行时,但基础容器镜像仍然保留在系统上:

    # podman images
    

    这样,我们可以快速启动另一个容器,而无需重新下载文件。

使用容器是一种快速有效的方式,可以迅速在系统上安装和部署软件。此外,SELinux 提供了一些额外的保护措施,确保这些容器不会出现不当行为。

使用 SELinux 的容器

当我们查看活动的运行时时,我们会注意到 SELinux 已经以我们理解的方式限制了这些容器:

# ps -efZ | grep postgres
system_u:system_r:container_t:s0:c182,c609 ... /opt/bitnami/postgres -D ...

正在运行的进程被分配到两个类别,并在container_t SELinux 域中执行。这是我们在第九章中看到的 sVirt 方法,安全虚拟化。然而,与虚拟机不同,容器通常以更为临时的方式使用:当容器基础的新版本发布时,容器会被废弃,新的容器会启动。虚拟机通常会进行系统内升级,因此具有更长的生命周期。

容器的临时性方式还意味着我们需要以不同的方式提供数据持久性。大多数容器使用的方法是允许从主机将位置映射到容器环境中。

让我们使用podman将容器外部的位置映射到容器内部的/bitnami/postgresql位置,这是 PostgreSQL 容器所需要的:

  1. 首先,在主机上创建我们想要存储 PostgreSQL 数据(库)的位置:

    # mkdir -p /srv/db/postgresql-test
    
  2. 接下来,将此位置的所有权更改为具有用户 ID 1001 的用户(这是容器内部使用的用户 ID):

    # chown -R 1001:1001 /srv/db/postgresql-test
    
  3. 现在启动容器,创建从此位置到容器的映射:

    /srv/db/postgresql-test. If we later delete the container and create a new one (for instance, because an update for the container base has been made available), the database itself is not affected.
    

映射本身包含一个 SELinux 特定的变量,即尾部的:Z。如果我们省略此映射中的:Z,该位置仍然可以在容器内部访问。然而,PostgreSQL 运行时将无法使用它。

重要提示

在目录映射中使用:Z(或稍后我们会看到的:Z(或:z))要比不使用好!

容器仍然是主机操作系统的一部分。当我们创建/srv/db/postgresql-test这个位置时,默认会收到var_t的 SELinux 类型。希望使用该位置的容器将需要var_t的写入权限。然而,这个权限并不是我们希望提供的。毕竟,容器应该尽可能与主机隔离——这正是 sVirt 技术的核心所在。

因此,我们需要相应地重新标记这个位置。通用容器使用的 SELinux 类型是container_file_t。此外,我们希望确保只有正确的容器能够访问这个位置。限制和隔离访问就是命令中:Z(大写的Z)所做的:用container_file_t类型标记该目录,并为其关联正确的类别。

如果我们希望某个位置可以被多个容器访问,我们可以告诉podman共享这个位置,但仍然使用container_file_t的 SELinux 类型进行标记。为了实现这一点,我们需要使用:z参数(小写的z),如下所示:

# podman run -dit --name postgresql-test \
 -e POSTGRESQL_PASSWORD="pgsqlpass" \
 -v /srv/db/postgresql-test:/bitnami/postgresql:z \
 -p 5432:5432 postgresql

创建适当的映射并不是 SELinux 配置中的唯一方法。如果需要,我们还可以告诉podman为容器使用不同的 SELinux 域。

更改容器的 SELinux 域

要控制容器启动时的 SELinux 上下文,我们可以在podman命令中使用--security-opt参数。例如,要使用container_logreader_t SELinux 域运行 Nginx 容器,我们可以使用如下命令:

# podman run -dit --name nginx-test -p 80:80 \
  --security-opt label=type:container_logreader_t nginx

该域比默认的container_t域权限稍高,因为它还具有对日志文件的读取权限。例如,我们可以用它来让 Web 服务器公开日志文件。

我们可以传递的其他标记选项如下:

  • SELinux 用户,使用label=user:<SELinux user>参数。

  • SELinux 角色,使用label=role:<SELinux role>参数。

  • SELinux 敏感度级别,使用label=level:<SELinux level>参数。

  • 文件的 SELinux 类型,使用label=filetype:<SELinux type>参数。这会设置带有:Z:z后缀的定位映射的 SELinux 上下文。所选类型必须是容器 SELinux 域的入口点。

我们还可以使用另一个选项,即label=disable。设置这个参数后,容器将运行时不使用任何 SELinux 隔离。这个选项并不禁用容器的 SELinux,而是将一个名为spc_t的未受限域与容器关联:

# podman run -dit --name nginx-test -p 80:80 \
  --security-opt label=disable \
  -v /srv/web/localhost:/usr/share/nginx/html nginx
# ps -efZ | grep nginx
unconfined_u:system_r:spc_t:s0 ... nginx: worker process

对于大多数使用场景,默认的container_t域已经具有足够的权限,但对于某些情况,它的权限可能过于宽泛。幸运的是,我们可以轻松创建与我们使用场景相匹配的新的 SELinux 域。

使用 udica 创建自定义域

container_t 域被配置为广泛可重用,这意味着它对常见用例有许多特权,这些特权您可能不想给每个容器。此外,如果我们启动一个容器,但需要为其关联更多特权,那么我们将不得不扩展 container_t 以获取更多特权,结果是所有容器都会收到此特权扩展。

为了快速建立新策略,可以使用名为 udica 的工具。udica 工具读取容器定义并从中创建自定义 SELinux 策略。然后,我们可以将此自定义策略用于此特定容器,允许其他容器保持不变。

让我们将其用于 Jupyter Notebook,我们希望为(共享的)用户主目录位置授予读/写权限:

  1. 首先,我们创建容器的定义:

    # podman run -dit --name notebook-test -p 8888:8888 \
      -v /home/lisa/work:/home/jovyan/shared scipy-notebook
    
  2. 接下来,使用 podman inspect 检查此容器并将结果存储在文件中:

    # podman inspect notebook-test > notebook-test.json
    
  3. 使用 udica 为其生成 SELinux 策略:

    # udica -j notebook-test.json custom-notebook-test
    Policy custom-notebook-test created!
    Please load these modules using:
    # semodule -i custom-notebook-test.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}
    Restart the container with: "--security-opt label=type:custom-notebook-test.process" parameter
    
  4. 根据 udica 输出加载自定义策略:

    # semodule -i custom-notebook-test.cil \
        /usr/share/udica/templates/base_container.cil \
        /usr/share/udica/templates/net_container.cil
    
  5. 停止并删除容器,然后根据 udica 输出重新创建它:

    # podman stop notebook-test
    # podman rm notebook-test
    # podman run -dit --name notebook-test -p 8888:8888 \
      -v /home/lisa/work:/home/jovyan/shared \
      --security-opt \
      label=type:custom-notebook-test.process scipy-notebook
    

自定义 SELinux 策略具有写入主目录的特权,因为容器从 /home/lisa/work 映射,并且 udica 自动为其创建了权限。如果我们希望容器仅具有只读特权,我们可以使用带有尾部 :ro 的映射(而不是 :Z:z 用于 SELinux 特定更改)。这将映射容器内部的位置,并且 udica 仅为关联的 SELinux 类型创建读权限。

如果创建自定义策略有点过于具体,我们还可以使用适当的 SELinux 布尔值来微调 container_t 域的特权。

使用 SELinux 布尔值切换 container_t 特权

container_t SELinux 域与 svirt_sandbox_domain 属性关联,通过这种关联,将自动由我们在第九章中看到的 virt_* SELinux 布尔值管理。

也有一些特定于容器的 SELinux 布尔值:

  • 使用 container_use_cephfs,容器可以使用基于 CephFS 的存储。这主要用于容器由较大的容器集群软件(如 Kubernetes)管理时。

  • 使用 container_manage_cgroup,容器可以管理 cgroups。当容器内部托管有 systemd 时,这是必需的,这种情况通常适用于完整的容器运行时(而不是特定进程的容器)。这些容器托管几乎完整的 Linux 系统。

  • 使用 container_connect_anycontainer_t SELinux 域可以连接到任何 TCP 端口。

请注意,这些布尔值影响 container_t 域的特权,因此对所有容器都有效。

调整容器托管环境

podman 工具默认会将其容器卷和基础镜像存储在/var/lib/containers目录中。管理员可以通过位于/etc/containersstorage.conf配置文件添加更多存储位置。然而,你还需要相应地调整 SELinux 配置。

假设我们将使用/srv/containers位置,那么我们需要创建一个等价规则,确保该位置的标签被正确设置:

# semanage fcontext -a -e /var/lib/containers \
  /srv/containers
# restorecon -R -v /srv/containers

如果该位置是网络挂载点,你可能还需要更改相应的 SELinux 布尔值。

利用 Kubernetes 的 SELinux 支持

当容器在更大的环境中使用时,它们通常通过容器编排框架进行管理,这些框架允许跨多个系统扩展容器部署和管理。Kubernetes 是一个受欢迎的容器编排框架,拥有良好的社区支持和商业支持。

Kubernetes 使用机器上找到的容器软件。例如,当我们在 Fedora 的 CoreOS 上安装 Kubernetes 时,它会检测到 Docker 可用,并使用 Docker 引擎来管理容器。

配置 Kubernetes 以支持 SELinux

安装 Kubernetes 可能是一项艰巨的任务,存在多种方法,范围从单节点的实验部署到商业支持的安装方法。在 Kubernetes 网站上,有一种经过充分记录的安装方法是使用kubeadm来引导 Kubernetes 集群的创建。

重要提示

Kubernetes 的安装文档可以在 Kubernetes 官网找到,链接为kubernetes.io/docs/setup/production-environment/tools/kubeadm。在本节中,我们不会逐步讲解如何设置一个工作的 Kubernetes 实例,而是提供一些指引,说明需要进行哪些更改以支持 SELinux。

当初始化 Kubernetes 集群时,kubeadm命令会下载并运行各种 Kubernetes 服务作为容器。不幸的是,Kubernetes 的服务使用了多个从主机系统到容器的映射,以便其正常运行。这些映射没有使用:Z:z选项——甚至这样做是错误的,因为这些位置是系统范围内的,应该保留当前的 SELinux 标签。

结果,Kubernetes 的服务将以默认的container_t SELinux 域运行(因为 Docker 会愉快地应用 sVirt 保护),而无法访问这些位置。我们可以应用的最明显更改是让服务暂时使用特权更高的spc_t域。然而,在安装过程中进行此更改是困难的,因为我们需要在安装失败之前快速更改域。

虽然我们可以为 Kubernetes 创建部署配置,这会立即使用spc_t配置服务,但也可以采取另一种方法:

  1. 在安装开始之前,将 container_t 类型标记为宽松域。虽然这会防止 SELinux 对容器的控制,但我们可以认为 Kubernetes 的安装是以封闭和受监督的方式进行的:

    # semanage permissive -a container_t
    
  2. 运行 kubeadm init,它将在系统上安装服务:

    # kubeadm init
    
  3. 安装服务后,转到 /etc/kubernetes/manifests。在此目录中,您将找到四个清单,每个清单代表一个 Kubernetes 服务:

    # cd /etc/kubernetes/manifests
    
  4. 编辑每个清单文件(etcd.yamlkube-apiserver.ymlkube-controller-manager.ymlkube-scheduler.yml),并添加一个安全上下文定义,使服务以 spc_t 域运行。此操作作为 containers 部分下的配置指令进行:

    apiVersion: v1
    kind: Pod
    metadata:
      name: etcd
    spec:
      containers:
      - command: …
      securityContext:
        seLinuxOptions:
          type: spc_t
        image: k8s.gcr.io/etcd:3.4.3-0
        …
    
  5. 在 Kubernetes 安装过程中,将安装 kubelet 服务,它会检测到这些文件已被更改,并会自动重启容器。如果没有,您可以关闭并删除 Docker 中的容器定义,kubelet 将自动重新创建它们:

    # docker ps
    CONTAINER ID      ...     NAMES
    548f0c3ed18e          k8s_POD_etcd-ppubssa3ed_kube…
    b7b1df2d0027          k8s_POD_kube-apiserver-…
    eecd4d4ad108          k8s_POD_kube-scheduler-…
    76da4910b927          k8s_POD_kube-controller-…
    # for n in 548f0c3ed18e b7b1df2d0027 eecd4d4ad108 76da4910b927; do docker stop $n; docker rm $n; done
    
  6. 验证服务是否现在以特权 spc_t 域运行:

    # ps -ef | grep spc_t
    
  7. 移除 container_t 的宽松状态,使其恢复到强制模式:

    # semanage permissive -d container_t
    

在安装过程中进行这些微小调整后,Kubernetes 现在可以正常运行,并启用了 SELinux 支持。

设置 SELinux 上下文以用于 pods

在 Kubernetes 中,容器是 pod 的一部分。podman 工具也能够使用 pod 的概念(因此得名)。例如,我们可以将 Nginx 容器放入一个名为 webserver 的 pod 中,如下所示:

# podman pod create -p 80:80 --name webserver
# podman pull docker.io/library/nginx
# podman run -dit --pod webserver --name nginx-test nginx

podman 不同,Kubernetes 不依赖命令行交互来创建和管理如 pods 之类的资源。相反,它使用清单文件(正如我们在 配置带 SELinux 支持的 Kubernetes 部分简要提到的那样)。Kubernetes 管理员或 DevOps 团队将创建清单文件,并将其应用于环境。

例如,为了让 Nginx 容器在 Kubernetes 上运行,可以使用以下清单:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-test-deployment
spec:
  selector:
    matchLabels:
      app: nginx-test
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx-test
    spec:
      containers:
      - name: nginx-test
        image: nginx:latest
        ports:
        - containerPort: 80

这个清单是一个 Kubernetes 部署,它告诉 Kubernetes 我们想要运行两个 Nginx 容器。要将其应用于环境,使用 kubectl apply

$ kubectl apply -f simple-nginx.yml

与 Kubernetes 服务的清单一样,我们可以告诉 Kubernetes 使用特定的 SELinux 类型:

…
spec:
  containers: ...
  securityContext:
    seLinuxOptions:
      type: "container_logreader_t"

seLinuxOptions 块可以包含 userroletypelevel,用于定义 SELinux 用户、SELinux 角色、SELinux 类型和 SELinux 敏感级别。

与常规容器管理服务(如 Docker 或 CRI-O)不同,Kubernetes 不允许更改映射卷上的 SELinux 标签(单节点部署除外):当我们将卷映射到容器时,它们会保留系统上当前的 SELinux 标签。因此,如果您希望确保资源可以从常规的 container_t 域访问,您需要确保这些位置被标记为 container_file_t

Kubernetes 确实提供了先进的访问控制。容器内的卷启用也是通过插件架构来处理的,已有多个插件可用。当插件启用 SELinux 标签时,Kubernetes 会尝试重新标记资源并分配类别(与 sVirt 相同)。然而,目前该功能仅在单节点部署(使用本地主机存储插件)中可用——对于这样的部署,使用 podman 要简单得多。

总结

容器化工作负载允许管理员快速且轻松地为系统添加功能,同时保持容器内可能的依赖关系。每个容器都托管自己的依赖项,允许容器从系统中移除或添加,而不会影响其他容器。通过 SELinux,这些工作负载进一步与主机隔离,并且在启用 sVirt 保护的情况下,相互之间也得到了隔离。

我们已经看到 systemd 支持容器,但缺乏基于 sVirt 的保护,也了解了 podman 如何在其容器环境中应用 sVirt 保护。我们了解到,Docker 和 podman 在使用上非常相似,但在底层实现上有所不同。这两个框架都允许我们将不同的 SELinux 类型应用于容器和资源,并且通过 udica,我们学会了如何在不进行大量开发的情况下创建自定义策略。最后,我们还了解了如何配置 Kubernetes 使用 SELinux 标签。

在所有这些支持 SELinux 的技术背后,我们已经准备好迎接 SELinux 策略的开发了。在接下来的章节中,我们将深入学习如何与 SELinux 策略打交道,并根据需要调整策略。

问题

  1. 为什么在 SELinux 中,dockerpodmanmachinectl 更受欢迎?

  2. 我们如何确保主机数据在容器内得到正确映射?

  3. 我们如何从容器定义中创建自定义策略?

  4. 在 Kubernetes 的清单中,我们可以将 SELinux 设置放在哪里?

第三部分:策略管理

SELinux 的行为由策略定义。在这一部分,我们涵盖了策略的变更与开发,包括自定义策略的创建。

本节包括以下章节:

  • 第十二章调整 SELinux 策略

  • 第十三章分析策略行为

  • 第十四章处理新应用程序

  • 第十五章使用参考策略

  • 第十六章使用 SELinux CIL 开发策略

第十二章:调整 SELinux 策略

到目前为止,我们一直在通过调整系统来处理现有的 SELinux 策略,确保系统适应适当的 SELinux 上下文,并为文件、目录甚至网络端口分配正确的标签。我们已经了解到,SELinux 执行的行为是由策略定义的。为了微调策略执行规则,我们已经简要介绍了 SELinux 布尔值。

现在是时候更详细地研究 SELinux 布尔值了,学习如何查看布尔值的影响。在本章中,我们将考虑 SELinux 策略模块本身,以及管理员在处理这些模块时可以选择的选项。最后,我们将探讨如何更新甚至替换现有的策略。

在本章中,我们将讨论以下主要主题:

  • 使用 SELinux 布尔值

  • 处理策略模块

  • 替换和更新现有策略

技术要求

查看以下视频,了解代码的实际应用:bit.ly/2T7MkVK

使用 SELinux 布尔值

操作 SELinux 策略的其中一种方法是切换 SELinux 布尔值。自第二章《理解 SELinux 决策和日志记录》以来,我们使用了secure_mode_policyload布尔值,这些可调设置在本书中随时出现。通过其简单的开/关状态,它们启用或禁用 SELinux 策略的部分内容。策略开发人员和管理员使用 SELinux 布尔值来切换策略的某些部分,因为并非所有部署都需要始终启用这些部分,但某些部署仍然需要。

这些布尔值是根据来自社区反馈并在社区帮助下,添加到策略中的。通过明确哪些策略规则是必要的,哪些是可选的,SELinux 开发人员可以提供适用于大多数系统的 SELinux 策略,即使这些系统的使用方式不同。

列出 SELinux 布尔值

可以通过使用semanage命令与boolean选项,获得 SELinux 布尔值的概览。在普通系统上,我们可以轻松找到上百个 SELinux 布尔值,因此需要筛选出我们需要的布尔值的描述:

# semanage boolean -l | grep policyload
secure_mode_policyload	(off, off)
  Boolean to determine whether the system permits loading
  policy, setting enforcing mode, and changing boolean values.
  Set this to true and you have to reboot to set it back.

输出不仅为我们简要描述了布尔值,还显示了当前值(实际上,它首先给出当前值,然后是等待策略变更的值,但这两个几乎总是相同的)。

获取布尔值当前值的另一种方法是通过getsebool命令,如下所示:

# getsebool secure_mode_policyload
secure_mode_policyload --> off

如果布尔值的名称不完全明确,我们可以请求所有布尔值(及其值)的概览,并筛选出我们需要的:

# getsebool -a | grep policy
secure_mode_policyload --> off

另一个可以用来查看 SELinux 布尔值描述的工具是sepolicy booleans命令:

# sepolicy booleans -b secure_mode_policyload
secure_mode_policyload=_("Boolean to ...")

然而,使用该命令无法显示布尔值的当前值。

最后,布尔值也通过/sys/fs/selinux文件系统表示:

# cat /sys/fs/selinux/booleans/secure_mode_policyload
0 0

在这里,布尔值可以像常规文件一样读取,并返回两个值:

  • 第一个值是布尔值的当前状态,0表示关闭,1表示开启。

  • 第二个值是布尔值的待提交状态。

待提交状态允许管理员同时更改多个布尔值,但只有在通过/sys/fs/selinux文件系统操作布尔值时才会发生,如我们接下来将要看到的那样。

更改布尔值

我们可以使用setsebool命令更改布尔值。例如,要切换httpd_can_sendmail的 SELinux 布尔值,可以使用以下命令:

# setsebool httpd_can_sendmail on

一些 Linux 发行版可能还提供togglesebool命令。此命令将切换布尔值,因此开启变为关闭,关闭变为开启:

# togglesebool httpd_can_sendmail

SELinux 布尔值的默认状态由策略管理员定义(因此由系统上活动的默认 SELinux 策略定义)。使用setsebool更改值时,会更新当前活动的访问控制,但该更改不会跨重启持久化(如果切换布尔值,则重启后会使用旧的值)。

为了使更改永久生效,请在setsebool命令中添加-P选项,如下所示:

# setsebool -P httpd_can_sendmail off

在后台,更新的 SELinux 布尔值会被包含在策略存储中。然后,当前的策略文件会被重建并加载。因此,位于/etc/selinux/targeted/policy中的策略文件(名为policy.##,其中##代表一个整数值)将会重新生成。这个过程需要一些时间,这也是为什么使布尔值持久生效(使用-P)比不持久更改布尔值(使用setsebool而不加-P或使用togglesebool)更耗时的原因。

改变并使布尔设置持久化的另一种方法是使用semanage boolean命令,如下所示:

# semanage boolean -m --on httpd_can_sendmail

在这种情况下,我们修改(-m)布尔值并将其设置为开启(--on)。

布尔值也可以通过其/sys/fs/selinux/booleans表示进行更改。当这种情况发生时,布尔值不会立即生效——该值的更改处于待提交状态。这允许管理员先通过/sys/fs/selinux/booleans修改多个布尔值:

# echo 0 > /sys/fs/selinux/booleans/httpd_can_sendmail
# getsebool httpd_can_sendmail
httpd_can_sendmail --> on pending: off

要提交更改,请将值1写入/sys/fs/selinux/commit_pending_bools

# echo 1 > /sys/fs/selinux/commit_pending_bools

只要通过semanagesetsebool命令修改布尔值,变更将立即提交。只有通过/sys/fs/selinux结构进行的操作才允许布尔值更改处于待提交状态。

检查布尔值的影响

要发现一个布尔值操作了哪些策略规则,通常其描述就足够了。但有时我们可能想知道,当我们改变布尔值时,哪些 SELinux 规则会发生变化。通过sesearch应用程序,我们可以查询 SELinux 策略,显示受特定布尔值影响的规则。要详细显示这些信息,我们使用-b选项(表示布尔值)和-A选项(显示所有allow规则):

# sesearch -b httpd_can_sendmail -A
allow httpd_suexec_t bin_t:dir { getattr open search }; [ httpd_can_sendmail ]:True
...
allow system_mail_t httpd_t:process sigchld; [ httpd_can_sendmail ]:True

当我们直接查询 SELinux 策略时,条件规则也可以作为输出的一部分显示:

# sesearch -s system_mail_t -t httpd_t -A
allow domain domain:key { link search };
allow system_mail_t httpd_t:fd use; [ httpd_can_sendmail ]:True
...

allow规则后面跟着一个 SELinux 布尔值并用方括号括起来,后面加上:True时,这些规则仅在布尔值处于活动状态时应用。如果布尔值后面跟着:False,则在布尔值不活动时应用规则。

然而,并不是所有情况都能被策略编写者完美定义。有时我们需要创建自己的 SELinux 策略模块并加载它们。让我们看看如何专门处理 SELinux 策略模块。

处理策略模块

当系统将 SELinux 策略加载到内存时,它使用policy.##文件,其中##代表策略版本,如第一章《基本 SELinux 概念》结尾所述。此文件位于/etc/selinux/targeted/policy目录下,每次策略修改时都会生成。修改可能发生在布尔值改变(并持久化)时,或者当 SELinux 策略模块被添加或删除时。

列出策略模块

SELinux 策略模块是一组可以加载和卸载的 SELinux 规则。这些带有.pp.cil后缀的模块可以根据需要由管理员加载和卸载。一旦加载,策略模块就成为 SELinux 策略存储的一部分,并将在系统重启后继续加载。与 SELinux 布尔值更改不同,SELinux 策略模块的加载始终是持久的。

要列出当前加载的 SELinux 策略模块,建议使用semodule命令。默认情况下,semodule会显示所有已加载的 SELinux 策略模块,但不包括任何详细信息:

# semodule -l
abrt
accountsd
...
zosremote

然而,SELinux 策略模块可以在指定的优先级下加载。这使得管理员能够加载一个覆盖已加载策略的策略:使用较高的--list-modules=full参数的 SELinux 策略模块:

# semodule --list-modules=full
100 abrt       pp
100 accountsd  pp
...
400 test       cil
...
100 zosremote  pp

除了优先级外,列表还显示策略模块是基于二进制模块格式(pp)还是较新的cil格式。

SELinux 工具将把活动策略模块复制到策略特定的位置。这使得管理员也可以通过常规的文件系统查询列出当前活动的模块:

# ls /var/lib/selinux/targeted/active/modules/*
/var/lib/selinux/targeted/active/modules/100:
abrt
accountsd
...
/var/lib/selinux/targeted/active/modules/400:
test

然而,使用文件系统位置查询活动策略并不推荐,因为我们无法保证加载的策略与文件系统匹配:非 SELinux 工具可以在不调整 SELinux 策略状态的情况下,添加或删除这些位置的文件。

加载和移除策略模块

替换和更新现有策略一节中,我们将学习如何生成新的策略模块。创建后,需要加载和/或移除它们。无论策略格式是(.pp还是.cil),我们都使用semodule加载策略模块:

# semodule -i screen.pp

默认情况下,SELinux 策略模块在管理员调用时会以 400 优先级加载,而作为默认系统策略一部分加载的 SELinux 策略模块会以 100 优先级加载。在加载策略时,可以使用 -X 选项调整优先级。例如,要以 500 优先级加载 test.cil 策略,可以使用如下的 -X 选项:

# semodule -X 500 -i test.cil
libsemanage.semanage_direct_install_info: Overriding test module at lower priority 400 with module at priority 500.

要使用 semodule 移除一个策略模块,可以使用 --remove-r 选项。在这里,我们并不是指 SELinux 策略模块的 文件,而是指通过 semodule 显示的 模块名称。因此,我们不需要传递后缀:

# semodule -r test

要从指定的优先级移除 SELinux 策略模块,可以使用 -X 选项:

# semodule -X 500 -r test
libsemanage.semanage_direct_remove_key: test module at priority 400 is now active.

参数的顺序非常重要:-X 选项会设置后续操作的优先级,而不是前面的操作。如果没有设置,则会使用优先级值 400

最后,可以保留 SELinux 策略模块但将其禁用。这会将模块保留在策略存储中,但禁用其中所有的 SELinux 策略规则。我们使用 --disable(或 -d)选项来实现这一点:

# semodule -d screen

要重新启用该策略,可以使用 --enable(或 -e)选项:

# semodule -e screen

SELinux 策略模块的禁用和启用状态会在重启后保持不变。此外,如果你禁用了一个 SELinux 模块,该模块的所有实例(包括优先级较低的实例)都会被禁用。

强烈建议在策略模块是发行版 SELinux 策略的一部分时禁用该策略,因为这些模块本身并不总是存在于系统中,可能需要重新安装策略包才能恢复它们。

通过加载和卸载策略的说明,让我们来看看如何生成当前 SELinux 策略的更新。

替换和更新现有策略

当我们替换或更新现有策略时,需要使用 semodule 命令加载它们,如 处理策略模块 部分所示。但我们究竟如何创建或更新策略呢?让我们考虑一些触发 SELinux 策略调整的使用场景。

使用 audit2allow 创建策略

当 SELinux 阻止某些操作时,我们知道它会在审计日志中记录相应的拒绝信息(前提是没有定义 dontaudit 语句)。这个拒绝信息可以作为生成自定义 SELinux 策略的来源,以允许该活动。

考虑以下拒绝情况:当一个受限用户使用 su 切换到 root 用户时发生的拒绝:

type=AVC msg=audit(...): avc: denied { write } for pid=58002 comm="su" name="btmp" dev="vda1" ino=4213650 scontext=staff_u:staff_r:staff_t:s0 tcontext=system_u:object_r:faillog_t:s0 tclass=file permissive=0

如果我们确定需要授权这些操作,可以使用 audit2allow 命令为我们生成一个允许这些活动的策略模块。allow 规则。这些规则可以保存到一个文件中,准备构建成一个 SELinux 策略模块,然后加载。

要生成 SELinux 策略 allow 规则,将拒绝信息通过 audit2allow 工具传递:

# grep btmp /var/log/audit/audit.log | audit2allow
#============ staff_t ============
allow staff_t faillog_t:file write;

根据拒绝记录,audit2allow准备了一个allow规则。我们还可以要求audit2allow立即创建一个 SELinux 策略模块:

# grep btmp /var/log/audit/audit.log | audit2allow -M localpolicy
************ IMPORTANT ************
To make this policy package active, execute:
semodule -i localpolicy.pp

一个名为localpolicy.pp的文件将出现在当前目录中,我们可以使用semodule命令加载它。源文件也将存在,名为localpolicy.te

如果发生的拒绝记录被认为是外观性的(即系统按预期运行,并且拒绝不应该对策略产生任何更新),可以使用audit2allow生成dontaudit规则,而不是allow规则。在这种情况下,拒绝记录将不再出现在审计日志中,但仍然会阻止相关操作发生:

# grep btmp /var/log/audit/audit.log | audit2allow -D -M localpolicy

在包含必要规则之后,可能会导致更多之前未触发的拒绝记录。只要之前的 AVC 拒绝记录仍然存在于审计日志中,重新生成策略并继续操作就足够了。毕竟,audit2allow会考虑到它遇到的所有 AVC 拒绝记录,而不管当前的 SELinux 策略状态如何。

另一种常见的方法是将系统(或应用程序领域)设置为宽松模式,以生成并填充所有与操作相关的 AVC 拒绝记录到审计日志中。虽然这会生成更多的 AVC 拒绝记录供我们处理,但也可能导致audit2allow命令做出错误的决策。因此,在生成新的策略构建之前,始终验证拒绝记录,并审查生成的策略,以确保它能够执行正确的访问控制集,并且不会授予超过所需的权限。

当先前的 AVC 拒绝记录不再出现在审计日志中时,就需要生成新的策略模块,否则之前已修复的访问将再次被拒绝:新生成的策略将不再包含以前的allow规则,而当我们加载新策略时,旧的策略不再生效。

使用合理的模块名称

在前面的章节中,我们使用了audit2allow命令生成了一个名为localpolicy的策略模块。然而,这个名称并没有揭示模块的具体用途。

一旦我们创建了一个(二进制)策略(如localpolicy.pp文件)并加载它,管理员和用户一开始通常无法清楚地知道这个模块的目的。虽然可以通过解包.pp文件(使用semodule_unpackage)然后将得到的.mod文件反汇编成.te文件,但这需要在大多数发行版上都没有的软件(例如,dismod应用程序,它是checkpolicy软件的一部分,通常不包括在内)。考虑到我们只是想了解模块中包含的规则,这是一个非常复杂且费时的方法。

模块的内容也可以从其 CIL 代码中推断出来。例如,活动的screen模块的代码可以在/var/lib/selinux/targeted/active/modules/100/screen中找到,文件名为cil。在某些发行版中,此文件可能是压缩文件,因此在查看之前可能需要解压缩:

# file screen/cil
cil: bzip2 compressed data, block size = 500k
# bzcat screen/cil
(typealias secadm_screen_home_t
...

然而,必须深入了解规则以了解localpolicy的内容,不仅非常繁琐,而且还需要足够的权限才能读取这些文件。

相反,最佳做法是为生成的模块命名以反映其预期用途。例如,修复sustaff_t域内执行时出现的几个 AVC 拒绝的 SELinux 策略,最好命名为custom_staff_su_faillog

还建议对自定义策略添加前缀(或后缀),以便更容易找到:

# semodule -l | grep ^custom_
custom_staff_su_faillog

这表明该策略模块是由管理员(或组织)添加的,而不是来自默认 Linux 发行版的策略。

使用 audit2allow 生成参考策略模块

参考策略项目为发行版和策略编写者提供了一组功能,这些功能简化了 SELinux 策略的开发。例如,我们来看一下参考策略函数(称为su情境):

# grep btmp /var/log/audit/audit.log | audit2allow -R
require {
  type staff_t;
}
#============ staff_t ============
auth_rw_faillog(staff_t)

示例中的规则是auth_rw_faillog(staff_t)。这是一个参考策略宏,它以更易读的方式解释了 SELinux 规则(或一组规则)。在这种情况下,它允许staff_t域对faillog_t标记的资源进行读/写操作。faillog_t类型是系统认证 SELinux 策略的一部分(由auth_前缀标识,该前缀表示源 SELinux 策略模块)。

重要说明

由于audit2allow -R使用自动化方法查找潜在的功能,我们需要仔细审查结果。有时它会选择一种方法,为一个域创建比所需更多的权限。

所有主要发行版都将其 SELinux 策略建立在参考策略提供的宏和内容之上。我们可以在本地文件系统的/usr/share/doc/selinux-policy/html中找到构建 SELinux 策略时可以调用的方法列表。

这些命名方法将一组与 SELinux 策略管理员希望启用的功能相关的规则捆绑在一起。例如,storage_read_tape()方法允许我们增强 SELinux 策略,授予指定域对磁带存储设备的读取访问权限。

构建参考策略风格模块

如果我们使用参考策略宏生成 SELinux 策略,但不再有访问二进制策略模块的权限,那么我们需要在加载之前构建该策略。基于 CIL 的策略可以直接加载,这就是为什么本书尽可能使用 CIL 的原因。然而,鉴于参考策略的广泛使用,了解如何构建这些模块同样很重要。

假设基于参考策略的 SELinux 策略代码位于名为custom_staff_su_faillog.te的文件中,那么我们可以按照以下方式将其构建为.pp文件:

# make -f /usr/share/selinux/devel/Makefile custom_staff_su_faillog.pp
Compiling targeted custom_staff_su_faillog.pp policy package
rm tmp/custom_staff_su_faillog.mod tmp/custom_staff_su_faillog.mod.fc

构建完成后,我们可以使用semodule加载它。每次我们修改策略代码(在.te文件中)或其他策略信息(如.fc文件中的文件上下文定义)时,我们都需要重新构建.pp文件,然后才能加载它。

构建传统风格的模块

如果我们要求audit2allow生成不使用参考策略样式宏(即我们称之为传统风格的 SELinux 策略)的策略规则,那么从中构建.pp文件需要不同的方法。

假设我们有audit2allow -M生成的.te文件,但没有.pp文件,那么我们可以按照以下步骤生成它:

  1. 首先,使用checkmodule创建.mod文件:

    # checkmodule -M -m -o custom_nonrefpol.mod \
      custom_nonrefpol.te
    
  2. 接下来,使用semodule_package生成.pp文件:

    # semodule_package -o custom_nonrefpol.pp \
      -m custom_nonrefpol.mod
    # semodule_package -o custom_nonrefpol.pp \
      -m custom_nonrefpol.mod -f custom_nonrefpol.fc
    

audit2allow命令将自动执行这些命令,因此仅在.pp文件不再存在,或者当这些较为传统风格的 SELinux 策略与您共享时,并且您需要手动构建和加载它们时才需要执行。

替换默认的分发策略

在添加自定义 SELinux 策略时,用户所能做的只是添加更多的allow规则。SELinux 没有可以用来移除当前允许访问规则的 deny 规则。

如果当前策略对管理员来说过于宽松,那么管理员需要更新策略,而不仅仅是增强它。这意味着管理员可以访问当前使用的 SELinux 策略规则。

要替换一个活动的 SELinux 策略,大多数 Linux 发行版允许你获取该策略的源代码。例如,对于基于 RPM 的 Linux 发行版,可以下载 SELinux 策略包的源 RPM 并解压,从而访问策略,方法如下:

  1. 首先,找出当前 SELinux 策略的版本:

    $ rpm -qi selinux-policy
    Name		: selinux-policy
    Version	: 3.14.3
    ...
    Source RPM	: selinux-policy-3.14.3-20.el8.src.rpm
    ...
    
  2. 接下来,尝试获取输出中显示的源 RPM。源 RPM 也可以从第三方仓库下载。如果难以找到该包,可以尝试通过rpmfind.net来查找。

  3. 接下来,使用rpmbuild工具提取源 RPM:

    ~/rpmbuild directory.
    
  4. 完成后,SELinux 策略源代码可以在~/rpmbuild/SOURCES目录下找到,可能命名为selinux-policy-9c02e99.tar.gz或类似名称,您可以进一步解压:

    screen.te file can be found in the ./selinux-policy-contrib-c8ebb*/policy/modules/contrib subdirectory.
    

现在,策略文件可以安全地复制、随意操作并构建,以替换现有的策略。如果我们以与已加载策略相同或更高的优先级加载更新后的 SELinux 策略模块,它将在策略中占据优先地位。

大多数发行版还将通过在线源代码控制仓库提供其活动的 SELinux 策略。例如,CentOS 当前的 SELinux 策略可以在github.com/fedora-selinux/selinux-policy找到。

概述

管理员可以通过 SELinux 布尔值(由 SELinux 策略本身提供)或加载新的 SELinux 策略模块来调整 SELinux 策略。这些模块可以自动生成,或者由策略开发人员手动构建。

在本章中,我们学习了如何使用 SELinux 布尔值以及如何查询活动策略,以了解布尔值对系统的影响。接着,我们学习了如何使用 semodule 加载和卸载策略,或在系统上启用/禁用模块。我们在本章结束时介绍了如何生成和替换策略。

在下一章中,我们将扩展对 SELinux 策略的查询,不仅仅局限于布尔值,并学习如何使用专业工具详细分析策略行为。

问题

  1. 如何将布尔值更改标记为待处理,但尚未提交?

  2. 哪个命令可以用来查询布尔值的影响?

  3. 为什么可以以不同的优先级加载 SELinux 策略模块?

  4. 如何将拒绝转化为新的 SELinux 策略模块?

第十三章:分析策略行为

尽管 SELinux 策略在系统上强制执行强制访问控制和应用程序行为,但了解策略的预期行为对于管理员执行评估和根本原因分析活动非常有用。

在本章中,我们将学习如何深入查询 SELinux 策略,使用多种工具查询进程转换、分析信息流并比较策略。我们将考虑 apol 工具,这是一个图形界面工具,可以帮助我们对策略进行多种分析,同时也会涉及命令行工具,如 sesearchsedtaseinfoflowsepolicy。最后,我们将使用 sediff 来比较策略。

本章将涵盖以下主要主题:

  • 执行单步分析

  • 调查领域转换

  • 分析信息流

  • 比较策略

技术要求

查看以下视频,了解代码的实际操作:bit.ly/3lY56LB

执行单步分析

到目前为止,我们已经通过命令行工具如 seinfosesearch 介绍了一些分析 SELinux 策略的方法。这些工具可以帮助用户执行单步分析:它们要么提供有关 SELinux 对象的即时信息(这正是 seinfo 的功能),要么能够查询直接的 SELinux 规则(这正是 sesearch 的范围)。

不过,seinfosesearch 工具的所有功能还未完全讨论,接下来让我们看看这些命令还有哪些其他技巧。

使用不同的 SELinux 策略文件

许多 SELinux 分析工具,包括 seinfosesearch,可以访问当前加载的 SELinux 策略和指定的 SELinux 策略文件。后者允许开发者查询他们无法直接访问的系统的 SELinux 策略(如移动设备),或者查询以前使用过(备份)且现在不再活跃的策略。

例如,要分析名为 policy.sepolicy-2 的策略文件,可以使用以下 seinfo 命令:

$ seinfo ./policy.sepolicy-2
Statistics for policy file: ./policy.sepolicy-2
Policy Version:             30 (MLS enabled)
Target Policy:              selinux
Handle unknown classes:     deny
  Classes:              63    Permissions:         286
  Sensitivities:         1    Categories:         1024
  Types:              1858    Attributes:           28
  Users:                 1    Roles:                 2
  Booleans:              0    Cond. Expr.:           0
  Allow:            108120    Neverallow:            0
  Auditallow:           24    Dontaudit:           553
  Type_trans:          639    Type_change:           0
  Type_member:           0    Range_trans:           0
  Role allow:            0    Role_trans:            0
  Constraints:           0    Validatetrans:         0
  MLS Constrain:        59    MLS Val. Tran:         0
  Permissives:           0    Polcap:                2
  Defaults:              0    Typebounds:            0
  Allowxperm:          185    Neverallowxperm:       0
  Auditallowxperm:       0    Dontauditxperm:        0
  Initial SIDs:         27    Fs_use:               16
  Genfscon:             83    Portcon:               0
  Netifcon:              0    Nodecon:               0

当命令没有明确指示解析给定的策略文件时,它会尝试通过 /sys/fs/selinux/policy 伪文件查询当前活动的策略。

显示策略对象信息

seinfo 应用程序的主要功能是显示 SELinux 对象信息。该信息是按对象逐个显示的。支持各种 SELinux 对象类型,从常见的类型、属性、角色和用户,到更为专门的 fs_use_* 声明或 genfscon 语句。

支持的对象类型的完整列表(及其对应的 seinfo 选项)可以在 seinfo 手册页中找到,或者通过直接帮助获取:

$ seinfo --help

无论用户感兴趣的对象类型是什么,seinfo 都有三种主要的操作模式:

  • 在第一种模式下,它列出给定类型的对象。为此,只需要传递选项,无需其他信息。例如,要列出策略中所有可用的对象类,请使用以下命令:

    $ seinfo --class
    
  • 在第二种模式下,它可以确认(或否认)某个对象实例的存在。要实现这一点,向命令中添加实例名称。例如,要验证memprotect类是否在策略中可用,请使用以下命令:

    $ seinfo --class memprotect
    Classes: 1
      memprotect
    

    可惜,无论实例是否可用,应用程序的返回码都是相同的,因此脚本在没有额外语句来验证实例是否存在的情况下无法使用它们。相反,脚本需要检查命令的输出(该输出会说明没有匹配名称的实例)。

  • 第三种模式显示关于所选实例的扩展信息。虽然并非所有信息对象都支持扩展集,但大多数常见对象支持。扩展信息通常显示与初始查询相关的(不同的)实例列表:

    $ seinfo --class memprotect -x
    Classes: 1
      class memprotect
    {
        mmap_zero
    }
    

seinfo可以查询的支持类型如下:

  • 使用--attribute-a),seinfo显示策略中当前已知的所有 SELinux 属性。当展开时,它会显示与给定属性相关的类型。

  • 使用--bool-b),seinfo显示策略中当前已知的所有 SELinux 布尔值。当展开时,它会显示该布尔值的当前值。

  • 使用--class-c),seinfo显示支持的 SELinux 类。当展开时,它会显示该类支持的权限。

  • 使用--role-r),seinfo显示策略中支持的 SELinux 角色。当展开时,它会显示该角色允许的域。

  • 使用--type-t),seinfo显示策略中的 SELinux 类型。当展开时,它会显示该类型的别名以及属性。

  • 使用--user-u),seinfo显示策略中已知的 SELinux 用户(不是 Linux 用户或登录)。当展开时,它会显示与该 SELinux 用户关联的角色和敏感性范围。

  • 使用--categoryseinfo显示当前支持的类别。当展开时,它会显示该类别关联的敏感性(仅在 MLS 策略中)。

  • 使用--commonseinfo显示常见的权限集。这些是不同类继承的权限集。当展开时,它显示该集合的权限部分。

  • 使用--constrainseinfo显示当前的约束。此查询没有扩展信息。

  • 使用--defaultseinfo显示策略中的default_*规则。例如,其中一条规则是类的默认敏感性范围(default_range)。此查询没有扩展信息。

  • 使用--fs_useseinfo显示 SELinux 策略中的fs_use_*规则。其中一条规则是为支持扩展属性的文件系统分配安全上下文(fs_use_xattr)。此查询没有扩展信息。

  • 使用--genfscon时,seinfo显示不支持扩展属性的文件系统的上下文分配。此查询没有扩展信息。

  • 使用--initialsid时,seinfo显示所有初始安全标识符SID)。这些是所有具有预定义上下文的类。展开时,显示与 SID 关联的上下文。

  • 使用--netifcon时,seinfo显示当前与网络接口相关联的上下文。仅在启用标记化网络时适用。此查询没有扩展信息。

  • 使用--nodecon时,seinfo显示当前与节点定义(主机)相关联的上下文。仅在启用标记化网络时适用。此查询没有扩展信息。

  • 使用--permissive时,seinfo显示当前标记为宽松域的类型。此查询没有扩展信息。

  • 使用--polcap时,seinfo显示策略能力(即定义 SELinux 子系统行为的策略设置,例如我们在第五章《控制网络通信》中看到的通过extended_socket_class策略能力支持 SCTP)。展开时,显示策略中的实际策略能力声明。

  • 使用--portcon时,seinfo显示当前端口映射及其相关的上下文(这也可以通过semanage port -l进行解释)。此查询没有扩展信息。

  • 使用--sensitivity时,seinfo显示当前支持的敏感性级别。展开时,显示声明敏感性的实际策略声明。

  • 使用--typebounds时,seinfo显示类型边界(由父域限定的 SELinux 类型或域)。此查询没有扩展信息。

  • 使用--validatetrans时,seinfo显示策略中活动的转换约束(这些是限制文件转换何时允许的约束)。大多数 Linux 发行版不使用此功能。此查询没有扩展信息。

seinfo命令还具有--all选项,可以显示它从策略文件中获取的所有可能信息。然而,这不包括扩展信息。

理解sesearch

seinfo显示 SELinux 对象的信息时,sesearch用于查询源资源与目标资源之间的 SELinux 规则和行为信息。

我们一直在使用sesearch查询标准allow规则(与类型强制相关的访问控制),以及 SELinux 布尔值对这些allow规则的影响。sesearch应用程序不仅允许我们基于规则类型查询规则,还可以基于其他参数进行过滤。让我们看看可以用于sesearch过滤器的参数:

  • 最常见的查询是使用--source-s)和/或--target-t)过滤与给定源表达式匹配的规则:

    $ sesearch -A -s mount_t -t unconfined_t
    $ sesearch -A -s svirt_sandbox_domain -ds
    
  • 使用--class-c)参数,我们可以仅搜索那些影响指定资源类(如filedirtcp_socket等——所有可能的类的列表可以通过seinfo --class获得)的规则:

    $ sesearch -A -s svirt_sandbox_domain -c file
    
  • 如果我们只对某个特定操作(或权限)感兴趣,可以使用--perm-p)参数。遇到某个操作(例如,write)被拒绝的情况时,这个选项特别有用,我们可以查看哪些域被允许执行此操作,这可能表明我们正在检查错误的源域。我们可以列出多个权限,在这种情况下,sesearch将显示至少包含一个权限的规则:

    -ep option, sesearch will only list the rules that have all permissions in them, rather than at least one.
    
  • 我们还可以仅查询受 SELinux 布尔值影响的那些规则,使用--bool-b)参数,正如我们在第十二章中看到的,调整 SELinux 策略

    如果我们使用-eb选项,那么命令行中列出的所有布尔值必须全部匹配,而不是至少匹配一个。

  • sesearch应用程序也可以使用正则表达式而不是实际值。这不是默认行为,但可以通过-rs(源类型或角色),-rt(目标类型或角色),-rc(类),-rd(默认类型或角色)和-rb(布尔值)来激活:

    $ sesearch -A -s staff_.*_t -c process -p transition -rs
    

由于这提供了对最常见的 SELinux 行为和访问控制的洞察,接下来我们将逐一介绍这些规则以及它们对系统的影响。

查询 allow 规则

第一类规则是allow规则,其中有许多子类型。标准的allow规则定义了源域可以成功触发哪些操作到目标类型或针对目标类型:

$ sesearch --allow -s guest_t -t cgroup_t -c dir
allow guest_usertype cgroup_t:dir { getattr ioctl lock open read search };
allow guest_usertype filesystem_type:dir { getattr open search };

SELinux 策略可以定义一些类似的规则,使用sesearch也可以类似查询,具体如下:

  • 使用--auditallow,我们可以查询哪些操作被 SELinux 允许,但仍会导致审计事件。

  • 使用--dontaudit,我们可以查询哪些操作在被拒绝时不会触发审计事件。

  • 使用--neverallow,我们可以查询哪些操作被禁止在策略中声明。定义这些操作后,如果它们违反了规则,系统将拒绝加载新的 SELinux 策略。不过,这不能用来否定现有规则,并且如果当前策略已经有偏离该规则的情况,那么尝试往策略中添加neverallow规则将会失败。

SELinux 也支持allow规则,但它需要额外的(数字)参数,以进一步限制规则的适用性,并用于为设备查询提供细粒度的访问控制。这些查询通常由ioctl()系统调用处理,但在扩展权限支持之前,SELinux 只能控制域是否被允许使用ioctl()系统调用,而不是过滤ioctl()的显式查询。

通过扩展权限规则,SELinux 策略开发人员可以指定哪些ioctl()查询是允许的,哪些是不允许的。例如,我们可以授予某个域获取硬件地址(被称为SIOCGIFHWADDR,其定义为编号0x8927)的权限,方法如下:

allowxperm <domain> <resource> : tcp_socket ioctl 0x8927;

sesearch中,我们可以使用--allowxperm查询这些规则。与常规的allow规则类似,我们还有--auditallowxperm--dontauditxperm--neverallowxperm选项,用于覆盖扩展权限等效规则。这些选项对查询以及扩展权限规则有相同的影响。

查询类型转换规则

第二类规则是类型转换规则。与其告知系统哪些操作被允许或不被允许,类型转换通过系统中进程执行的操作影响对象和资源的 SELinux 上下文。例如,类型转换规则定义了当一个文件在某个具有特定 SELinux 类型的目录中被某个域写入时,文件会接收到什么上下文;或者定义了当一个进程从某个源域执行时,该进程会接收到什么域:

$ sesearch -T -s guest_t -c process
type_transition guest_t abrt_helper_exec_t:process abrt_helper_t;
type_transition guest_t chfn_exec_t:process chfn_t;
...

在此输出中,我们可以看到,当访客域成功执行一个标记为abrt_helper_exec_t的二进制文件时,最终生成的进程将被分配abrt_helper_t上下文。

这些规则通过调查域转换部分中的各种工具进行查询和解释。

查询其他类型规则

除了allow规则和类型转换规则外,sesearch还可以查询另外两类与类型相关的规则:type_changetype_member。这些规则是为 SELinux 感知应用程序设计的,并不会被内核中的 SELinux 子系统强制执行:

  • 使用type_change语句(可以通过在sesearch中使用--type_change选项进行过滤),开发人员通知 SELinux 感知的应用程序,目标资源应由源域代表进行重新标记,赋予给定类型。

    type_change guest_t tty_device_t:chr_file user_tty_device_t;
    

    由于设备文件本身已经存在,并且仅被重新分配给用户,因此不会进行类型转换。相反,type_change规则由 SELinux 感知的应用程序进行解释,并相应地重新标记设备文件。

  • type_member规则(可以通过在sesearch中使用--type_member选项进行过滤)告知参与设置多实例化位置的 SELinux 感知应用程序(如我们在第三章中看到的,管理用户登录)这些目录的目标 SELinux 类型。例如,当/tmp位置(被标记为tmp_t)为某个用户进行多实例化时,则使用以下规则来理解该用户的/tmp视图应被标记为user_tmp_t

    type_member guest_t tmp_t:dir user_tmp_t;
    

    负责处理多实例化的 PAM 模块是 SELinux 感知的,并将使用这些规则来推断所创建位置的目标类型。

除了与类型相关的语句,sesearch还可以处理与角色相关的查询。

查询与角色相关的规则

SELinux 还具有与角色活动和转换相关的规则。通过sesearch应用,我们可以查询哪些 SELinux 角色可以从其他角色访问,以及何时执行角色转换(例如,从用户角色切换到系统角色):

$ sesearch --role_allow -s dbadm_r;
allow dbadm_r sysadm_r;
$ sesearch --role_trans -s dbadm_r;
role_transition dbadm_r mysqld_initrc_exec_t:process system_r;
role_transition dbadm_r postgresql_initrc_exec_t:process system_r;

两者的区别在于,允许的访问(使用--role_allow)显示哪些角色可以从给定角色访问,但它们并不指示何时进行转换。角色转换(使用--role_trans)则显示系统在执行脚本或二进制文件时,尝试自动更改角色的时机(以及会切换到哪个角色)。因此,它们可以与allow规则(指定允许的内容)和类型转换(定义 SELinux 行为)进行比较。

分析角色转换和角色allow规则有助于管理员推测哪些角色权限过大,可能导致潜在的安全问题。例如,允许dbadm_r角色通过postgresql_initrc_exec_t类型切换到system_r角色,如果该角色也具有修改postgresql_initrc_exec_t资源的权限,可能会使该角色执行超出其范围的操作:

$ sesearch -A -s dbadm_t -t postgresql_initrc_exec_t -c file;
allow dbadm_t postgresql_initrc_exec_t:file { execute execute_no_trans getattr ioctl map open read };

虽然直接修改postgresql_initrc_exec_t文件是不可允许的,但仅仅查看主要的用户类型是不够的。一个好的分析需要包括dbadm_r角色能够访问的所有类型,后续我们将在调查域转换分析信息流部分中介绍这些内容。这些部分将使用apol,所以让我们首先看看这个应用程序的功能。

使用 apol 进行浏览

一个用于执行策略分析的高级工具是apol,可以通过直接执行该命令而不带任何参数来启动。该工具是图形化的,允许分析人员和管理员对 SELinux 策略执行广泛的分析操作。

启动后,使用apol的第一步是加载目标策略(可以是当前活动的策略或从其他系统复制过来的文件)。这可以通过打开策略按钮完成,或者通过导航到文件 | 打开策略

工具将显示加载的策略的一般概览:

图 13.1 – 加载策略文件后的 apol 应用

图 13.1 – 加载策略文件后的 apol 应用

加载完成后,选择新建分析以启动策略分析功能:

图 13.2 – apol 支持的分析方法概述

图 13.2 – apol 支持的分析方法概述

提供了相当数量的分析方法。让我们选择类型浏览可用的类型,或者选择一个属性查看分配给该属性的 SELinux 类型:

图 13.3 – 使用 apol 进行类型浏览

图 13.3 – 使用 apol 浏览类型

同样地,使用sesearch应用程序:

图 13.4 – 使用 apol 查询类型强制规则

图 13.4 – 使用 apol 查询类型强制规则

更高级的分析方法在调查域转移分析信息流章节中有所介绍。

使用 apol 工作区

分析 SELinux 策略可能需要一些时间,特别是当涉及多个分析阶段和微调时。apol工具允许你将当前工作区保存到磁盘,以便稍后从保存点恢复分析:

图 13.5 – apol 中的工作区管理

图 13.5 – apol 中的工作区管理

工作区不仅保留了到目前为止查询的设置,还保留了你可能添加的备注。备注是apol中的一个重要功能,你可以在其中写下你在查询过程中想到的内容和观察结果。备注与打开的标签页相关联,使你能够根据需要在不同查询之间切换。

现在我们已经知道了apol应用程序的工作原理,接下来看看我们如何利用它(以及其他工具)进行更深入的分析。

调查域转移

处理 SELinux 策略时,一个重要的分析方法是执行域转移分析。域是由给定域的访问控制所界定的,但用户或进程可以通过执行正确的应用程序集转移到其他域。

分析两个 SELinux 域之间是否以及如何发生转移,允许管理员验证策略的安全状态。考虑到 SELinux 的强制性,攻击者很难执行目标应用程序,如果域转移分析表明源域无法直接或间接执行该应用程序。

管理员应使用域转移分析确认域是否正确隔离,并确保在域内运行的应用程序中的漏洞不会导致特权提升。

使用 apol 进行域转移分析

启动apol后,选择新分析,然后选择域转移分析。分析界面本身将展示几种可能的分析方法:

图 13.6 – 查询 staff_t 和 unconfined_t 之间可能的转移路径

图 13.6 – 查询 staff_t 和 unconfined_t 之间可能的转移路径

该分析将尝试在给定的源域和目标域之间找到一条路径,并显示可能导致过渡的执行轨迹。管理员可以验证与这些域过渡相关的应用程序是否可信。当我们需要确认某些域无法突破其限制,或在我们开发新策略并希望确保限制在我们希望的边界内时,这样的分析是有意义的。

过渡分析可以通过以下设置进行微调:

  • 使用apol时,它会显示源域和目标域之间的域过渡,寻找可能的最短过渡路径。例如,从staff_tstaff_sudo_t再到unconfined_t是一个两步路径。当找到路径时,apol不会继续搜索更长的路径。

  • 当我们选择apol时,它将执行分析,直到某个特定步骤数。当我们使用最多一步时,这类似于使用seinfosesearch进行直接查询。

  • 使用从源域的过渡到目标域的过渡将显示从给定源域或到目标域的所有可能过渡。这用于更互动的会话,用户可以点击各个域,查看可以过渡到的下一个域集合。

为了进一步微调分析,可以选择一些选项。例如,我们可以排除某些类型不参与域过渡分析。这样,我们就可以将某些域标记为受信任(如*_sudo_t域),这将使得apol忽略这些域,从而找到更合适的过渡链进行分析。

使用 sedta 进行域过渡分析

apol执行的路径分析也可以通过一个名为sedta的命令行应用程序来执行。它具有与apol中域过渡分析功能相同的能力。

分析类型通过命令行参数选择:-S用于最短路径分析,而-A(后跟数字)执行所有路径直到的等效操作。

例如,要检查staff_t域和unconfined_t域之间的域过渡路径,排除staff_sudo_tnewrole_tinit_t域,可以使用以下命令:

$ sedta -S -s staff_t -t unconfined_t staff_sudo_t newrole_t
Domain transition path 1:
Step 1: staff_t -> oddjob_t
Domain transition rule(s):
allow staff_t oddjob_t:process transition;
Set execution context rule(s):
allow staff_t staff_t:process { dyntransition fork getattr getcap getpgid getrlimit getsched getsession noatsecure rlimitinh setcap setcurrent setexec setfscreate setkeycreate setpgid setrlimit setsched setsockcreate share sigchld siginh sigkill signal signull sigstop transition };
Entrypoint oddjob_exec_t:
        Domain entrypoint rule(s):
        allow oddjob_t oddjob_exec_t:file { entrypoint execute getattr ioctl lock map open read };
        File execute rule(s):
        allow staff_t oddjob_exec_t:file { execute execute_no_trans getattr ioctl map open read };
        Type transition rule(s):
        type_transition staff_t oddjob_exec_t:process oddjob_t;
Step 2: oddjob_t -> openshift_initrc_t
...

我们可以使用-p选项分析与当前系统策略不同的策略。

使用 sepolicy 进行域过渡分析

sepolicy工具具有内建的域过渡分析功能,使用transition参数。不过,它不像sedtaapol那样灵活,因为该命令无法进行调整。它似乎也无法涵盖所有可能的路径,通常会显示非常复杂且详尽的路线,而这些路线本可以更简单:

$ sepolicy transition -s mount_t -t unconfined_t
mount_t ... glusterd_t ... ipsec_t ... ipsec_mgmt_t
  ... initrc_t ... condor_schedd_t ... condor_startd_t
  ... openshift_initrc_t ... stunnel_t ... telnetd_t
  ... remote_login_t @ shell_exec_t --> unconfined_t
  -- Allowed True [ unconfined_login=1 ]
mount_t ... glusterd_t ... ipsec_t ... ipsec_mgmt_t
  ... initrc_t ... condor_schedd_t ... condor_startd_t
  ... openshift_initrc_t ... kmscon_t ... 
  local_login_t @ shell_exec_t --> unconfined_t 
  -- Allowed True [ unconfined_login=1 ]
mount_t ... glusterd_t ... ipsec_t ... ipsec_mgmt_t
  ... initrc_t ... condor_schedd_t ... condor_startd_t
  ... openshift_initrc_t ... kdumpgui_t ... kdumpctl_t
  ... sge_execd_t ... sge_shepherd_t ...
  sshd_t @ shell_exec_t --> unconfined_t
  -- Allowed True [ ssh_sysadm_login=0 || unconfined_login=1 ]
mount_t ... glusterd_t ... ipsec_t ... ipsec_mgmt_t
  ... initrc_t ... condor_schedd_t ... condor_startd_t
  ... openshift_initrc_t ... kdumpgui_t ... kdumpctl_t ...
  sulogin_t @ shell_exec_t --> unconfined_t
  -- Allowed True [ unconfined_login=1 ]
mount_t ... glusterd_t ... ipsec_t ... ipsec_mgmt_t
  ... initrc_t ... condor_schedd_t ... condor_startd_t
  ... openshift_initrc_t ... kdumpgui_t ... kdumpctl_t
  ... inetd_t ...
  rshd_t @ shell_exec_t --> unconfined_t
  -- Allowed True [ unconfined_login=1 ]
mount_t ... glusterd_t ... ipsec_t ... ipsec_mgmt_t
  ... initrc_t ... condor_schedd_t ... condor_startd_t
  ... openshift_initrc_t ... kdumpgui_t ... kdumpctl_t
  ... piranha_pulse_t ...
  crond_t @ shell_exec_t --> unconfined_t
  -- Allowed True [ cron_userdomain_transition=1 || unconfined_login=1 ]
mount_t ... glusterd_t ... ipsec_t ... ipsec_mgmt_t
  ... initrc_t ... condor_schedd_t ... condor_startd_t
  ... openshift_initrc_t ... kdumpgui_t ... kdumpctl_t
  ... piranha_pulse_t ... cockpit_ws_t ...
  cockpit_session_t @ unconfined_exec_t --> unconfined_t

让我们将其与sedta进行比较,我们使用相同的策略和相同的域过渡:

$ sedta -S -s mount_t -t unconfined_t | \
  grep -E '(transition path|Step)'
Domain transition path 1:
Step 1: mount_t -> glusterd_t
Step 2: glusterd_t -> sulogin_t
Step 3: sulogin_t -> unconfined_t
Domain transition path 2:
Step 1: mount_t -> glusterd_t
Step 2: glusterd_t -> virtd_lxc_t
Step 3: virtd_lxc_t -> unconfined_t
Domain transition path 3:
Step 1: mount_t -> glusterd_t
Step 2: glusterd_t -> xdm_t
Step 3: xdm_t -> unconfined_t
Domain transition path 4:
Step 1: mount_t -> glusterd_t
Step 2: glusterd_t -> crond_t
Step 3: crond_t -> unconfined_t
Domain transition path 5:
Step 1: mount_t -> glusterd_t
Step 2: glusterd_t -> sshd_t
Step 3: sshd_t -> unconfined_t
Domain transition path 6:
Step 1: mount_t -> glusterd_t
Step 2: glusterd_t -> virtd_t
Step 3: virtd_t -> unconfined_t
6 domain transition path(s) found.

在将转换路径与 sedta 生成的路径进行比较时,你会注意到 sedta 经常找到比 sepolicy transition 更短的域转换路径。因此,不建议仅依赖 sepolicy transition 进行域转换分析。

分析信息流

在 SELinux 策略中,另一项可以进行的分析是信息流分析。与域转换不同,域转换分析考察的是一个域如何通过转换到另一个域获得某一组权限,而 信息流分析 则关注一个域如何将信息泄漏(有意或无意)到另一个域。

信息流分析通过查看两个类型之间发生的所有操作来执行。源类型可以被一个域读取,随后可以将信息写入另一个类型,然后该类型可以被另一个域访问。虽然这仍然可以按步骤进行分析,但很快变得非常具有挑战性,因为我们不能仅限于读取和写入操作。

信息可以通过文件名、文件描述符等方式泄漏。信息流分析必须考虑到所有这些方法。

使用 apol 进行信息流分析

加载 SELinux 策略后,选择 信息流分析。我们收到的界面类似于域转换分析,但现在有几个切换按钮可以微调特定于信息流的路径分析:

图 13.7 – 分析两个域之间的信息流

图 13.7 – 分析两个域之间的信息流

与域转换不同,信息流可以通过的路径数量呈指数增长。为了进行合理的信息流分析,我们需要微调搜索标准:

  • lock 操作,其权重为 1,与高优先级操作(例如 write 操作,权重为 10)进行比较。这些权重的目的是定义哪些操作在信息流中是可行的,哪些操作则更难(但并非不可能)用于故意的信息交换。

  • 使用 排除的权限,我们可以选择性地启用或禁用某些权限进行分析。

其他选项与域转换分析中的选项类似。

信息流分析中最重要的区域是权限映射,我们可以在启用或禁用权限的同时,部分微调它。然而,我们可能不满意当前权限映射所使用的权重。

要编辑权限映射,选择 apol 菜单:

图 13.8 – 编辑权限映射和权限权重

图 13.8 – 编辑权限映射和权限权重

在此编辑器中,我们可以根据需要微调权限的权重,以及操作的 方向性

  • None(没有信息流)

  • Write(信息流向资源)

  • Read(信息从资源中获取)

  • Both(信息可以同时流向和流出资源)

一旦我们对结果感到满意,我们可以(也可能应该)保存权限映射,以便以后重用(如果不保存,修改仅适用于当前会话,apol关闭后将会丢失)。

使用 seinfoflow 进行信息流分析

像用于域转换分析的sedta应用程序一样,也有一个命令行应用程序提供类似apol的信息流分析功能,即seinfoflow。每次调用seinfoflow命令时,都需要传入权限映射以供分析。如果你自己没有创建并保存权限映射,可以使用默认的权限映射,路径为/var/lib/sepolgen/perm_map

让我们分析staff_tguest_t域之间的信息流动可能性,使用默认的权限映射,并仅考虑权重为10的权限:

$ seinfoflow -S -m /var/lib/sepolgen/perm_map \
  -s staff_t -t guest_t -w 10

权限映射越复杂,分析完成所需的时间就越长。

使用 sepolicy communicate 进行简单的信息流分析

sepolicy命令可以使用communicate选项执行简单的流量分析。给定源域和目标域,sepolicy将检查信息可以通过哪些中间类型在这两个域之间流动:

$ sepolicy communicate -s postgresql_t -t staff_t
krb5_host_rcache_t
cluster_conf_t
security_t
postgresql_t
postgresql_tmp_t
hugetlbfs_t

上述流量分析主要是检查源域可以写入的类型,以及目标域可以读取的类型。

比较策略

到目前为止,我们已经分析了单一策略集,找到了域转换和信息流动路径。我们使用的命令和应用程序都专注于单一策略的分析。另一个重要的分析是比较两个策略。策略开发者可以用它来比较新策略和旧策略,或者比较两个系统策略,查看管理员添加了哪些额外的规则。

使用 sediff 比较策略

sediff工具查看两个策略文件之间的差异,并将这些差异报告给用户。通常不建议对完全不同的策略使用它,但它在查找策略之间的细微差异时非常有用,这可以帮助在不同系统之间排除故障。

sediff的一个常见使用案例是验证源构建的策略文件是否与发行版提供的二进制策略文件相同。管理员可以确保他们用来构建策略文件的源代码与发行版使用的相同,即使二进制文件本身(policy.##文件)的校验和不同:

$ sediff policy.31 /sys/fs/selinux/policy
Policy Properties (0 Modified)
Classes (1 Added, 0 Removed, 4 Modified)
   Added Classes: 1
      + xdp_socket
   Modified Classes: 4
      * capability2 (1 Removed permissions)
          - compromise_kernel
      * process (1 Added permissions, 1 Removed permissions)
          + getrlimit
...

可以指示sediff仅显示指定区域或 SELinux 策略部分的差异(例如可用的类型、角色、布尔值或类型强制规则)。

总结

SELinux 有许多可以用来分析策略的工具。我们已经看到如何使用sesearch对当前策略进行深入评估,但也注意到它未能验证更动态的分析需求。

使用apol时,我们看到了一个能够进行更动态分析的图形化应用程序,包括域转换(检查从当前点可以到达哪些域)和信息流分析(研究信息如何从一个域流向另一个域)。通过这次经验,我们了解到这样的分析非常密集,需要大量的解释才能正确完成。

除了apol,我们还了解到存在具有类似功能的命令行工具:sedta用于域转换分析,seinfoflow用于信息流分析,以及sepolicy,它具有一些开箱即用的功能,但不如我们查看过的其他选项那样广泛或灵活。

最终,我们学会了如何使用sediff来比较策略。这在新策略开发时非常有用,这是我们将在接下来的章节中做的事情。我们首先将在下一章中对现有策略进行对齐和扩展,以适应新应用程序,并在最后两章中继续进行完整的应用程序策略开发。

问题

  1. seinfosesearch之间有什么区别?

  2. 如何检查你是否可以到达一个域?

  3. 为什么分析信息流需要这么长时间?

  4. 我们能生成两个策略之间的差异并加载它吗?

第十四章:处理新应用程序

新应用程序通常还未通过特定于应用程序的 SELinux 策略进行支持,因为大多数应用程序项目并未自行开发 SELinux 策略,而是依赖于社区(或更具体地说是 Linux 发行版)来创建和维护这些策略。一些 Linux 发行版已经实施了回退机制,允许这些应用程序运行,即使它们可能未正确隔离。然而,管理员可能不喜欢没有任何 SELinux 强制执行的情况下,运行不受信任的新应用程序。

因此,本章将讨论管理员如何在多个隔离环境中运行新应用程序,从(通常是默认的)未保护域,到沙箱系统,最终通过重用现有的 SELinux 域,而无需完全开发新的域。

本章将涵盖以下主要内容:

  • 无限制地运行应用程序

  • 使用沙箱应用程序

  • 为新应用程序分配通用策略

  • 扩展生成的策略

技术要求

查看以下视频,看看代码如何运行:bit.ly/3dGG5Bu

无限制地运行应用程序

在许多 Linux 发行版中,默认行为是通过未受限制的域来运行新应用程序。这些是精心设计的域,虽然仍然受到 SELinux 的控制,但授予了非常广泛的权限。你可以将这样的未受限制域与允许任何可能流量的防火墙进行比较:尽管防火墙正在运行,但它几乎没有进行任何强制执行。

然而,也可以采用另一种方法,即将应用程序作为宽松域运行。与未受限制的域不同,宽松域不会通过 SELinux 强制执行:该域的所有操作都是允许的,即使 SELinux 可能会记录每一次违规行为。在第三章中,我们简要讨论了宽松域,理解 SELinux 决策和日志记录

让我们首先看看未受限制的域,以及管理员如何修改系统配置,将未受限制的域应用于其他应用程序,或将应用程序移除出未受限制的状态。

理解未受限制的域如何工作

未受限制的域是一个具有广泛权限的 SELinux 域,仅限制域可以执行的极少数操作。未受限制的域实际上并不是 SELinux 作为技术所支持的概念。相反,它是由 SELinux 策略开发者使用的,他们创建了一组他们认为是未受限制的权限。

许多 Linux 发行版的最终用户会注意到,他们自己的上下文是 unconfined_t。虽然这确实是指一个未受限制的域,但实际上存在比 unconfined_t 更多的未受限制域。

SELinux 策略开发者将大部分与不受限制域相关的权限聚集在域本身(如参考策略所示)或 SELinux 属性中,如unconfined_domain_typeunconfined_user_type(如 CentOS 和相关的 Linux 发行版所示)。对于属性,这些属性然后会分配给一个或多个域,从而有效地使它们本质上不受限制:

$ seinfo -a | grep unconfined
$ seinfo -a unconfined_domain_type -x

一旦一个进程在不受限制的域中运行,并不意味着该域的每个操作都保持不受限制。当不受限制的域执行一个有适当 SELinux 策略的进程时,这个执行仍然可能会引发域转换,从而将执行的命令置于一个(可能受限的)SELinux 域中。

由于是否允许域转换的决定属于 SELinux 策略的一部分,建议管理员查询哪些域转换是被允许的,哪些不是。我们在第十三章中看到了如何分析域转换,分析策略行为。考虑到我们主要关心的是单步分析,我们可以使用sesearch工具快速概览支持的域转换:

$ sesearch -A -s unconfined_service_t -c process -p transition
allow unconfined_service_t chronyc_t:process transition;
allow unconfined_service_t rpm_script_t:process transition;
allow unconfined_service_t unconfined_service_t:process { transition ...};
allow unconfined_service_t virt_domain:process { transition ...};

我们可以通过检查单一域的权限或直接检查表示不受限制域的属性,来查看与不受限制域相关的(多个)权限:

$ sesearch -A -s unconfined_domain_type -ds

使用不受限制的域比使域宽松更为优先,因此让我们来看一下如何标记一个新的应用程序,以使其在不受限制的域中运行。

使新的应用程序在不受限制的域中运行

当执行应用程序时,有多个检查需要通过,才能导致域转换:

  • 源 SELinux 域必须能够执行应用程序(意味着在与应用程序的二进制文件或脚本相关联的 SELinux 类型上拥有执行权限)。

  • 源 SELinux 域必须能够转换到目标域。

  • 目标域必须为其应用程序的二进制文件或脚本打上一个 SELinux 类型标签,该标签被标记为该域的入口点。

  • 目标域必须允许源域所运行的 SELinux 角色(或者必须允许角色转换,但这是一个边缘案例)。

所有这些检查都与 SELinux 策略和标签相关。因此,毫不奇怪,为了使应用程序能够在不受限制的域中运行,我们需要关联正确的标签。

让我们在接下来的章节中考虑两个示例,一个是用户触发的应用程序,另一个是守护进程服务。

在显式不受限制的域中运行应用程序

对于用户执行的应用程序,举个例子,我们在第七章中介绍了 Jailkit,配置特定应用程序的 SELinux 控制。默认情况下,该应用程序未与任何域关联,因此它与父进程在同一域中运行。如果我们通过unconfined_u用户登录系统(在unconfined_t SELinux 域中),那么我们无需做任何事情。但是,假设我们的 staff 用户是受限的,但我们希望命令在unconfined_t域中运行。

重要说明

这是一个示例,展示了如何让应用程序在目标域中运行——在我们的案例中,是在一个非约束域中。允许受限用户运行非受限应用程序总是有一定风险的,因为他们可能利用这个机会突破限制。确保只有对那些你有信心不会违反安全的应用程序或用户才执行此操作。

要允许应用程序在unconfined_t域中运行,我们将使用sudo及其 SELinux 支持。虽然我们也可以扩展 SELinux 策略以透明地允许它,但这并不推荐。更新 SELinux 策略以允许受限用户运行非受限命令意味着要推翻策略中列出的一些原则。例如,你可能需要透明地允许受限用户切换到unconfined_r角色(通常出于安全原因不允许这样做)。这将需要进行大量分析,以确保它无法被用来突破受限角色。

使用sudo可以限制通过这些更高权限命令执行的方法。例如,SELinux-wise,适当的控制措施会被应用于staff_sudo_t域,这个域仅在执行sudo命令时才会分配,而不是staff_t域,后者是大多数用户交互所在的域。

让我们允许lisa用户作为一个非受限进程运行jk_init命令:

  1. 首先,检查我们要执行命令的 SELinux 用户是否被允许使用unconfined_r SELinux 角色(如果没有,则将该角色添加到 SELinux 用户配置中):

    # semanage user -l
    

    允许某个角色并不意味着用户域会自动在需要时切换角色,而是意味着该角色是用户的允许角色。

  2. 接下来,更新/etc/sudoers文件,在执行以下命令时添加转换:

    ROLE and TYPE transition, but we also allow the command to be executed as the root user, as that is a requirement for the jk_init command. Of course, this can be adjusted as needed.
    
  3. 我们的用户现在可以运行命令,并通过sudo前缀使其在正确的域和使用正确的角色下执行:

    $ sudo /usr/sbin/jk_init -v -j /srv/chroot \
      extshellplusnet
    

对于最终用户应用程序,使用sudo很常见,当用户的权限也需要切换时(从用户权限切换到 root 权限)。但在保持在 Linux 用户上下文中时,使用它则不太常见。

在显式非受限域中运行守护进程

第二种使用场景,或许比最终用户应用程序更常见,是将守护进程服务在非受限域中运行。大多数使用非受限域的 Linux 发行版(如 CentOS)默认会让新安装的软件也以非受限域运行。例如,任何通过 systemd 启用和激活的服务(它以init_t SELinux 域运行)且没有显式标签设置(即可执行命令被标记为bin_t)将运行在unconfined_service_t域中。

但如果我们有一个受限应用程序,想要在非受限域中运行呢?以 PostgreSQL 为例。假设这是一个隔离的数据库,启用了某些与现有 PostgreSQL SELinux 域(postgresql_t)不兼容的扩展。管理员可能没有时间使用audit2allow等方法扩展现有的 SELinux 策略,正如在第十二章《调整 SELinux 策略》中所看到的那样。

幸运的是,我们可以轻松地将 PostgreSQL 移至非受限域并运行。有两种方法可以实现这一点:

  • 我们可以移除其可执行文件上的现有标签(postgresql_exec_t),并将其设置为bin_t。这将触发启动 PostgreSQL 二进制文件时的默认转换,转换到unconfined_service_t域。

  • 我们可以更新 SELinux 策略,使postgresql_t成为一个非受限域。

切换标签很容易,但这不是最推荐的方法。然而,这是一种快速且简单的方式,可以立即查看在unconfined_service_t域中运行服务是否足以解决问题:

# chcon -t bin_t /usr/bin/postgres

如果可以接受,确保标签更改在重新标签操作后仍然保持:

# semanage fcontext -a -t bin_t /usr/bin/postgres

推荐更新 PostgreSQL 守护进程的 SELinux 策略,因为它保留了策略中现有的支持(包括文件转换和postgresql_t域与其他域及资源的集成)。它还允许管理员在以后有更多时间时,根据需要更新策略。

为了使postgresql_t域变为非受限域,我们需要将unconfined_domain_type属性分配给postgresql_t域。这可以通过加载以下基于 CIL 的 SELinux 策略来完成:

(typeattributeset cil_gen_require postgresql_t)
(typeattributeset cil_gen_require unconfined_domain_type)
(typeattributeset unconfined_domain_type (postgresql_t))

将此内容保存到文件中,并使用semodule -i加载,从此以后,postgresql_t域将扩展为与unconfined_domain_type属性相关的权限。

扩展非受限域

由于非受限域仍然受到强制执行,可能仍然有一些操作被 SELinux 阻止。我们可以调整 SELinux 策略,以扩展非受限域并授予更多权限。尽管默认的unconfined_service_t域几乎设置了所有可能的权限,但更具体地说,某些已识别的域可能没有那么广泛。

向域添加更多权限的技巧是为其分配适当的属性。这个方法与在运行守护进程于明确的非限制性域中看到的方法相同,根据需要添加更多属性。我们可以添加的属性列表非常重要(如从seinfo -a中看到的那样),但最重要的几个属性,尤其是对于基于 CentOS 的 SELinux 策略,主要包括以下几个:

  • files_unconfined_type 允许该域管理任何可能的文件或基于文件系统的资源。

  • devices_unconfined_type 允许该域与任何设备资源进行交互和管理。

  • filesystem_unconfined_type 允许该域与所有文件系统进行交互和管理。

  • selinux_unconfined_type 允许该域与 SELinux 子系统及其配置进行交互和管理。

  • storage_unconfined_type 允许该域与存储系统和可移动设备进行交互。

  • dbusd_unconfined 允许该域与所有可能的 D-Bus 服务进行交互。

  • xserver_unconfined_type 允许该域与并管理所有 X 服务器资源。

此外,还有若干个can_*属性,它们精细调整了非常具体的、安全敏感的操作。这些属性的名称很好地解释了它们的用途。例如,can_write_shadow_passwords 允许该域写入/etc/shadow,而can_change_object_identity 表示该域可以更改对象的 SELinux 用户。

并非所有属性的权限都能通过常规的allow规则或过渡来反映,这些规则或过渡可以通过sesearch查询。例如,can_change_object_identity 是在 SELinux 约束中使用的:

# seinfo --constrain | grep can_change_object_identity

查询约束是一个常常被忽视的方法,可以查看某个特定权限是否被分配到域,或者为什么没有被分配。

假设现在一个应用程序在非限制性域内仍然无法正确运行,那么我们可以使用宽容域来允许该应用程序在没有保护的情况下运行,同时保持系统的其余部分处于强制模式。

将域标记为宽容

正如我们在第二章中看到的,理解 SELinux 决策和日志记录,我们可以使用semanage permissive将一个域标记为宽容:

# semanage permissive -a postgresql_t

同样的命令可以用来查询(-l)或移除(-d)宽容状态。然而,管理员在将域标记为宽容时应特别小心:

  • 首先,如果你将一个域标记为宽容,那么该 SELinux 域下运行的所有进程都将没有任何活动的 SELinux 强制执行。作为管理员,你确实希望限制通过宽容域运行的进程数量,因此不要将广泛使用的 SELinux 域标记为宽容。

    在一个非限制性域内运行的守护进程,如果仍然存在问题,不应导致该非限制性域被标记为宽容。相反,应该让该守护进程以不同的域运行,并将该域标记为宽容。

  • 其次,宽松的域仍然会触发 SELinux 子系统的 SELinux 行为。过渡规则,包括进程过渡和文件过渡,仍然会执行。这当然是设计上的考虑,因为宽松域旨在短期使用,允许管理员和开发人员捕获信息并根据需要调整策略,然后才能再次移除宽松标志。

    这也意味着,如果域没有设置适当的过渡规则,可能会导致系统上创建具有错误 SELinux 类型的文件。因此,对于对系统影响较大的应用程序或守护进程,不应考虑使用宽松的域,而应使用更为隔离的情况,在这些情况下,作为管理员的你确信在需要时可以轻松地微调策略。

假设我们部署 pgpool-II,这是一个 PostgreSQL 数据库的负载均衡器,并发现该应用程序在非受限域中无法正常运行,尽管它已经在 unconfined_service_t SELinux 域中运行。虽然我们可以将此域设置为宽松模式,但这也会影响在 unconfined_service_t 域中运行的其他各种服务。

我们可以做的是重新标记其资源(主要是可执行文件),使得该应用程序通过另一个 SELinux 域运行,然后将该域标记为宽松模式。我们可以重用一个现有的、未使用的域,或者生成一个新域,正如我们将在 使用 sepolicy generate 生成策略 章节中看到的那样。

然而,当我们希望以(严格)受限的方式运行应用程序时,我们需要采取完全不同的方法,并寻求将此类应用程序放入类似沙盒的域中。

使用沙盒化的应用程序

应该仅具有非常有限权限且天生不可信的新应用程序应完全受限。虽然我们可以为这些应用程序查看自定义的 SELinux 策略,但对于每个应用程序来说,这几乎是不可能的。

相反,我们可以考虑将应用程序沙盒化,将其与系统的访问隔离开来。借助其他 Linux 原语,如命名空间支持,已经创建了一个名为 SELinux 沙盒的工具,它将在一个严格受限的域中启动应用程序。这主要适用于终端用户应用程序。

重要提示

SELinux 沙盒、其 SELinux 策略以及与其相关的命令,特定于使用或遵循 Red Hat 包的 Linux 发行版,如 CentOS。这可能不适用于你的 Linux 发行版。

对于面向服务的域,使用容器运行时和保护措施更为合适。有关使用容器保护的更多信息,请参见 第十一章增强容器化工作负载的安全性

理解 SELinux 沙盒

SELinux 沙箱是多种技术和保护措施的结合体。虽然 SELinux 策略在其中起着重要作用,但也采取了其他隔离措施,真正为应用程序和用户创建了沙箱体验。

沙箱的目的是创建一个低权限环境,阻止任何可能危及系统或用户数据安全的操作。这也意味着网络交互默认是被阻止的(没有数据外泄),并且许多系统资源对沙箱进程是隐藏的。

许多访问控制本身是由 SELinux 策略处理的。沙箱 SELinux 域 sandbox_t及其衍生域(如sandbox_xserver_t)没有太多对其他资源的权限。沙箱实用程序还将应用类似 sVirt 的类别,以区分不同的沙箱进程。

然而,隔离是通过不同的手段实现的。seunshare应用程序负责执行这些隔离任务。

让我们看看 SELinux 沙箱在实践中是如何工作的。

使用沙箱命令

SELinux 沙箱使用 sandbox命令。现在,在我们使用它之前,需要确保我们的 SELinux 用户已经设置了多个类别,否则 SELinux 沙箱无法随机分配两个类别进行隔离:

# semanage login -l
# semanage login -m -r "s0-s0:c0.c100" lisa

一旦分配了类别,我们就可以准备在沙箱中运行不可信的应用程序。例如,我们可以从www.ioccc.org下载国际混淆 C 代码竞赛的一个应用程序,编译它,然后仅在沙箱模式下运行它,以防代码表现出恶意行为:

  1. 假设我们使用的是 adamovsky的 2019 年条目,我们应该已经准备好了 prog二进制文件和 advent.unl文件。创建一个存储这些文件的位置,并将它们复制过来:

    $ mkdir sandbox
    $ cp 2019/adamovsky/* sandbox
    
  2. 接下来,从沙箱中运行 prog命令:

    $ sandbox -H sandbox/ prog advent.unl
    Welcome to Adventure!! Would you like instructions?
    **
    
  3. 在应用程序运行时,我们可以通过 ps查看其当前上下文:

    prog command itself, which will be running in the sandbox_t SELinux domain and with a certain category pair set, you will notice that a seunshare command will run alongside it. This command provides the isolation for the process, not only by triggering the SELinux context change, but also removing unnecessary mount and filesystem views from the process's viewpoint.
    
  4. 如果退出应用程序,我们可以看到沙箱位置已被标记为一个类似 sVirt 的 MCS 对:

    $ ls -Z sandbox/
    staff_u:object_r:sandbox_file_t:s0:c29,c94 advent.unl
    staff_u:object_r:sandbox_file_t:s0:c29,c94 prog
    

我们在这里使用的方法是明确告诉沙箱基于 sandbox/文件夹创建一个隔离的主目录,并从该位置运行 prog二进制文件(并且将 advent.unl作为 prog命令的参数)。然而,这并不是唯一的方法。

如果没有明确提供主目录,则沙箱将创建一个临时目录(并在之后清理)。但是,在这种情况下,除非我们允许沙箱域执行标有 user_home_t标签的资源,否则我们无法执行系统中未安装的命令:

(typeattributeset cil_gen_require sandbox_t)
(allow sandbox_t user_home_t (file (execute map)))

加载此策略后,我们可以使用最少选项来使用沙箱。例如,使用 Burton 竞赛提交(也来自 IOCCC 2019 年竞赛),我们有以下内容:

$ cat prog.c | sandbox ./prog
      1      1   127

然而,使用一个更常见的位置可以提供更多的灵活性,同时还允许沙箱在多个会话之间保持数据(因为指向的目录不会被清理)。

SELinux 沙箱还支持在沙箱中运行图形应用程序。要实现这一点,可以在sandbox命令中添加-X选项。由此启动的进程将运行在sandbox_xserver_t域中,而不是sandbox_t域,因为运行图形应用程序需要更多的权限。不过需要注意的是,沙箱域的权限非常有限;不允许连接到网络资源,因此,在没有额外修改和 SELinux 策略调整的情况下,无法使用沙箱(无法与不安全网站交互的沙箱浏览器)。

将通用策略分配给新应用程序

在 SELinux 沙箱的强隔离与未受限制域(或甚至宽松域)的广泛权限之间,存在着具有足够特权的应用程序域。对于大多数管理员来说,拥有一个适当的 SELinux 域来管理应用程序是最好的方法,因为它可以允许所有常见的行为,并限制不希望出现的行为。

然而,当我们开始查看应用程序域时,我们会注意到复杂性存在差异,作为管理员,我们需要理解这种复杂性到底是什么,然后才能做出正确的选择。

了解域复杂性

SELinux 能够提供完整的系统限制:每个应用程序都在自己无法突破的受限环境中运行。但这需要细粒度的策略,这些策略必须随着所有应用程序的新版本快速开发。

以这样的速度制定细粒度的策略是不可能的,因此必须在策略的可维护性和域的安全性之间找到平衡。这个平衡就是策略设计的复杂性或域复杂性,可以大致分为以下几类:

  • 细粒度策略为应用程序或服务的每个子组件设置独立的、单独的域。这种策略的优点在于它们尽可能地限制应用程序。通过细粒度策略,针对用户和管理员开发的角色也会变得更精细,例如通过区分应用程序中的子角色。

    这种策略的缺点是它们很难维护,需要随着应用程序本身的演变频繁更新。策略还需要考虑应用程序所支持的各种配置选项的影响。

    这样的细粒度策略并不常见。一个例子是为 Postfix 邮件基础设施提供的策略集。Postfix 基础设施的每个子服务都有自己的 SELinux 域。

  • 应用程序级别的策略为应用程序使用单一域,无论其子组件如何。这在应用程序的限制性需求与应用程序及其 SELinux 策略的可维护性之间达到了平衡。

    这种应用程序级别的策略是 SELinux 策略中最常见的策略。尽管随着应用程序功能的扩展,它们确实面临常规维护问题,但其复杂性是有限的,SELinux 策略开发人员应该不会在维护这些策略时遇到太多问题。

  • 类别广泛的策略为一组实现相同功能的应用程序使用单一的域定义。这种策略适用于那些行为非常相似的服务,它们的用户角色定义可以在不考虑特定应用性质的情况下描述。

    类别广泛策略的一个好例子是针对 web 服务器的策略。虽然这个策略最初是为 Apache HTTP 守护进程编写的,但它已经变得可以重用,适用于多个 web 服务器,例如 Cherokee、Hiawatha、Nginx 和 Lighttpd 项目。

    虽然此类策略更易于维护,但类别广泛策略的缺点是,它们往往拥有比实际需要更广泛的权限。随着更多应用加入类别广泛策略,为了支持这些特定功能,会增加更多的规则和权限。

  • 粗粒度策略用于那些难以定义行为的应用程序或服务。最终用户域是粗粒度策略的一个例子,未受限的域也是如此。

当我们处理一个新的应用程序,并希望快速分配一个足够合适的策略时,最常见的方法是查看是否存在可以为该应用程序重用的类别广泛策略。

在特定策略中运行应用程序

让我们考虑一下 pgpool-II 应用程序的情况。当我们在没有任何额外更改的情况下安装它时,按照 标记域为宽松 部分所述,它将以 unconfined_service_t 域运行。但也许我们可以找到一个合适的策略,使得 pgpool-II 应用程序能够在更受限制的环境中运行。

由于 pgpool-II 解决方案是一个类似负载均衡的 PostgreSQL 数据库应用程序,因此我们可能可以将其运行在 PostgreSQL 域中。如果系统上没有运行 PostgreSQL 数据库,那么将这个域分配给 pgpool-II 应用程序可能不会造成太大损害。让我们看看效果如何:

  1. PostgreSQL 策略使用 postgresql_exec_t SELinux 类型来处理其可执行文件,因此我们将其分配给 pgpool 二进制文件:

    # chcon -t postgresql_exec_t /usr/bin/pgpool
    
  2. 如果我们尝试启动 pgpool 系统服务,可能会遇到一个或多个失败:

    # systemctl start pgpool
    # systemctl status pgpool
    ...
    WARNING: Failed to open status file at: "/var/log/pgpool/pgpool_status"
    FATAL: could not read pid file
    
  3. 提到的一个故障是守护进程无法访问其日志(位于/var/log/pgpool),另一个则抱怨进程 ID 文件(位于/var/run/pgpool)无法访问。由于这些文件之前是由一个不受限的域创建的,确实有可能它们的上下文也不正确。让我们将 PostgreSQL 特定的类型应用到这些位置:

    # chcon -R -t postgresql_log_t /var/log/pgpool
    # chcon -R -t postgresql_var_run_t /var/run/pgpool
    
  4. 在重新启动pgpool后,我们发现它出现了一个新的故障:

    pgpool wants to listen on port 9999, but SELinux is refusing this.
    
  5. 让我们创建一个小的策略增强,允许postgresql_t绑定到这个端口:

    (typeattributeset cil_gen_require jboss_management_port_t)
    (typeattributeset cil_gen_require postgresql_t)
    (allow postgresql_t jboss_management_port_t (tcp_socket (name_bind)))
    
  6. 加载这个策略并重启pgpool。这样一来,pgpool就可以正常启动了。

    当然,守护进程能够启动而不出错并不意味着它能正常工作,因此建议继续进行测试,按照预期使用该服务。

找出哪个策略可以重用给一个进程需要一点实践和搜索。例如,你可以查询哪个域能够绑定到守护进程需要的端口,或者你可以搜索一个行为非常类似于应用程序的域。在我们的例子中,我们只需要允许该域绑定到9999端口。我们也可以使用这一信息点来寻找一个不同的策略——一个允许绑定到该端口的策略(例如httpd_t域),然后看看它是否更合适。

虽然这种方法是试错法,但它可能会使服务在比不受限域更受限的域中运行。然而,更好的方法是生成一个新的自定义策略,并从那里开始工作。

扩展生成的策略

当我们为一个新的应用程序分配一个不同的策略时,我们实际上是在重用并可能扩展现有的策略。我们可以进一步生成新的策略,然后进一步扩展这些策略,实际上进入了自己开发新策略的领域。

第十五章,《使用参考策略》,以及第十六章,《使用 SELinux CIL 开发策略》中,我们将进一步扩展策略开发的相关内容,以实现更细粒度的控制。通过使用策略生成工具,我们可以快速创建一个初稿策略,并根据需要进行调整。

一个重要的警告是,策略生成工具通常只限于单一策略格式,要么是参考策略风格,要么是 CIL 风格。管理员和组织应该尝试专注于一种风格并坚持使用,这样新开发人员和管理员的学习曲线就不会太陡峭。

理解生成的策略的局限性

策略生成器,例如我们在 第十一章 中看到的 udica 工具,增强容器化工作负载的安全性,通常有一个非常特定的目的。例如,udica 工具专注于生成新的容器 SELinux 域,仅对这些容器有用。生成器通常有一个明确的目标,来定义它们的策略应该是什么样的。

生成的策略通常是应用层的策略。通过生成器创建精细粒度的策略是困难的,而定义跨类别的策略需要多个步骤和多次操作,而生成器通常采用单步生成。

此外,大多数生成的策略仅一般支持基于角色的 SELinux 访问控制:用户要么被允许访问目标 SELinux 域并与其交互,要么不允许访问。生成的策略中通常不包含角色区分(例如应用管理员与应用用户)。

管理员应当意识到,生成器也必须假设应用程序的工作方式。虽然这使得生成器适用于大多数简单的服务和应用程序,但它们显然还不能替代一支有经验的 SELinux 策略开发团队。

引入 sepolicy generate

sepolicy 命令能够生成初始的 SELinux 策略模块,管理员和开发者可以进一步调整。这些生成器会利用系统上的一些资源(例如发行版的包数据库),更好地了解应包含哪些资源,并生成一系列 SELinux 策略文件。

由于应用程序种类繁多,sepolicy generate 命令还需要用户提供应用程序类型。目前支持以下类型:

  • 用户应用程序通过 --application 选项进行识别。这类应用程序是供最终用户启动并与之交互的。

  • 系统服务应用程序通过 --init 选项进行识别。以守护进程模式运行或拥有自己用户的应用程序通常是系统服务应用程序。

  • D-Bus 系统服务应用程序通过 --dbus 选项进行识别。这类服务由 D-Bus 调用。

  • --cgi 选项。使用 CGI 特定域可以让 CGI 应用程序在自己的域中运行,而不是扩展 Web 服务器域本身的权限。

  • --inetd 选项。

  • 沙箱应用程序类似于用户应用程序,但更为封闭,并通过 --sandbox 选项提供支持。

除了应用层策略生成,sepolicy generate 还支持生成用户域和角色:

  • 支持图形桌面的标准用户可以通过 --desktop_user 选项生成。这是一种常见的非管理型用户角色。

  • 使用--x_user选项可以生成一个更轻量、最小的用户角色,仍然支持图形桌面。此域专注于最小权限,因此需要进一步扩展,才能更好地发挥作用。

  • 如果不需要支持图形用户界面,则可以使用--term_user选项。这将生成一个没有桌面支持的受限用户域。

  • 可以使用--admin_user选项生成面向管理的用户域。此选项适用于具有广泛管理权限的用户。

  • 可以使用--confined_admin选项生成更多受限的管理域。这使您能够生成仅对少数应用程序域具有管理角色的用户域,而不是对整个系统进行管理。

生成器还支持进一步自定义现有域(使用--customize)或生成特定类型(使用--newtype)。

我们使用sepolicy generate为 pgpool-II 应用程序生成策略。

使用 sepolicy generate 生成策略

sepolicy generate命令将创建一个骨架 SELinux 策略,采用参考策略代码样式。然后可以逐步扩展此策略,以满足应用程序所需的权限。

让我们为pgpool创建并调整策略:

  1. 首先,我们告诉sepolicy生成一个名为pgpool的新策略,适用于/usr/bin/pgpool二进制文件:

    # sepolicy generate -n pgpool --init /usr/bin/pgpool
    
  2. 接下来,构建生成的 SELinux 策略:

    # make -f /usr/share/selinux/devel/Makefile pgpool.pp
    
  3. 将策略加载到内存中:

    # semodule -i pgpool.pp
    
  4. 重新标记文件系统,或者至少是pgpool.fc文件中提到的位置:

    # restorecon -RvF /usr/bin/pgpool /var/log/pgpool \
      /var/run/pgpool
    
  5. 启动pgpool服务:

    pgpool is running flawlessly.
    

现在,您可能会觉得这太简单了。是的,的确如此。sepolicy generate提供的默认 SELinux 策略是宽容的,正如您可以在pgpool.te文件中看到的那样:

permissive pgpool_t;

如果我们删除此语句,重新构建并重新加载策略,那么我们会注意到失败再次出现,例如进程无法绑定到所选端口。我们现在可以使用audit2allow,例如,帮助我们根据需要扩展策略:

# cat /var/log/audit/audit.log | audit2allow -R

逐步扩展、重建并重新加载策略,直到应用程序无故障运行。

总结

Linux 管理员可以使用 SELinux 控制来防止或限制对应用程序的访问,但这并不总是当前的需求。能够以正确的权限集运行应用程序才是关键,而正确的权限集取决于用户的意图和环境。

在本章中,我们学习了如何对应用程序域应用适当的限制,从高度隔离的容器环境,到常规应用程序域、类别范围的权限集,再到无限制域甚至宽容域。我们了解到,这是通过首先找到适当的域,理解该域使用的标签,然后为文件分配正确的标签,从而确保应用程序在正确的域中执行。

我们还学习了如何自己生成新的策略(使用sepolicy generate),而不需要立即深入全面的 SELinux 策略开发方法,这将在最后两章中讨论。

问题

  1. 非受限域和宽容域有什么区别?

  2. 我们如何在一个非常受限的域中运行应用程序?

  3. 我们如何轻松切换服务运行的域?

  4. 为什么由sepolicy生成的策略似乎可以顺利运行?

第十五章:使用参考策略

到目前为止,我们已经介绍了如何与 SELinux 子系统进行交互,并逐步调整 SELinux 策略以满足我们的需求。当我们添加更多的应用程序和用户时,我们会发现开发自定义的 SELinux 策略可能有助于更好地调整系统。开发 SELinux 策略有两种主要方法,其中使用参考策略样式开发是其中之一。另一个方法在第十六章中讨论,SELinux 通用中间语言

为了正确地开发 SELinux 策略,我们将学习如何使用和理解参考策略提供的宏,并应用项目所要求的主要编码和开发风格模式,以确保 SELinux 策略模块之间的一致性。然后,我们将其应用于两种主要类型的模块:应用程序策略和用户策略。

本章我们将涵盖以下主要内容:

  • 介绍参考策略

  • 使用和理解策略宏

  • 创建应用程序级别的策略

  • 获取支持工具的帮助

技术要求

本章的代码文件可以在我们的 Git 仓库中找到:github.com/PacktPublishing/SELinux-System-Administration-Third-Edition

查看以下视频,了解代码的实际应用:bit.ly/3jcBDvI

介绍参考策略

通过github.com/SELinuxProject/refpolicy提供的参考策略是大多数(如果不是全部)Linux 发行版的源 SELinux 策略。虽然纯粹的参考策略可能无法直接在任何 Linux 发行版上使用(因为许多 Linux 发行版会对策略进行自定义或调整,以适应安装的应用程序和支持工具),但参考策略使用的开发方法、结构和方式适用于所有主要的发行版策略。

我们建议查看你所在发行版的 SELinux 策略,以查看并轻松修改系统的 SELinux 策略。在本章中,我们将使用参考策略的一个版本:

$ git clone https://github.com/SELinuxProject/refpolicy.git

Linux 发行版的 SELinux 策略库应由发行版本身进行文档化。以下列出了几个示例库:

如果 Linux 发行版没有公开可访问的 SELinux 策略库,我们通常仍然可以通过软件包本身获得它,如 第十二章 中所使用的,与 SELinux 策略一起工作

虽然我们并不打算进行完整的策略重建,但我们可以轻松地将必要的策略文件复制到自己的开发环境中,并根据需要调整或扩展策略。

导航策略

在其基础目录中,参考策略托管所有用于构建策略的公共文件,说明如何安装这些策略等。策略本身位于 policy 文件夹中,该文件夹包含三个目录:

  • flask 包含用于启动 SELinux 的初始定义,如列出支持的类、创建初始安全标识符等。我们将不再进一步讨论这个位置。

  • modules 包含 SELinux 策略代码,是所有策略规则的主要位置。

  • support 包含在策略中重用的宏和定义,这些宏和定义不与单个策略模块相关联。

如果我们进一步进入 modules 目录,就会得到表示模块或策略类型的目录。此表示本身只是为了在开发的数百个模块中有一些结构:

  • admin 包含与系统管理相关的策略模块。

  • apps 包含一般应用程序的策略模块。

  • kernel 包含核心系统策略模块(不仅仅是与内核相关的模块)。

  • roles 包含 SELinux 角色定义和默认用户域策略模块。

  • services 包含一般服务策略模块(也是最多的策略模块集合)。

  • system 包含常见的系统相关策略模块。

策略放置在哪个文件夹中的解释留给参考策略项目本身,如果不明显,会在其邮件列表中讨论。由于策略文件需要有唯一的名称,我们可以轻松找到合适的位置。例如,要查看 ipsec 策略模块存储的位置:

$ ls policy/modules/*/ipsec.te
policy/modules/system/ipsec.te

在浏览时,你会注意到策略模块总是由三个文件组成,我们接下来会描述这些文件。

结构化策略模块

如果我们分析一个 SELinux 策略模块的代码,例如 services 文件夹中的 dhcp 模块,我们会注意到它与三个文件相关联:

  • dhcp.te 包含类型强制规则,是大多数更改的主要关注区域。

  • dhcp.fc 包含文件上下文定义,告知策略哪些文件或资源需要标记为与 dhcp 相关的 SELinux 类型。

  • dhcp.if 包含接口定义,它是可以在 dhcp SELinux 策略代码及其他地方重复使用的函数或宏。

让我们快速了解一下这些文件的结构。

理解类型强制文件

类型强制文件,例子中是 dhcp.te,具有以下结构:

policy_module(dhcp, 1.18.2)
## Declarations
# SELinux booleans
# SELinux types
## Local policy
# Internal SELinux rules
# Core interfaced SELinux rules
# SELinux boolean controlled SELinux rules
# Non-blocking interfaced SELinux rules

让我们通过一个示例来逐一看看这些内容。

声明 SELinux 对象

策略中的声明部分告诉我们在该模块内定义了哪些 SELinux 类型,或者其他 SELinux 对象,如 SELinux 布尔值和 SELinux 角色。

以下声明在 SELinux 策略中是常见的:

  • dhcp SELinux 策略中的第一个声明声明了该模块的 SELinux 布尔值。最好将布尔值以 SELinux 策略模块名称开头,尽管在此情况下,选择明确使用 dhcpd 而不是 dhcp,以便管理员可以明显知道这是关于 DHCP 守护进程,而不是可能的客户端或其他用例:

    ## <desc>
    ##   <p>Determine whether DHCP daemon can use LDAP
    ##   backends</p>
    ## </desc>
    gen_tunable(dhcpd_use_ldap, false)
    

    SELinux 布尔值伴随着一个特定结构的注释。在参考策略中,使用双哈希前缀(##)的注释将被构建代码解析,并用于更新 SELinux 策略外的信息。在这种情况下,会创建 SELinux 布尔值的描述,稍后通过如 semanage boolean 等命令显示出来。

    一旦加载了定义 SELinux 策略的模块,其他模块也可以使用这个布尔值。

  • 一些域可能还会声明角色属性,这可以轻松管理哪些角色可以使用该域:

    attribute_role dhcpd_roles;
    
  • dhcp SELinux 策略中的其余声明声明了该策略拥有的 SELinux 类型:

    type dhcpd_t;      # The SELinux domain for the daemon
    type dhcpd_exec_t; # The executable label for the daemon
    init_daemon_domain(dhcpd_t, dhcpd_exec_t)
                       # Linking the executable to the domain
    

    参考策略中的 SELinux 策略模块仅声明它们拥有的类型和其他对象,而不包括它们使用的对象。由其他模块定义但使用的对象应该始终被隐藏,并通过接口调用进行交互。

虽然其他定义也可以添加到此部分,但这些是最常见的。接下来是本地策略规则。

添加域的本地规则

类型强制内的本地策略定义了由 SELinux 策略模块拥有的域的允许行为。对于 dhcp SELinux 策略模块,这只关注 dhcpd_t SELinux 域。其他 SELinux 策略模块,尤其是如果它们提供更细粒度的策略结构,会对多个 SELinux 域或甚至 SELinux 用户角色进行定义。

让我们来看一看我们正在查看的 dhcp.te 示例中的 SELinux 策略规则:

  • 策略从 SELinux 内部规则开始,这些规则是 SELinux 策略模块本身拥有的 SELinux 类型之间的交互:

    allow dhcpcd_t self:process { getcap signal_perms };
    manage_files_pattern(dhcpd_t, dhcpd_tmp_t, dhcpd_tmp_t)
    

    最简单的规则是标准的allow规则,类似于 audit2allow 推荐的规则。这些 allow 规则可以引用支持宏(例如 signal_perms),我们将在 使用和理解策略宏 部分进行讨论。第二行,调用 manage_files_pattern,也是一个支持宏。

  • 第二组本地策略定义是核心接口化 SELinux 规则:

    kernel_read_system_state(dhcpd_t)
    

    这些调用使用由另一个 SELinux 策略模块在其接口文件中定义的代码。在kernel_read_system_state接口的情况下,这将授予dhcpd_t SELinux 域读取proc_t标记资源的权限。由于proc_t未由dhcp SELinux 策略模块定义,因此必须使用接口调用。

    核心接口化 SELinux 规则是始终应当对系统可用的规则。与可以禁用或卸载的应用程序相关 SELinux 策略模块不同,这些核心规则与类型定义相关,无法从系统中删除或随意禁用。

  • 第三组本地策略定义是由 SELinux 布尔值控制的调用:

    tunable_policy(`dhcpd_use_ldap', `
      # If boolean is true
      sysnet_use_ldap(dhcpd_t)
    ', `
      # If boolean is false
    ')
    

    在这里,SELinux 接口调用(也可以是标准规则,如allow规则)被一个tunable_policy()调用包围,该调用标识将影响 SELinux 策略规则的 SELinux 布尔值(在我们的例子中是dhcpd_use_ldap)。大多数策略模块只会有一个块(用于激活的规则,如果 SELinux 布尔值为 true),但也可以有两个块,其中第二个块定义了如果 SELinux 布尔值为 false 时的规则。

  • 最终的本地策略定义集是非阻塞的接口化 SELinux 规则:

    optional_policy(`
      bind_read_dnssec_keys(dhcpd_t)
    ')
    

    这些是使用其他 SELinux 策略模块提供的定义的调用,但这些 SELinux 策略模块可能并未在系统中加载。

    在我们的示例中,bind_read_dnssec_keys()调用允许dhcpd_t SELinux 域读取dnssec_t标记的资源,这是由 bind SELinux 策略模块定义的。然而,BIND 可能未安装在系统上,因此 Linux 发行版可能没有加载其策略。因此,这个调用是可选的,只有在 bind SELinux 策略模块加载时才会激活。

类型强制文件是最常变化的文件。接下来我们讨论的文件上下文定义文件紧随其后。

声明文件上下文

文件上下文定义文件,带有.fc后缀,告诉 SELinux 子系统哪些 SELinux 类型必须与系统上的文件资源关联。此信息由restorecon等工具使用,以适当重置上下文。

文件中的规则通常按规则适用的目录进行分组。每条规则的结构如下:

<path expression>    [<type/class>]    <context>

让我们看看这些条目意味着什么:

  • 路径表达式与我们在第四章中看到的一样,使用文件上下文和进程域。请记住,像点号(.)这样的字符有特定含义(在此情况下,它表示任何可能的字符),因此真正需要在路径中使用点号时,必须对点号进行转义。

  • 类型/类别 是一个可选的设置。如果省略,则意味着使用任何可能的类别。最常见的值有常规文件(--)、目录(-d)、套接字(-s)和符号链接(-l)。

  • 上下文 是对该资源目标 SELinux 类型的引用。在参考策略中,这些上下文引用总是需要被 gen_context() 宏包裹,这样会根据策略中内置的 MLS 或 MCS 支持来添加或移除敏感度。

让我们来看一个来自 dhcp SELinux 策略模块的简单示例:

/var/named/data(/.*)?  gen_context(system_u:object_r:named_cache_t,s0)

在这个例子中,/var/named/data 目录及其下的所有资源,将被标记为 named_cache_t SELinux 类型。

与 SELinux 策略模块相关的最后一个文件是接口定义文件。

通过接口暴露 SELinux 规则

SELinux 策略模块中的接口旨在支持在不同模块之间更灵活、模块化的 SELinux 策略开发。每当一个域或 SELinux 角色需要与在另一个 SELinux 策略模块中定义的资源进行交互时,该模块应该为该交互创建一个正确命名的接口。

接口应该附带最少量的文档,尽管这些文档仅在构建整个策略文档时使用。当构建完成时,生成的文档会在系统上提供,例如在 /usr/share/doc/selinux-policy/html

让我们来看一下 dhcpd_domtrans() 接口的定义:

########################################
## <summary>
##	Execute a domain transition to run dhcpd.
## </summary>
## <param name="domain">
##	<summary>
##	Domain allowed to transition.
##	</summary>
## </param>
#
interface(`dhcpd_domtrans',`
  gen_require(`
    type dhcpd_t, dhcpd_exec_t;
  ')
  corecmd_search_bin($1)
  domtrans_pattern($1, dhcpd_exec_t, dhcpd_t)
')

按照最佳实践,接口名称以 SELinux 策略名称开头,后面跟着允许的操作。有时,这个操作会加上目标资源的后缀。接口本身可以通过 $1(第一个参数)、$2(第二个参数)等引用传递给接口的参数。所以像 dhcpd_domtrans(init_t) 这样的调用会调用接口,其中 $1 会被替换为 init_t

让我们看看几个常见操作的示例:

  • dhcpd_domtrans()。如果有多个域,则目标资源将定义允许哪个域(如 bind_domtrans_ndc())进行到 ndc_t SELinux 域的域转换。

  • dhcpd_setattr_state_files() 允许域设置 dhcpd_state_t 标签资源的属性,而 bind_signal() 允许域向 named_t 标签进程发送信号。大多数接口定义将涉及权限交互。

  • dhcpd_admin() 角色将允许管理与 dhcpd 相关的资源,启动和停止 dhcpd 服务等。

    optional_policy(`
      dhcpd_admin(webadm_r, webadm_t)
    ')
    

在开发 SELinux 策略时,建议查看 SELinux 策略模块的接口定义,了解哪些接口存在以及它们提供了什么。策略开发人员通常会将常见的权限放入这些接口中,因此可用的接口可以很好地显示你可能需要的 SELinux 策略模块内容。

接口定义也可以在系统的 /usr/share/selinux/devel/include 位置找到,这样即使不检查主源代码库,你也可以创建和修改 SELinux 策略模块。每当我们构建参考策略风格的模块时,都会使用如下命令:

$ make -f /usr/share/selinux/devel/Makefile <name>.pp

这将导致构建过程查找 /usr/share/selinux/devel/include 位置中的接口,同时也会在当前工作目录中查找。

使用和理解策略宏

在各种 SELinux 策略定义中,我们遇到了一些不与特定 SELinux 策略模块绑定的宏。这些是支持宏,位于 policy/support/*.spt 文件中。

最常见的宏是在 obj_perm_sets.spt 文件中声明的(该文件将同一类别的常见权限组合为一个定义),以及 *_patterns.spt 文件(将跨不同类别的权限组合为一个定义)。

利用单一类别权限组

execute 权限。你还需要 openread 权限(否则域无法读取可执行文件),以及 map 权限(以允许将文件映射到内存中)。

如果你将所有这些权限放入你自己的 SELinux 策略模块中,那么规则可能会是这样:

allow dhcpd_t dhcpd_exec_t:file { getattr open map read execute ioctl execute_no_trans };

如果以后 SELinux 策略扩展了一个与执行资源相关的新权限,那么你将需要在不同的 SELinux 策略模块中查找并更新这些权限。

因此,参考策略将所有这些权限放入名为 exec_file_perms 的宏中,其定义如下:

define(`exec_file_perms',`{ getattr open map read execute ioctl execute_no_trans }')

定义了这个宏后,我们的策略行可以简化如下:

allow dhcpd_t dhcpd_exec_t:file { exec_file_perms };

如果在某个时刻需要扩展权限,只需扩展宏定义本身,SELinux 策略模块可以保持不变。

调用权限组

虽然单一类别的权限组适用于简化策略开发,但涵盖多个类别的权限组更加常见。

例如,如果某个域需要对 /var/lib/dhcpd 中的资源具有完整的管理权限(意味着 readwrite,以及创建和删除资源),那么不仅这些权限在该目录中的文件上是必要的(这些文件被标记为 dhcpd_state_t SELinux 类型),你还需要对该目录本身的读/写权限。

这样的权限定义可能会是这样:

allow $1 dhcpd_state_t:dir { rw_dir_perms };
allow $1 dhcpd_state_t:file { manage_file_perms };

与其将这些声明为单独的调用,不如将它们组合到一个调用中:

manage_files_pattern($1, dhcpd_state_t, dhcpd_state_t)

SELinux 策略开发者最好熟悉各种可用的宏,以便快速高效地进行 SELinux 策略开发。

创建应用级别策略

应用级别的策略为应用程序或服务提供隔离。有多种不同类型的应用级别策略:

  • 终端用户应用程序策略,主要关注访问终端用户数据,并且通常会调用各种userdom_*接口(这些接口通过system/userdomain.if文件提供)。这些应用程序大多位于apps/目录下。

  • 管理应用程序,尽管仍面向用户,但更可能启用与系统服务和资源的交互。

  • 服务,通常是守护进程化的应用程序,通常主要与自己的资源交互,并且结构较为简单。

当我们在第十四章《处理新应用程序》中介绍sepolicy generate命令时,我们可以选择这些类型(以及更多)来为这些应用程序生成一个简单的骨架。

让我们看看一些示例策略,并识别在开发自己策略时可能需要的有用调用。

构建面向网络的服务策略

面向网络的服务(即可以从系统外部进行交互的服务)是需要被限制的第一批服务。因此,为面向网络的服务构建 SELinux 策略模块应该是任何需要确保尚未有有效策略的应用程序被限制的 Linux 管理员的主要关注点。

如果我们查看 OpenVPN 服务,发现services/openvpn.te下有一个 SELinux 策略,我们可以查看。

确定服务与哪些资源进行交互

由于策略始于识别类型和其他 SELinux 对象,我们需要考虑系统中服务交互的资源。在比较服务策略时,你会发现这些定义通常非常相似:

  • 首先声明主域类型及其入口可执行文件。根据服务类型,它包含了如何启动该服务的调用:作为系统服务(使用init_daemon_domain())或通过 D-Bus 系统总线(使用dbus_system_domain())。

  • 服务的配置文件(例如openvpn_etc_t),这些文件也可以区分只读文件和可读写文件(例如openvpn_etc_rw_t)。

  • 运行时文件(通常存储在/var/run中),例如openvpn_runtime_t

  • 临时文件(通常存储在/tmp/var/tmp中),例如openvpn_tmp_t

  • 日志文件(通常存储在/var/log中),例如openvpn_var_log_t

每个类型声明后面跟着一个调用,它会正确地标记该类型。例如,logging_log_file()调用会将类型与日志文件 SELinux 属性关联。这使得一般的日志文件管理域能够通过此属性处理新创建的资源。

处理内部 SELinux 规则

在声明了资源后,我们必须在 SELinux 策略中定义内部的 SELinux 规则。这些规则告诉 SELinux 域可以如何处理其自己的资源,以及当资源被交互时,SELinux 应如何行为。

我们通常会声明两组内部规则。一组是域本身的细粒度权限,例如是否允许该域拥有任何能力、创建套接字等。这些规则的开发是基于试错的:从几乎没有权限开始,查看哪些 AVC 拒绝出现,扩展策略,然后重复。

另一组内部规则侧重于与之前声明的类型的交互。这不仅包括域拥有的权限(例如通过manage_files_pattern()调用),还包括是否必须发生过渡。

设置正确的过渡集是开发应用策略时需要采取的更重要的第一步之一,因为audit2allow和 AVC 拒绝通常没有考虑到目标资源被分配了错误类型这一事实。因此,当我们有一个在/tmp中创建文件的服务时(该目录被标记为tmp_t),我们真的希望目标文件被正确标记(如openvpn_tmp_t),而不是继承目录的tmp_t标签:

allow openvpn_t openvpn_tmp_t:file manage_file_perms;
files_tmp_filetrans(openvpn_t, openvpn_tmp_t, file)

文件过渡应为所有涉及的资源声明。如果文件和目录都需要发生过渡,可以在一次调用中混合类,如下所示:

files_tmp_filetrans(openvpn_t, openvpn_tmp_t, { file dir })

我们还可以告诉 SELinux,仅在使用特定文件名时才应该发生过渡:

logging_log_filetrans(openvpn_t, openvpn_status_t, file, "openvpn-status.log")

强烈建议在扩展域的实际权限之前,首先考虑文件过渡(和其他资源过渡),以确保我们不会因为不必要的原因而允许域的权限应用到一般类型。

添加网络相关的权限

在开发和扩展策略时,将添加几个核心功能,如kernel_*调用,允许进程与proc_t资源、系统控制设置等进行交互。像audit2allow这样的工具可以合理地推断出正确的接口进行调用,尽管检查接口以确保不会分配过多权限也是有益的。

另一方面,网络相关的权限可能需要更多关注。正如我们在第五章中看到的,控制网络通信,SELinux 可以根据系统配置动态处理某些网络流量。

如果系统没有特别的控制措施,例如标签化的网络或 SECMARK,则很可能会发现三个接口调用可以让应用程序按预期工作:

corenet_tcp_bind_generic_node(openvpn_t)
corenet_tcp_bind_openvpn_port(openvpn_t)
corenet_tcp_connect_http_port(openvpn_t)

这三种接口调用允许域面向网络(corenet_tcp_bind_generic_node)、监听 OpenVPN 端口(corenet_tcp_bind_openvpn_port),以及作为客户端连接到 HTTP 端口(corenet_tcp_connect_http_port)。

但也存在其他你可能需要添加的调用,尽管它们目前未被检测到。当系统进一步调优时,它们可能变得必要,比如添加对标记网络或引入 SECMARK 过滤的支持。

第一组是允许在通用节点(主机)和接口上发送和接收数据包:

corenet_tcp_sendrecv_generic_node(openvpn_t)
corenet_tcp_sendrecv_generic_if(openvpn_t)

对于 NetLabel 支持,你可能需要添加支持接收标记网络数据包:

corenet_all_recvfrom_netlabel(openvpn_t)

对于 SECMARK 支持,你需要添加对发送和接收 SECMARK 标记数据包的支持:

corenet_sendrecv_openvpn_server_packets(openvpn_t)
corenet_sendrecv_http_client_packets(openvpn_t)

这些调用在早期测试中可能不会出现,但以后可能会需要,因此建议从一开始就考虑标记网络和 SECMARK 对你策略的影响。

构建服务接口方法

我们接下来将重点关注接口方法。这些方法用于促进其他 SELinux 策略模块与我们正在开发的域进行交互,尽管它们也可以用于简化你自己策略的开发。

以下是三个最常见的接口定义,其他策略开发者会假设它们已存在:

  • 一个openvpn_domtrans,允许给定的 SELinux 域执行适当的二进制文件或脚本,并使得执行的命令或应用程序在我们的域中运行(因此从源域过渡到我们的域)。

  • 一个openvpn_run,它类似于域过渡接口(实际上会调用它),但也允许我们的域使用角色。没有这个接口,某些角色可能无法进行过渡,即使它们调用了域过渡接口。

  • 一个openvpn_admin,将分配给用户角色/域,允许他们管理我们的服务。这将允许用户与我们域的进程进行交互(包括杀死进程、追踪其操作等),以及管理使用的文件和资源。

在接口中,我们需要声明将显式引用的 SELinux 对象。这允许 SELinux 子系统验证代码是否适用:如果这些对象在当前策略中不存在,那么该接口无效,将不会使用。声明对象是通过gen_require()宏完成的:

interface(`openvpn_run',`
  gen_require(`
    attribute_role openvpn_roles;
  ')
  openvpn_domtrans($1)
  roleattribute $2 openvpn_roles;
')

根据需要可以添加其他接口。虽然你可以提前添加接口以防万一,但请注意,一旦定义了接口,它可能会被其他策略使用,如果你不是自己开发所有策略,可能无法意识到这一点。如果你之后想要更改接口的行为或删除它们,可能会破坏其他策略。

处理用户应用程序

如果我们开发最终用户应用程序,它们的结构将与面向服务的应用程序非常相似。然而,在内容方面,有一些注意事项需要考虑。我们以apps/thunderbird.te策略为例:

  • 我们首先注意到,许多资源定义接口的前缀是userdom_。例如,临时文件不是files_tmp_file(),而是userdom_user_tmp_file()。这样可以确保这些资源被识别为用户管理的临时文件,而不是普通的系统临时文件。

  • 另一个重要的补充是对~/.cache的支持,应用程序缓存数据存储在此,而配置数据则存储在~/.config中。

    xdg_manage_downloads(thunderbird_t)
    
  • 为了方便地建立用户内容访问,用户应用程序还应该调用userdom_user_content_access_template()模板。这个模板会自动创建布尔值,管理员可以切换这些布尔值。例如,对于 Thunderbird SELinux 策略,它会创建thunderbird_manage_generic_user_content。如果设置了此项,则 Thunderbird 不仅可以访问与下载相关的资源,还可以访问所有用户资源。

  • 如果用户应用程序是图形化的,还需要另一个模板,即xserver_user_x_domain_template()。这个模板会为应用程序生成与 X 服务器相关的 SELinux 对象,并允许应用程序在服务器上使用图形环境。

    重要提示

    参考策略区分了常规接口和模板。接口授予传递给它的域和角色特权。而模板则会生成新的对象,如 SELinux 布尔值、类型、属性等。从代码角度看,模板不能成为布尔触发语句的一部分(因为它们不仅仅添加类型强制规则)。

当起草一个用户应用程序策略的基线时,包括上述模板,然后通过反复试验来扩展策略应该是足够的。不过,请确保您正在测试应用程序的用户位置上的所有资源都已正确标记,否则拒绝访问可能会误导您赋予该域更多的权限。

添加用户级别策略

如果我们要创建自定义的用户和角色策略,那么最令人困惑的选择是选择哪个用户模板。这个模板会创建一个具有特定目的的角色和用户域,并默认授予一些权限:

图 15.1 – 用户域模板之间的关系

图 15.1 – 用户域模板之间的关系

用于用户/角色策略的最常见模板如下:

  • userdom_restricted_user_template()用于(默认)没有特权的最终用户角色。

  • userdom_admin_user_template()用于(默认)具有高度特权的最终用户角色。

其他模板也可以使用,特别是当需要对角色和用户域进行更细粒度的控制时。然而,请注意,模板所分配的权限被称为默认权限。如果我们要为管理特定服务创建角色和用户域,则不应使用userdom_admin_user_template(),因为它将授予比所需更多的权限。

作为示例,考虑roles/dbadm.te数据库管理角色的 SELinux 策略。该角色基于userdom_base_user_template()接口,以确保仅授予最小权限。该角色并不打算直接使用(登录),而是要通过(例如,通过newrole命令或通过策略中的明确定义的角色过渡)进行过渡。

获取支持工具的帮助

市面上有一些工具可以帮助开发 SELinux 策略,如果需要,我们也可以构建自己的支持工具。让我们来看看可以使用哪些支持环境。

使用 selint 验证代码

尽管 SELinux 策略在功能上可以正常工作,但验证代码本身是否正确,并遵循最佳实践,确保代码在长期内可维护,是非常重要的。

支持验证 SELinux 策略代码的工具之一是selint,它可以从github.com/TresysTechnology/selint获取。一旦构建并安装,selint可以提供四个主要领域的洞察:

  • 约定检查验证 SELinux 策略是否遵循参考策略的约定,确定代码应该如何结构化和文档化。

  • 样式检查会给出可能错误的代码风格提示,以及开发者可能想要不同行为的地方。

  • 当代码中有可能触发运行时问题或安全问题的错误调用时,会触发警告。

  • 错误捕获构造缺陷,这些缺陷可能会导致编译问题或运行时问题。

这使得可以在自动化构建过程中使用selint,并有助于政策的开发。

调用selint很简单:

$ selint minecraft.te
minecraft.te:  31: (C): Permissions in av rule not ordered
                        (signull before execmem) (C-005)
minecraft.te: 118: (C): Require block used in te file (use an 
                        interface call instead) (S-001)

在这种情况下,检测到两个约定上的不当行为。一个是权限的顺序问题,另一个是在某个域中显式提到的require块,该域不属于该策略模块。

本地查询接口和宏

为了帮助快速找到正确的接口或宏,我们还希望能够快速显示接口和宏信息。通过一些 Shell 脚本,我们可以创建一些帮助我们的函数。

这些函数作为代码与本书一起提供。你可能需要更改脚本开头中POLICY_LOCATION变量指向的路径。默认情况下,它指向系统安装的接口和宏,但你也可以将其指向存储库中的检出路径:

POLICY_LOCATION="/usr/share/selinux/devel"

引入文件以访问辅助函数:

$ source ./localfuncs

你可以使用的辅助函数如下:

  • 使用 sefindif 可以搜索具有特定 SELinux 规则的 SELinux 接口。您可以使用正则表达式找到合适的接口。

    $ sefindif "manage.* cert_t"
    interface(`miscfiles_manage_all_certs',`
      manage_files_pattern($1, cert_type, cert_type)
      manage_lnk_files_pattern($1, cert_type, cert_type)
    interface(`miscfiles_manage_generic_cert_dirs',`
      manage_dirs_pattern($1, cert_t, cert_t)
    interface(`miscfiles_manage_generic_cert_files',`
      manage_files_pattern($1, cert_t, cert_t)
      manage_lnk_files_pattern($1, cert_t, cert_t)
    
  • 使用 seshowif 可以显示接口的整体内容(不包括注释)。

    $ seshowif miscfiles_manage_all_certs
    interface(`miscfiles_manage_all_certs',`
      gen_require(`
        attribute cert_type;
      ')
      allow $1 cert_type:dir list_dir_perms;
      manage_files_pattern($1, cert_type, cert_type)
      manage_lnk_files_pattern($1, cert_type, cert_type)
    ')
    
  • 使用 sefinddefseshowdef,也可以为支持的宏做同样的事情。

    $ seshowdef admin_pattern
    define(`admin_pattern',`
      manage_dirs_pattern($1,$2,$2)
      manage_files_pattern($1,$2,$2)
      manage_lnk_files_pattern($1,$2,$2)
      manage_fifo_files_pattern($1,$2,$2)
      manage_sock_files_pattern($1,$2,$2)
      relabel_dirs_pattern($1,$2,$2)
      relabel_files_pattern($1,$2,$2)
      relabel_lnk_files_pattern($1,$2,$2)
      relabel_fifo_files_pattern($1,$2,$2)
      relabel_sock_files_pattern($1,$2,$2)
    ')
    

尽管这些功能不像全功能策略编辑套件那样多才多艺,但它们可以帮助快速找到正确的接口或宏。

摘要

参考策略是 SELinux 策略开发中最常见的来源,经过多年的开发和维护,它已经发展成一个完整的策略集,拥有活跃的开发社区和各种工具的积极支持(包括 audit2allow,以及 selint 应用程序)。

我们已经学习了通常如何组织策略,并且如何开始为最常见的用例构建 SELinux 策略模块:应用服务、终端用户应用程序和用户角色。为了帮助我们开发这些策略,我们已经看到 selint 可以进行代码风格分析,而一些 shell 脚本可以帮助我们解析接口文件以获取快速帮助。

在我们的最后一章中,我们将研究 CIL 风格的 SELinux 开发。

问题

  1. 为什么 Linux 发行版没有原生支持参考策略?

  2. 一个 SELinux 模块所需的三个主要策略文件是什么,它们各自的作用是什么?

  3. 为什么像 exec_file_perms 这样的权限集合比明确列出权限更受青睐?

  4. 接口和模板有什么区别?

  5. 为什么 dbadm.te 中定义的数据库管理角色没有使用 userdom_admin_user_template

第十六章:使用 SELinux CIL 开发策略

虽然参考策略是 SELinux 策略中最常用的语言和开发风格,但 通用中间语言 (CIL) 是一个功能强大但更低级的语言结构,可以用于开发 SELinux 策略。尽管它比较低级,但它仍然非常易读,并且得到了良好的支持,因为 SELinux 工具在使用其他语言时会在后台使用 CIL。

由于 CIL 是主要使用的语言,我们知道它可以用于构建整个策略。遗憾的是,与参考策略不同,开发人员没有可以使用的支持构造。然而,我们仍然可以学习如何定制当前的策略,创建参考策略无法实现的特定定义,甚至如果我们选择的话,构建一个完整的应用程序策略。

在本章中,我们将讨论以下主要内容:

  • 介绍 CIL

  • 创建细粒度定义

  • 构建完整的应用程序策略

技术要求

查看以下视频,查看代码实际运行:bit.ly/3dLYP2Q

介绍 CIL

CIL 的设计目的是成为构建策略的主要语言,并且是最低级的可读格式。在 CIL 之后,SELinux 代码会被转换为二进制格式,以便发送到 Linux 内核(和 SELinux 子系统)以加载到内存中。

管理员可能倾向于认为,在使用参考策略方法构建 SELinux 策略模块时生成的二进制文件就是最终的二进制文件。然而,正如我们在 第一章《基础 SELinux 概念》中看到的那样,semodule 命令会在构建最终格式之前将其转换并翻译成 CIL。

让我们看看这些翻译是如何工作的,以及我们可以从中学到什么。

将 .pp 文件转换为 CIL

当加载一个非 CIL 的 SELinux 策略模块时,semodule 命令会首先将该模块视为一种未知格式,并提取 .pp 文件,支持作为高级语言(HLL)。

由于使用参考策略(或其他经典 SELinux 开发)构建的 SELinux 策略模块的高级语言(HLL)格式与生成的模块相同,因此此阶段通常只是涉及创建一个副本。我们可以通过将一个 HLL 文件与原始文件进行比较来看到这一点:

$ cp /var/lib/selinux/targeted/active/modules/400/pgpool/hll pgpool.hll.bz2
$ bunzip2 pgpool.hll.bz2
$ sha512sum pgpool.pp pgpool.hll
b81ba4ac...c0db pgpool.pp
b81ba4ac...c0db pgpool.hll

一旦 semodule 转换或提取了数据,它将把数据转换为 CIL 代码。对于每种支持的高级语言(HLL),都可以在 /usr/libexec/selinux/hll 中找到相应的转换器。目前,只有 pp 命令可用,用于将这种旧的风格转换为 CIL。

让我们看看它是如何工作的:

$ /usr/libexec/selinux/hll/pp pgpool.pp
(type pgpool_t)
(roletype object_r pgpool_t)
(type pgpool_exec_t)
...
(filecon "/var/run/pgpool(/.*)?" any (system_u object_r pgpool_var_run_t ((s0) (s0))))

所以,从本质上讲,当我们开发参考策略风格的模块时,它们无论如何都会被转换为 CIL。然而,生成的 CIL 代码内部并没有任何辅助的构造。例如,所有权限都会展开,并且与 SELinux 策略模块外部其他资源或类型的所有交互也会列出。那里不再有任何支持宏或接口。在 构建完整的应用程序策略 部分,我们将看到 CIL 确实支持抽象,因此当前的观察仅仅是由于 pp 命令执行的转换所致。

理解 CIL 语法

在我们开发 CIL 时,最明显的观察是它喜欢括号。CIL 使用 S 表达式语法,这种语法由 Lisp 推广,使得数据呈树状结构。表达式中的第一个标识符告诉 CIL 该构造是关于什么的。

让我们来看一下我们在将 pgpool.pp 二进制文件转换为 CIL 时收到的最后一条语句,现在为方便起见已格式化:

(filecon
  "/var/run/pgpool(/.*)?"
  any
  (
    system_u
    object_r
    pgpool_var_run_t
    (
      (s0)
      (s0)
    )
  )
)

如果我们仔细查看此语句,我们可以推断出以下内容:

  • 我们有一个 filecon 语句,它接受三个参数:路径表达式、适用的资源类型,以及要与之关联的 SELinux 上下文。

  • SELinux 上下文有四个相关字段:SELinux 用户、SELinux 角色、SELinux 类型和 SELinux 敏感度范围。

  • SELinux 敏感度范围有两个值:低端值和高端值。

这条语句等同于参考策略在策略模块的文件上下文部分(带 .fc 后缀的文件)中定义的内容,如下所示:

/var/run/pgpool(/.*)?  gen_context(system_u:object_r:pgpool_var_run_t,s0)

幸运的是,我们不需要为了理解和查看发生了什么而去查找和解释代码。SELinux 项目提供了广泛的 CIL 文档,解释了该语言的工作原理及其所支持的内容。相关信息可以在 github.com/SELinuxProject/selinux/tree/master/secilc/docs 查阅。不过需要注意的是,CIL 策略开发仍处于初期阶段,因此 HLL 转换机制未使用的 CIL 构造的覆盖率非常低。

现在我们来看看如何使用 CIL。

创建细粒度的定义

在本书中,大多数小型 SELinux 策略调整都是使用 CIL 完成的。这些是小型、细粒度的定义,开发工作量很小,且具有直接加载的优点。

根据角色或类型

CIL 语言要求在策略中如何链接类型或角色有一定的顺序。有时候,在开发 CIL 策略时,类型的顺序可能没有得到正确处理。

为了绕过这个问题,使用了一个默认属性 cil_gen_require。当类型或角色被分配给 cil_gen_require 属性时,它们会在策略中自动正确链接。然而,这并不是 CIL 的要求,而是 SELinux 工具使用的一种约定。

这个属性实际上存在两次,一次作为类型属性,一次作为角色属性。它们可能有相同的名称,但是是两个不同的属性:

(roleattributeset cil_gen_require system_r)
(typeattributeset cil_gen_require direct_run_init)

使用的函数 roleattributesettypeattributeset,将第二个参数(即属性名称)分配给第三个参数(即角色或类型)。角色或类型本身可以是属性,就像 direct_run_init 属性所示。

对于开发人员来说,建议始终将要使用的类型或角色作为 cil_gen_require 属性的一部分,并尽可能使用属性来简化开发活动。与其为每个可能与您的策略资源交互的域授予所有可能的 allow 规则,不如将它们授予一个属性,然后将此属性分配给域。这样可以创建一个更小的策略,并且更容易维护。

定义一个新的端口类型

当开发更复杂的 SELinux 策略时,有一些设置我们不能在 SELinux 模块中添加,至少在使用传统或参考策略样式编码时不能添加。当我们想要使用这些设置时,我们需要重建整个策略并在所谓的基本策略中进行调整;这是加载模块之前加载的主要策略。然而,这需要访问完整的 SELinux 策略源代码,并使用一个过程来使用它们(因为您将覆盖 Linux 发行版的 SELinux 策略,应确保任何系统更新不会再次覆盖您的策略)。

除了简单测试之外,确定一个语句是否在 SELinux 模块中受支持的另一种方法是查看在线文档。例如,让我们看看端口声明语句 portcon,它是网络导向语句的一部分。在 selinuxproject.org/page/NetworkStatements 上有文档说明,我们可以看到 portcon 在模块策略中不是有效的,也不能通过 SELinux 布尔值进行切换。

幸运的是,在使用 CIL 时情况并非如此。例如,让我们创建一个自定义端口类型,pgpool_port_t,并将其映射到一个空闲端口,比如 TCP 端口 50123

; Port type
(type pgpool_port_t)
; Some attributes to match the current SELinux policy requirements
(typeattributeset defined_port_type pgpool_port_t)
(typeattributeset reserved_port_type pgpool_port_t)
(typeattributeset port_type pgpool_port_t)
; Our dependency mappings
(roleattributeset cil_gen_require object_r)
(typeattributeset cil_gen_require pgpool_port_t)
; Make sure object_r is allowed for pgpool_port_t
(roletype object_r pgpool_port_t)
; The port mapping itself
(portcon tcp 50123 (system_u object_r pgpool_port_t ((s0) (s0))))

在加载策略之前,我们可以清楚地看到这个端口没有被分配:

# seinfo --port 50123 | grep tcp
 portcon tcp 32768-60999 system_u:object_r:ephemeral_port_t:s0

正如我们在整本书中看到的,我们可以立即加载这个策略文件,而不需要构建或编译它:

# semodule -i pgpool_port.cil

加载后,将类型分配给端口,我们可以使用它来优化我们的 SELinux 策略:

# seinfo --port 50123 | grep tcp
 portcon tcp 50123 system_u:object_r:pgpool_port_t:s0
 portcon tcp 32768-60999 system_u:object_r:ephemeral_port_t:s0
# semanage port -l | grep 50123
pgpool_port_t     tcp    50123

当使用传统的 SELinux 风格开发时,许多策略开发者遇到的约束条件在使用 CIL 时不再适用。由于 SELinux 工具将所有高级结构转换为 CIL,SELinux 开发人员可能会完全移除这些约束,尽管这必须经过仔细评估,以确保不会出现意外的副作用。

添加策略的约束条件:

另一个对于普通策略开发者来说不可访问的领域是向策略中添加约束。约束基于整个 SELinux 上下文(而不仅仅是类型)来限制操作,它们是我们能够找到的最接近否定现有规则的机制。

重要提示

我们不建议仅仅为了绕过不喜欢的规则而向现有策略中添加约束。约束在常规的策略查询中不可见,比如在 sesearch 中。管理员可能会非常困惑,当 sesearch 表明某个操作是允许的,但系统却拒绝执行该操作。

使用 CIL,我们可以向实时策略中添加约束,做到这一点。但请记住,约束本身并不实际允许任何操作 —— 它们仅仅是限制 SELinux 子系统认为是有效操作的范围。一个支持读取所有可能类型的约束语句并不意味着它实际允许此操作,因为仍然需要有类型强制规则来实际允许这些操作。

例如,如果我们想要移除 staff_t 域读取 /etc/passwd 文件(该文件具有 SELinux 类型 passwd_file_t)的能力,我们可以添加一个支持读取所有可能类型的约束,除非源域是 staff_t,在这种情况下,我们支持读取所有可能类型,除了 passwd_file_t

(constrain (file (read))
  (or
    (and
      (eq t1 staff_t)
      (not (eq t2 passwd_file_t))
    )
    (not (eq t1 staff_t))
  )
)

加载后,我们可以确认约束已经生效:

# seinfo --constrain | grep passwd_file_t
constrain file read (t1 == staff_t and not ( ( t2 == passwd_file_t ) ) or not ( t1 == staff_t ) ));

而实际上,尝试读取 passwd 文件是被禁止的:

$ cat /etc/passwd
cat: /etc/passwd: Permission denied

为了展示约束对管理员的困扰,我们来看一下 sesearch 对此有什么说法:

# sesearch -A -s staff_t -t passwd_file_t -c file -p read;
allow nsswitch_domain passwd_file_t:file { ... read };
allow staff_t passwd_file_t:file { ... read };

因此,尽管策略中有两个规则允许此操作(一个针对 nsswitch_domain 属性,另一个明确针对 staff_t 域),约束已限制了该操作,但从 sesearch 输出中并不明显。

构建完整的应用策略

我们也可以使用 CIL 构建完整的应用策略。然而,请记住,目前没有现成的接口或支持宏可以快速开发策略。此外,也没有可用的模板或类似工具来启动这些工作。

但这不应该阻止我们,这将让我们展示更多 CIL 语言的细节。我们还会看到,CIL 语言确实支持接口构造(它们甚至被推荐使用),但社区尚未通过类似参考策略的项目完全接受它。

使用命名空间

CIL 语言支持命名空间,这使得在开发策略时具有更高的灵活性。生成的 CIL 策略始终使用主全局命名空间,因此在生成的策略中我们不会找到命名空间的示例。

然而,我们可以轻松地展示它是如何工作的。让我们创建一个骨架文件,里面包含我们用 CIL 开发的 pgpool 策略:

; Dependencies
; Pgpool support
(block pgpool
  ; Declarations
  (type domain)
  ; Local policy
  ; Behavior
  ; File contexts
)

前面的代码中创建的命名空间是pgpool命名空间,通过block语句标识。在 CIL 中,命名空间是层次结构的。如果需要,我们可以通过在block语句内嵌套另一个block语句,在pgpool中创建一个新的命名空间。

当我们在一般结构中遇到命名空间时,需要使用点分隔符。在示例中,我们在pgpool命名空间中定义了一个名为domain的类型。如果以后想通过sesearch查询它,完整的名称将是pgpool.domain(这与在更传统开发的策略中使用的pgpool_t名称有相同的作用)。

说明性注释

在后续的策略代码中,我们将只显示添加的最相关语句,而不是包括所有先前添加的语句。如果没有支持的接口(CIL 称之为宏),策略文件将迅速变得相当庞大,这对可读性没有帮助。通过集中展示相关和新增的语句,CIL 策略的开发模式更容易解释。

由于主策略中的所有对象都位于全局命名空间中,我们需要显式地引用这个全局命名空间。这是通过在名称前加一个点来实现的。例如,如果我们想将pgpool.domain类型分配给system_r角色,我们的策略需要调整如下:

(roleattributeset cil_gen_require system_r)
(block pgpool
  (type domain)
  (roletype .system_r domain)
)

在这里,roletype语句用于将pgpool命名空间中定义的domain类型分配给system_r角色(该角色在global命名空间中定义)。

扩展策略并分配属性

在开发策略时,建议尽可能使用属性。许多属性会自动授予必要的权限,以启动策略开发,减少需要参与策略的allow语句数量。

系统中仍在使用的主策略定义了很多属性,正如前面各章节所见。所以,让我们将daemon属性分配给我们的域:

(typeattributeset cil_gen_require daemon)
(block pgpool
  (type domain)
  (typeattributeset .daemon domain)
)

通过分配daemon属性,所有现有的守护进程策略规则会自动应用到pgpool.domain SELinux 域。

要找出哪些属性是合理添加的,我们可以看看现有的守护进程域:

# seinfo -t postgresql_t -x
Types: 1
  type postgresql_t, nsswitch_domain, can_change_object_identity, corenet_unlabeled_type, domain, kernel_system_state_reader, netlabel_peer_type, daemon, syslog_client_type, pcmcia_typeattr_1;

现在,我们不需要盲目地采用所有属性,而是从我们有信心的属性开始。

添加入口点信息

下一步是将入口点信息添加到策略中。在我们开始测试之前,这是一个必要的步骤,因为我们希望该域变为活动状态。为了实现这一点,它必须能够被初始化系统(或 systemd)执行,并且过渡到我们刚刚声明的域。

让我们从定义entrypoint类型(pgpool.exec)并将其与正确的属性关联开始:

(roleattributeset cil_gen_require object_r)
(typeattributeset cil_gen_require file_type)
(typeattributeset cil_gen_require direct_init_entry)
(block pgpool
  (type exec)
  (roletype .object_r exec)
  (typeattributeset .file_type exec)
  (typeattributeset .direct_init_entry exec)
  (allow domain exec (file (entrypoint ioctl read getattr lock map execute open)))
  (typetransition .initrc_domain exec process domain)
)

在这个代码块中,我们执行了多个步骤以确保会发生过渡:

  • 我们将pgpool.exec类型与file_type属性(这是文件的通用属性)和direct_init_entry属性(这是用于启动系统服务的文件类型)关联起来。

  • 我们将pgpool.exec类型标记为pgpool.domain类型的entrypoint,并授予该域必要的权限来读取、打开和执行pgpool.exec标记的资源(这是启动进程所需的)。

  • 我们声明了一个类型转换,使得任何标记为initrc_domain的进程执行标记为pgpool.exec的资源时,会导致域向pgpool.domain转换。

我们现在可以通过添加文件上下文定义来完成这一步:

(block pgpool
  (filecon "/usr/bin/pgpool" file 
    (.system_u .object_r exec ((s0) (s0)))
  )
)

做出这些更改后,我们可以加载策略并重新标记文件:

# restorecon -v /usr/bin/pgpool
Relabeled /usr/bin/pgpool from system_u:object_r:bin_t:s0 to system_u:object_r:pgpool.exec:s0

我们现在可以尝试启动pgpool服务,并希望它失败(因为这将表明转换成功,考虑到pgpool.domain SELinux 域几乎没有足够的权限来成功启动整个服务)。

逐步扩展策略

一旦域的转换成功,我们可以通过反复试验逐步扩展策略,就像我们在使用参考策略风格开发 SELinux 策略时一样。然而,与其使用audit2allow来引导我们,我们需要自己解释拒绝信息,并看看如何更好地处理它。

考虑启动服务后出现的失败情况:

# ausearch -i -m avc -ts recent
(Output reformatted for readability)
avc: denied { map } for scontext=pgpool.domain 
                        tcontext=ld_so_t
                        tclass=file
avc: denied { read write open } for scontext=pgpool.domain 
                                    tcontext=null_device_t
                                    tclass=chr_file

现在,在立即为这些类型添加allow规则之前,让我们看看其他系统守护进程是如何完成这个任务的:

# sesearch -A -s postgresql_t -t ld_so_t -c file -p map
allow domain file_type:file map; [ domain_can_mmap_files ]:True
allow domain ld_so_t:file { execute getattr ioctl map open read };

因此,这个权限是基于domain属性的,事实上我们确实忘记将其添加到策略中。让我们纠正这个问题并重新尝试:

(typeattributeset cil_gen_require domain)
(block pgpool
  (type domain)
  (typeattributeset .domain domain)
)

在这个例子中,我们还可以清楚地看到 CIL 中的命名空间的影响。我们将(全局命名空间托管的)domain属性分配给(pgpool命名空间托管的)domain类型。它们都被命名为domain,但有不同的命名空间。这也展示了属性的重要性。

当然,并不是所有权限都可以通过属性授予。通过将目标类型作为依赖项添加,我们可以像对entrypoint声明一样,直接在策略中包含allow语句。

例如,如果我们希望显式地允许我们的域向postgresql_t域发送信号,可以执行以下命令:

(typeattributeset cil_gen_require postgresql_t)
(block pgpool
  (allow domain .postgresql_t (process (signal)))
)

随着我们向策略中添加越来越多的权限,我们可能希望优化一些定义。CIL 支持两种优化,这两种优化与参考策略的简化方式一致,因为 CIL 是由同一社区开发的。

引入权限集

我们可以做的第一个简化是简化我们使用的权限集。记得我们添加的allow规则,以允许我们的域执行其入口文件:

(allow domain exec 
  (file 
    (entrypoint ioctl read getattr lock map execute open)
  )
)

如果我们使用引用策略风格的方法,我们将通过exec_file_perms宏组合这些权限。好消息是,CIL 通过一个名为classpermissionset的语句支持类似的功能。

如果我们想要完全模拟引用策略风格的方法,我们会在全局命名空间中定义classpermissionset,然后按如下方式使用它:

(classpermission exec_file_perms)
(classpermissionset exec_file_perms (file (ioctl read getattr lock map execute open)))
(block pgpool
  (allow domain exec (file (entrypoint)))
  (allow domain exec exec_file_perms)
)

在这个例子中,我们在全局命名空间中定义了classpermissionset,然后引用它。然而,与引用策略不同的是,我们不能仅仅将exec_file_permsentrypoint一起添加到权限中。classpermissionset语句明确引用了与之关联的类。因此,CIL 中的allow语句是一个独立的语句,它本身不包含类引用。

此外,在示例中,你会注意到我们并没有在exec_file_perms名前加上点符号,以引用全局命名空间。虽然我们可以加上点符号,以与其余策略保持一致,但如果没有潜在的冲突,使用点符号前缀并不是强制性的。如果当前命名空间中不存在某个名称的本地定义,策略将检查父命名空间(因此也包括全局命名空间)中是否定义了该名称。

所以,尽管前面的策略完全可以正常工作,但我们确实建议在全局命名空间相关的名称前加上点符号,以确保后续没有本地覆盖可能会干扰策略。

添加宏

我们可以引入的最后一个简化是添加。CIL 明确支持宏,允许它们成为加载策略的一部分,而不仅仅是在文件系统中引用。通过 CIL 宏,代码成为策略的一部分。构建策略时无需再引用 CIL 代码。

虽然这是与面向对象编程一致的最佳实践(因为我们可以将宏添加到命名空间中,使它们保持在同一对象内),但缺点是当前的 SELinux 工具无法快速显示策略中可用的宏(以及它们需要的接口)。

现在,让我们通过一个域转换宏来增强我们的pgpool策略,类似于通过引用策略风格开发创建的pgpool_domtrans()接口:

(block pgpool
  (macro domtrans ((type SOURCEDOMAIN))
    (allow SOURCEDOMAIN exec exec_file_perms)
    (allow SOURCEDOMAIN domain (process (transition)))
    (typetransition SOURCEDOMAIN exec process domain)
  )
)

宏定义本身以名称开始(在我们的例子中是domtrans),后跟接口。该接口定义了传递给宏的参数数量及其类型。在我们的示例中,只传递了一个参数,它是一个 SELinux 类型。

宏后面跟着应用的代码。代码中引用了参数本身(SOURCEDOMAIN),并将在宏被显式调用时用后面给出的参数进行替换。虽然我们的示例使用了大写的变量名,但这并不是强制性的,仅仅是为了视觉效果,表示将要被替换的内容。

在另一个 CIL 策略中,我们可以通过call语句引用这个宏。例如,要允许postgresql_t域转到pgpool.domain SELinux 域,我们可以将以下call语句添加到我们的策略中:

; Equivalent to "pgpool_domtrans(postgresql_t)" in refpolicy
(typeattributeset cil_gen_require postgresql_t)
(call pgpool.domtrans (postgresql_t))

CIL 宏提供了所需的一切,以便在开发 SELinux 策略时实现与参考策略相同的简洁性,甚至更多,因为有许多约束不适用于 CIL 策略。

虽然未来有可能会发生这种情况,但目前由于种种原因并没有计划这样做:

  • 当前的参考策略包含大量代码,这些代码都需要重新编写。此外,Linux 发行版已经在此策略基础上做了许多扩展,因此将 SELinux 策略代码重写为 CIL 的工作量非常大。这并非不可能,但不是几周内能够完成的任务。

  • 几乎所有帮助开发者编写 SELinux 策略的在线信息和文档都是基于当前的参考策略。这个重要的信息源一旦发生切换就会变得过时,而针对基于 CIL 的策略开发的在线文档目前仍然非常稀少。

  • CIL 策略虽然非常强大,但由于其 S 表达式,它也有些复杂。CIL 的设计初衷并不是用 CIL 替代 SELinux 策略开发,而是允许开发更高级的语言,这些语言可以轻松地转换为 CIL。因此,如果需要重新设计,用户友好和开发者友好的语言很可能会被设计出来,并且能够轻松转换为 CIL。

随着 SELinux 开发的进展,无论是在策略层面,还是在用户空间和内核支持方面,我们可以期待 CIL 及其支持工具会有更多的新增功能。

总结

SELinux 的 CIL 是一种强大且较低级的语法和语言,用于表达所有可能的 SELinux 策略代码。SELinux 用户空间工具会自动将现有策略转换为 CIL 代码,但通过这种转换,许多 CIL 构造并未被使用:转换只使用了一个较小的 CIL 功能集来建立有效的翻译。

更高级的 CIL 功能,例如命名空间支持、宏以及通过classpermissionset语句进行的权限集,在开发我们自己的基于 CIL 的 SELinux 策略时非常有用。在本章中,我们已经学习了如何使用 CIL 来构建完整的应用程序策略。由于没有类似参考策略的框架来简化开发,我们必须自己编写所有必要的代码构造。

虽然这意味着开发基于 CIL 的策略需要更多的资源,但我们也看到 CIL 具有一些参考策略开发无法解决的优势,例如声明端口或向活动策略添加 SELinux 约束的能力。

我们以简要概述结束本章,解释了为什么基于 CIL 的开发没有被广泛使用,但在可预见的未来,我们将在 SELinux 中看到这一方面的持续改进。

这就是我们本书的全部内容以及我们能提供的信息。然而,这仅仅是一个旅程的开始,而非结束。SELinux 是一种广泛使用的技术,我们希望本书能为您提供正确的材料和知识,以便理解、成长并为生态系统做出贡献。感谢您的关注与投入。

问题

  1. 我们如何知道 CIL 会持续存在?

  2. cil_gen_require 属性在 CIL 开发中是必需的吗?

  3. 开发者可以使用 CIL 进行的声明,其他 SELinux 语言风格不能做的例子有哪些?

  4. 我们如何在 CIL 中创建类似接口的支持结构?

第十七章:评估

第一章

  1. 最主要的区别在于,使用 DAC 系统时,用户可以完全控制谁可以访问用户的数据及其访问权限。这由用户自由决定,因此得名。对于 MAC 系统,系统管理员(或安全管理员)定义如何处理和执行访问控制。访问权限由策略强制执行,如果管理员不允许,用户无法绕过该策略。

  2. Linux 在其内核代码中引入了钩子,开发人员可以通过自己的代码订阅这些钩子。这些钩子是Linux 安全模块LSM)框架的一部分,LSM 框架是 Linux 内核的原生扩展框架。

    SELinux 是使用此 LSM 框架(及其提供的钩子)来为 Linux 内核及其应用程序提供强制访问控制功能的 MAC 技术之一。还有其他技术存在,包括 AppArmor。

    SELinux 子系统的代码本身也成为了主 Linux 内核的一部分,与其他主要的 LSM 实现一样,尽管这对于支持 LSM 的技术不是强制性要求。然而,这支持了 SELinux 作为一种成熟的开源技术的概念。

  3. SELinux 上下文的四个字段如下:SELinux 用户、SELinux 角色、SELinux 类型和敏感度级别(或敏感度范围)。敏感度级别可能并不总是存在:Linux 发行版可能选择在其策略中禁用敏感度的支持。在这种情况下,SELinux 上下文将只有前三个字段。

  4. SELinux 具有角色的概念,SELinux 类型可以与角色相关联。由于 SELinux 主要关注类型来执行强制控制(SELinux 主要是类型强制系统),基于角色的访问控制通过限制角色可以关联的类型来实现。

    拥有与 DBA 相关角色的用户只能在 DBA 关联的类型内与系统进行交互。由于该角色与其他类型没有任何关联,因此用户也无法获得这些其他类型的权限。

  5. 虽然有一个叫做参考策略的项目,但大多数 Linux 发行版会因多种原因偏离该策略。没有一个统一的 SELinux 策略存在的主要原因是,SELinux 是一个精细化的系统,因此可以根据 Linux 发行版的设计和使用原则进行调整和优化。

    问为什么没有一个适用于所有 Linux 发行版的 SELinux 策略,几乎等同于问为什么会有多个 Linux 发行版。每个发行版都有自己的重点、设计、原则和背后的决策,SELinux 策略需要与这些相一致,才能取得成功。

第二章

  1. 管理员应首先分析情况,找出问题触发的原因。问题可能是由于分配的上下文不正确,或进程未使用正确的方法启动。

    如果拒绝本身被允许,管理员应更新 SELinux 策略(就像他们需要时更新防火墙规则一样)。

    如果这不可行,则管理员应考虑将 SELinux 设置为宽容模式,但仅针对导致问题的特定应用程序。

    如果这也不可行,则管理员应将系统设置为宽容模式,但要确保组织和环境的安全原则接受此做法。

    只有在这些方法都不可行或不能解决问题时,管理员才应该摇头,咒骂更高的力量,并禁用 SELinux。

  2. 如果系统运行了审计守护进程,则 SELinux 日志将成为审计日志的一部分。可以使用像 ausearch 这样的工具显示它们,或者直接从系统的 /var/log/audit 中读取。

    如果没有运行审计守护进程,则 SELinux 日志事件将由系统日志记录器接收,或者通过内核环形缓冲区获取。可以使用dmesg命令读取内核环形缓冲区。如果系统日志记录器接收到事件,它们很可能会存储在/var/log/messages中。

  3. 主动查询 SELinux 策略或 SELinux 系统的应用程序将链接到 libselinux 库。如果是这种情况,可以使用 readelflddobjdump 查看,显示使用了 /lib64/libselinux.so.1(或类似文件):

    libselinux library), this is more the exception than the rule for most Linux systems.
    
  4. AVC(访问向量缓存)是一个缓存,包含最近和最常用的强制检查,允许 SELinux 子系统更快速地查询是否可以授予某个操作。如果没有 AVC,SELinux 子系统将需要一遍又一遍地遍历整个策略,检查系统上每个操作的权限。

    仅此而已,这会极大地拖慢系统速度。

  5. 不,还有一些其他日志事件,管理员在处理 SELinux 时需要注意。一个是 USER_AVC,它用于类似 AVC 的事件,但由使用 SELinux 策略的应用程序触发,并且由应用程序自己执行强制(而不是通过 Linux 内核)。另一个是 SELINUX_ERR,当触发与常规类型强制无关的内部错误或违规时使用。

    还有其他与 SELinux 密切相关,但并不专属于 SELinux 的事件类型。例如,MAC_POLICY_LOADMAC_POLICY_CHANGEMAC_STATUS 是每当 MAC 系统状态或策略更改时触发的事件。

第三章

  1. 需要一个中间步骤来将角色与 Linux 账户关联,那就是 SELinux 用户。一个 Linux 账户(或登录)被映射到一个 SELinux 用户。然后,SELinux 用户被映射到一个或多个 SELinux 角色,SELinux 用户可以属于这些角色。

    如果我们想为 Linux 用户分配额外的角色,我们需要将其添加到该 Linux 账户所映射的 SELinux 角色中。然而,如果多个 Linux 账户映射到相同的 SELinux 用户,则我们首先需要确保所有这些账户确实被允许使用此角色。如果不允许,则必须为该 Linux 账户创建一个专用的 SELinux 用户。

  2. 是的,当用户通过特定服务登录时,会考虑这些映射。管理员可以调整映射,使其依赖于服务,如在 定制服务登录 部分所示。

  3. 大多数 SELinux 域不允许更改上下文的 SELinux 用户。这使得可以根据 SELinux 用户跟踪活动,即使常规 Linux 用户已更改其用户 ID。需要注意的是,这不仅限于 SELinux,Linux 也支持区分真实用户 ID(尽可能保持静态)和有效用户 ID(例如,在执行 setuid 应用程序时可能会变化)。

    SELinux 用户还允许针对 SELinux 策略的粒度控制,例如,当使用基于用户的访问控制时。在这种情况下,SELinux 用户无法访问由其他 SELinux 用户拥有的资源。

  4. PAM 是一个灵活、模块化的系统,Linux 用于用户认证。系统上的各种技术和服务并不重复实现认证,而是通过 PAM 来处理认证流程。管理员只需关注 PAM 或与 PAM 相关的配置,以确保系统的正确访问。

    对于 SELinux,PAM 是必需的,它允许认证检查映射(在 Linux 用户和 SELinux 用户之间),这通过 pam_selinux.so 得到支持。

第四章

  1. 最常用的选项是-Z,支持的工具包括 ls、mv 和 ps。相同的字符也被 systemd 的 tmpfiles 应用程序用来显式设置资源的 SELinux 上下文。然而,虽然这是最常用的选项,并不是所有工具都遵循这个约定,因此我们建议始终查阅工具的帮助文档或手册页。

  2. 在大多数情况下,上下文作为文件或目录在文件系统中的扩展属性进行存储。这个扩展属性是 security.selinux 属性,可以通过 getfattrstat 等工具查询。

    然而,并不是所有文件系统都支持扩展属性。在这种情况下,SELinux 上下文是通过该文件系统的挂载选项获得的,所有文件系统上的资源都将使用相同的上下文。

  3. chcon 应用程序直接更改文件的 SELinux 上下文,但不会调整系统的文件上下文定义。如果系统或管理员在任何时候重新标记该文件或整个文件系统(这是 SELinux 问题的常见修复方式),文件的 SELinux 上下文将被恢复。

    因此,chcon 仅推荐用于临时的 SELinux 上下文变更或验证上下文变更是否解决了问题。一旦确定新上下文是必要的,应通过 semanage fcontext 在系统的文件上下文定义中注册。

  4. 是的。虽然 SELinux 工具对 Linux 发行版提供的上下文定义有 最具体规则优先 的概念,但这个概念不适用于系统本地的文件上下文定义(换句话说,由系统管理员执行)。

    对于本地定义的文件上下文,匹配到的第一个规则将被使用,而不管后续的上下文定义是什么。

  5. 如果只想重新标记选定的一组文件,例如递归应用于某个目录,应使用 restorecon 命令。如果需要重新标记整个文件系统,可以使用 fixfiles(CentOS 及相关发行版)或 rlpkg(Gentoo)。

    另一种方法是创建一个空文件,命名为 /.autorelabel,然后重启系统。系统会检测到该文件,对整个文件系统进行重新标签化,删除该文件后再重新启动系统。

  6. 源域需要具备针对目标域的过渡权限。它还需要对可执行文件具备执行权限。该可执行文件必须被标记为目标域的入口点。最后,进行过渡的角色必须允许使用目标域作为类型。

  7. 可以将多个 SELinux 类型分配给一个 SELinux 属性,之后 SELinux 策略可以将此属性作为规则的源或目标。基于属性的规则将自动应用于所有分配该属性的类型。

第五章

  1. 用于将类型应用于 TCP 端口的命令是通过 semanage 创建的。例如,要将 ssh_port_t 类型应用于 TCP 端口 10122,执行以下命令:

    # semanage port -a -t ssh_port_t -p tcp 10122
    # sepolicy network -p 10122
    

    如果端口属于一个未保留的范围,则可以进行修改。

  2. 不,SECMARK 是本地于系统的。一旦网络数据包被 Linux 主机接收,SECMARK 规则将为该数据包关联一个标签,但这个标签只会保留在系统内存中。一旦数据包离开 Linux 系统,它将不再显示 SECMARK 标签的任何痕迹。

  3. semanage 使用的子命令有 ibendport(将标签或敏感性应用于 InfiniBand 网络端口)和 ibpkey(将标签或敏感性应用于分区密钥)。

  4. 虽然标记的 IPsec 本身不需要专用设备,但它确实要求所有参与的主机对每个标签的含义有相同的理解。这意味着,在基于 SELinux 标记的情况下,所有主机需要启用 SELinux,并且最好具有完全相同的 SELinux 策略。

第六章

  1. Ansible(在文件模块中使用 setype)和 Puppet(在其文件模块中使用 seltype)是唯一两个原生支持显式设置资源的 SELinux 上下文的工具。然而,Chef 会根据定义的文件上下文规则自动重新标记资源,但你无法原生覆盖此行为。

  2. 除了 SaltStack,所有的编排工具都支持由社区构建和支持的模块,这些模块扩展了工具的原生支持。Ansible 的 Galaxy、Puppet 的 Forge 和 Chef 的 Supermarket 是这些自定义模块的主要社区。

    所有编排工具(包括 SaltStack)都足够灵活,可以使用命令和简单的检查来检查状态并进行更改,从而有效地允许管理员根据自己的喜好自定义定义。

  3. 所有工具都有自己处理问题的视角和设计。例如,Ansible 将其更改推送到远程节点,而其他工具通常使用远程代理连接到中央系统以获取最新的更改。SaltStack 甚至支持这两种方法。

    虽然所有工具都包含了一些 SELinux 支持,但有些工具启用了更多的 SELinux 特性。幸运的是,通过使用社区支持的附加模块,几乎所有编排工具的 SELinux 支持都可以轻松扩展。

第七章

  1. /usr/lib/systemd/system 中的单元文件由 Linux 发行版本身管理。每当系统上部署了新的软件更新时,这些文件就会被覆盖。

    单元文件的修改应当放置在 /etc/systemd/system 中,因为它们会覆盖 /usr/lib/systemd 中的设置,并且软件部署不应将其单元文件放在该位置。

  2. 应用程序是 tmpfiles,它是 systemd 套件的一部分。要让它重置上下文,必须创建一个配置文件(对于本地定义的更改,位于 /etc/tmpfiles.d 中),并使用 z 指令(重置单个文件的上下文)或 Z 指令(递归地设置整个目录的上下文)。

  3. journalctl 命令允许基于从事件本身获取的变量进行筛选。这些变量之一是生成该事件的服务的 SELinux 上下文。

    _SELINUX_CONTEXT= in the preceding command, and then press *Tab* twice to see all the valid values.
    
  4. 如果 SELinux 策略本身没有合适的命名文件转换规则(该规则会自动创建带有正确 SELinux 上下文的节点),那么你可以告诉 udev 为你执行此操作。

    /etc/udev/rules.d rather than /usr/lib/udev/rules.d as the latter location is managed by the distribution, and new installations or updates will overwrite the files located therein.
    
  5. 不行。只有当 D-Bus 策略文件本身引用了 SELinux 上下文(使用busconfig > selinux > associate XML 实体)时,D-Bus 才会检查 SELinux 策略。如果策略中未定义 SELinux 映射,则 D-Bus 无法知道要验证哪个关联。

    然而,这与消息流不同,消息流直接受 D-Bus 通过 SELinux 策略的管理。

  6. Apache 可以使其支持 SELinux,因为它具有模块化设计,并允许将第三方模块应用到其自身环境中。虽然核心 Apache 代码中没有启用 SELinux 支持,但可以添加额外的模块(如mod_selinux),这些模块会启用 SELinux 支持。

第八章

  1. 是的,尽管它默认没有启用。SEPostgreSQL 通过 PostgreSQL 中附加的一个模块提供,该模块名为sepgsql。因此,它是默认技术的一部分,但默认情况下并未启用。

  2. 由于sepgsql模块需要一个会话上下文,PostgreSQL 数据库需要仅从本地系统访问(使用 Unix 域套接字),或者需要在网络中启用并设置标签网络。

    如果没有标签网络,任何远程连接到数据库的尝试将无法提供任何上下文信息,sepgsql将拒绝该连接。

  3. 当在 PostgreSQL 中创建数据库对象时,它将自动接收一个 SELinux 标签。管理员或数据库所有者可以使用 PostgreSQL 中的SECURITY LABEL语句更改标签:

    LIKE and use % as a glob character.
    
  4. sepgsql模块不与 Linux 审计子系统交互,而是依赖 PostgreSQL 使用的日志功能和接口。因此,sepgsql进行的任何决策日志都将记录在 PostgreSQL 系统日志中。

第九章

  1. sVirt 与更标准的 SELinux 配置不同的独特之处在于,它将 SELinux 的 MCS 支持提升到了一个新的层次。通过随机为每个虚拟机分配两个类别,sVirt 即使在可用类别数量较少时,也能处理隔离成千上万的虚拟机。

  2. SELinux 在虚拟化层之上实施的两项主要安全措施如下:

    • 客户机内部隔离,确保客户机之间不能相互攻击或泄露信息

    • 客户机/主机隔离,确保客户机只能访问并与主机上需要的资源进行交互

    尽管这两者当然也在虚拟化监控程序代码中实现,但任何设计缺陷都可能导致重大问题。通过在 SELinux 中实施这些隔离,我们利用 SELinux 子系统作为独立(且更灵活)访问控制系统的优势。

  3. virt_image_t标签用于客户机镜像,当客户机未运行时。运行后,镜像会重新标记为svirt_image_t并分配正确的类别集合。另一方面,virt_content_t标签用于只读媒体,如 CD 镜像。

  4. 可以通过编辑客户机的 XML 信息来更改标签:

    seclabel tags can be added to define target labels. 
    
  5. Vagrant 默认不支持 sVirt,但通过其插件模型,我们可以为 Vagrant 安装 libvirt 插件。安装后,Vagrant 将使用 libvirt 作为虚拟化层,自动允许我们在 Vagrant 中使用 sVirt。

第十章

  1. SELinux 在 Linux 内核中工作。而 Xen 是一个位于硬件和操作系统之间的虚拟机监控器,并不像 QEMU 和 KVM 那样使用完整的操作系统作为基础。

    当我们通过 Linux 与 Xen 交互时,实际上是通过 dom0 客户机与 Xen 进行交互。在这个客户机中,SELinux 可以运行(我们甚至推荐这样做),但 SELinux 将保持在虚拟化客户机内。

    然而,Xen 借鉴了 SELinux 的做法,并在其 Xen 安全模块框架中实现了这一方法。

  2. 你可以通过编辑 Xen 客户机的配置文件(位于/etc/xen目录)并添加seclabel参数来为其分配标签,格式如下:

    seclabel = 'system_u:system_r:prot_domU_t'
    

    你需要重新启动客户机才能使更改生效。一旦客户机重新启动(使用xl create),你可以通过xl list -Z查看其活动标签。

    处理 XSM 标签的常见 Xen 命令有哪些?

    常用的命令如下:

    使用xl list -Z,我们可以列出客户机及其当前分配的标签。

    使用xl getenforce,我们可以查询 XSM 的当前执行状态。

    使用xl setenforce,我们可以设置 XSM 的新执行状态。

    使用xl dmesg,我们可以查看 Xen 日志,包括 XSM AVC 日志条目。

    使用flask-get-bool,我们可以查询当前的 XSM-FLASK 布尔值及其值。

    使用flask-set-bool,我们可以为 XSM-FLASK 布尔值设置一个新的值。

    使用flask-label-pci,我们可以为 PCI 设备分配一个新的 XSM-FLASK 类型。

    我们还可以使用 SELinux 工具来分析策略文件,如seinfosesearch

  3. 加载自定义策略的命令是xl loadpolicy,或者flask-loadpolicy。只要新策略文件没有放在/boot目录中以便自动加载,这个加载的策略将在重启或加载新策略之前一直处于活动状态。

第十一章

  1. machinectl命令不允许管理员更改正在运行的容器的 SELinux 类型。这导致所有容器默认在未受限制的域中运行,而我们希望使用受限域——最好还能启用 sVirt 支持,这样容器就不能相互影响了。

  2. 当容器启动并进行位置映射时,应使用 :Z 选项(如果是私有映射)或 :z 选项(如果是共享映射),以确保资源被重新标记为容器可访问的 SELinux 类型:

    # podman run -dit --name postgresql-test -v /srv/db/postgresql-test:/bitnami/postgresql:Z -p 5432:5432 postgresql
    

    如果没有此选项,资源的标签将保持不变,这通常意味着容器运行时根本无法访问该资源。

  3. 我们可以使用 udica 应用程序生成自定义策略。该应用程序使用 podman inspect(或 docker inspect)命令提供的信息,该命令显示当前的容器定义,并为该容器构建特定的自定义策略。

    策略一旦加载,容器就可以通过 --security-opt 参数使用它。

  4. SELinux 设置的主要位置是在清单中的 spec 配置参数下。在那里,我们可以创建 securityContext 定义,通过 seLinuxOptions 对象支持 SELinux 选项。

第十二章

  1. 当 SELinux 布尔值通过 /sys/fs/selinux/booleans 文件系统进行更改时,更改不会自动提交。为了实现这一点,还需要将值 1 写入 /sys/fs/selinux/commit_pending_bools

  2. sesearch 命令用于查询活动策略,也可以用于查询 SELinux 布尔值的影响。添加 -b <boolean> 参数可以将查询限制为受 SELinux 布尔值影响的规则。

  3. 当 SELinux 策略模块加载时,它会被分配一个优先级,告诉系统是否应为活动模块。管理员可以以更高的优先级加载新模块进行测试,并再次删除它们,而不会冒着系统上没有有效 SELinux 规则的风险。

    同样,管理员可以以较低的优先级加载策略,确保它尚未激活,稍后再以较高优先级移除模块,使得新加载的策略变为活跃。

    这不同于启用或禁用模块,这会影响所有优先级。

  4. SELinux 工具 audit2allow 将所有与 SELinux 相关的审计事件转换为 SELinux 策略代码。该代码可以使用传统样式(使用 -M)或参考策略样式(使用 -R -M)。无论选择哪种样式,都将创建一个可加载的 SELinux 策略模块(后缀为 .pp):

    custom_staff_su_faillog.pp) can be loaded using semodule -i.
    

第十三章

  1. seinfo 应用程序用于查询策略的类型内容,但不查询其规则。例如,您可以使用 seinfo 列出策略中的类型,但不会查询这些类型可以做什么。

    另一方面,sesearch 应用程序用于查询策略中的规则,但不会显示策略中不是真正规则的各种定义(如属性定义和支持的类别)。

    因此,主要的区别在于 seinfo 侧重于策略的结构,而 sesearch 侧重于策略中定义的强制执行内容。

  2. 达到一个域意味着域转换。因此,我们要寻找的是你如何从当前域(例如 staff_t)转换到目标域(例如 unconfined_t),以及通过何种方式—通常,这是通过执行触发类型转换的二进制文件或脚本来完成的。

    分析域转换可以使用 apol(图形用户界面)、sedtasepolicy transition 来完成。然而,后者可能无法揭示正确的路径,因此推荐使用 sedtaapol 进行分析。

  3. 信息流分析必须涵盖比域转换更多的路径。域转换发生在进程域之间,只有少数操作可以触发转换。另一方面,信息流可以通过许多许多操作来实现。

    这样的分析不仅需要考虑读写语句,还要考虑文件描述符使用、套接字使用、信号传递、资源的锁定与解锁等。结果是,信息流分析使用权限映射来识别要检查的各种权限,以及一个权限在信息流分析会话中的重要性(权重)。

  4. 当前没有使用手头的工具。用于比较策略的工具 sediff 显示策略之间的差异,但它本身不能生成包含策略差异的 SELinux 策略。

    此外,SELinux 策略模块只能向活动策略添加额外的规则,而不能删除它们。因此,即使 sediff 生成了兼容的输出,它仍然无法生成任何删除活动策略中现有规则的语句。

第十四章

  1. 非限制性域仍然完全由 SELinux 控制并强制执行。之所以称为非限制性,是因为这些域在 SELinux 策略中被赋予了广泛的权限。然而,尽管名字中如此表示,它们仍然在某种程度上是受限的。

    相反,宽容域不受限制。SELinux 只会记录违反策略的行为,但不会强制执行这些行为。

  2. SELinux 沙箱工具可以用来在一个非常受限的域中运行应用程序。该工具将强制应用程序在非常受限的域中运行(对于常规非图形终端用户应用程序为 sandbox_t,对于图形应用程序为 sandbox_xserver_t),并通过使用 Linux 的命名空间功能,隔离或隐藏对其他系统资源的访问。

  3. 当初始化系统(如 systemd)启动一个守护进程时,它会为该守护进程执行一个特定的二进制文件或脚本。该二进制文件或脚本的标签通常会定义目标域。例如,如果资源的标签为bin_t,那么 systemd 将确保目标服务以unconfined_service_t身份运行。如果它被标记为postgresql_exec_t,则目标服务将以postgresql_t类型运行。

    尽管也涉及到其他权限(如源上下文要求过渡权限到目标),除非我们从头开始构建新策略,否则切换域将像更改其可执行资源的标签,并重新标记其在文件系统中的主要位置(如日志位置和运行时信息)一样简单。

  4. sepolicy generate生成的默认骨架应用程序包含permissive语句,这意味着策略将在宽松模式下运行。由于这意味着 SELinux 不会执行任何控制,因此启用此策略时,应用程序很可能会完美运行。

    然而,这并不是目标状态,管理员需要从策略中移除permissive设置并根据需要进行调整。

第十五章

  1. 许多 Linux 发行版增加了符合发行版目的和原则的服务和工具,但这些可能与参考策略所描述的内容相矛盾。例如,Red Hat Enterprise Linux 及其衍生的 Linux 发行版会为许多应用启用未限制域,而参考策略则会力求限制所有应用程序。

    因此,许多 Linux 发行版基于参考策略制定其策略,但会根据特定目的进行增强和调整。

  2. 三个主要的策略文件如下:

    • 一个类型强制文件,后缀为.te,它包含 SELinux 策略模块的规则,专注于其拥有的域。

    • 一个接口文件,后缀为.if,它暴露了与该 SELinux 策略模块所拥有的域和资源之间的交互模式和权限。这些接口随后会被其他 SELinux 策略模块使用。

    • 一个上下文文件,后缀为.fc,它包含与此 SELinux 策略模块相关的各种路径的文件上下文。

    策略模块可以通过单个文件创建。在这种情况下,构建系统将假定其他文件为空。

  3. 使用权限集允许策略开发人员在需要时轻松调整和扩展权限集,而无需更改所有可能的 SELinux 策略模块代码条目。

    当 Linux(及 SELinux 子系统)添加新权限时,这一点尤其重要。例如,假设内存映射系统调用(map)尚不存在,并在之后被引入。我们需要将 map 权限添加到所有执行调用中。通过使用权限集,我们可以仅将其添加到适当的权限集中。

  4. 接口将授予领域或角色特权。它们不会向策略中添加或删除 SELinux 对象。而模板则会生成新的 SELinux 类型、角色、布尔值或其他对象。因此,不允许从任何布尔触发块内调用模板。

  5. 数据库管理角色不使用 userdom_admin_user_template,因为它不是一个系统范围的管理角色,而是非常特定于数据库的。userdom_admin_user_template 会授予该角色比管理数据库所需的更多权限。

第十六章

  1. SELinux 的通用中间语言CIL)不是一个可以轻易删除的 SELinux 扩展。它是 SELinux 策略开发和支持的核心,尽管主要处于幕后:所有加载到系统中的 SELinux 策略模块都会先转换为 CIL,然后才会被加载到内存中。

    CIL 格式是与 Linux 内核和 SELinux 子系统交互的唯一格式。由于它作为 SELinux 用户空间工具的一部分使用,因此管理员或开发人员不总是能立即察觉,但它绝对是 SELinux 的核心组件。

  2. 不是强制性的,但建议使用。该属性用于以模块化的方式引用类型和角色,并确保这些引用是有效的。CIL 内部要求在使用之前先定义类型和角色,若不使用属性强制此类声明,则模块加载顺序可能会导致失败。

    虽然可以声明其他属性或引入其他方法来实现此目的,但通过 SELinux 用户空间工具支持使用 cil_gen_require 属性。因此,建议遵循这一做法。

  3. 使用 CIL,开发人员可以创建额外的端口映射,声明一个新类型并将其分配给一个可用端口。使用其他 SELinux 语言风格时,这只能在重建整个策略时实现,而不是通过模块来实现。

    另一个例子是引入约束。SELinux 约束不支持通过其他语言风格在 SELinux 模块中加载。然而,尽管约束是 SELinux 中强大的构造,它们可能会让管理员感到困惑,因为与约束相关的失败不会产生明显的消息,而查询当前策略中的允许规则的管理员可能会发现即使出现失败,仍然存在允许规则。

  4. CIL 支持宏,这些宏成为 SELinux 模块(和命名空间)的一部分,可以通过 SELinux 策略中的 call 语句从其他地方调用。我们可以将宏作为模块的一部分创建,类似于参考策略中的接口,同时将宏作为全局命名空间的一部分创建,类似于参考策略中的支持宏。

第十八章:您可能喜欢的其他书籍

如果您喜欢本书,您可能对 Packt 出版的其他书籍感兴趣:

Linux 安全与加固技术精通

Donald Tevault

ISBN: 978-1-83898-177-8

  • 创建具有强密码的受限用户账户

  • 使用 iptables、UFW、nftables 和 firewalld 配置防火墙

  • 使用不同的加密技术保护您的数据

  • 加固安全外壳服务,防止安全入侵

  • 使用强制访问控制保护系统免受漏洞利用

  • 加固内核参数并设置内核级审计系统

  • 应用 OpenSCAP 安全配置文件并设置入侵检测

  • 安全配置 GRUB 2 启动加载程序和 BIOS/UEFI

网络安全攻击 – 红队策略

Johann Rehberger

ISBN: 978-1-83882-886-8

  • 了解与安全漏洞相关的风险

  • 实施构建高效渗透测试团队的策略

  • 使用知识图谱绘制主场图

  • 使用索引和其他实用技术进行凭证猎取

  • 获取蓝队工具的见解,提升您的红队技能

  • 使用适当的数据沟通结果并影响决策者

留下评论 - 告诉其他读者您的看法

请通过在您购买书籍的网站上留下评论,与他人分享您对本书的想法。如果您是通过亚马逊购买本书,请在本书的亚马逊页面上留下真实的评论。这对其他潜在读者至关重要,他们可以利用您的客观意见做出购买决策;我们可以了解客户对我们产品的看法;我们的作者也能看到您对他们与 Packt 合作编写的书籍的反馈。它只需花费您几分钟时间,但对其他潜在客户、我们的作者以及 Packt 都非常有价值。谢谢!

目录

  1. SELinux 系统管理 第三版

  2. 为什么订阅?

  3. 贡献者

  4. 关于作者

  5. 关于审阅者

  6. Packt 正在寻找像您这样的作者

  7. 前言

    1. 本书适用对象

    2. 本书涵盖内容

    3. 最大化本书价值

    4. 下载示例代码文件

    5. 代码实战

    6. 下载彩色图像

    7. 使用的约定

    8. 联系我们

    9. 评审

  8. 第一章:使用 SELinux

  9. 第一章:基本的 SELinux 概念

    1. 技术要求

    2. 为 Linux 提供更多安全性

      1. 介绍 Linux 安全模块(LSM)

      2. 使用 SELinux 扩展常规 DAC

      3. 限制 root 权限

      4. 减少漏洞影响

      5. 启用 SELinux 支持

    3. 标记所有资源和对象

      1. 剖析 SELinux 上下文

      2. 通过类型强制访问

      3. 通过角色授予域访问

      4. 通过用户限制角色

      5. 通过敏感性控制信息流

    4. 定义和分发策略

      1. 编写 SELinux 策略

      2. 通过模块分发策略

      3. 在策略存储中捆绑模块

    5. 区分不同的策略

      1. 支持 MLS

      2. 处理未知权限

      3. 支持无约束域

      4. 限制跨用户共享

      5. 递增策略版本

      6. 不同的策略内容

    6. 总结

    7. 问题

  10. 第二章:理解 SELinux 决策和日志记录

    1. 技术要求

    2. 开关 SELinux

      1. 设置全局 SELinux 状态

      2. 切换到宽容或强制模式

      3. 使用内核启动参数

      4. 为单个服务禁用 SELinux 保护

      5. 理解 SELinux-aware 应用程序

    3. SELinux 日志记录与审计

      1. 跟踪审计事件

      2. 调优 AVC

      3. 揭示更多日志记录

      4. 配置 Linux 审计

      5. 配置本地系统日志记录器

      6. 阅读 SELinux 拒绝信息

      7. 其他 SELinux 相关事件类型

      8. 使用 ausearch

    4. 获取拒绝帮助

      1. 使用 setroubleshoot 进行故障排除

      2. 当 SELinux 拒绝发生时发送电子邮件

      3. 使用 audit2why

      4. 与 systemd-journal 交互

      5. 运用常识

    5. 总结

    6. 问题

  11. 第三章:管理用户登录

    1. 技术要求

    2. 面向用户的 SELinux 上下文

    3. SELinux 用户与角色

      1. 列出 SELinux 用户映射

      2. 将登录映射到 SELinux 用户

      3. 为服务定制登录

      4. 创建 SELinux 用户

      5. 列出可访问的域

      6. 管理分类

    4. 处理 SELinux 角色

      1. 定义允许的 SELinux 上下文

      2. 使用 getseuser 验证上下文

      3. 使用 newrole 切换角色

      4. 通过 sudo 管理角色访问

      5. 使用 runcon 访问其他域

      6. 切换到系统角色

    5. SELinux 与 PAM

      1. 通过 PAM 分配上下文

      2. 在宽容模式下禁止访问

      3. 多重实例化目录

    6. 总结

    7. 问题

  12. 第四章:使用文件上下文和进程域

    1. 技术要求

    2. SELinux 文件上下文简介

      1. 获取上下文信息

      2. 解释 SELinux 上下文类型

    3. 保留或忽略上下文

      1. 继承默认上下文

      2. 查询过渡规则

      3. 复制和移动文件

      4. 临时更改文件上下文

      5. 将类别放置在文件和目录上

      6. 在文件上使用多级安全

      7. 备份和恢复扩展属性

      8. 使用挂载选项设置 SELinux 上下文

    4. SELinux 文件上下文表达式

      1. 使用上下文表达式

      2. 注册文件上下文更改

      3. 优化递归上下文操作

      4. 使用可自定义类型

      5. 编译不同的 file_contexts 文件

      6. 交换本地修改

    5. 修改文件上下文

      1. 使用 setfiles、rlpkg 和 fixfiles

      2. 重新标记整个文件系统

      3. 使用 restorecond 自动设置上下文

      4. 使用 tmpfiles 在启动时设置 SELinux 上下文

    6. 进程的上下文

      1. 获取进程上下文

      2. 过渡到域

      3. 验证目标上下文

      4. 其他支持的过渡

      5. 查询初始上下文

      6. 调整内存保护

    7. 限制过渡的范围

      1. 过渡期间的环境清理

      2. 禁用不受约束的过渡

      3. 使用 Linux 的 NO_NEW_PRIVS

    8. 类型、权限和约束

      1. 理解类型属性

      2. 查询域权限

      3. 学习约束

    9. 总结

    10. 问题

  13. 第五章:控制网络通信

    1. 技术要求

    2. 控制进程间通信

      1. 使用共享内存

      2. 通过管道进行本地通信

      3. 通过 UNIX 域套接字进行通信

      4. 理解 netlink 套接字

      5. 处理 TCP、UDP 和 SCTP 套接字

      6. 列出连接上下文

    3. Linux 防火墙和 SECMARK 支持

      1. 介绍 netfilter

      2. 实施安全标记

      3. 为数据包分配标签

      4. 过渡到 nftables

      5. 评估 eBPF

    4. 保护高速 InfiniBand 网络

      1. 直接访问内存

      2. 保护 InfiniBand 网络

      3. 管理 InfiniBand 子网

      4. 控制对 InfiniBand 分区的访问

    5. 理解带标签的网络

      1. 使用 NetLabel 进行回退标签化

      2. 基于网络接口限制流量

      3. 接受来自选定主机的对等通信

      4. 验证点对点流量

      5. 使用旧式控制

    6. 使用带标签的 IPsec 与 SELinux

      1. 设置常规 IPsec

      2. 启用带标签的 IPsec

    7. 使用 NetLabel 和 SELinux 支持 CIPSO

      1. 配置 CIPSO 映射

      2. 添加特定领域的映射

      3. 使用本地 CIPSO 定义

      4. 支持 IPv6 CALIPSO

    8. 总结

    9. 问题

  14. 第六章:通过基础设施即代码编排配置 SELinux

    1. 技术要求

    2. 目标设置和策略介绍

      1. 操作的幂等性

      2. 策略和状态管理

      3. SELinux 配置设置

      4. 设置文件上下文

      5. 从错误中恢复

      6. 比较框架

    3. 使用 Ansible 进行 SELinux 系统管理

      1. Ansible 的工作原理

      2. 安装和配置 Ansible

      3. 创建和测试 Ansible 角色

      4. 使用 Ansible 为文件系统资源分配 SELinux 上下文

      5. 使用 Ansible 加载自定义 SELinux 策略

      6. 使用 Ansible 的即插即用 SELinux 支持

    4. 利用 SaltStack 配置 SELinux

      1. SaltStack 的工作原理

      2. 安装和配置 SaltStack

      3. 使用 SaltStack 创建和测试我们的 SELinux 状态

      4. 使用 SaltStack 为文件系统资源分配 SELinux 上下文

      5. 使用 SaltStack 加载自定义 SELinux 策略

      6. 使用 SaltStack 的即插即用 SELinux 支持

    5. 使用 Puppet 自动化系统管理

      1. Puppet 的工作原理

      2. 安装和配置 Puppet

      3. 使用 Puppet 创建和测试 SELinux 类

      4. 使用 Puppet 为文件系统资源分配 SELinux 上下文

      5. 使用 Puppet 加载自定义 SELinux 策略

      6. 使用 Puppet 的即插即用 SELinux 支持

    6. 利用 Chef 进行系统自动化

      1. Chef 的工作原理

      2. 安装和配置 Chef

      3. 创建 SELinux 配方书

      4. 使用 Chef 为文件系统资源分配 SELinux 上下文

      5. 使用 Chef 加载自定义 SELinux 策略

      6. 使用 Chef 的即插即用 SELinux 支持

    7. 总结

    8. 问题

  15. 第二部分:SELinux 感知平台

  16. 第七章:配置特定应用程序的 SELinux 控制

    1. 技术要求

    2. 调整 systemd 服务、日志记录和设备管理

      1. systemd 中的服务支持

      2. 使用 systemd 进行日志记录

      3. 处理设备文件

    3. 通过 D-Bus 通信

      1. 理解 D-Bus

      2. 使用 SELinux 控制服务获取

      3. 管理消息流

    4. 配置 PAM 服务

      1. Cockpit

      2. Cron

      3. OpenSSH

    5. 与 Apache 一起使用 mod_selinux

      1. 介绍 mod_selinux

      2. 配置 Apache 的通用 SELinux 灵敏度

      3. 将最终用户映射到特定领域

      4. 根据源更改领域

    6. 总结

    7. 问题

  17. 第八章:SEPostgreSQL – 用 SELinux 扩展 PostgreSQL

    1. 技术要求

    2. 介绍 PostgreSQL 和 sepgsql

      1. 使用 sepgsql 重新配置 PostgreSQL

      2. 创建测试账户

      3. 调整 PostgreSQL 中的 sepgsql

      4. 故障排除 sepgsql

    3. 理解 SELinux 的数据库特定对象类和权限

      1. 理解 sepgsql 权限

      2. 使用默认支持的类型

      3. 创建可信程序

      4. 使用 sepgsql 特定函数

    4. 使用 MCS 和 MLS

      1. 基于类别限制对列的访问

      2. 限制用户领域用于敏感度范围操作

    5. 将 SEPostgreSQL 集成到网络中

      1. 为远程会话创建回退标签

      2. 调优 SELinux 策略

    6. 总结

    7. 问题

  18. 第九章:安全虚拟化

    1. 技术要求

    2. 理解 SELinux 安全虚拟化

      1. 介绍虚拟化

      2. 审查虚拟化风险

      3. 重用现有虚拟化域

      4. 微调虚拟化支持的 SELinux 策略

      5. 理解 sVirt 使用 MCS

    3. 增强 libvirt 与 SELinux 支持

      1. 区分共享资源与专用资源

      2. 评估 libvirt 架构

      3. 为 sVirt 配置 libvirt

      4. 更改客户机的 SELinux 标签

      5. 自定义资源标签

      6. 控制可用类别

      7. 更改存储池位置

    4. 使用 Vagrant 与 libvirt

      1. 部署 Vagrant 和 libvirt 插件

      2. 安装兼容 libvirt 的框

      3. 配置 Vagrant 框

    5. 总结

    6. 问题

  19. 第十章:使用 Xen 安全模块与 FLASK

    1. 技术要求

    2. 理解 Xen 和 XSM

      1. 介绍 Xen 虚拟机监控器

      2. 安装 Xen

      3. 创建无特权客户机

      4. 理解 Xen 安全模块

    3. 运行启用 XSM 的 Xen

      1. 重建带有 XSM 支持的 Xen

      2. 使用 XSM 标签

      3. 操作 XSM

    4. 应用自定义 XSM 策略

    5. 总结

    6. 问题

  20. 第十一章:增强容器化工作负载的安全性

    1. 技术要求

    2. 使用 SELinux 和 systemd 的容器支持

      1. 初始化 systemd 容器

      2. 使用特定的 SELinux 上下文

      3. 使用 machinectl 促进容器管理

    3. 配置 podman

      1. 选择 podman 而不是 Docker

      2. 使用 SELinux 与容器

      3. 更改容器的 SELinux 域

      4. 使用 udica 创建自定义域

      5. 使用 SELinux 布尔值切换 container_t 特权

      6. 调整容器托管环境

    4. 利用 Kubernetes 的 SELinux 支持

      1. 配置带有 SELinux 支持的 Kubernetes

      2. 为 pod 设置 SELinux 上下文

    5. 总结

    6. 问题

  21. 第三部分:策略管理

  22. 第十二章:调整 SELinux 策略

    1. 技术要求

    2. 使用 SELinux 布尔值

      1. 列出 SELinux 布尔值

      2. 更改布尔值

      3. 检查布尔值的影响

    3. 处理策略模块

      1. 列出策略模块

      2. 加载和移除策略模块

    4. 替换和更新现有策略

      1. 使用 audit2allow 创建策略

      2. 使用合理的模块名称

      3. 使用 audit2allow 生成参考策略风格模块

      4. 构建参考策略风格模块

      5. 构建传统风格模块

      6. 替换默认的分发策略

    5. 总结

    6. 问题

  23. 第十三章:分析策略行为

    1. 技术要求

    2. 执行单步分析

      1. 使用不同的 SELinux 策略文件

      2. 显示策略对象信息

      3. 理解 sesearch

      4. 查询允许规则

      5. 查询类型转换规则

      6. 查询其他类型规则

      7. 查询与角色相关的规则

      8. 使用 apol 浏览

      9. 使用 apol 工作区

    3. 调查域转换

      1. 使用 apol 进行域转换分析

      2. 使用 sedta 进行域转换分析

      3. 使用 sepolicy 进行域转换分析

    4. 分析信息流

      1. 使用 apol 进行信息流分析

      2. 使用 seinfoflow 进行信息流分析

      3. 使用 sepolicy communicate 进行简单的信息流分析

    5. 比较策略

      1. 使用 sediff 比较策略
    6. 总结

    7. 问题

  24. 第十四章:处理新应用程序

    1. 技术要求

    2. 无任何限制地运行应用程序

      1. 理解无约束域的工作原理

      2. 将新应用程序作为无约束域运行

      3. 扩展无约束域

      4. 将域标记为宽松模式

    3. 使用沙箱化的应用程序

      1. 理解 SELinux 沙箱

      2. 使用沙箱命令

    4. 为新应用程序分配常见策略

      1. 理解领域复杂性

      2. 在特定策略中运行应用程序

    5. 扩展生成的策略

      1. 理解生成策略的局限性

      2. 介绍 sepolicy generate

      3. 使用 sepolicy generate 生成策略

    6. 总结

    7. 问题

  25. 第十五章:使用参考策略

    1. 技术要求

    2. 引入参考策略

      1. 浏览策略

      2. 构建策略模块结构

    3. 使用和理解策略宏

      1. 使用单类权限组

      2. 调用权限组

    4. 创建应用程序级策略

      1. 构建面向网络的服务策略

      2. 解决用户应用程序问题

    5. 添加用户级策略

    6. 使用支持工具获取帮助

      1. 使用 selint 验证代码

      2. 本地查询接口和宏

    7. 总结

    8. 问题

  26. 第十六章:使用 SELinux CIL 开发策略

    1. 技术要求

    2. 引入 CIL

      1. 将 .pp 文件转换为 CIL

      2. 理解 CIL 语法

    3. 创建精细粒度的定义

      1. 根据角色或类型

      2. 定义新的端口类型

      3. 向策略添加约束

    4. 构建完整的应用程序策略

      1. 使用命名空间

      2. 通过属性分配扩展策略

      3. 添加入口点信息

      4. 逐步扩展策略

      5. 引入权限集

      6. 添加宏

    5. 总结

    6. 问题

  27. 评估

    1. 第一章

    2. 第二章

    3. 第三章

    4. 第四章

    5. 第五章

    6. 第六章

    7. 第七章

    8. 第八章

    9. 第九章

    10. 第十章

    11. 第十一章

    12. 第十二章

    13. 第十三章

    14. 第十四章

    15. 第十五章

    16. 第十六章

  28. 你可能会喜欢的其他书籍

    1. 留下评论 - 让其他读者知道你的想法

地标

  1. 封面

  2. 目录

posted @ 2025-07-04 15:40  绝不原创的飞龙  阅读(98)  评论(0)    收藏  举报