EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Security / ปลด CSF block อัตโนมัติ คัด ghost device ออกจาก botnet brute force
Security

ปลด CSF block อัตโนมัติ คัด ghost device ออกจาก botnet brute force

โดย Ton-Or ·20 พ.ค. 2026·อ่าน 11 นาที
ปลด CSF block อัตโนมัติ คัด ghost device ออกจาก botnet brute force

ลูกค้าผมบ่นทุกเช้าว่าใช้เมลไม่ได้ พอ trace ลงไปจึงเจอว่าปัญหาคือเครื่องผีในออฟฟิศตัวเดียว แต่จะปลด block มันออกโดยไม่เผลอปลด botnet ที่ brute force รหัสตัวเดียวกัน ต้องคิดให้รอบ

ในซีรีส์ migration ตอนที่ 5 ผมเล่าเรื่อง CSF BFM auto-unblock ไว้แบบย่อ ๆ พอโพสต์ไปก็มีคน DM เข้ามาถามรายละเอียดเพิ่มหลายคน ผมเลยขอแยกออกมาเขียนเป็นกระทู้ของมันเอง พร้อมเล่าเคสจริงของลูกค้าผมที่เป็นต้นเรื่องของ workaround ชุดนี้

เรื่องนี้ไม่ใช่ทฤษฎี ผมเจอกับตัวบนเซิร์ฟเวอร์ที่ดูแลอยู่ และแก้แบบเดียวกันให้ลูกค้าหลายรายที่อาการเหมือนกัน

ปัญหาเดิม เครื่องผีในออฟฟิศ ทำทั้งออฟฟิศใช้เมลไม่ได้

ลูกค้าบริษัทหนึ่ง สมมติเรียกว่า acme-co ใช้ mail account หลายตัวบนโดเมน @acme-co.example.com อาการคือเกือบทุกเช้า IP ของออฟฟิศจะโดน BFM block แล้วทั้งออฟฟิศใช้เมลไม่ได้ คนในออฟฟิศก็บ่นมาทุกเช้าเหมือนนาฬิกาปลุก

ผม troubleshoot อยู่นานพอควรกว่าจะเข้าใจว่าจริง ๆ แล้วมันมีสองแหล่งที่มา ที่หน้าตาในล็อกดูเหมือนกันมากแต่เป็นคนละเรื่องกันโดยสิ้นเชิง

แหล่งที่ปลดได้

Ghost device ในออฟฟิศ

  • เครื่องในออฟฟิศมี Outlook หรือ iPhone ที่ cache รหัสเก่าค้างไว้
  • เปลี่ยน password แล้วแต่ลืม update บนเครื่องเดียว
  • เครื่องนั้น retry SMTP/IMAP ด้วยรหัสผิดซ้ำ ๆ ทั้งวัน
  • LFD/DA BFM เห็น auth fail เยอะ จึง block public IP ของออฟฟิศ
  • ผลคือคนทั้งออฟฟิศพลอยใช้เมลไม่ได้ทั้งที่ไม่ได้ทำอะไรผิด
แหล่งที่ห้ามปลด

Credential stuffing จาก botnet

  • รหัสเก่าน่าจะหลุดออกไปแล้ว
  • ตอนนี้มี bot กระจายอยู่ทั่วโลก (CN/IN/VN/KR/BR/US)
  • วน brute force รหัสชุดเดิมเข้ามาเรื่อย ๆ
  • อันนี้คือ attacker จริง ห้ามปลด block เด็ดขาด

จุดที่ทำให้งานนี้ยากคือ ทั้งสองฝั่ง fail บัญชีบนโดเมนเดียวกัน ใช้รหัสชุดเดียวกัน ดูจากจำนวน auth fail เฉย ๆ แยกไม่ออก

เป้าหมายของผมจึงชัดเจน auto-unblock IP ของออฟฟิศ (ฝั่ง a) แต่ keep block ตัว attacker (ฝั่ง b) เอาไว้ โดยที่ลูกค้าไม่ต้องรู้สึกว่ามี downtime ตอนเช้าอีก

เคสแบบนี้เหมือนกันแทบทุกร้านที่มี ghost device ค้างอยู่ในออฟฟิศ ผมเลยตัดสินใจเขียนเป็นระบบป้องกัน 4 ชั้น ที่ใช้ logic ตัดสินใจชุดเดียวกัน คือกรองผ่าน 3 เงื่อนไขก่อนถึงจะปลด แต่ตัว trigger ของแต่ละชั้นต่างกันเพื่อให้ catch ได้ครบทุกเส้นทางที่ block เกิดขึ้น

