在家做 SIEM - 讓家用無線路由器即時發送資安事件
| | | 3 | |
這些年我新學到不少資安名詞,SIEM (Security Information and Event Management,安全性資訊與事件管理)是其中之一,指的是即時收集、彙總及分析來自全組織應用程式、裝置、伺服器和使用者的大量資安相關資料,合併到單一整合式平台,以提供組織安全性態勢的全方位檢視,讓安全性作業中心 (Security Operation Center, SOC)(這是另一個資安名詞)能夠快速且有效地偵測、調查及回應安全性事件。
身為平日疑神疑鬼、老擔心家裡無線路由器正被人偷連的網路被害妄想症患者 (我甚至幹過清查家裡所有平板手機 MAC 地址 + IP 造冊列管的瘋事,不意外地被家人當成有病),在家裡搞一套 SIEM 也是很合情合理滴。我想做到每次有新裝置連上無線路由器、用 OpenVPN 登入,要能第一時間接到通知。企業級的地端 SIEM 產品如 Splunk、QRadar 價格不斐,最近家用迷你伺服器剛升級 Debian 12,剛好可以跑 syslog-ng Docker 接收 Asus RT-AC66U syslog 訊息,再寫幾行程式串接 Slack 發即時通知,不花一毛錢就能搞出一套簡單的資安事件即時通知機制。
先說原理,Asus 路由器支援將所有 Log 轉發到指定的 syslog 伺服器:

