Systemd 单位加固
先决条件¶
- 熟悉命令行工具
- 基本了解
systemd
和文件权限 - 能够阅读手册页
介绍¶
许多服务以其正常运行不需要的特权运行。systemd
附带了许多工具,这些工具通过强制实施安全措施和限制权限来帮助最大限度地降低进程被破坏时的风险。
目标¶
- 提高
systemd
单位的安全性
免责声明¶
本指南解释了保护 systemd
单位的机制,但不涵盖任何特定单位的正确配置。一些概念过于简化。理解它们和使用的一些命令需要更深入地研究该主题。
资源¶
分析¶
systemd
包含一个很棒的工具,可以快速概述 systemd
单位的整体安全配置。systemd-analyze security
提供了对 systemd
单位安全配置的快速概述。以下是新安装的 httpd
的得分
[user@rocky-vm ~]$ systemd-analyze security httpd
NAME DESCRIPTION EXPOSURE
✗ RootDirectory=/RootImage= Service runs within the host's root directory 0.1
SupplementaryGroups= Service runs as root, option does not matter
RemoveIPC= Service runs as root, option does not apply
✗ User=/DynamicUser= Service runs as root user 0.4
✗ CapabilityBoundingSet=~CAP_SYS_TIME Service processes may change the system clock 0.2
✗ NoNewPrivileges= Service processes may acquire new privileges 0.2
...
...
...
✓ NotifyAccess= Service child processes cannot alter service state
✓ PrivateMounts= Service cannot install system mounts
✗ UMask= Files created by service are world-readable by default 0.1
→ Overall exposure level for httpd.service: 9.2 UNSAFE 😨
功能¶
功能的概念可能非常令人困惑。理解它对于提高 systemd
单位的安全性至关重要。以下是 Capabilities(7)
手册页的摘录
For the purpose of performing permission checks, traditional UNIX implementations distinguish two categories of processes: privileged processes (whose effective user ID is 0, referred to as superuser or root), and unprivileged processes (whose effective UID is nonzero). Privileged processes bypass all kernel permission checks, while unprivileged processes are subject to full permission checking based on the process's credentials (usually: effective UID, effective GID, and supplementary group list).
Starting with Linux 2.2, Linux divides the privileges traditionally associated with superuser into distinct units, known as capabilities, which can be independently enabled and disabled. Capabilities are a per-thread attribute.
这基本上意味着功能可以向非特权进程授予一些 root
特权,但也可以限制由 root
运行的进程的特权。
目前有 41 种功能。这意味着 root
用户的特权有 41 组特权。以下是一些示例
- **CAP_CHOWN**: 对文件 UID 和 GID 进行任意更改
- **CAP_KILL**: 绕过发送信号的权限检查
- **CAP_NET_BIND_SERVICE**: 将套接字绑定到 Internet 域特权端口(端口号小于 1024)
Capabilities(7)
手册页包含完整列表。
有两种类型的功能
- 文件功能
- 线程功能
文件功能¶
文件功能允许将特权与可执行文件关联,类似于 suid
。它们包括存储在扩展属性中的三组:Permitted
、Inheritable
和 Effective
。
有关完整说明,请参阅 Capabilities(7)
手册页。
文件功能不会影响单位的整体暴露级别,因此与本指南关系不大。但是,理解它们可能会有所帮助。因此,快速演示
让我们尝试以非特权用户身份在默认(特权)端口 80 上运行 httpd
[user@rocky-vm ~]$ sudo -u apache /usr/sbin/httpd
(13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:80
no listening sockets available, shutting down
如预期,操作失败。让我们为 httpd
二进制文件配备先前提到的 **CAP_NET_BIND_SERVICE** 和 **CAP_DAC_OVERRIDE**(为了本练习的缘故,覆盖日志和 pid 文件上的文件权限检查),然后重试
[user@rocky-vm ~]$ sudo setcap "cap_net_bind_service=+ep cap_dac_override=+ep" /usr/sbin/httpd
[user@rocky-vm ~]$ sudo -u apache /usr/sbin/httpd
[user@rocky-vm ~]$ curl --head localhost
HTTP/1.1 403 Forbidden
...
如预期,Web 服务器已成功启动。
线程功能¶
线程功能适用于进程及其子进程。有五组线程功能
- 已允许
- 可继承
- 有效
- 绑定
- 环境
有关完整说明,请参阅 Capabilities(7)
手册页。
您已经确定 httpd
不需要 root
用户可用的所有特权。让我们从 httpd
二进制文件中删除先前授予的功能,启动 httpd
守护程序并检查其特权
[user@rocky-vm ~]$ sudo setcap -r /usr/sbin/httpd
[user@rocky-vm ~]$ sudo systemctl start httpd
[user@rocky-vm ~]$ grep Cap /proc/$(pgrep --uid 0 httpd)/status
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
[user@rocky-vm ~]$ capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
主 httpd
进程使用所有可用功能运行,即使大多数功能不需要。
限制功能¶
systemd
将功能集减少到以下内容
- **CapabilityBoundingSet**: 限制在
execve
期间获得的功能 - **AmbientCapabilities**: 如果您想以非特权用户身份执行进程,但仍然想赋予它一些功能,这很有用
为了在包更新后保留配置,请在 /lib/systemd/system/httpd.service.d/
目录中创建一个 override.conf
文件。
由于服务需要访问特权端口,并且以root
身份启动,但将线程作为apache
进行分叉,因此需要在/lib/systemd/system/httpd.service.d/override.conf
文件的[Service]
部分中指定以下功能。
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID
可以将总体暴露级别从UNSAFE
降低到MEDIUM
。
[user@rocky-vm ~]$ sudo systemctl daemon-reload
[user@rocky-vm ~]$ sudo systemctl restart httpd
[user@rocky-vm ~]$ systemd-analyze security --no-pager httpd | grep Overall
→ Overall exposure level for httpd.service: 7.1 MEDIUM 😐
但是,此进程仍然以root
身份运行。可以通过将其专门作为apache
运行来进一步降低暴露级别。
除了访问端口 80 外,进程还需要写入位于/etc/httpd/logs/
的日志,并能够创建/run/httpd/
并写入其中。在以前,可以通过chown
更改权限来实现这一点,而在后者中,则可以通过使用systemd-tmpfiles
实用程序来实现。您可以使用--create
选项来创建文件而无需重启,但从现在开始,它将在每次系统启动时自动创建。
[user@rocky-vm ~]$ sudo chown -R apache:apache /etc/httpd/logs/
[user@rocky-vm ~]$ echo 'd /run/httpd 0755 apache apache -' | sudo tee /etc/tmpfiles.d/httpd.conf
d /run/httpd 0755 apache apache -
[user@rocky-vm ~]$ sudo systemd-tmpfiles --create /etc/tmpfiles.d/httpd.conf
[user@rocky-vm ~]$ ls -ld /run/httpd/
drwxr-xr-x. 2 apache apache 40 Jun 30 08:29 /run/httpd/
您需要调整/lib/systemd/system/httpd.service.d/override.conf
中的配置。您需要使用**AmbientCapabilities**授予新功能。如果httpd
在启动时已启用,则必须扩展[Unit]
部分中的依赖项,以便服务在临时文件创建后启动。
[Unit]
After=systemd-tmpfiles-setup.service
[Service]
User=apache
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
[user@rocky-vm ~]$ sudo systemctl daemon-reload
[user@rocky-vm ~]$ sudo systemctl restart httpd
[user@rocky-vm ~]$ grep Cap /proc/$(pgrep httpd | head -1)/status
CapInh: 0000000000000400
CapPrm: 0000000000000400
CapEff: 0000000000000400
CapBnd: 0000000000000400
CapAmb: 0000000000000400
[user@rocky-vm ~]$ capsh --decode=0000000000000400
0x0000000000000400=cap_net_bind_service
[user@rocky-vm ~]$ systemd-analyze security --no-pager httpd | grep Overall
→ Overall exposure level for httpd.service: 6.5 MEDIUM 😐
文件系统限制¶
通过设置UMask
来控制进程创建的文件的权限。UMask
参数通过执行按位运算来修改默认文件权限。这主要将默认权限设置为八进制0644
(-rw-r--r--
),而默认的UMask
为0022
。这意味着UMask
不会更改默认集
[user@rocky-vm ~]$ printf "%o\n" $(echo $(( 00644 & ~00022 )))
644
假设守护程序创建的文件所需的权限集为0640
(-rw-r-----
),您可以将UMask
设置为7137
。即使默认权限设置为7777
,它也能实现目标。
[user@rocky-vm ~]$ printf "%o\n" $(echo $(( 07777 & ~07137 )))
640
此外
ProtectSystem=
: “如果设置为“strict
”,则整个文件系统层次结构将被挂载为只读,除了 API 文件系统子树/dev/
、/proc/
和/sys/
(使用PrivateDevices=
、ProtectKernelTunables=
、ProtectControlGroups=
保护这些目录)。”ReadWritePaths=
: 使特定路径再次可写ProtectHome=
: 使/home/
、/root
和/run/user
无法访问PrivateDevices=
: 关闭对物理设备的访问,仅允许访问伪设备,如/dev/null
、/dev/zero
、/dev/random
ProtectKernelTunables=
: 使/proc/
和/sys/
成为只读ProtectControlGroups=
: 使cgroups
可读写ProtectKernelModules=
: 拒绝显式模块加载ProtectKernelLogs=
: 限制对内核日志缓冲区的访问ProtectProc=
: “当设置为“invisible”时,其他用户拥有的进程将从 /proc/ 中隐藏。”ProcSubset=
: “如果为“pid”,则与进程管理和自检无关的所有文件和目录在为单元进程配置的 /proc/ 文件系统中将被隐藏。”
也可以限制可执行路径。守护程序只需要执行其二进制文件和库。ldd
实用程序可以告诉我们二进制文件使用哪些库
[user@rocky-vm ~]$ ldd /usr/sbin/httpd
linux-vdso.so.1 (0x00007ffc0e823000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fa360d61000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fa360d34000)
libaprutil-1.so.0 => /lib64/libaprutil-1.so.0 (0x00007fa360d05000)
libcrypt.so.2 => /lib64/libcrypt.so.2 (0x00007fa360ccb000)
libexpat.so.1 => /lib64/libexpat.so.1 (0x00007fa360c9a000)
libapr-1.so.0 => /lib64/libapr-1.so.0 (0x00007fa360c5a000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa360a00000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007fa360964000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa360e70000)
libuuid.so.1 => /lib64/libuuid.so.1 (0x00007fa360c4e000)
libm.so.6 => /lib64/libm.so.6 (0x00007fa360889000)
以下行将被追加到override.conf
文件中的[Service]
部分
UMask=7177
ProtectSystem=strict
ReadWritePaths=/run/httpd /etc/httpd/logs
ProtectHome=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectProc=invisible
ProcSubset=pid
NoExecPaths=/
ExecPaths=/usr/sbin/httpd /lib64
让我们重新加载配置并检查对分数的影响
[user@rocky-vm ~]$ sudo systemctl daemon-reload
[user@rocky-vm ~]$ sudo systemctl restart httpd
[user@rocky-vm ~]$ systemd-analyze security --no-pager httpd | grep Overall
→ Overall exposure level for httpd.service: 4.9 OK 🙂
系统限制¶
各种参数可以限制系统操作以增强安全性
NoNewPrivileges=
: 确保进程无法通过setuid
、setgid
位和文件系统功能获得新权限ProtectClock=
: 拒绝写入系统和硬件时钟SystemCallArchitectures=
: 如果设置为native
,则进程只能进行本机syscalls
(在大多数情况下为x86-64
)RestrictNamespaces=
: 命名空间主要与容器相关,因此可以限制此单元的命名空间RestrictSUIDSGID=
: 阻止进程在文件上设置setuid
和setgid
位LockPersonality=
: 阻止执行域发生更改,这可能只对运行旧版应用程序或为其他类 Unix 系统设计的软件有用RestrictRealtime=
: 实时调度仅与需要严格时间保证的应用程序相关,例如工业控制系统、音频/视频处理和科学模拟RestrictAddressFamilies=
: 限制可用的套接字地址族;可以设置为AF_(INET|INET6)
以仅允许 IPv4 和 IPv6 套接字;某些服务将需要AF_UNIX
用于内部通信和日志记录MemoryDenyWriteExecute=
: 确保进程无法分配既可写又可执行的新内存区域,阻止某些类型的攻击,在这些攻击中恶意代码被注入可写内存并执行;可能会导致 JavaScript、Java 或 .NET 使用的 JIT 编译器失败ProtectHostname=
: 阻止进程使用syscalls
sethostname()
、setdomainname()
让我们将以下内容追加到override.conf
文件中,重新加载配置并检查对分数的影响
NoNewPrivileges=true
ProtectClock=true
SystemCallArchitectures=native
RestrictNamespaces=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictRealtime=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
MemoryDenyWriteExecute=true
ProtectHostname=true
[user@rocky-vm ~]$ sudo systemctl daemon-reload
[user@rocky-vm ~]$ sudo systemctl restart httpd
[user@rocky-vm ~]$ systemd-analyze security --no-pager httpd | grep Overall
→ Overall exposure level for httpd.service: 3.0 OK 🙂
系统调用过滤¶
限制系统调用可能并不容易。很难确定某些守护程序需要进行哪些系统调用才能正常运行。
strace
实用程序可以帮助确定创建了哪些系统调用。-f
选项指定跟踪分叉进程,-o
将输出保存到名为httpd.strace
的文件中。
[user@rocky-vm ~]$ sudo strace -f -o httpd.strace /usr/sbin/httpd
在运行进程一段时间并与之交互后,停止执行以检查输出
[user@rocky-vm ~]$ awk '{print $2}' httpd.strace | cut -d '(' -f 1 | sort | uniq | sed '/^[^a-zA-Z0-9]*$/d' | wc -l
79
该程序在其运行期间进行了 79 个独特的系统调用。您可以使用以下单行代码设置允许的系统调用列表
[user@rocky-vm ~]$ echo SystemCallFilter=$(awk '{print $2}' httpd.strace | cut -d '(' -f 1 | sort | uniq | sed '/^[^a-zA-Z0-9]*$/d' | tr "\n" " ") | sudo tee -a /lib/systemd/system/httpd.service.d/override.conf
...
...
...
[user@rocky-vm ~]$ sudo systemctl daemon-reload
[user@rocky-vm ~]$ sudo systemctl restart httpd
[user@rocky-vm ~]$ systemd-analyze security --no-pager httpd | grep Overall
→ Overall exposure level for httpd.service: 1.5 OK 🙂
[user@rocky-vm ~]$ curl --head localhost
HTTP/1.1 403 Forbidden
Web 服务器仍在运行,并且暴露级别已显着降低。
上述方法非常精确。如果遗漏了系统调用,可能会导致程序崩溃。systemd
将系统调用分组到预定义的集合中。为了使限制系统调用变得更容易,而不是在允许或禁止列表中设置单个系统调用,可以在允许或禁止列表中设置整个组。要查找列表
[user@rocky-vm ~]$ systemd-analyze syscall-filter
@default
# System calls that are always permitted
arch_prctl
brk
cacheflush
clock_getres
...
...
...
组中的系统调用可能重叠,尤其是对于包含其他组的某些组。因此,可以通过使用~
符号指定单个调用或组来禁止它们。override.conf
文件中的以下指令应该适用于此单元
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @mount @swap @reboot
结论¶
大多数systemd
单元的默认安全配置比较宽松。对其进行加固可能需要一些时间,但这样做是值得的,尤其是在暴露于互联网的更大环境中。如果攻击者利用漏洞或错误配置,加固的单元可以阻止他们控制系统。
作者:朱利安·帕托基
贡献者:史蒂文·斯宾塞,安娜·兹尔诺娃