Architecture ภาพรวม 4 ชั้น

ก่อนลงรายละเอียดแต่ละชั้น ขอวางภาพการไหลของเหตุการณ์ให้เห็นก่อนว่า block หนึ่งครั้งมันเดินทางอย่างไร และแต่ละ layer ไปรับมือตรงไหน

เส้นทาง blockbrute force มาSMTP/IMAP fail->LFD triggerLayer 1 hook->DA BFM ตรงbypass LFD->path watcherLayer 3->sweep ตัดสินLayer 2->cron กันพลาดLayer 4

อธิบายเป็นข้อความ เหตุการณ์มาได้สองทาง ทางแรกคือ LFD เป็นคนสั่ง block เอง (จาก LF_SMTPAUTH หรือ DISTATTACK) ทางนี้ LFD จะเขียน csf.tempban แล้วยิง BLOCK_REPORT ออกมา ซึ่ง Layer 1 จะคอยดักตรงนี้

ทางที่สองคือ DA BFM สั่ง csf -d เข้าไปตรง ๆ เขียนลง csf.deny โดยไม่ผ่าน LFD จึงไม่มี hook ใด ๆ ยิงออกมา ทางนี้ผมใช้ systemd path watcher (Layer 3) คอยเฝ้าดูไฟล์ พอไฟล์ถูกแก้ก็เรียก sweep script (Layer 2) ขึ้นมาตัดสินใจทันที และมี cron (Layer 4) เป็นตาข่ายกันพลาดในช่วงเช้าที่ออฟฟิศเปิดเครื่อง

หัวใจการตัดสินใจของ Layer 2 คือ ถ้าผ่านครบทั้ง 3 filter ถึงจะรัน csf -dr ปลด perm ban, csf -tr ปลด temp ban แล้ว csf -ta 86400 whitelist ไว้ 24 ชั่วโมง ถ้าไม่ผ่านก็แค่ log แล้วข้าม ปล่อยให้ block ค้างไว้ตามเดิม

ด่านตัดสินของ Layer 2 ต้องผ่านครบสามข้อถึงจะปลดIP ที่โดนblockด่าน 1block ของเมลdovecot/eximด่าน 2ทุก set_id เป็นโดเมนลูกค้าด่าน 3geoiplookupได้ THผ่านครบcsf -dr / -trcsf -ta 86400ไม่ผ่านข้อใดข้อหนึ่ง คง block ไว้ บันทึก log แล้วข้ามไม่ผ่านไม่ผ่านไม่ผ่านหา geoiplookup ไม่เจอ ให้ abort ทั้งหมด ไม่ปลดเลย (fail-closed)
IP ต้องผ่านทั้งสามด่านถึงถูกปลดและ whitelist 24 ชั่วโมง ไม่ผ่านข้อใดก็คง block ไว้

Layer 1 BLOCK_REPORT hook จับ block ที่ LFD เป็นคนสั่ง

โดย default DA ไม่ได้ตั้ง BLOCK_REPORT script เอาไว้เลย แปลว่าทุก block จะอยู่ครบเวลา ban ของมันโดยไม่มีเงื่อนไขใด ๆ มาปลดให้ ออฟฟิศโดน block ก็ต้องนั่งรอจน ban หมดอายุเอง

ผมเขียนสคริปต์ไว้ที่ /usr/local/sbin/csf-cond-unblock.sh แล้วชี้ให้ CSF เรียกมันผ่าน /etc/csf/csf.conf

BLOCK_REPORT = "/usr/local/sbin/csf-cond-unblock.sh"

เวลา CSF เรียก script นี้ มันจะส่ง argument มาให้ตามลำดับนี้

$1=IP  $2=ports  $3=perm  $4=inout  $5=ttl_seconds  $6=reason_text  $7=app

ตัวที่ผมใช้เป็นจุดเริ่มกรองคือ $6 ซึ่งเป็น reason ของการ block เพราะมันบอกได้ว่า block นี้มาจากบริการอะไร

#!/bin/bash
set -e
IP="$1"; REASON="$6"
[ -z "$IP" ] && exit 0

# Mail-only scope รับเฉพาะ block ที่เกี่ยวกับเมลเท่านั้น
echo "$REASON" | grep -qiE 'smtpauth|distattack|pop3d|imapd' || exit 0