syslog 是一個古老的日誌傳輸與格式協定,裝置會傳送標註 facility(事件類別) 與 severity(嚴重度 0–7) 的訊息,並附帶時間與來源位址以便後續查詢與告警。在 Unix/Linux 系統上,可執行 rsyslog、syslog-ng 或 journald 等服務透過 UDP 514 (輕量但不保證送達)、TCP 514/601 或加密的 TLS (常見 6514) Port 接收第三方裝置送來的 Log。經過評估,syslog-ng 可用 Docker 架設,效能跟可擴充性不錯且使用者眾多,其來源/過濾/目的地的管線設計挺直覺,定義語法雖然得花點時間熟悉,但「遇到特定關鍵字可拋給外部自訂程式處理」的架構十分彈性,用 Bash、PowerShell、.NET 寫點程式再複雜的偵測與判斷邏輯都能實現,就決定是它了。
我先用下面的 docker-compose.yaml 宣告 Docker 容器在 Debian 上跑 syslog-ng 服務,聽 514/601/6514 Port 接收遠端裝置的 syslog,/config 對映設定檔目錄放 syslog-ng.conf 設定檔、/var/log 對映 Log 目錄,再多設一個 /batch 來放自訂的訊息處理程式,SLACK_CHANNEL、SLACK_BOT_TOKEN 環境變數則用來發送 Slack 訊息用的:
---
services:
syslog-ng:
image: lscr.io/linuxserver/syslog-ng:latest
container_name: syslog-ng
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- LOG_TO_STDOUT= #optional
- SLACK_CHANNEL=資安通知
- SLACK_BOT_TOKEN=xoxb-...<slack-api-key>...
volumes:
- ./config:/config
- ./log:/var/log
- ./batch:/batch
ports:
- 514:5514/udp
- 601:6601/tcp
- 6514:6514/tcp
restart: unless-stopped
用 Slack 訊息我用得蠻多的,之前寫過 .NET Console 版 和 ASP.NET Minimal API 版,有 Github Copilot 撐腰,這回我嘗試直接用 Bash 腳本寫,再次擴展程式開發經驗:(註:Bash 腳本功能強大,實現各式邏輯都不成問題,但程式語法跟 JavaScript/C# 差異頗大,要上手得花點功夫,所幸現在寫 Bash 的苦差事可以交給 Copilot,我們學著看懂就好)
SLACK_TOKEN="${SLACK_BOT_TOKEN:-}"
CHANNEL="${SLACK_CHANNEL:-}"
usage() {
echo "Usage:"
echo " $0 \"Your message\" # uses SLACK_CHANNEL env var"
echo " $0 \"#channel\" \"Your message\" # specify channel as first arg"
echo " echo \"Your message\" | $0 \"#channel\" # read message from stdin"
echo "Environment:"
echo " SLACK_BOT_TOKEN - required bot OAuth token (xoxb-...)"
echo " SLACK_CHANNEL - optional default channel (used when only one arg)"
exit 1
}
if [ -z "$SLACK_TOKEN" ]; then
echo "Error: SLACK_BOT_TOKEN is not set."
usage
fi
# Check if input is coming from pipeline
if [ ! -t 0 ]; then
# Read from stdin (pipeline)
MESSAGE=$(cat)
if [ "$#" -ge 1 ]; then
CHANNEL="$1"
else
if [ -z "$CHANNEL" ]; then
echo "Error: Channel not provided and SLACK_CHANNEL is not set."
usage
fi
fi
else
# Read from command line arguments (existing logic)
if [ "$#" -eq 0 ]; then
usage
elif [ "$#" -ge 2 ]; then
CHANNEL="$1"
shift
MESSAGE="$*"
else
MESSAGE="$1"
if [ -z "$CHANNEL" ]; then
echo "Error: Channel not provided and SLACK_CHANNEL is not set."
usage
fi
fi
fi
# simple JSON-escaping (handles backslashes, quotes, CR/LF)
esc=${MESSAGE//\\/\\\\}
esc=${esc//\"/\\\"}
esc=${esc//$'\r'/\\r}
esc=${esc//$'\n'/\\n}
PAYLOAD="{\"channel\":\"${CHANNEL}\",\"text\":\"${esc}\"}"
# send message
RESP=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data "$PAYLOAD")
if echo "$RESP" | grep -q '"ok":true'; then
echo "Message posted"
exit 0
else
echo "Slack API error"
echo "$RESP"
exit 2
先用 OpenVPN 連線事件當偵測對象,我鎖定以下三種事件,遇到特定關鍵字時將整條 Log 發送到 Slack 頻道,便能實現即時通知。
- 建立連線:Sep 21 13:32:41 vpnserver1[23420]: TCP connection established with [AF_INET6]::ffff:168.192.1.1:12345
- 登入失敗:Sep 21 13:32:42 vpnserver1[23420]: 168.192.1.1:12345 TLS Auth Error: Auth Username/Password verification failed for peer
- 登入成功:Sep 21 14:37:12 vpnserver1[16894]: client/168.192.1.1:12345 TLS: Username/Password authentication succeeded for username 'user'
要實現以上邏輯,./config/syslog-ng.config 的寫法如下:
@version: 4.2
source s_net {
udp(ip("0.0.0.0") port(5514)); # 設定接收 UDP 5514 及 6601
syslog(ip("0.0.0.0") transport("tcp") port(6601));
};
destination d_file { # 將所有 Log 以日期分檔、月份分目錄保存備查
file("/var/log/${YEAR}${MONTH}/${YEAR}-${MONTH}-${DAY}.log"
create_dirs(yes)
owner("1000") group("1000") perm(0644)
template("$ISODATE $MSG\n")
template_escape(no)
);
};
filter f_openvpn { # 設定過濾器,偵測訊息是否出現特定關鍵字
(message("TCP connection established with") or
message("TLS: Username/Password authentication succeeded") or
message("TLS Auth Error"))
};
destination d_post_slack { # 若符合過濾條件,將 Log 傳送給指定 Bash 腳本發送 Slack 通知
program("/batch/post-loop.sh");
};
destination d_debug_file { # 偵錯用 Log,可用於輸出符合過濾條件的 Log,方便確認比對是否成功
file("/var/log/debug.log"
owner("1000") group("1000") perm(0644)
template("$ISODATE $MSG\n")
template_escape(no)
);
};
log { # 設定 Pipeline
source(s_net); # 來源 UDP 5514 /TCP 6601
destination(d_file); # 一律寫入日期分檔 Log
if {
filter(f_openvpn); # 比對符合過濾條件才繼續往下執行
# destination(d_debug_file);
destination(d_post_slack); # 發送通知
};
};
# echo "<14>Test UDP syslog message" | nc -u -w5 localhost 514
/batch/post-loop.sh 的寫法較特殊,要寫成無窮迴圈持續接收 stdin 傳入的內容,我原以為是一條訊息呼叫一次,搞錯觀念在這裡卡了一陣子。另外有個小眉角是每隔 20 分鐘 syslog-ng 會發送類似 Heartbeat 的 MARK -- 訊息,要忽略以免收到無意義的通知。
#!/bin/bash
while read line ; do
# if it contains "MARK --" heartbeat, ignore it
[[ "$line" == *"MARK --"* ]] && continue
echo $line | /batch/post-slack-msg.sh
done
就醬,現在只要有人嘗試連線 OpenVPN 時我都會在 Slack 接到即時通知 (OpenVPN 預設 3600 秒自動重新協商 TLS 加密金鑰,持續連線時每小時會定期收到成功登入記錄),甚至意外在週日晚上偵測到 AbusedIPDB 通報在案的可疑 IP 跑來掃 Port:

我的家用型土砲 SIEM 成功邁出第一步,哈!
Comments
# by Kayson
黑大: SIEM (Security Enformation and Event Management,安全性資訊與事件管理) Enformation是不是Information
# by Jeffrey
to Kayson, YES, 打錯了~ 謝提醒
# by Raven
前幾個月也是在徹查wifi連線列表然後查到一個不知名MAC 用Raspi + nmcli 才找出來是甚麼IoT裝置