SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Linux 伺服器指令稽核:如何記錄使用者操作

Shell history 並非可靠的稽核軌跡。本文比較 sudo 日誌、工作階段錄製、shell hooks 與 auditd execve 規則的差異,並教您如何將記錄即時傳送至外部伺服器,確保稽核資料不被竄改。

伺服器上使用者執行指令的實際記錄方式

若要稽核使用者在伺服器上執行的指令,您需要一份使用者無法編輯的記錄。Shell history 並非此類記錄。它僅是便利性質的檔案,由寫入該檔案的帳號所擁有,任何能在該 shell 輸入指令的人都可以將其關閉或刪除。

有四個層級可以保存真正的記錄,且每一層都有其成本。sudo 會將每個指令寫入一行至 syslog。sudo I/O logging 可擷取單一帳號的完整工作階段。如 PROMPT_COMMAND 這類的 shell hook 可記錄互動式 bash 使用者輸入的內容。核心稽核子系統會記錄 execve 系統呼叫本身,這也是為何它是唯一能觀察到每個處理程序的層級。本指南將逐步說明這些層級,指出各層的限制,並以決定這些記錄是否有價值的關鍵步驟作結:在受稽核者接觸到記錄前,將其傳送至機器外部。

開始前請注意:稽核子系統屬於核心層級的工作,因此無法在共享宿主核心的容器內進行測試。請在核心由您掌控的 KVM VPS 上執行這些指令。

為什麼 shell history 不能作為稽核軌跡

~/.bash_history 無法作為證據,原因有四點,且完全不需要攻擊者具備高超技巧。

它屬於使用者。 該檔案權限為 600 且由該帳號擁有,因此 rm ~/.bash_history 完全不需要任何權限即可修改。直接使用編輯器開啟並刪除關鍵的 20 行紀錄也同樣輕而易舉。

它在 shell 結束時寫入。 若連線以 kill -9 $$ 結束,或因連線中斷而終止,則不會寫入任何內容。在 exit 之前執行 history -c 效果相同,看起來就像什麼都沒發生過。

只需一個指令即可關閉。 unset HISTFILE 會停止該 session 寫入檔案。set +o history 會立即停止紀錄。HISTCONTROL=ignorespace 會隱藏所有開頭帶有空白的指令。這些功能皆包含在 man bash 中,因為其設計初衷就是由使用者自行控制。

它紀錄的是輸入內容,而非執行內容。 若使用了 alias 或 shell function,檔案中的文字並非核心實際執行的程式。

此外,除非在寫入項目時已設定 HISTTIMEFORMAT,否則不會有時間戳記,因為 bash 僅在設定該變數時才會寫入 #1755043200 標記行。

在共用登入的情況下,它也無法辨識使用者身分。三個人使用同一個 deploy 帳號,會產生一個交錯的檔案,且歸屬於同一個 uid。當兩個人共用一個 uid 時,沒有任何日誌層級能將操作歸咎於特定個人,這也是 每人使用一個無特權帳號而非共用登入 的實務論點。

Shell history 在其本職工作上表現良好,即協助您重新輸入昨天的指令。請將其視為提示,絕不可將其作為證據。

sudo 記錄的內容與其限制

sudo 會將其執行的每一條指令記錄至 authpriv syslog 工具。

sudo grep 'sudo:' /var/log/auth.log | tail -5
journalctl -t sudo -n 5

每一行記錄都會標註使用者、終端機、工作目錄、目標使用者以及執行的指令:

sudo:    alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/apt update

/var/log/auth.log 不存在,表示該映像檔未安裝 rsyslog,相關記錄僅會存在於 journal 中。在依賴 journal 之前,請先確認其並非 volatile(暫存)模式:

journalctl --list-boots

若僅列出當前開機的記錄,表示 /var/log/journal 不存在,這意味著 journal 儲存在 /run 中,所有記錄會在下次重新開機時消失。請將其改為持續儲存:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

接下來是其限制。sudo 僅記錄它被要求執行的指令,不會記錄該指令後續的行為。因此,單一行記錄即為追蹤的終點:

