哈喽大家好,我是咸鱼

今天我们来看一个关于 Keepalived 检测脚本无法执行的问题

一位粉丝后台私信我,说他部署的 keepalived 集群 vrrp_script 模块中的脚本执行失败了,但是手动执行这个脚本却没有任何问题

SElinux 导致 Keepalived 检测脚本无法执行-小白菜博客
这个问题也是咸鱼第一次遇到,为了能让更多的小伙伴以后不会踩这个坑,便有了今天这篇文章

前言

在正式开始之前,我们先来简单复习一下 Keepalived 中的资源检测功能

vrrp_script 模块

Keepalived 中,vrrp_script 模块是用于定义和配置虚拟路由冗余协议(VRRP)的自定义脚本检查,这个模块专门用于对集群中的服务资源进行监控

vrrp_script 模块搭配使用的是 track_script 模块,这个模块中可以引入监控脚本、命令组合、Shell 语句来实现对服务资源的监控

track_script 通过调用 vrrp_script,可以灵活地定义需要监测的服务或资源,例如网络连接、服务状态、系统资源等

当监测到故障时,Keepalived 可以触发状态转移,将主节点切换到备用节点,以确保服务的高可用性

  • 通过 killall -l 命令监测

killall 命令会发送一个信号给进程,以信号 0 为例,如果发现进程关闭或者异常,将返回状态码 1,反之进程运行正常,状态码返回0

vrrp_script nginx_check {
	script "killall -0 nginx"
	interval 2
}

track_script {
	nginx_check
}
  • 通过端口监测

检测端口的运行状态也是较常见的监控方式

vrrp_script nginx_check {
	script "</dev/tcp/127.0.0.1/80"
	interval 2
	fall 2
	rise 1
}

track_script {
	nginx_check
}

其中 fall表示检测到失败的最大次数(如果请求失败两次,就认为该节点发生了故障)

rise 表示如果请求一次成功,就认为该节点恢复正常

  • 通过 shell 语句监测
vrrp_script nginx_check {
	script "if [ $(pidof nginx | wc -l) -eq 0 ]; then exit 1; else exit 0; fi"
	interval 2
	fall 2
	rise 1
}

track_script {
	nginx_check
}
  • 通过脚本监测
vrrp_script nginx_check {
	script "/etc/keepalived/nginx_check.sh"
	interval 2
	fall 2
	rise 1
}

track_script {
	nginx_check
}

问题

在介绍完了 keepalived 的监测功能之后,我们来看下这个问题

我根据他的描述复现了这个场景:keepalived 通过监测上游网络来判断该节点是否正常运行,如果到上游的网络不通,则认为该节点发生了故障

检测脚本如下:

[root@localhost ~]# cat /etc/keepalived/check.sh 
#!/bin/bash
# 检查上行链路
check_route="192.168.149.135"
ping -c 3 $check_route >/dev/null 2>&1
result=$?
echo "${date}----checking..... result:${result}" >> /tools/log.log
if [ $? -eq 0 ]; then
        exit 0          # 正常
else
        exit 1          # 链路异常
fi

一切准备就绪之后启动 keepalived 服务发现报 Keepalived_vrrp[2653]: /etc/keepalived/check.sh exited with status 1

[root@localhost ~]# systemctl status keepalived.service 
● keepalived.service - LVS and VRRP High Availability Monitor
   Loaded: loaded (/usr/lib/systemd/system/keepalived.service; disabled; vendor preset: disabled)
   Active: active (running) since 三 2023-08-14 23:53:49 CST; 4min 56s ago
   ...
8月 14 23:53:49 localhost.localdomain Keepalived_vrrp[2653]: /etc/keepalived/check.sh exited with status 1
...

说明脚本没有执行成功,返回状态码 1 了,我尝试着手动执行,发现脚本没有任何问题

[root@localhost ~]# sh /etc/keepalived/check.sh 
[root@localhost ~]# echo $?
0

排查

首先看一下 /var/log/messages(如果 keepalived 没有专门指定日志文件路径,这个便是默认的日志文件路径)

...
Aug 14 23:53:49 localhost Keepalived_vrrp[17889]: SECURITY VIOLATION - scripts are being executed but script_security not enabled
...
Aug 14 23:53:49 localhost Keepalived_vrrp[17889]: /etc/keepalived/check.sh exited with status 1
...

SECURITY VIOLATION - scripts are being executed but script_security not enabled 这条信息引起了我的注意

”安全违规-脚本正在执行,但 script_security 未启用“,看输出应该是 keepalived 进程想要执行该脚本,但是受到了安全限制

既然是跟系统安全相关的,我们就先来看看这个脚本的权限吧