# ดึง exim mainlog 50 บรรทัดล่าสุดของ IP นี้ ทุก set_id ต้องเป็นโดเมนลูกค้า
USERS=$(grep -F "$IP" /var/log/exim/mainlog /var/log/exim/mainlog.1 2>/dev/null \
    | grep 'authenticator failed' | tail -50 \
    | grep -oE "set_id=[^)[:space:]]+" | sort -u)
[ -z "$USERS" ] && exit 0

# ถ้ามี user นอกโดเมนลูกค้าปนใน fail แสดงว่าเป็น attacker จริง ปล่อย block ไว้
if echo "$USERS" | grep -qvE '^set_id=[^@][email protected]$'; then
    exit 0
fi

# ผ่านหมด ปลด block แล้ว whitelist 24 ชั่วโมง
/usr/sbin/csf -dr "$IP" >/dev/null 2>&1 || true
/usr/sbin/csf -tr "$IP" >/dev/null 2>&1 || true
/usr/sbin/csf -ta "$IP" 86400 "trusted office device" >/dev/null 2>&1 || true
logger -t csf-cond-unblock "unblocked $IP — only @acme-co.example.com fails recent"

หลักคิดของ script นี้ตรงไปตรงมา ผมไม่ได้เชื่อแค่ว่า IP นี้ดูเหมือนออฟฟิศ แต่ผมไปอ่าน exim mainlog 50 บรรทัดล่าสุดของ IP นั้นจริง ๆ แล้วเก็บทุก set_id ที่ auth fail มา ถ้า user ที่ fail มาจากโดเมนลูกค้า 100 เปอร์เซ็นต์ ผมถือว่าเป็น ghost device ของเขาเอง ปลดได้ แต่ถ้ามี user แปลก ๆ ที่ไม่ใช่โดเมนลูกค้าปนเข้ามาแม้แต่ตัวเดียว ผมถือว่าเป็น attacker ที่กำลังเดารหัสมั่ว ปล่อย block ไว้ทันที

ข้อจำกัดของ Layer 1

hook ตัวนี้ fire เฉพาะ block ที่ LFD เป็นคนสั่งเท่านั้น ตอนที่ DA BFM เรียก csf -d เข้าไปตรง ๆ (เช่นรายงานแบบ dovecot1=6 และ exim1=116) มันจะ bypass LFD ไปเลย hook นี้จึงไม่ทำงาน นี่คือเหตุผลที่ต้องมี Layer 2-3-4 ตามมา

Layer 2 Sweep script จับ block ที่ DA BFM สั่งตรง

หลักการของชั้นนี้คือ ไม่รอ hook แต่ไปไล่ scan csf.deny กับ csf -t เป็นระยะเอง โดยใช้ logic ตัดสินใจชุดเดียวกับ BLOCK_REPORT hook แล้วเพิ่ม geo filter เข้ามาอีกชั้น คือต้องเป็น IP ในไทยเท่านั้น เผื่อกรณีที่ attacker ดันยิงมาจากไทยจริง ๆ

สคริปต์อยู่ที่ /usr/local/sbin/csf-sweep.sh ผมเรียงลำดับการเช็คไว้แบบนี้

  1. Lock-file debounce ใช้ไฟล์ /var/run/csf-sweep.last ถ้าเพิ่งรันไปเมื่อราว 5 วินาทีที่แล้วก็ข้าม เหตุผลคือกัน hot-loop ตอนโดน credscan flood เพราะ CSF จะเพิ่ม block ใหม่ถี่มาก ซึ่งจะ trigger sweep ถี่ตามไปด้วย

  2. Collect candidates scan ทั้ง csf -t DENY และ csf.deny เลือกเฉพาะรายการที่ match BFM:.*(dovecot|exim) เหตุผลคือจำกัด scope ให้แตะเฉพาะเมล ไม่ไปยุ่งกับ brute force ของ web/SSH/FTP

  3. Skip whitelisted ถ้า IP อยู่ใน csf.allow (perm) หรือ csf -t ALLOW (temp) อยู่แล้ว ก็ไม่ต้องทำซ้ำ

  4. User filter grep exim mainlog ดู fail 50 รายการล่าสุดของ IP นั้น ทุก set_id ต้องเป็น @acme-co.example.com เหมือน Layer 1

  5. Geo filter geoiplookup $IP ต้องได้ country เป็น TH เหตุผลคือออฟฟิศจริงต่อเน็ตด้วย PPPoE ของไทย ส่วน botnet ที่ brute force เข้ามาจาก CN/IN/VN/KR/BR/US ไม่มีทางเป็นออฟฟิศได้

  6. Unblock + whitelist ถ้าผ่านครบ ก็ csf -dr แล้ว csf -tr แล้ว csf -ta 86400