sudo -i

日誌只會針對 shell 產生一筆記錄。在該 root shell 內輸入的任何指令對 sudo 而言都是不可見的,因為 sudo 已不在執行路徑中。sudo su -sudo bash 以及 sudo vim /etc/shadow 後接 :!bash 的情況皆是如此。若 sudoers 規則允許任何具備 shell escape 功能的程式(例如 vimfind),該規則即等同於授予未經記錄的 root 權限。在信任日誌記錄之前,請務必先確認該帳號實際可存取的範圍:

sudo -l -U alice

記錄單一帳號的完整連線階段

首先確認您使用的是哪種 sudo,因為 Rust 重寫版本並不具備此功能:

sudo --version | head -1

若輸出顯示為 sudo-rs,請跳過本節並改用 audit 子系統。Ubuntu 在 25.10 與 26.04 版本的官方文件中,已列出 I/O logging 與 sudoreplay 為不支援項目,此狀況截至 2026 年 8 月依然適用。這點至關重要,因為 sudo-rs 是上述版本中的預設 sudo,升級可能會導致您原先依賴的控制機制失效。在規劃任何基於 sudo 的記錄方案前,建議先閱讀 sudo-rs 行為變更完整列表

若使用 Ubuntu 24.04 LTS 內建的原始 sudo,可針對單一帳號啟用 I/O logging:

sudo visudo -f /etc/sudoers.d/iolog
Defaults:deploy log_output
Defaults!/usr/bin/sudoreplay !log_output

請使用 visudo 而非一般編輯器,因為它會拒絕儲存語法錯誤的檔案。損毀的 sudoers 檔案會導致所有使用者無法使用 sudo。接著即可重播連線階段:

sudo sudoreplay -l user=deploy
sudo sudoreplay 000001

sudoreplay -l 會列出所有連線階段及其 ID;若 log_output 從未套用於該使用者,則不會顯示任何內容。其代價在於:終端機傳輸的每一個位元組都會儲存在 /var/log/sudo-io 下,因此詳細的連線階段會佔用大量空間。第二行 sudoers 設定可避免重播動作本身被重複記錄。真正的風險在於機敏資訊,因為 I/O log 會記錄輸入與輸出的所有內容,包含在連線階段中輸入的密碼,因此該檔案需具備與密碼儲存庫同等級的保護。此外,其涵蓋範圍有限,僅能記錄透過 sudo 執行的指令。若使用者登入後全程以自身權限操作,則完全不會被記錄。

Shell hook 與其繞過方式

網路上流傳的「記錄所有指令」方案,是將 PROMPT_COMMAND hook 放入 /etc/profile.d/

# /etc/profile.d/00-cmdlog.sh
PROMPT_COMMAND='logger -p local6.info -t cmdlog "$(whoami) $$ $(history 1 | sed "s/^ *[0-9]* *//")"'

bash 會在繪製每個提示字元前執行 PROMPT_COMMAND,因此指令在輸入時就會寫入 syslog,而非等到結束時才寫入;此外,logger 透過系統日誌守護行程寫入,因此不會受到使用者檔案權限的限制。開啟新的登入 shell 並使用 sudo tail -f /var/log/syslog 進行驗證,若系統沒有 rsyslog,則使用 journalctl -t cmdlog -f

然而,此方法會失效,以下五種方式皆可在幾分鐘內重現:

  • 非互動式 shell 不會繪製提示字元。ssh you@server 'id' 執行指令後隨即返回,由於 PROMPT_COMMAND 從未被執行,因此不會留下任何記錄。
  • 這只是一個變數。unset PROMPT_COMMAND 可在當前 session 剩餘時間內停用該變數,且無需任何權限。
  • 該檔案僅由登入 shell 讀取。bash --noprofile --norc 完全不會載入 /etc/profile.d/
  • 這是 bash 專屬的。在 vim 內使用 zshshpython3 -c 'import os; os.system("id")':!id 執行的程式,bash 的提示字元 hook 皆無法偵測。
  • 它記錄的是輸入的原始字串,因此 alias 或 function 仍可隱藏實際執行的指令。