# 查看脚本权限
[root@localhost ~]# ll /etc/keepalived/check.sh 
-rwxr-xr-x. 1 root root 281 8月   9 15:52 /etc/keepalived/check.sh

# 查看是 keepalived 进程的属主
[root@localhost ~]# ps -ef | grep keep
root      19163      1  0 01:00 ?        00:00:00 /usr/sbin/keepalived -D
...

由上面的输出我们可以得知 keepalived 进程的属主是 root ,而 root 用户是可以去执行这个脚本的(有 x 权限)

权限没问题,我们再来查看下 /var/log/audit/audit.log

/var/log/audit/audit.log 是一个存储系统审计日志的文件

这个文件记录了系统中发生的各种安全事件、用户操作和系统行为,以及与安全相关的信息

系统审计日志是用来监控系统活动、检测潜在的安全问题和追踪系统事件的重要工具之一

...
type=SYSCALL msg=audit(1692033018.624:12555): arch=c000003e syscall=4 success=no exit=-13 a0=190abc0 a1=7ffd028eafc0 a2=7ffd028eafc0 a3=7ffd028eaae0 items=0 ppid=19472 pid=19473 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="check.sh" exe="/usr/bin/bash" subj=system_u:system_r:keepalived_t:s0 key=(null)

type=AVC msg=audit(1692033018.624:12555): avc:  denied  { getattr } for  pid=19473 comm="check.sh" path="/usr/bin/ping" dev="dm-0" ino=50555278 scontext=system_u:system_r:keepalived_t:s0 tcontext=system_u:object_r:ping_exec_t:s0 tclass=file permissive=0
...

我们现在来看一下上面日志输出表示什么

先看第一段:

type=SYSCALL: 这个部分是系统调用事件类型。arch=c000003e syscall=4 success=no exit=-13: 这里描述了系统调用的详细信息,包括系统调用号、是否成功等

  • subj=system_u:system_r:keepalived_t:s0:描述了进程的安全上下文信息
  • 后面内容则是一些更多与系统调用相关的信息,如参数、进程信息、用户信息等

再看第二段:

type=AVC: 这个部分指示了事件类型为 AVC(Access Vector Cache),是 SELinux 的访问控制事件,

  • avc: denied { getattr }: 表示操作是一个 getattr(获取属性)操作,但是被拒绝。
  • pid=19473 comm="check.sh": 进程 ID 是 19473,进程名称是 check.sh
  • path="/usr/bin/ping": 文件路径是 /usr/bin/ping,表示被操作的文件。
  • scontext=system_u:system_r:keepalived_t:s0: 发起操作的进程的安全上下文。
  • tcontext=system_u:object_r:ping_exec_t:s0: 被操作文件的安全上下文。
  • tclass=file: 被操作对象的类型是文件。
  • permissive=0: 表示 SELinux 不是处于宽松模式

总结一下:

这个是 SELinux 的审计日志,这两条日志记录了一个操作,即 keepalived 进程试图通过执行 check.sh" 脚本访问 /usr/bin/ping 文件,但被 SELinux 拒绝了

解决

排查到这思路就逐渐清晰起来了,这是因为 SELinux 权限导致的,在解决这个问题之前,我们先来简单复习一下 SELinux (后面我会专门写一篇文章来介绍 SELinux)

SELinux(Security Enhanced Linux,安全强化的 Linux) ,由美国国家安全局(NSA)开发的

为什么要开发 SELinux

我们知道系统的账户主要分为系统管理员(root)和普通用户,这两种身份能否使用系统上面的文件资源则与 rwx 权限设置有关

所以当某个进程想要对文件进行读写操作时,系统就会根据该进程的属主和属组去比对文件权限,只有通过权限检查,才能够进一步操作文件

这种方式被称为自主访问控制(Discretionary Access Control, DAC),DAC 是 Linux 操作系统中的一种基本权限控制机制,用于限制用户对系统资源的访问权限

但是 DAC 的一大问题就是当用户获取到进程之后,他可以通过这个进程与自己所获得的权限去处理对应的文件资源

万一用户对系统不熟悉,就很容易导致资源误用的问题出现

举个例子,你们公司的新人为了自身方便,将网页所在目录 /var/www/html 目录的权限设置成了 777,则代表所有进程都可以对该目录进行读写

如果黑客接触到了某个进程,就极有可能向你的系统里面写入某些东西

为了避免 DAC 的问题,便有了 SELinux

SELinux MAC 机制

SELinux 引入了强制访问控制(Mandatory Access Control, MAC)机制