Fail-safe สำคัญ

ถ้าหา binary geoiplookup ไม่เจอ ให้ abort ทั้งหมด ไม่ปลด IP ใดเลย เป็นแบบ fail-closed คือถ้าตรวจสัญชาติ IP ไม่ได้ ก็ยอมไม่ปลดดีกว่าเผลอปลด attacker เข้ามา better safe than sorry

Layer 3 systemd path watcher ปลดเกือบจะทันที

ถามว่าแค่มี Layer 2 ก็พอไหม ในเชิง logic พอ แต่ถ้าผมตั้งให้มันรันด้วย cron ทุก 15 นาทีเฉย ๆ ลูกค้าก็จะต้องนั่งรอนานสุดถึง 15 นาทีกว่าสคริปต์จะรอบมาปลดให้ ระหว่างนั้นก็ใช้เมลไม่ได้ ซึ่งมันไม่โอเคสำหรับช่วงเช้าที่ทุกคนกำลังเร่งทำงาน

ผมเลยใช้ systemd path unit คอยเฝ้าไฟล์ csf.deny กับ csf.tempban พอไฟล์ถูกแก้เมื่อไหร่ ก็ยิง service ขึ้นมาทันที ทำให้ block window เหลือน้อยกว่า 5 วินาทีจริง ๆ

ไฟล์ /etc/systemd/system/csf-sweep.path

[Unit]
Description=Watch CSF ban files for ghost-device sweep trigger

[Path]
PathModified=/etc/csf/csf.deny
PathModified=/var/lib/csf/csf.tempban
Unit=csf-sweep.service

[Install]
WantedBy=multi-user.target

ไฟล์ /etc/systemd/system/csf-sweep.service

[Unit]
Description=ghost-device auto-unblock sweep
After=lfd.service csf.service
StartLimitIntervalSec=30
StartLimitBurst=10

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/csf-sweep.sh
Nice=10

ตรง StartLimit 10 ครั้งใน 30 วินาที ผมตั้งไว้กัน hot-loop อีกชั้น ถ้า CSF เพิ่ม credscan block เข้ามาเยอะมากใน 30 วินาที systemd จะ refuse to start เอง เป็น defense-in-depth ซ้อนกับ debounce ใน Layer 2

ทำไมไม่ watch csf.tempallow

เพราะ sweep ของเราเองเป็นคนเขียน csf -ta ซึ่งไปแก้ไฟล์ tempallow ถ้าเราไป watch ไฟล์นั้นด้วย มันก็จะ trigger ตัวเองวนไม่จบ กลายเป็น infinite loop จึงตัดออก เฝ้าเฉพาะไฟล์ฝั่ง block พอ

Layer 4 Cron safety net ตาข่ายกันพลาดช่วงเช้า

ถึง systemd path watcher จะไว แต่มันก็มีโอกาส miss ได้ เช่นตอน service กำลัง restart, inotify queue เต็ม, หรือโดน debounce ตัดออกพอดี ผมเลยวาง cron ไว้รันซ้ำในช่วงที่ออฟฟิศกำลังเปิดเครื่องตอนเช้า

30,45 7 * * * root /usr/local/sbin/csf-sweep.sh
0,15,30 8 * * * root /usr/local/sbin/csf-sweep.sh

รวมเป็น 5 ครั้งในหนึ่งชั่วโมง คือ 07:30, 07:45, 08:00, 08:15 และ 08:30 ตรงกับช่วงที่คนในออฟฟิศทยอยเปิดเครื่อง ถ้า path watcher พลาดไป cron จะ catch ให้ได้ภายในไม่เกิน 15 นาที ซึ่งยอมรับได้สำหรับเป็นทางหนีทีไล่

Behaviour Matrix สรุปว่าเคสไหนปลด เคสไหนไม่ปลด

ตารางนี้คือสรุปว่าระบบทั้งชุดจะตัดสินใจอย่างไรในแต่ละสถานการณ์

ที่มาของ IP รูปแบบ user ที่ fail ผลลัพธ์
TH (TOT/AIS/True/3BB) เป็น @acme-co.example.com 100% unblock + whitelist 24h
TH มี user อื่นปนใน fail keep block (attacker จริงจากไทย)
ไม่ใช่ TH (CN/IN/VN/KR/BR/US) เป็น @acme-co.example.com 100% keep block (credential stuffing)
ไม่ใช่ TH มี user อื่นปน keep block (attacker ทั่วไป)
ไม่มี exim fail log ของ IP นี้ กรณีใดก็ตาม keep block (อาจเป็น web/SSH brute นอก scope)