請將 shell hook 視為一種便利工具。對於配合的使用者,它能回答「我上週二執行了什麼指令」的問題。切勿將其視為稽核控制手段。

核心審核子系統會監控所有 execve 系統呼叫

Linux 審核子系統由 auditd daemon 驅動,這是此處唯一使用者無法繞過的層級,因為記錄是在系統呼叫執行當下於核心內部產生的。只要處理程序執行程式,就會產生事件。無論使用何種 shell、程式語言或是否具備終端機,結果皆相同。

sudo apt update && sudo apt install -y auditd audispd-plugins
sudo systemctl enable --now auditd
sudo auditctl -s

auditctl -s 會顯示 daemon 的狀態。若 enabled 1pid 非零,代表服務正在執行;若 lost 0 為零,則表示尚未遺失任何記錄。請記住 lost 計數器,稍後會再用到。

auid 是讓審核功能具備價值的關鍵欄位。 當工作階段開始時,PAM 會設定登入 uid,核心會將其傳遞給後續所有的子處理程序。請檢查您的數值:

cat /proc/self/loginuid

互動式 SSH 工作階段會顯示您的 uid,因為 /etc/pam.d/sshd 包含了 pam_loginuid.so。數值 4294967295 代表 loginuid 從未被設定,這對於開機時由系統 daemon 啟動的處理程序而言是正常的。重點在於 sudo -i 不會改變此數值:由 alice 開啟的 root shell 仍會保留 auid 1000,因此其中的每一道指令皆可歸咎於 alice。這正是 sudo 留下的稽核漏洞。若要變更已設定的 loginuid,需要 CAP_AUDIT_CONTROL 權限,一般使用者並不具備;而 sudo auditctl --loginuid-immutable 則會將此權限封鎖,即使是 root 也無法變更,直到下次重新開機為止。

請確認 /etc/pam.d/sshd/etc/pam.d/login/etc/pam.d/cron 皆包含 pam_loginuid.so,否則產生的事件將無法關聯到任何使用者。這與您在 強化 VPS 的 SSH 存取 時所修改的檔案清單相同,建議將這兩項工作一併完成。

auditd 的初始規則集