强制访问控制(MAC,Mandatory Access Control)是 Linux 操作系统中一种更加严格和细粒度的访问控制机制,用于加强对系统资源的保护和控制

MAC 有趣的地方在于它可以针对特定的进程与特定的文件资源来管理权限,即使你是 root 用户,在使用不同的进程时你所能获取的权限也不一定是 root

SElinux 导致 Keepalived 检测脚本无法执行-小白菜博客
安全上下文

前面我们知道,SELinux 是通过 MAC 的方式来管理进程的权限的

它控制的主体是【进程】,而【目标】则是该【进程】能否读取的文件资源

  • 主体

SELinux 主要管理的就是进程,进程和主体可以画上等号

  • 目标

主体能否读写的目标一般就是文件资源

除了策略指定之外,主体与目标的安全上下文必须一致才能够顺利读写

安全上下文有点类似于文件系统的 rwx ,如果设置错误,主体就无法读写目标资源了
SElinux 导致 Keepalived 检测脚本无法执行-小白菜博客
安全上下文存放在文件的 inode 内,可以通过 ll -Z 命令去查看(前提是 SELinux 是开启着的)

回到我们的案例上来,日志输出说进程 check.sh 试图获取 /usr/bin/ping 的属性(getattr)操作,但被 SELinux 拒绝了

但是我们发现没有这个进程

[root@localhost ~]# ps -ef | grep check

也就是说,由于 SELinux 权限问题 ,keepalived 一开始想要调用 check.sh 脚本就失败了

我们分别来看一下 keepalived 进程和 check.sh 的安全上下文

[root@localhost ~]# ps -eZ | grep keepalived 
system_u:system_r:keepalived_t:s0 19609 ?       00:00:00 keepalived

[root@localhost ~]# ll -Z /etc/keepalived/check.sh 
-rwxr-xr-x. root root system_u:object_r:etc_t:s0       /etc/keepalived/check.sh

可以看到 keepalived 进程的安全上下文类型为 keepalived_t;而 check.sh 的安全上下文是 etc_t

即——主体与目标的安全上下文并不一致,就算有 rwx 权限也无法执行

  • 解决方案一:关闭 SELinux

简单粗暴,直接关闭 SELinux ,就没有这么多乱七八糟的限制了

# 以 CentOS 7 为例
# 临时关闭
[root@localhost ~]# setenforce 0

#永久关闭,将 SELINUX=enforcing 改为 SELINUX=disabled,然后保存退出
[root@localhost ~]# sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
  • 解决方案二(失败了):修改 check.sh 的安全上下文与 keepalived 一致

我想把 check.sh 的安全上下文改成与 keepalived 一致,可是失败了,有小伙伴知道原因吗

[root@localhost ~]# chcon -t keepalived_t  /etc/keepalived/check.sh
chcon: 改变"/etc/keepalived/check.sh" 的环境到"system_u:object_r:keepalived_t:s0" 失败: 权限不够
  • 解决方案三:修改 check.sh 的安全上下文

依旧是修改 check.sh 的安全上下文,不过这次参考了资料

[root@localhost ~]# chcon -t keepalived_unconfined_script_exec_t  /etc/keepalived/check.sh

keepalived_unconfined_script_exec_t 是一个在 SELinux 中用于标识 Keepalived 执行未限制脚本的上下文,这个上下文允许 Keepalived 进程在执行脚本时绕过一些 SELinux 限制,从而可以在需要的情况下执行脚本

  • 解决方案四:将脚本转移到 /usr/libexec/keepalived 目录中
# keepalived 配置
vrrp_script nginx_check {
	script "/usr/libexec/keepalived/check.sh"
	interval 2
	fall 2
	rise 1
}

这个目录的安全上下文是 keepalived_unconfined_script_exec_t,与解决方案三同理

[root@localhost ~]# ll -Z /usr/libexec/keepalived/ -d
drwxr-xr-x. root root system_u:object_r:keepalived_unconfined_script_exec_t:s0 /usr/libexec/keepalived/
  • 解决方案五: keepalived 全局配置添加 enable_script_security 字段

加了这个字段意味着如果脚本路径的任一部分对于非 root 用户来说,都具有可写权限,则不会以 root 身份运行脚本

以非 root 身份运行的脚本就能够通过 SELinux 的审查吗?这一块我不太懂,有懂的小伙伴可以告诉我

global_defs {
	...
	enable_script_security
	...
}

参考资料:

https://github.com/acassen/keepalived/issues/1322

https://serverfault.com/questions/709428/track-script-doesnt-work-after-keepalived-update

https://www.mankier.com/8/keepalived_selinux

https://linux.vbird.org/linux_basic/centos7/0440processcontrol.php#selinux