本篇文章的目的爲收集在命令行執行的全部命令,除了將全部的命令發送到 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 複製代碼