本篇文章的目的為收集在命令列執行的所有命令,除了將所有的命令傳送到 Elasticsearch 進行儲存之外,還需要為敏感命令做告警。
要做到這些的核心在於 PROMPT_COMMAND
這個環境變數,它的作用是,在出現 shell 命令輸入提示符之前,作為命令來執行這個變數。
因此,我們可以將這個變數定義為一個命令,然後看看它的效果:
# 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。有了這個基礎之後,我們就可以讓其收集所有使用者執行的命令。
read 命令
首先,由於 PROMPT_COMMAND 執行的時機是上個命令結束,命令列輸出之前,因此我們可以使用 histroy 1
獲取上一次執行的命令。但是由於該命令的輸出結果前面會帶上命令的序號,我們需要去掉它。
這樣一來,第一個版的 PROMPT_COMMAND 的結果為:
# export PROMPT_COMMAND="history 1 | { read _ cmd; echo \$cmd; }"
複製程式碼
看起來挺複雜,其實很簡單:
histroy 1
的結果會傳遞到大括號,目的是去掉命令前面的序號;- 大括號相當於開啟了一個匿名函式,將這兩個命令作為一個整體,不過它不會開啟一個子 shell;
- read 是 shell 內部的子命令,它會將空格作為分隔符。
我們一般使用 read 來讀取鍵盤的輸入,不過它還可以幫我們去掉命令前的序號。由於我們這裡定義兩個變數,因此 read 會對輸入的結果使用空格分割 1 次,分割後的結果第一部分給變數 _
,另一部分給變數 cmd
。
很顯然,序號給了 _
,然後我們 echo $cmd
就能夠拿到上一次執行的命令了。只所以這裡的 $
前面加了轉義符號 \
,是因為這個命令是在 shell 環境下輸入的,它會直接將 $cmd
作為變數解釋了,使用轉義是防止它直接解釋。
其實你可以直接在命令列來測試 read 的效果:
# read x y z
23232 xxxxx ewewewe ssssss zzzzz
# echo "$x | $y | $z"
23232 | xxxxx | ewewewe ssssss zzzzz
複製程式碼
收集相關資訊
我們光收集歷史命令是沒什麼用的,還應該收集如下資訊:
- 命令執行的時間
- 執行命令時所在的目錄
- 當前執行命令的使用者
- 登入的使用者(登入後可能會 su 切換到其他使用者)
- 使用者所在的 tty(可能會同時開多個 shell)
- 登入的 ip
- 執行的命令
以上這些資訊都需要執行命令來獲取,組合起來就是這樣的:
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 命令
我們拿到這些資訊之後肯定不能只是將其輸出,而是將其存放在一個檔案中。如果直接將其追加到一個檔案中,是會有問題的。作業系統上肯定不止一個使用者,不同的使用者都會執行命令,那麼這個日誌檔案的屬主屬組應該改成啥?日誌檔案要不要切割?要不要刪除?這些都是要考慮的問題。
雖然我們最終都是要輸出到檔案中,直接追加的方式雖然簡單,但是不好控制。最好的方式是使用 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 自動新增的,後面才是我們傳送過去的內容。
升級 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 沒有關係。
簡單的解釋下它的作用:
- version=2 必須處於第一行,並且這一行只能是這幾個字元,不能加任意字元進去。它表示使用的是 v2 引擎,官方推薦使用 v2,但是 v2 不一定比 v1 功能更豐富,但是我們用夠了。如果沒寫,或者寫錯了,將使用 v1 引擎;
- 規則的寫法就是
rule=
,它後面的冒號:
用來分割 tag 的,也就是說等號和冒號之間可以加上 tag。我們不需要 tag,但是得把冒號寫上; - 冒號
:
就是欄位解析了,要解析的欄位使用百分號%
包起來。百分號中通過冒號:
進行分割,冒號前是欄位的名稱(json 中的物件名),冒號後是 Liblognorm 內建的欄位型別,欄位型別後面可以加引數,使用中括號{}
引用,只是上面沒有使用,每個欄位的引數都不一樣,有的有,有的沒有; - 百分號中允許存在空格和換行符,這樣就可以寫成多行,而不用都寫在一行,看起來更美觀;
- 使用 Liblognorm 進行解析時,空格是一對一的。假如兩個欄位間有三個空格,那麼寫解析規則時,兩個百分號之間必須要有三個空格。有時你無法確定空格數量怎麼辦?使用 whitespace 這種型別;
- 欄位型別(這裡只列出常用的,更多的看官方文件即可):
- word:空格外的任意字元,也就是看到空格後就終止匹配;
- whitespace:匹配所有空格,直到碰到第一個非空格字元。也就是在有不止一個空格的情況下使用它非常合適;
- date-rfc3164:rsyslog 的時間欄位;
- ipv4:ipv4 地址;
- rest:直接匹配到行尾;
- -:匹配但不顯示,它一般用於丟棄欄位,比如它和 whitespace 型別配在一起就非常合適。
測試一把解析庫:
# 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
複製程式碼