> nginx / Apache access log 告警
幫你的 access log養一隻看門狗。
httpdog 一行一行讀 nginx 與 Apache 的 access log,一看到不對勁的請求就通知你:有人在掃 /.env、連續登入失敗、突然一堆 5xx。
- 單一靜態執行檔,約 4 MB
- 記憶體約 5 MB
- 不需要資料庫
- MIT 授權
- 1listen: 127.0.0.1:5514
- 2instance: web-01
- 3
- 4notifiers:
- 5 console: { type: stderr }
- 6 ops:
- 7 type: webhook
- 8 url_env: HTTPDOG_WEBHOOK_URL
- 9 secret_env: HTTPDOG_WEBHOOK_SECRET
- 10
- 11rules:
- 12 # someone is looking for leaked config files
- 13 - id: sensitive-file-probe
- 14 when:
- 15 any:
- 16 - { field: uri, op: suffix, value: /.env }
- 17 - { field: uri, op: prefix, value: /.git/ }
- 18 - { field: uri, op: regex, value: '\.(sql|bak)$' }
- 19 group_by: remote_addr
- 20 cooldown: 10m
- 21 notify: [console, ops]
- 22 message: "{remote_addr} tried to access {uri} (status {status})"
- 23
- 24 # the same IP fails to log in 20 times within 60 seconds
- 25 - id: login-bruteforce
- 26 when:
- 27 all:
- 28 - { field: uri, op: prefix, value: /login }
- 29 - { field: request_method, op: eq, value: POST }
- 30 - { field: status, op: in, value: [401, 403] }
- 31 group_by: remote_addr
- 32 threshold: { count: 20, window: 60s }
- 33 cooldown: 10m
- 34 notify: [console, ops]
- 35 message: "{remote_addr} repeated login failures"
- 36
- 37 # slow requests, except the export that is slow by design
- 38 - id: slow-request
- 39 when:
- 40 all:
- 41 - { field: request_time, op: gt, value: 5 }
- 42 - not: { field: uri, op: prefix, value: /export/ }
- 43 cooldown: 5m
- 44 notify: [console, ops]
- 45 message: "{uri} took {request_time}s"
- 等待第一筆符合的請求 ...
進來一行 log,出去一個判斷。
你把「什麼情況值得通知」寫成規則檔,httpdog 把網頁伺服器寫出的每一行 log 拿來比對。它不儲存 log,也沒有查詢介面:符合就通知你,僅此而已。
- 01
網頁伺服器把 access log 送過來
nginx 透過 syslog(UDP)一行一行送,Apache 用管線接。兩者預設的 log 格式都能直接讀,網頁伺服器的設定可以維持原樣。
- 02
httpdog 照你的規則比對
條件可以用 log 的任何欄位,還有門檻(60 秒內 20 次)、分組(每個 IP 各算各的)與冷卻(10 分鐘內最多通知一次)。
- 03
符合就變成一則通知
以有簽章的 JSON 送到你自己的接收端,或是寫一行到系統 log。接收端再慢、甚至掛掉,都不會拖住收 log。
!httpdog 不是 log 儲存系統。要回頭查歷史 log,請搭配 Loki 或 Graylog 這類工具。
告訴它什麼事該叫醒你。
一條規則就是「log 欄位的條件」加上「多久可以通知一次」。整個規則檔在載入時就會完整檢查,打錯字當場指出位置,不會等到半夜三點才發現。
有人在找 /.env、/.git/ 或資料庫備份。單筆請求就觸發,同一個 IP 10 分鐘內最多通知一次。
- id: sensitive-file-probe
when:
any:
- { field: uri, op: suffix, value: /.env }
- { field: uri, op: prefix, value: /.git/ }
- { field: uri, op: regex, value: '\.(sql|bak|tar\.gz)$' }
group_by: remote_addr
cooldown: 10m
notify: [console, ops]
message: "{remote_addr} tried to access {uri} (status {status})"
ALERT [httpdog] sensitive-file-probe | 203.0.113.7 tried to access /.env (status 404) | remote_addr=203.0.113.7
一條規則的組成
- when
- 條件:看哪個欄位、怎麼比、跟什麼比。可以用 all / any / not 組合
- threshold
- 一段時間內累積幾次才通知
- group_by
- 依某個欄位的值分開計算,例如每個 IP 各算各的
- cooldown
- 同一組在這段時間內不重複通知
- notify
- 要送到哪些 notifier
- message
- 通知的文字,{欄位名} 會換成 log 裡的值
12 種比對方式
- eq
- ne
- in
- prefix
- suffix
- contains
- regex
- gt
- gte
- lt
- lte
- exists
6 份可以直接用的規則檔
涵蓋資安探測、登入濫用、站台健康、流量異常與關鍵業務路徑,每一份都是完整的設定。
刻意做得很小。
只做一件事,而且不礙事。
- [ 輕量 ]
單一靜態執行檔
檔案約 4 MB,閒置時記憶體約 5 MB。不需要資料庫、不需要 agent,也沒有其他服務要顧。
- [ 免改設定 ]
直接讀你現有的 log
nginx 與 Apache 預設的 combined 格式可以直接讀。想比對任意欄位時,再改用 JSON 格式。
- [ reload ]
改規則不用重啟
systemctl reload 就生效。新的規則檔寫壞了會沿用舊規則,監控不會因為一個錯字而中斷。
- [ 不阻塞 ]
通知不會拖慢比對
通知在獨立的執行緒送出,每個佇列都有上限。webhook 掛掉不會卡住收 log,也不會吃光記憶體。
- [ 簽章 ]
可以驗證的 webhook
每個請求都帶 HMAC-SHA256 簽章與時間戳。沒設密鑰的 webhook 連載入都過不了。
- [ 嚴格 ]
寫錯在載入時就失敗,不是半夜三點
不認得的鍵、不合法的 regex、型別不對的值、沒設的密鑰,載入時一律擋下。--check 可以只驗證不啟動。
- [ 不信任輸入 ]
log 的內容一律當成惡意的
log 的內容是送請求的人決定的。解析器與規則引擎都做過模糊測試,請求沒辦法在你的系統 log 裡偽造一行。
- [ systemd ]
守規矩的服務
Type=notify、卡死時會重啟的 watchdog、不以 root 執行,而且結束碼會告訴 systemd 什麼時候重啟也沒用。
幾分鐘就能跑起來。
先重播一份範例 log 看規則怎麼觸發,再安裝 Release 的執行檔,把網頁伺服器的 log 接過來。
不需要網頁伺服器:編譯後重播附的範例 log 即可。需要 Rust 1.85 以上。
git clone https://github.com/guanting112/httpdog.git && cd httpdog
cargo build --release
# the example rules notify a webhook; a dummy URL and secret will do for now
export HTTPDOG_WEBHOOK_URL=http://127.0.0.1:9/dummy
export HTTPDOG_WEBHOOK_SECRET=dummy
./target/release/httpdog --stdin --config rules.example.yaml < sample.log
httpdog started: 4 rule(s), reading from stdin
ALERT [httpdog] sensitive-file-probe | 203.0.113.7 tried to access /.env (status 404) | remote_addr=203.0.113.7
ALERT [httpdog] slow-request | /report/monthly took 7.31s
ALERT [httpdog] sensitive-file-probe | 192.0.2.44 tried to access /backup/db.sql (status 404) | remote_addr=192.0.2.44
done: 6 lines, 0 unparseable, 3 alerts fired
你的程式可以信任的告警。
webhook 就是把 JSON 用 POST 送到你指定的網址。告警的每個部分各自一個欄位,接收端可以依規則分流、存起來,或轉成給人看的樣子。
一定有簽章
簽的是時間戳加上實際送出的位元組,接收端可以確認請求真的來自 httpdog,並拒絕被重送的舊請求。
每個鍵都固定出現
不適用的欄位是 null,而不是不見。接收端不用處理「鍵不存在」的情況。
知道是哪一台發的
多台 httpdog 送到同一個接收端時,instance 會告訴你是哪一台。
會重試,但不會沒完沒了
逾時 5 秒,總共試 3 次。都失敗就把告警寫進系統 log,不會一直重送。
X-Httpdog-Timestamp: 1794416274
X-Httpdog-Signature: sha256=1728aa567d7ef419cab2bf10fd5e1c764cdb608bd22d6f4d051b2043db19b8fe
{
"source": "httpdog",
"instance": "web-01",
"version": 1,
"kind": "rule",
"rule": "login-bruteforce",
"timestamp": 1794416274,
"text": "[httpdog] login-bruteforce | 198.51.100.9 repeated login failures | 3x in 60s | remote_addr=198.51.100.9",
"message": "198.51.100.9 repeated login failures",
"group_by": "remote_addr",
"group": "198.51.100.9",
"hits": 3,
"window_secs": 60,
"dropped": null,
"event": { "remote_addr": "198.51.100.9", "uri": "/login", "status": 401 }
}
在真的系統上驗證過。
以下每一台都是拿 Release 的執行檔,在真的 systemd 底下搭配真的 nginx 與 Apache 實際跑過:啟動、reload、當掉後重啟、watchdog、webhook 簽章、overload 告警。
| 發行版 | 架構 | systemd | nginx | Apache | 檢查項目 |
|---|---|---|---|---|---|
| Ubuntu 22.04 | arm64 | 249 | 1.18.0 | 2.4.52 | 53 / 53 |
| Ubuntu 24.04 | arm64 | 255 | 1.24.0 | 2.4.58 | 53 / 53 |
| Ubuntu 24.04 | amd64 | 255 | 1.24.0 | 2.4.58 | 53 / 53 |
| Ubuntu 26.04 | arm64 | 259 | 1.28.3 | 2.4.66 | 53 / 53 |
| Debian 11 | arm64 | 247 | 1.18.0 | 2.4.67 | 53 / 53 |
| Debian 12 | arm64 | 252 | 1.22.1 | 2.4.71 | 53 / 53 |
| Debian 13 | arm64 | 257 | 1.26.3 | 2.4.71 | 53 / 53 |
| Fedora 44 | arm64 | 259 | 1.30.6 | - | 50 / 50 |
| Rocky Linux 10 | arm64 | 257 | 1.26.3 | - | 50 / 50 |
- 測試用的是 OrbStack 的機器,它們共用同一個 kernel;Fedora 與 Rocky Linux 上 SELinux 是關閉的。Apache 只在 Ubuntu 與 Debian 上測過。
- 每次 push 都會對 log 解析器與規則引擎做模糊測試。