這些年我新學到不少資安名詞,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_CHANNELSLACK_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裝置

Post a comment