จุดที่อยากให้สังเกตคือ แม้ fail บัญชีจะเป็นโดเมนลูกค้า 100 เปอร์เซ็นต์ แต่ถ้า IP ไม่ใช่ของไทยก็ยังไม่ปลด เพราะกรณีนั้นคือ botnet ที่ได้รหัสไปแล้วและกำลัง stuff เข้ามา ไม่ใช่ ghost device ในออฟฟิศแน่ ๆ

ไฟล์ทั้งหมดในระบบนี้

NEW       /usr/local/sbin/csf-cond-unblock.sh           (BLOCK_REPORT hook — Layer 1)
NEW       /usr/local/sbin/csf-sweep.sh                  (sweep + TH filter + debounce — Layer 2)
NEW       /etc/systemd/system/csf-sweep.path            (Layer 3 trigger)
NEW       /etc/systemd/system/csf-sweep.service         (Layer 3 runtime)
NEW       /etc/cron.d/csf-sweep                         (Layer 4 safety net)
MODIFIED  /etc/csf/csf.conf                             (BLOCK_REPORT setting)
NEW       /var/log/csf-sweep.log                        (audit log per-event)
runtime   /var/run/csf-sweep.last                       (debounce lock file)

ที่ผมเก็บ audit log แยกไว้ที่ /var/log/csf-sweep.log ก็เพราะอยากให้ทุกครั้งที่ระบบตัดสินใจปลดหรือไม่ปลด มันมีร่องรอยให้ย้อนดูได้ เวลามีอะไรผิดพลาดจะได้รู้ว่าตัดสินใจจากข้อมูลอะไร ไม่ใช่ระบบที่ทำงานเงียบ ๆ แล้วเดาไม่ออกว่ามันทำอะไรไป

ข้อจำกัดที่ต้องรู้ก่อนเอาไปใช้

รู้ขอบเขตก่อนใช้

นี่คือ workaround ที่ออกแบบมาแก้ปัญหาเฉพาะหน้า ไม่ใช่ทางแก้ถาวร มันมีจุดที่จงใจไม่ครอบเอาไว้

  • ไม่ครอบ web/SSH/FTP brute sweep scope เป็น mail-only ตามขอบเขตของปัญหาเดิม ถ้าลูกค้าโดน brute ผ่าน Roundcube webmail ระบบนี้จะไม่ auto-unblock ให้

  • TH filter อาจกระทบ VPN หรือคนเดินทาง ถ้า staff ใช้ VPN ต่างประเทศแล้วบังเอิญเจอ ghost device fail ตรงกัน ระบบจะไม่ปลดให้ ซึ่งผมยอมรับเงื่อนไขนี้ได้ ถ้าเจอก็ขอให้ admin ปลดมือเอา

  • Long-term fix อยู่ที่ลูกค้า ทางแก้จริงคือให้ลูกค้าเปลี่ยน password ของบัญชีที่หลุด แล้วเคลียร์ cached credential บนทุกเครื่องในออฟฟิศ พอทำเสร็จก็เลิกใช้ workaround นี้ได้เลย อันนี้สำคัญที่สุด แต่ลูกค้าผมตอบกลับช้า ผมก็เลยต้องอยู่ในโหมด workaround ไปเรื่อย ๆ จนกว่าเขาจะลงมือ

ปิดท้าย

ใครดูแล DA shared host แล้วมีลูกค้าเจอเคสคล้าย ๆ กัน คือ ghost device ในออฟฟิศปนกับ brute force จากข้างนอก โดยทั้งคู่ fail บัญชีชุดเดียวกัน ลอง pattern นี้ดูได้ครับ logic เหมือนกันหมด แค่เปลี่ยน @acme-co.example.com เป็นโดเมนของลูกค้าตัวเอง แล้วปรับ geo filter ให้ตรงกับประเทศที่ออฟฟิศจริงต่อเน็ตอยู่

จะหยิบไปทั้ง 4 ชั้น หรือเอาแค่ Layer 1 กับ Layer 2 ไปก่อนก็ตามสะดวก หลักการเหมือนกันหมด คืออย่าเชื่อแค่ว่า IP ดูเหมือนออฟฟิศ ให้ไปอ่านล็อกจริงแล้วยืนยันด้วยหลายเงื่อนไขก่อนปลดทุกครั้ง

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย