使用 Cfengine 3 进行安全监控和强制执行
从一开始,Cfengine 就将安全性作为其设计和使用场景的关键部分。在这里,我将演示如何使用 Cfengine 3 通过监控文件校验和、监控文件系统中可疑的文件名、监控正在运行的进程、监控开放端口和管理 sshd.conf 来提高 Linux 系统的安全性。
由于 Cfengine 3 正在积极开发中,我建议您从 Cfengine 源代码存档(请参阅“资源”)安装最新版本。
本文的目的是提供关于如何使用 Cfengine 来提高 Linux 系统安全性的实际示例。有关学习 Cfengine 语言的帮助,请参阅本文“资源”部分中的《快速入门指南》(我在此处不提供有关 Cfengine 语言的教程)。本文基于 Cfengine 3.1.5a1 版本。
Cfengine 3.1.4 附带了 214 个单元测试,这些测试可以兼作 Cfengine 功能的示例。它们安装在 /usr/local/share/doc/cfengine/ 中。我已将 unit_change_detect.cf 采纳到 detect_changes_in_etc.cf 中(清单 1)。
清单 1. detect_changes_in_etc.cf
# GNU GPL 3 ################################################### # # Change detect # ################################################### body common control { bundlesequence => { "detect_changes_in_etc" }; } ################################################### bundle agent detect_changes_in_etc { files: "/etc" changes => detect_all_change, depth_search => recurse("inf"); } ################################################### body changes detect_all_change { report_changes => "all"; update_hashes => "true"; } ################################################### body depth_search recurse(d) { depth => "$(d)"; }
使用以下命令运行:
cf-agent -KIf detect_changes_in_etc.cf
cf-agent 是 Cfengine 中实际对系统进行更改的组件。(还有其他组件用于服务文件、监控系统活动等等。cf-agent 是对系统进行更改的组件,也是您开始学习 Cfengine 时会使用的组件。)在上面的命令中,
-K — 告诉 cf-agent 忽略基于时间的锁定,并允许您重复运行 cf-agent(没有“冷却”期,否则可能会启动以防止系统过载)。
-I — 告诉 cf-agent 通知您其操作以及对系统所做的任何更改。
-f — 指定策略文件名。
在第一次运行时,cf-agent 构建一个文件信息数据库,其中包含文件时间戳和 inode 编号,并为每个文件构建一个 MD5 哈希值。您应该看到类似这样的内容:
# cf-agent -KIf detect_changes_in_etc.cf !! File /etc/hosts.allow was not in MD5 database - new file found I: Made in version 'not specified' of 'detect_changes_in_etc.cf' near line 22 ... #
这里有两条消息,alert 和 info。
Cfengine 为其输出添加前缀,以帮助您了解输出的类型(换句话说,是元数据)。
信息性消息以 “I” 开头。
报告以 “R” 开头。
警报以 “!!” 或 ALERT 开头。
系统更改通知以 “->” 开头。
在上面的示例中,警报消息附带一条 info 消息,其中包含生成警报时生效的策略、其版本号(如果已提供)和行号。
我没有指定版本号,但行号很有用。第 22 行是:
changes => detect_all_change,
这是负责 Cfengine 将 /etc/passwd 添加到 MD5 数据库的行。它告诉 Cfengine 如何处理更改 - 检测它们。
现在,我再次运行 cf-agent,它会安静地运行。/etc 的内容与 MD5 校验和数据库匹配。
# cf-agent -KIf detect_changes_in_etc.cf #
接下来,我编辑 /etc/hosts.allow 以添加 “sshd: ALL” 来模拟未经授权的更改。观察 cf-agent 的尖叫:
# cf-agent -KIf detect_changes_in_etc.cf !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ALERT: Hash (MD5) for /etc/hosts.allow changed! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -> Updating hash for /etc/hosts.allow to MD5=2637c1edeb55081b330a1829b4b98c45 I: Made in version 'not specified' of './detect_changes_in_etc.cf' near line 22 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ALERT: inode for /etc/hosts.allow changed 38901878 -> 38901854 ALERT: Last modified time for /etc/hosts.allow changed Sat Jan 29 17:09:26 2011 -> Mon Jan 31 08:00:02 2011 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! #
有三个警报:
MD5 哈希值已更改(因为内容已更改)。
inode 编号已更改(当 vi 保存文件时)。
修改时间已更改(当 vi 保存文件时)。
提醒:有关 Cfengine 采取的操作的消息以 “->” 为前缀。
-> Updating hash for /etc/hosts.allow to MD5=2637c1edeb55081b330a1829b4b98c45
您可以设置 Cfengine 以通过电子邮件或 syslog 发出警报,因此即使入侵者篡改了 MD5 数据库,也会发出警报。在商业版本的 Cfengine (Nova) 中,您可以设置多个 Cfengine 节点来共享其 MD5 数据库并相互监控和交叉检查。
您可以相当频繁地运行此检查 - 如果您愿意且硬件能够承受,则每五分钟一次。(计算大量 MD5 校验和在 CPU 和磁盘 I/O 上可能会非常昂贵。)增加的安全性对您来说值得吗?
Cfengine 有一个特殊的 cf-agent 控制变量,称为suspiciousnames。您可以将名称列表放入其中,以便在任何文件搜索期间(例如在 MD5 哈希检查期间完成的文件搜索)发出警告。如果 Cfengine 在递归(深度)文件搜索期间看到这些名称,它将发出警告。如果suspiciousnames未设置,cf-agent 将不会检查它们。默认情况下未设置。
让我通过将以下控制块添加到 detect_changes_in_etc.cf 来演示其工作原理:
body agent control { suspiciousnames => { ".mo", "lrk3", "rootkit" }; }
cf-agent 控制块控制 cf-agent 的行为。您可以在其中设置诸如试运行模式(不更改任何内容,但仅报告会进行的更改 - 对于学习 Cfengine 很有用)、Cfengine 将编辑的最大文件大小等等。因此,suspiciousnames变量在代理控制块中设置。它是一个字符串数组。
让我们创建一个可疑命名的文件,看看 cf-agent 是否会兴奋起来:
# date > /etc/rootkit # cf-agent -IKf detect_changes_in_etc.cf Suspicious file rootkit found in /etc #
因此,如果您正在扫描系统目录进行 MD5 哈希检查,您也可以添加可疑名称检查。
我遵循通过禁用不必要的服务来保护服务器的最佳实践。我经常想确保我的 Web 服务器没有运行 CUPS - 通常,Web 服务器不需要打印!
清单 2 中显示的示例基于 Cfengine 单元测试 unit_process_kill.cf。
清单 2. cups_not_running.cf
body common control { bundlesequence => { "cups_not_running" }; } ######################################## bundle agent cups_not_running { processes: "cupsd" signals => { "term", "kill" }; }
清单 2 中感兴趣的行是:
processes: "cupsd" signals => { "term", "kill" };
这意味着如果在进程表中存在与 “cupsd” 匹配的条目,则该进程将被发送 TERM 信号,然后发送 KILL 信号。
# cf-agent -IKf cups_not_running.cf -> Signalled 'term' (15) to observed process match '28140' -> Signalled 'kill' (9) to observed process match '28140' #
但是,我们不要如此残酷。Cfengine 可以报告可疑的进程名称。您可以使用清单 3 中所示的策略来监视密码嗅探器、破解程序、IRC 机器人等。
清单 3. report_suspicious_process_names.cf
body common control { bundlesequence => { "report_suspicious_process_names" }; } ################################################### bundle agent report_suspicious_process_names { vars: "suspicious_process_names" slist => { "sniff", "eggdrop", "r00t", "^\./", "john", "crack" }; processes: ".*" process_select => proc_finder("$(suspicious_process_names)"); } ################################################### body process_select proc_finder(pattern) { command => ".*$(pattern).*"; process_result => "command"; }
这里的关键行是:
vars: "suspicious_process_names" slist => { "sniff", "eggdrop", "r00t", "^\./", "john", "crack" };
一个名为 “suspicious_process_names” 的变量是一个字符串列表;我们认为可疑的进程名称包括,例如,任何以 ./. 开头的进程。如您所见,此列表可以包含正则表达式。Cfengine 使用 Perl 兼容的正则表达式。
您可以设置此数组的内容以反映您认为可疑的进程名称。然后,Cfengine 扫描整个进程表(即processes: .*),并循环遍历 “suspicious_process_names” 数组的内容。Cfengine 具有对数组的隐式循环,因此如果您有一个数组 @{suspicious_process_names} 并且您引用 ${suspicious_process_names},您实际上是在说:
for ${suspicious_process_names} in (@{suspicious_process_names} do ... done
这就是您说process_select => proc_finder("$(suspicious_process_names)");时发生的情况。您实际上是在说,对于 @(suspicious_process_names) 中的每个元素,找到与该正则表达式匹配的进程。
无论如何,我希望这是一个安全演示,而不是语言入门,所以让我们继续。
# cf-agent -IKf report_suspicious_process_names.cf !! Matched: root 20044 20002 20044 0.0 0.0 4956 19 664 1 22:05 00:00:00 ./eggdrop #
第一个数字字段 (20044) 是 PID。最后一个字段是进程名称。(为什么我的 Web 服务器上有一个 IRC 机器人?)
案例分析
2000 年,芝加哥大学的 David Ressman 和 John Valdes 在 LISA 论文 “使用 Cfengine 进行自动化、多平台软件和补丁分发” 中报告了他们如何在 Cfengine 2 中使用类似的功能检测到破解者:
由于闯入我们系统的人几乎完全使用受损系统来运行嗅探器、IRC 机器人或 DoS 工具,我们决定创建一个可疑进程名称列表,让 Cfengine 查找并在每次运行时向我们发出警告。除了通常的可疑对象(多个 inetd 运行副本,进程名称中带有 “sniff”、“r00t”、“eggdrop” 等的任何内容,密码破解程序等),我们还让 Cfengine 监视进程名称中带有 “./” 的任何进程。
一天下午,我们收到来自 Cfengine 的电子邮件,内容是关于我们的一台计算机注意到该机器的常规用户正在以 “./irc” 身份运行程序。看到我们的用户使用 “./” 运行程序并不罕见,我们也不反对我们的用户运行 IRC,但在这种情况下,对于这位特定用户来说,运行 irc 进程有点不寻常(良好的 UNIX 系统管理实践也要求您了解您的用户)。
在系统上四处查看后,我们发现运行此程序的人不是该机器的常规用户,而是显然从其他地方嗅探到我们用户的密码并在 Cfengine 警告我们之前几分钟远程登录到他的系统的人。此人正在设置 IRC 机器人,但尚未尝试获取 root shell。
您可以通过监控可疑进程名称来增强您的纵深防御。
您可以通过了解服务器正在侦听哪些端口来提高您的安全态势感知能力。入侵者可能会安装 FTP 服务器来托管盗版软件,或者安装 IRC 服务器来进行机器人命令和控制。无论哪种方式,您的服务器的 TCP 配置文件都在其侦听的 TCP 端口方面发生了更改(增加)。
通过不断比较期望的和实际的开放 TCP 端口,Cfengine 可以快速检测到入侵。Cfengine 3 默认每五分钟运行一次,因此它可以非常快速地检测到泄露。
清单 4 中显示的代码示例首先使用硬编码列表,列出系统上期望的 TCP 端口和相应的进程名称:22 sshd 80 httpd 443 httpd 5308 cf-server。然后它使用lsof来获取 TCP 端口和进程名称的实际列表,比较它们并报告危险如果比较失败。
清单 4. check_listening_ports.cf
body common control { bundlesequence => { "check_listening_ports" }; inputs => { "Cfengine_stdlib.cf" }; } bundle agent check_listening_ports { vars: "listening_ports_and_processes_ideal_scene" string => "22 sshd 80 httpd 443 httpd 5308 cf-server"; # this is our expected configuration vars: "listening_ports_and_processes" string => execresult("/usr/sbin/lsof -i -n -P | \ /bin/grep LISTEN | \ /bin/sed -e 's#*:##' | \ /bin/grep -v 127.0.0.1 | \ /bin/grep -v ::1 | \ /bin/awk '{print $8,$1}' | \ /bin/sort | \ /usr/bin/uniq | \ /bin/sort -n | \ /usr/bin/xargs echo", "useshell"); # actual config. # tell Cfengine to use a shell with "useshell" # to do a command pipeline. classes: "reality_does_not_match_ideal_scene" not => regcmp ( "$(listening_ports_and_processes)", "$(listening_ports_and_processes_ideal_scene)" ); # check whether expected config matches actual reports: reality_does_not_match_ideal_scene:: " DANGER! DANGER! Expected open ports and processes: DANGER! $(listening_ports_and_processes_ideal_scene) DANGER! DANGER! Actual open ports and processes: DANGER! $(listening_ports_and_processes) DANGER! "; # and yell loudly if it does not match. # Note: A "commands" promise could be used in # addition to "reports" to send a text message # to a sysadmin cell phone or to feed # CRITICAL status to a monitoring system. }
这是一个运行示例:
# cf-agent -IKf ./check_listening_ports.cf R: DANGER! DANGER! Expected open ports and processes: DANGER! 22 sshd 80 httpd 443 httpd 5308 cf-server DANGER! DANGER! Actual open ports and processes: DANGER! 22 sshd 80 httpd 443 httpd 3306 mysqld 5308 cf-server DANGER!!! #
同样,这是一个安全演示,而不是语言入门,但如果您想了解策略,请遵循 Cfengine 的《快速入门指南》。如果您需要任何关于理解此策略的帮助,请访问 help-cfengine 邮件列表或直接通过 aleksey@verticalsysadmin.com 向我咨询。
下一个示例是 Diego Zamboni 的 Cfengine 捆绑包,用于编辑 sshd 配置文件,并在进行任何更改时重新启动 sshd。它分为两个部分(以抽象底层细节)。在第一部分中,系统管理员编辑 sshd 数组以设置与 sshd 配置参数对应的变量。例如,要强制使用 SSH 的 Protocol 2,请设置:
"sshd[Protocol]" string => "2";
如果参数被注释掉,Cfengine 会取消注释并将其设置为所需的值。如果参数不存在,Cfengine 会添加它并将其设置为所需的值。此外,如果对 sshd_config 进行了任何更改,sshd 将重新启动以激活更改。
清单 5. use_edit_sshd.cf
bundle agent configfiles { vars: "sshdconfig" string => "/etc/ssh/sshd_config"; # SSHD configuration to set "sshd[Protocol]" string => "2"; "sshd[X11Forwarding]" string => "yes"; "sshd[UseDNS]" string => "no"; methods: "sshd" usebundle => edit_sshd("$(sshdconfig)", "configfiles.sshd"); }
清单 6. edit_sshd.cf
# Parameters are: # file: file to edit # params: an array indexed by parameter name, containing # the corresponding values. For example: # "sshd[Protocol]" string => "2"; # "sshd[X11Forwarding]" string => "yes"; # "sshd[UseDNS]" string => "no"; # Diego Zamboni, November 2010 bundle agent edit_sshd(file,params) { files: "$(file)" handle => "edit_sshd", comment => "Set desired sshd_config parameters", edit_line => set_config_values("$(params)"), classes => if_repaired("restart_sshd"); # set_config_values is a bundle Diego wrote based on # set_variable_values from Cfengine_stdlib.cf. commands: restart_sshd.!no_restarts:: "/etc/init.d/sshd restart" handle => "sshd_restart", comment => "Restart sshd if the configuration file was modified"; } bundle edit_line set_config_values(v) # Sets the RHS of configuration items in the file of the form # LHS RHS # If the line is commented out with #, it gets uncommented first. # Adds a new line if none exists. # The argument is an associative array containing v[LHS]="rhs" # Based on set_variable_values from Cfengine_stdlib.cf, modified to # use whitespace as separator, and to handle commented-out lines. { vars: "index" slist => getindices("$(v)"); # Be careful if the index string contains funny chars "cindex[$(index)]" string => canonify("$(index)"); field_edits: # If the line is there, but commented out, first uncomment it "#+$(index)\s+.*" edit_field => col("\s+","1","$(index)","set"); # match a line starting like the key something "$(index)\s+.*" edit_field => col("\s+","2","$($(v)[$(index)])","set"), classes => if_ok("not_$(cindex[$(index)])"); insert_lines: "$(index) $($(v)[$(index)])", ifvarclass => "!not_$(cindex[$(index)])"; }
有关所做更改的示例,请运行:diff在 Cfengine 编辑 sshd_config 以设置 Protocol、X11Forwarding 和 UseDNS 之前和之后:
# diff /etc/ssh/sshd_config /etc/ssh/sshd_config.cf-before-edit 14c14 < #Protocol 2,1 --- > Protocol 2 95,96c95,96 < #X11Forwarding no < X11Forwarding no --- > X11Forwarding yes > X11Forwarding yes 109c109 < #UseDNS yes --- > UseDNS no #
您可能会注意到,在编辑后,X11Forwarding 在那里出现了两次,因为它在编辑之前在文件中出现了两次,一次被注释掉,一次未被注释掉。但是,这不会破坏任何东西。拥有X11Forwarding yes是有效的语法,并且/usr/sbin/sshd -t语法检查器不会报错。
您可能还会注意到,cf-agent 保存了原始文件的副本,以防万一。
资源
Cfengine 源代码存档:www.cfengine.org/pages/source_code
快速入门指南:www.cfengine.org/pages/getting_started
“使用 GNU Cfengine 自动化安全”,Kirk Bauer,2004 年 2 月 5 日(尽管基于 Cfengine 2,但本文对 Cfengine 的理念和功能进行了出色的概述):www.linuxjournal.com/article/6848
Diego Zamboni 的 Cfengine 捆绑包,用于编辑 sshd 配置文件并在需要时重新启动 sshd:https://gist.github.com/714948
下载本文中使用的 Cfengine 策略:www.verticalsysadmin.com/cfengine/LJ-May-2011
Aleksey Tsalolikhin 担任 UNIX 系统管理员已有 13 年,其中包括在 EarthLink 工作的 7 年。通过手动管理 EarthLink 的服务器群,他对自动化服务器配置管理产生了浓厚的兴趣。Aleksey 在 2010 年俄亥俄州 Linux 展和 2011 年南加州 Linux 博览会上,作为专业系统管理员联盟的讲师,教授了“Cfengine 3 系统管理自动化入门”课程。