規則存放在 /etc/audit/rules.d/*.rulesaugenrules 會依檔案名稱順序將規則串接成單一列表,且順序決定了行為,因為核心會在匹配到第一條規則時停止。在新增任何規則前,請先閱讀現有內容,因為後續檔案中的 -D 會清除先前載入的所有規則。

ls /etc/audit/rules.d/
cat /etc/audit/rules.d/audit.rules

接著寫入 /etc/audit/rules.d/50-exec.rules

## Suppressions first: the kernel takes the first matching rule.
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-deb
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-query
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-split
-a never,exit -F arch=b64 -S execve -F exe=/usr/bin/dpkg-trigger

## Every program started by a logged-in human.
-a always,exit -F arch=b64 -S execve,execveat -F auid>=1000 -F auid!=unset -k exec
-a always,exit -F arch=b32 -S execve,execveat -F auid>=1000 -F auid!=unset -k exec

## Changes to who may become root.
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity

## Changes to the audit configuration itself.
-w /etc/audit/ -p wa -k auditconfig

載入並確認:

sudo augenrules --load
sudo auditctl -l

auditctl -l 若能印出您的規則,代表規則已生效。No rules 代表載入失敗,而 journalctl -u auditd -n 20 會指出解析器拒絕的檔案與行號。較舊的 audit 使用者空間工具不支援 unset 關鍵字。若載入器回報該欄位錯誤,請改寫為 -F auid!=4294967295,兩者代表相同的值。

現在讀取事件:

sudo ausearch -k exec -ts recent -i | tail -40
sudo ausearch -ul 1000 -ts today -i
sudo aureport -k --summary -i

-i 會將 UID 與系統呼叫編號轉換為名稱,實務上不可或缺。-ts recent 涵蓋過去 10 分鐘的紀錄。每次執行會產生一組紀錄:包含 UID、AUID、結束狀態與金鑰的 SYSCALL 紀錄、包含完整參數列表的 EXECVE 紀錄,以及用於上下文的 CWDPATH 紀錄。

有一個誠實的限制,因為它常讓使用者困惑。audit 紀錄的是系統呼叫,而 shell 內建指令不會產生額外的系統呼叫。cd /root 不會執行任何程式。在 bash 提示字元輸入 echo evil >> /etc/passwd 也不會執行程式,因為 echo 與重新導向皆在已執行的 shell 程序內完成。因此,execve 規則用來觀察程式,而 -w 規則用來觀察寫入動作。單獨使用其中一種皆不足夠。

最後,鎖定設定:

## /etc/audit/rules.d/99-finalize.rules
-e 2

-e 2 會使規則集變為不可變,直到下次重新開機。載入後,auditctl -s 會回報 enabled 2,此時任何新增或刪除規則的嘗試都會失敗並顯示 Operation not permitted,即使是 root 權限也一樣。請將此檔案放在最後載入,並預期每次修改規則時都必須重新開機。這種權衡正是重點所在:若任何人都能輕易關閉規則集,它便無法作為證據。

沒人閱讀的稽核日誌僅是合規性的裝飾品

auditd 的失敗模式並非遺漏事件,而是記錄了過多資訊導致無人查閱。最終,日誌的存在僅是為了滿足檢查清單,而非用於解決實際問題。

在調整任何設定前,請先在您的機器上進行計算:

sudo aureport -k --summary -i
sudo du -sh /var/log/audit

單一 sudo apt upgrade 會執行數千個短暫存在的行程,且每個行程都帶有您的 auid,因此一次套件更新產生的日誌量可能超過一週的人工輸入總和。這就是為什麼上述排除規則指定了 dpkg 及其輔助程式。請務必依執行檔進行排除,切勿依使用者排除:針對 /usr/bin/dpkg 的排除規則是一個可以用一句話描述的漏洞,而針對帳號的排除規則,則會形成一個與您試圖監控的目標完全吻合的漏洞。

每條規則上的 -k 鍵值是讓日誌在一個月後仍可被檢索的關鍵。ausearch -k sudoers 是一個有明確答案的問題。沒有過濾器的 ausearch 只是一堵文字牆,會讓您養成不再閱讀的習慣。如果您的收集器需要 JSON 格式而非原生格式,laurel 是一個 auditd 外掛程式,它會將每個事件重寫為包含已解碼參數的單一 JSON 物件。它像其他外掛程式一樣在 /etc/audit/plugins.d/ 中註冊,而 auditd 會在 sudo pkill -HUP auditd 時載入外掛程式的變更。

auditd 的真實成本

每個符合條件的系統呼叫(syscall)都會產生一筆記錄,由核心格式化後傳送至使用者空間。其成本體現在兩個面向,且兩者皆可針對您的工作負載進行測量,而非盲目參考他人的數據。

  • CPU 與延遲。頻繁執行 fork 的機器(如建置主機或 CI runner)會為每個 exec 產生一筆記錄。當核心的 backlog 填滿時,--backlog_wait_time 會強制暫停產生事件的處理程序,直到空間釋出為止;因此 audit 的影響通常表現為建置變慢,而非 CPU 使用率飆升。請在實際負載下監控 sudo auditctl -s 中的 backloglost。若 lost 持續上升,代表記錄已遺失;一份存在缺漏的日誌比沒有日誌更糟,因為您仍會誤信其內容。
  • 磁碟。請閱讀 /etc/audit/auditd.conf 並審慎決定磁碟空間耗盡時的處理方式,因為預設值僅供參考。max_log_filenum_logsmax_log_file_action 用於控制輪替(rotation)。space_left_actionadmin_space_left_actiondisk_full_action 用於控制緊急狀態,其中部分可用動作(包含 haltsingle)會導致機器關機,以確保記錄不遺失。

/etc/audit/rules.d/audit.rules 中的 -f 設定行代表核心層級的相同決策:-f 1 會將 audit 失敗回報至 syslog,而 -f 2 則會引發核心 panic。僅在您確實認為「失去伺服器比遺失記錄更嚴重」時,才選擇 2。對於執行關鍵服務的 VPS,請改用輪替機制,並將儲存問題移至外部處理。

將日誌即時傳輸至外部

這正是事故報告不斷證實的關鍵。若日誌留在被入侵的主機上,入侵者即可隨意竄改。root 權限可以重寫 /var/log/auth.log、刪除 /var/log/audit/audit.log 並停止該 daemon。-e 2 能防止規則被卸載,但對 rm 無能為力。上述所有防護層,唯有在日誌副本先行離開主機後,才能確保證據有效。

Audit 本身的傳輸機制是來自 audispd-pluginsaudisp-remote 外掛。請在 /etc/audit/plugins.d/au-remote.conf 中啟用它:

active = yes
direction = out
path = /usr/sbin/audisp-remote
type = always
format = string

在重新載入前,請務必檢查 path 對應的 command -v audisp-remote 是否正確,因為路徑錯誤將導致無法產生任何記錄,僅會在 journal 中留下一行錯誤訊息。在 /etc/audit/audisp-remote.conf 中設定 remote_serverport,並在收集器端於其對應的 auditd.conf 中設定 tcp_listen_port = 60。使用 sudo pkill -HUP auditd 進行重新載入。在許多映像檔中,systemctl restart auditd 會被拒絕,因為單元檔案設定了 RefuseManualStop=yes,因此發送訊號才是可靠的做法。

另一種選擇是將 Audit 納入您既有的 syslog 轉發串流中。active = no 隨附了 /etc/audit/plugins.d/syslog.conf。將其設定為 yes 並重新載入,Audit 事件便會與 sudo 等其他日誌合併。接著透過 TLS (transport layer security) 使用 rsyslog 轉發所有日誌,這需要 rsyslog-gnutls 套件:

# /etc/rsyslog.d/60-forward.conf
*.* action(type="omfwd"
    target="logs.example.net" port="6514" protocol="tcp"
    StreamDriver="gtls" StreamDriverMode="1"
    StreamDriverAuthMode="x509/name"
    StreamDriverPermittedPeers="logs.example.net"
    action.resumeRetryCount="-1"
    queue.type="linkedList" queue.filename="fwd" queue.saveOnShutdown="on")

佇列設定是其中的重點。action.resumeRetryCount="-1" 會無限期重試,而透過 queue.saveOnShutdown="on" 啟用的磁碟輔助佇列,能在收集器無法連線時暫存記錄,並在恢復連線後發送。若缺少這兩項設定,收集器重啟將導致證據出現斷層,且您將無法得知斷層的存在。設定完成後請執行 sudo systemctl restart rsyslog,並在信任該機制前,務必確認記錄確實已抵達收集器。

最後還有一點必須補強:收集器所在的機器,必須是受稽核人員無法登入的環境。若同一組管理員同時擁有日誌伺服器的 root 權限,您僅是複製了檔案,並未達到保護目的。請務必區隔憑證、金鑰,理想情況下應使用不同的供應商帳號。這與為何要預先建立 集中管理多台 Linux 伺服器的方法 是相同的邏輯;這也是當您 處理受入侵的 VPS 時,能否在第一小時內有效應對的關鍵差異。

驗證一般使用者無法覆寫記錄

請直接測試此聲明,而非預設其正確。請使用無 sudo 權限的一般帳號執行:

echo test >> /var/log/auth.log
cat /var/log/audit/audit.log
auditctl -D
id -nG

預期結果依序為:Permission denied,因為 auth.log 的擁有者為 syslog,群組為 adm,且權限模式為 640;再次出現 Permission denied,因為稽核日誌的模式為 600 且擁有者為 root;拒絕執行的錯誤訊息,因為變更稽核規則需要 CAP_AUDIT_CONTROL;以及一個既不包含 adm 也不包含 systemd-journal 的群組列表。

最後一項檢查是常見的失敗點。成為 adm 成員可獲得 /var/log/auth.log 的讀取權限,而成為 systemd-journal 成員則可讀取整個 journal。兩者皆不提供寫入權限,因此皆無法進行竄改。這兩者僅允許使用者讀取系統上所有的驗證記錄行,這應是經過審慎評估後的決定,而非僅是從論壇回答中複製 usermod -aG 行。

最後,確認兩項必須在重新開機後持續生效的設定:

sudo auditctl -s
systemctl is-enabled auditd

enabled 2 表示規則集已鎖定,直到下次開機為止。第二個指令回傳 enabled 表示 auditd 會在開機後自動啟動。若規則集僅能維持到下次核心更新,則無法作為稽核軌跡使用。

FAQ

如何查看特定使用者執行過的所有指令?

使用 id -u alice 找出該使用者的 uid,接著透過 login uid 搜尋稽核日誌:sudo ausearch -ul 1000 -ts today -i。加入 -k exec 可將範圍限制在 execve 規則。Login uid 在登入時設定,且在執行 susudo -i 後仍會保留,因此這能捕捉到該帳號開啟的 root shell 中所執行的指令。此方法僅適用於規則載入後執行的指令,因為 audit 不會記錄未設定監控的歷史事件。若想先了解資料概況,可使用 sudo aureport -k --summary -i 查看各規則的計數。

使用者可以刪除 bash 歷史記錄來隱藏執行過的指令嗎?

可以,且不需要任何權限。~/.bash_history 由該使用者擁有且權限為 600,因此他們可以編輯、截斷或刪除該檔案。他們也可以透過 unset HISTFILE 停止寫入記錄、使用 set +o history 在工作階段中途停止記錄,或在設定 HISTCONTROL=ignorespace 時,於指令前加上空格來隱藏特定指令。Bash 會在 shell 結束時寫入檔案,因此若工作階段以 kill -9 $$ 強制終止,則不會留下任何記錄。請將 shell 歷史記錄視為參考線索,而非證據。

sudo 會記錄 sudo -i 內部的操作嗎?

不會。sudo 只會記錄它被要求執行的指令,因此 sudo -i 只會產生一行關於 shell 的記錄,之後便無任何資訊。在該 root shell 中輸入的每一條指令對 sudo 而言都是隱形的,因為 sudo 已不再參與其中。sudo su -sudo bash 以及任何允許 shell escape 的程式行為皆相同。有兩種方式可彌補此缺口:對 execve 設定稽核規則(記錄每個帶有原始 login uid 的程式),以及在 sudoers 規則中避免直接提供 shell 權限。

auditd 會拖慢伺服器速度嗎?

這完全取決於您的工作負載啟動了多少處理程序,請進行實際測量而非憑空猜測。主要處理請求的伺服器執行程序較少,幾乎不會有感覺。但建置主機或 CI runner 會頻繁執行程序,可能會產生顯著影響,因為當核心的 audit backlog 滿載時,產生事件的處理程序會被暫停,直到空間釋出為止。請在實際負載下執行 sudo auditctl -s 並觀察 backloglost。任何大於零的 lost 都代表記錄遺失,這是最糟的情況,因為這意味著日誌出現了無法追蹤的斷層。

稽核日誌應該儲存在哪裡?

應儲存在另一台機器上,並設定秒級的延遲。任何取得該稽核主機 root 權限的人都可以刪除 /var/log/audit/audit.log 並改寫 /var/log/auth.log,因此本機副本只能回答關於無人試圖隱藏的事件。請使用 audisp-remote 外掛程式將日誌轉發至中央 auditd,或啟用 audit syslog 外掛程式,並透過 rsyslog 以 TLS 轉發整個 syslog 串流。請為收集器提供專屬憑證,並確保被稽核的帳號無法存取該收集器。