本篇文章的目的为收集在命令行执行的全部命令,除了将全部的命令发送到 Elasticsearch 进行保存以外,还须要为敏感命令作告警。html
要作到这些的核心在于 PROMPT_COMMAND
这个环境变量,它的做用是,在出现 shell 命令输入提示符以前,做为命令来执行这个变量。shell
所以,咱们能够将这个变量定义为一个命令,而后看看它的效果:json
# export PROMPT_COMMAND="date '+%F %T'" 2019-04-23 11:25:53 # 在出现下面的提示符以前执行了 date 命令 # a -bash: a: command not found 2019-04-23 11:25:56 # 再次出现 # v -bash: v: command not found 2019-04-23 11:25:58 # 每次命令行提示符出现以前它都会出现 复制代码
这就至关于每执行一次命令就会执行一次 PROMPT_COMMAND。有了这个基础以后,咱们就可让其收集全部用户执行的命令。vim
首先,因为 PROMPT_COMMAND 执行的时机是上个命令结束,命令行输出以前,所以咱们可使用 histroy 1
获取上一次执行的命令。可是因为该命令的输出结果前面会带上命令的序号,咱们须要去掉它。ruby
这样一来,第一个版的 PROMPT_COMMAND 的结果为:bash
# export PROMPT_COMMAND="history 1 | { read _ cmd; echo \$cmd; }" 复制代码
看起来挺复杂,其实很简单:markdown
histroy 1
的结果会传递到大括号,目的是去掉命令前面的序号;咱们通常使用 read 来读取键盘的输入,不过它还能够帮咱们去掉命令前的序号。因为咱们这里定义两个变量,所以 read 会对输入的结果使用空格分割 1 次,分割后的结果第一部分给变量 _
,另外一部分给变量 cmd
。并发
很显然,序号给了 _
,而后咱们 echo $cmd
就可以拿到上一次执行的命令了。只因此这里的 $
前面加了转义符号 \
,是由于这个命令是在 shell 环境下输入的,它会直接将 $cmd
做为变量解释了,使用转义是防止它直接解释。elasticsearch
其实你能够直接在命令行来测试 read 的效果:函数
# read x y z 23232 xxxxx ewewewe ssssss zzzzz # echo "$x | $y | $z" 23232 | xxxxx | ewewewe ssssss zzzzz 复制代码
咱们光收集历史命令是没什么用的,还应该收集以下信息:
以上这些信息都须要执行命令来获取,组合起来就是这样的:
date "+%F %T"; pwd; whoami; who -u am i | { read user tty _ _ _ _ ip; echo $user $tty $ip| tr -d "()"; }; history 1 | { read _ cmd; echo $cmd; } 复制代码
别急着使用,你须要看看有哪些命令,以及这些命令是干啥的就行,由于这毕竟不是最终版本。
若是定义了 HISTTIMEFORMAT
环境变量,history 的输出结果可能就不是咱们想要的了。所以咱们应该在用户登陆以后将 HISTTIMEFORMAT
设置为空,为了防止用户修改,你能够将它设置为只读。可是一旦将其设置为只读,那么在系统重启以前,这个值没法修改。
vim /etc/profile export HISTTIMEFORMAT="" readonly HISTTIMEFORMAT 复制代码
咱们拿到这些信息以后确定不能只是将其输出,而是将其存放在一个文件中。若是直接将其追加到一个文件中,是会有问题的。操做系统上确定不止一个用户,不一样的用户都会执行命令,那么这个日志文件的属主属组应该改为啥?日志文件要不要切割?要不要删除?这些都是要考虑的问题。
虽然咱们最终都是要输出到文件中,直接追加的方式虽然简单,可是很差控制。最好的方式是使用 logger 命令将其输出 rsyslog,让 rsyslog 帮咱们写到文件中,依托 rsyslog 强大的功能,咱们能够对日志文件作更多的事情。
logger 命令咱们只会用到两个选项:
-p
:指定输出的基础设施和日志等级;-t
:指定 tag咱们须要修改 rsyslog 的配置文件,让其接收咱们发送给它的日志并输出到文件中。这里将日志输出到 /var/log/bashlog。
# vim /etc/rsyslog.d/bashlog.conf local6.debug /var/log/bashlog 复制代码
检查 rsyslog 配置,而后重启:
# rsyslogd -N1 # /etc/init.d/rsyslog/restart 复制代码
而后测试一把,看看 /var/log/bashlog 是否存在你想要的内容。
echo "hehe" | logger -t bashlog -p local6.debug 复制代码
若是将这些都赋值给 PROMPT_COMMAND
变量,会显得很复杂,咱们能够将这些命令定义到一个文件中,而后将这个文件赋值给 PROMPT_COMMAND。
# vim /etc/collect_cmd.sh echo `date "+%F_%T"; pwd; whoami; who -u am i | { read user tty _ _ _ _ ip; echo $user $tty $ip| tr -d "()"; }; history 1 | { read _ cmd; echo $cmd; }` | logger -t bashlog -p local6.debug # chmod +x /etc/collect_cmd.sh # export PROMPT_COMMAND="/etc/collect_cmd.sh" 复制代码
须要注意的是,脚本就写这一行,不要加上 #!/bin/bash
,不然 history 命令执行不会有任何结果,缘由不明。
最终用户每在命令行执行一次命令,就会在日志文件中增长一条相似这样的行:
Apr 25 14:23:03 localhost bashlog: 2019-04-25_14:23:03 root root pts/0 10.201.2.170 cat /etc/collect_cmd.sh
复制代码
前面的日期日志、主机名、程序名都是 rsyslog 自动添加的,后面才是咱们发送过去的内容。
咱们之因此可以收集用户执行的命令,核心就是 PROMPT_COMMAND
。虽然咱们如今定义好了它,可是不免它被人修改(普通用户也行),用户只要登陆后执行 unset PROMPT_COMMAND
,那么你的一切设置都将付诸东流。因此最好的方式就是将这个变量设置为只读。
前面咱们已经将所收集到的日志存放到了文件中了,其实到这一步日志收集已经完成,可是为了便于以后发送到 Elasticsearch,我准备将这些日志以 json 格式写入到文件中。
怎么作呢?仍是经过 rsyslog。只不过 CentOS6 默认的 rsyslog 版本过低,功能有限,须要将其升级到最新版才行。
升级 rsyslog 没有什么风险,我司生产环境升级到 rsyslog8 跑了两年多,没有任何问题。
在官网能够直接下载对应的 yum repo 文件,而后 yum update rsyslog
就升级到最新版了。
我这里将全部 rsyslog 相关的包都下载下来,而后建立了一个本地的 yum 仓库,便于内网机器下载升级。
升级后须要修改一行配置,有些配置不兼容:
# 修改前
*.emerg *
# 修改后
*.emerg :omusrmsg:*
复制代码
修改完成后直接重启:
service rsyslog restart
复制代码
rsyslog 经过 mmnormalize 模块进行日志解析,解析后的内容为 json 格式,而这个模块使用的解析功能来自于 Liblognorm。关于 Liblognorm 的解析语法直接看官方文档便可。
为何要解析成 json 格式?主要的缘由是 Elasticsearch 存储的就是 json 格式的数据,咱们能够直接将解析好的数据直接发送到 Elasticsearch,而无需使用 Logstash 解析。
先下载模块:
yum install rsyslog-mmnormalize liblognorm5-utils
复制代码
liblognorm5-utils 用来检测解析规则是否正确,下面会用到。
当咱们将执行的命令发送给 rsyslog 后,咱们须要解析的是下面的内容,不包括咱们上面看到的 rsyslog 自动添加的时间日期等信息。
2019-04-30_14:01:45 /root root root pts/0 10.201.2.170 vim hehe
复制代码
它会在开头添加一个空格,这个空格是怎么来的我也不清楚,所以咱们要预留一个空格。
mmnormalize 使用时,须要指定一个解析库。这个解析库遵循 liblognorm5 的语法:
# vim /etc/bashlog.rb version=2 # 冒号后面的空格就是上面提到的空格 rule=: % time:word # 两个百分号之间的空格是 date 和 pwd 命令之间的空格 % % directory:word % % exec_user:word % % login_user:word % % tty:word % % src_ip:ipv4 % % command:rest % 复制代码
这个文件就是一个解析库,用于解析上面的内容。虽然以 rb 结尾,可是和 ruby 没有关系。
简单的解释下它的做用:
rule=
,它后面的冒号 :
用来分割 tag 的,也就是说等号和冒号之间能够加上 tag。咱们不须要 tag,可是得把冒号写上;:
就是字段解析了,要解析的字段使用百分号 %
包起来。百分号中经过冒号 :
进行分割,冒号前是字段的名称(json 中的对象名),冒号后是 Liblognorm 内置的字段类型,字段类型后面能够加参数,使用中括号 {}
引用,只是上面没有使用,每一个字段的参数都不同,有的有,有的没有;测试一把解析库:
# echo " 2019-04-30_14:01:45 /root root root pts/0 10.201.2.170 vim hehe" | lognormalizer -r /etc/bashlog.rb -e json { "command": "vim hehe", "src_ip": "10.201.2.170", "tty": "pts\/0", "login_user": "root", "exec_user": "root", "directory": "\/root", "time": "2019-04-30_14:01:45" } 复制代码
这就是解析后的结果,惟一的缺点就是会在 /
前面加上转译符 \
。
如今只须要简单的配置下 rsyslog 就可以将解析后的 json 数据保存到文件中。
# vim /etc/rsyslog.d/bashlog.conf # 加载模块 module(load="mmnormalize") template(name="all-json" type="list"){ property(name="$!all-json") constant(value="\n") # 若是没有这行,解析后的信息不会换行 } if $syslogfacility-text == 'local6' and $syslogseverity-text == 'debug' then { action(type="mmnormalize" rulebase="/etc/bashlog.rb") action(type="omfile" File="/var/log/bashlog" template="all-json") } 复制代码
该文件以前的内容能够删掉了。
咱们首先定义了一个模板,这个模板是配合解析用的,解析一条消息,就将解析后的 json 格式的信息保存在 $!all-json
这个变量中,而后就能够定义 action 将其保存在文件中,或者 NoSQL 中。
原本是打算将其直接发送到 kafka/elasticsearch,可是考虑到 rsyslog 只会当时将消息发送出去,若是发送不成功它不会重发,所以仍是将其保存到文件中,而后经过 filebeat 对文件进行读取并发送。
经过 omfile 还能定义文件的属主属组,文件权限等,默认属主属组为 root,权限 600。
重启 rsyslog 以后,咱们就能够在 /var/log/bashlog 中看到咱们执行的命令了。
为了让 PROMPT_COMMAND
用户登陆就生效,咱们能够将之定义在 /etc/profile
中,且将其定义成只读。
vim /etc/profile export PROMPT_COMMAND="/etc/collect_cmd.sh" readonly PROMPT_COMMAND 复制代码
经过上面的方式咱们能够收集命令行日志并将其解析,可是仍是会存在问题。当你在命令行空回车而不输入任何东西时,执行 history 1
会得到上一次执行的命令,没有什么问题。可是若是你上一次都为空,那么你此次的收集的命令就是空,解析会失败。
你会从解析的日志文件中看到这样的内容:
{ "originalmsg": " 2019-05-01_14:19:19 \/home\/user1 user1 root pts\/0 10.201.2.170", "unparsed-data": "" } 复制代码
咱们解析规则是默认登陆后面的 ip 后面还会有内容,当其没有内容时就会解析失败。这种状况会出如今你使用 su -
命令切换到其余用户,且切换后直接空回车。
针对这样的状况,咱们应该判断用户输入的命令是否为空,若是为空就直接退出脚本。所以咱们的 /etc/collect_cmd.sh
能够作以下修改:
cmd=`history 1 | { read _ cmd; echo $cmd; }` [ -z "$cmd" ] && exit # 当其为空时就直接退出 echo `date "+%F_%T"; pwd; whoami; who -u am i | { read user tty _ _ _ _ ip; echo $user $tty $ip| tr -d "()"; }; echo $cmd` | logger -t bashlog -p local6.debug 复制代码
还有最后一点是针对 HISTTIMEFORMAT
和 PROMPT_COMMAND
这两个环境变量的,为了不出现解析异常,最好将这两个变量都设为只读,这样任何用户都没法修改它的内容。若是不设置为只读,用户在本身家目录下的 .bashrc
或者直接在命令行就能够对其从新设置。
咱们能够将之都定义在 /etc/profile
,若是想要在 /etc/profile.d
中单独用一个文件来保存,不要将 HISTTIMEFORMAT
的定义放在其中,否则其余用户登陆会报错,提示修改只读的变量。
unset HISTTIMEFORMAT readonly HISTTIMEFORMAT export PROMPT_COMMAND="/etc/collect_cmd.sh" readonly PROMPT_COMMAND 复制代码