อ่าน exim mainlog หา stuck client — Mail Forensics ภาคแรก
ลูกค้าโทรเข้ามาเรื่องส่งเมลไม่ได้แทบทุกวัน บ้าง IP โดน block บ้างบอกรหัสรั่ว ผมไล่บ่อยจนทำเป็นท่ามาตรฐานในหัว เลยรวบคำสั่งกับวิธีคิดมาเขียนไว้
สรุปใน 1 นาที
- อาการ/ปัญหา
- ลูกค้าส่งเมลไม่ได้ และต้องแยกเครื่องในออฟฟิศที่ใส่รหัสผิดออกจาก attacker brute force
- ใครควรอ่าน
- แอดมินเมล exim ที่ต้องตอบลูกค้าให้เร็ว
- ทางแก้แบบสั้น
- ไล่อ่าน /var/log/exim/mainlog ด้วยชุดคำสั่งที่ใช้จริง เพื่อแยกเครื่องรหัสผิดออกจาก attacker
ช่วงนี้ลูกค้าโทรเข้ามาบ่อยมาก เรื่องส่งเมลไม่ได้ บางรายโดน IP block บางรายบอกว่ารหัสรั่ว บางรายตามหาเครื่องในออฟฟิศที่ใส่รหัสผิดจนทำให้ทั้ง IP โดน firewall block พร้อมกันหมด ผมไล่เคสแบบนี้บ่อยจนทำเป็นลำดับขั้นมาตรฐานในหัวไปแล้ว เลยถือโอกาสรวบรวมคำสั่งและวิธีคิดที่ใช้ประจำมาเขียนไว้ เผื่อใครเจอเคสคล้ายกันจะได้หยิบไปใช้ได้เลย
ระบบที่ผมดูแลเป็น DirectAdmin บน Debian, mail server เป็น exim, firewall เป็น CSF/LFD ใครใช้ stack ใกล้เคียงน่าจะนำไป apply ได้ตรงๆ ส่วนคนที่ใช้ postfix หรือ iptables ตรงๆ ก็ใช้แนวคิดเดียวกันได้ แต่ regex ต้องปรับให้เข้ากับรูปแบบ log ของตัวเอง
เป้าหมายของบทความชุดนี้คือทำให้คนที่ต้องดูแล mail server หลายโดเมนมีคู่มืออ้างอิงเร็วๆ ตอนลูกค้าเร่งคำตอบ ไม่ต้องมานั่งนึก regex ใหม่ทุกครั้ง
ข้อตกลงเรื่อง placeholder ในบทความ
ทุกตัวอย่างในนี้ผมแทนค่าจริงด้วย placeholder เพื่อความปลอดภัย เวลานำไปใช้ให้แทนด้วยค่าจริงของคุณเอง
customer.com= โดเมนลูกค้าตัวอย่าง203.0.113.x,198.51.100.x= public IP ตัวอย่าง (X.X.X.X / Y.Y.Y.Y)192.168.1.x= LAN IP (RFC1918)[email protected]= mail account ตัวอย่าง/home/<user>/...= home directory ของแต่ละบัญชี
ก่อนลงรายละเอียด ลองดูแผนผังการตัดสินใจตามอาการที่ลูกค้าโทรเข้ามาก่อน แต่ละกล่องตรงกับหัวข้อด้านล่าง
1. รู้จัก log ก่อน — สองตัวที่ใช้บ่อยที่สุด
ก่อนจะ grep อะไรได้ ต้องรู้ว่าข้อมูลอยู่ที่ไหนและหน้าตาเป็นอย่างไร ผมเปิดสองไฟล์นี้บ่อยที่สุด
/var/log/exim/mainlog— รวมทั้ง success, fail, queue, delivery (logrotate รายวัน)/var/log/lfd.log— เหตุการณ์ block จาก CSF/LFD (รูปแบบ syslog แบบMay 23 ...)
ส่วนสถานะของ CSF เก็บไว้ในไฟล์เหล่านี้
/var/lib/csf/csf.tempban— temp ban ที่ auto-expire ได้เอง/etc/csf/csf.deny— permanent deny (มาจาก BFM และที่เติมเอง)
รูปแบบบรรทัด SUCCESS (auth ผ่าน)
2026-05-23 10:05:38 1wPAm1-... <= [email protected] H=ppp-203-0-113-x.isp.example ([192.168.1.20]) [203.0.113.x] P=esmtpsa A=plain:[email protected] S=1234
จุดที่ต้องอ่านให้ออก
<=คือ inbound แปลว่า server กำลังรับเมลจาก clientH=hostname ([internal])คือ EHLO ที่ client ประกาศตัวเอง พร้อม LAN IP ถ้ามี[203.0.113.x]คือ public IP จริงของ client ตัวนี้A=plain:[email protected]คือ authenticator บวกกับ account ที่ login ผ่าน นี่คือ marker สำคัญที่สุดของบรรทัด success
รูปแบบบรรทัด FAIL (รหัสผิด)
2026-05-23 08:07:19 plain authenticator failed for H=ppp-203-0-113-x.isp.example ([IPv6:::ffff:192.168.1.56]) [203.0.113.x]: 535 Incorrect authentication data ([email protected])
จุดที่ต้องอ่านให้ออก
authenticator failedคือ marker สำคัญของบรรทัด fail[IPv6:::ffff:192.168.1.56]คือ LAN IP ของ client มีเฉพาะ client บางตัวที่ส่งมา[email protected]คือ account ที่กำลังถูกเดารหัสอยู่
ผมสังเกตว่า client ตัวเดียวกันมักจะ fail ด้วย plain แล้วตามด้วย login ในวินาทีเดียวกัน เพราะ mail client ลองทั้งสอง mechanism (PLAIN กับ LOGIN) มันจะถูกนับเป็น 2 fail ทั้งที่จริงเป็นการลองครั้งเดียว เวลาดู threshold ของ LFD ต้องเผื่อตรงนี้ ไม่งั้นจะตีความ traffic ผิด
2. เจาะ 1 account — รหัสผิดมาจากไหน
เคสตั้งต้น ลูกค้าโทรมาบอกว่า [email protected] ส่งเมลไม่ออก โดน block ซ้ำๆ ขั้นแรกผมจะดูว่ามี IP ไหน login เข้ามาบ้าง ผ่านหรือไม่ผ่านกี่ครั้ง
สรุปยอดรวมก่อน
[email protected]
LOG=/var/log/exim/mainlog
ok=$(grep -c "A=[a-z_]*:$ACCT" $LOG)
fail=$(grep "set_id=$ACCT" $LOG | grep -c 'authenticator failed')
echo "success=$ok fail=$fail"
ตาราง Per-IP ที่แสดง OK / FAIL / ประเทศ / HELO / rDNS ตัวนี้ผมใช้บ่อยที่สุดในการแกะเคส
printf '%-16s %-5s %-5s %-4s %-30s %s\n' 'IP' 'OK' 'FAIL' 'CC' 'HELO (top)' 'rDNS'
echo '------------------------------------------------------------------------------------------'
( grep "A=[a-z_]*:$ACCT" $LOG | sed -nE 's/.*\[([0-9.]+)\].*/\1/p'
grep "set_id=$ACCT" $LOG | grep 'authenticator failed' | sed -nE 's/.*\[([0-9.]+)\].*/\1/p'
) | sort -u | while read ip; do
[ -z "$ip" ] && continue
ok=$(grep -F "$ip" $LOG | grep -c "A=[a-z_]*:$ACCT")
fail=$(grep -F "$ip" $LOG | grep "set_id=$ACCT" | grep -c 'authenticator failed')
cc=$(geoiplookup $ip 2>/dev/null | grep -oE '[A-Z]{2},' | head -1 | tr -d ',')
helo=$(grep -F "$ip" $LOG | grep -oE 'H=\([^)]+\)' | sort | uniq -c | sort -rn | head -1 \
| sed -E 's/^[ ]*[0-9]+[ ]+H=\(//; s/\)$//')
rdns=$(getent hosts $ip 2>/dev/null | awk '{print $2}' | head -1)
printf '%-16s %-5s %-5s %-4s %-30s %s\n' "$ip" "$ok" "$fail" "${cc:-??}" "${helo:--}" "${rdns:--}"
done
geoiplookup ส่งคืนข้อความแบบ GeoIP Country Edition: TH, Thailand ซึ่งคำว่า IP ก็เป็นตัวพิมพ์ใหญ่สองตัวที่อยู่ก่อน TH ถ้าใช้ grep -oE '[A-Z]{2}' เฉยๆ จะจับคำว่า IP มาก่อน ต้องใช้ grep -oE '[A-Z]{2},' ที่มี comma แล้วค่อย tr -d ',' เพื่อให้ได้ country code อย่างเดียว
ดูว่าเป็น bot หรือมนุษย์
แยกการกระจายตัวของ fail ตามชั่วโมง จะบอกได้คร่าวๆ ว่าใครเป็นคนกด
grep "set_id=$ACCT" $LOG | grep 'authenticator failed' \
| awk '{print substr($2,1,2)":00"}' | sort | uniq -c
ตีความ
- peak ช่วง 08:00 / 13:00 / 16:00 = เครื่อง office จริง (Outlook auto-poll ตามรอบเวลาทำงาน)
- กระจายเท่าๆ กันทุกชั่วโมง = bot หรือ cron retry ตายตัว
- burst หนักช่วงเดียว เช่น 10 ครั้งขึ้นใน 1 นาที = brute force ของจริง
10 fail ล่าสุด
grep "set_id=$ACCT" $LOG | grep 'authenticator failed' | tail -10
ใน 10 บรรทัดนี้ผมจะมองหา
H=(hostname)ซ้ำกันไหม ถ้าซ้ำแปลว่าเครื่องเดียวกันลองซ้ำๆ[IPv6:::ffff:192.168.1.x]ตัวไหนโผล่บ่อย นั่นคือ LAN IP ของเครื่องที่รหัสผิด- timestamp กระชั้น น้อยกว่า 5 วินาที แปลว่าเป็น mail client retry อัตโนมัติ
3. เจาะ 1 IP — ใช้ account ไหนบ้าง
ใช้ตอนเห็น IP โผล่ใน csf.deny หรือ lfd.log แล้วอยากรู้ว่าเป็น attacker จริง หรือเป็น office ที่รหัสผิด
Account ที่ IP นี้พยายาม login แล้ว fail
IP=203.0.113.x
LOG=/var/log/exim/mainlog
grep -F "$IP" $LOG | grep 'authenticator failed' \
| grep -oE 'set_id=[^ )]+' | sort | uniq -c | sort -rn
ตีความ
- 1 account แต่ fail หลายร้อยครั้ง = stuck client (cached password ผิด)
- หลาย account fail กระจาย = credential stuffing (attacker ไล่ลิสต์ leaked creds)
- 1 account แต่มี success อื่นมาก่อน = ผู้ใช้จริงที่เพิ่งเปลี่ยนรหัส หรือ device ค้าง
HELO hostname ที่ IP นี้ประกาศ
grep -F "$IP" $LOG | grep -oE 'H=\([^)]+\)' | sort | uniq -c | sort -rn | head
H=(DESKTOP-AB1234) ตามด้วย Outlook คือ Windows, H=(iPhone) คือ device ส่วนตัว, H=(localhost) หรือ H=(WIN-XXXXXX) มักเป็น bot หรือ VPS
LAN IP ที่ IP นี้ประกาศ เห็นได้เฉพาะกรณีอยู่หลัง NAT
grep -F "$IP" $LOG | grep -oE 'IPv6:::ffff:[0-9.]+' | sort | uniq -c
Success ที่ IP นี้เคยมีไหม เอาไว้ sanity check
grep -F "$IP" $LOG | grep -oE 'A=[a-z_]*:[^ ]+' | sort | uniq -c
ถ้า IP นี้เคยมี A= success ในอดีต ด้วย account เดียวกับที่ fail อยู่ตอนนี้ ให้ถือว่าเป็น stuck client แล้ว whitelist ไว้ พร้อมแจ้งลูกค้าให้ไปแก้ที่ device แต่ถ้าไม่เคยมี success เลย ให้ถือว่าเป็น attacker แล้วปล่อย block ไว้ตามเดิม แค่นี้ตัดสินใจได้แล้วในเคสส่วนใหญ่
4. หาเครื่องใน LAN — ใช้ internal NAT IP เป็น fingerprint
เคสที่เจอบ่อย ลูกค้าบอกว่า office มีหลายเครื่อง แล้วเครื่องไหนรหัสผิด ผมใช้ LAN IP ที่อยู่ใน [IPv6:::ffff:192.168.1.x] เป็น signature เฉพาะตัวของแต่ละเครื่อง
ดูว่า LAN IP นี้ลอง login account ไหน
INT=192.168.1.56
LOG=/var/log/exim/mainlog
grep "IPv6:::ffff:$INT" $LOG | grep 'authenticator failed' \
| grep -oE 'set_id=[^ )]+' | sort | uniq -c | sort -rn
LAN IP นี้เคย login สำเร็จด้วย account ไหน เผื่อมันเปลี่ยน password แล้วแต่ยังค้างของเก่า
grep "IPv6:::ffff:$INT" $LOG | grep -oE 'A=[a-z_]*:[^ ]+' | sort | uniq -c
รายการ LAN IP ใน office ที่ login เข้ามาวันนี้ทั้งหมด
TODAY=$(date +%F)
LOG=/var/log/exim/mainlog
awk -v d="$TODAY" '$1==d' $LOG \
| grep -oE 'IPv6:::ffff:(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)[0-9.]+' \
| sed 's/IPv6:::ffff://' | sort | uniq -c | sort -rn
ผมเจอบ่อยว่าไม่ใช่ทุก mail client จะส่ง LAN IP มาใน EHLO โดยทั่วไป Outlook บน Desktop ส่ง แต่ iPhone, Mac Mail และ webmail ไม่ส่ง ถ้าหาไม่เจอ LAN IP เลย แปลว่าลูกค้าน่าจะใช้ iPhone หรือ webmail นั่นเอง อย่าเพิ่งสรุปว่าไม่มีเครื่องในออฟฟิศ
5. หลักฐาน stuck client = IP office เดียวกัน
ใช้ตอนลูกค้ายืนยันว่าไม่มี device ไหนรหัสผิด เราต้องพิสูจน์ให้เห็นว่า IP ที่ fail คือ IP เดียวกับที่ office กำลัง success อยู่ตอนนี้
DOM=customer.com
LOG=/var/log/exim/mainlog
echo "== Office IPs (มี success อย่างน้อย 1 ครั้ง) =="
grep "A=[a-z_]*:.*@$DOM" $LOG | sed -nE 's/.*\[([0-9.]+)\].*/\1/p' \
| sort | uniq -c | sort -rn
echo ""
echo "== OK vs FAIL ต่อแต่ละ IP =="
for ip in $(grep "A=[a-z_]*:.*@$DOM" $LOG | sed -nE 's/.*\[([0-9.]+)\].*/\1/p' | sort -u); do
ok=$(grep "A=[a-z_]*:.*@$DOM" $LOG | grep -c "\[$ip\]")
fail=$(grep "set_id=.*@$DOM" $LOG | grep "\[$ip\]" | grep -c 'authenticator failed')
printf '%-16s OK=%-4s FAIL=%s\n' "$ip" "$ok" "$fail"
done | sort -k3 -t= -rn
echo ""
echo "== LAN IP ที่ login OK (เครื่อง office จริง) =="
grep "A=[a-z_]*:.*@$DOM" $LOG | grep -oE '\(\[192\.168\.[0-9.]+\]\)' \
| tr -d '()[]' | sort | uniq -c
echo ""
echo "== LAN IP ที่ fail =="
grep "set_id=.*@$DOM" $LOG | grep 'authenticator failed' \
| grep -oE 'IPv6:::ffff:192\.168\.[0-9.]+' \
| sed 's/IPv6:::ffff://' | sort | uniq -c
ถ้า IP ที่ fail ตรงกับ IP ที่ office success ให้ส่งข้อความสั้นๆ ทำนองว่า "LAN IP 192.168.1.y ของ office รหัสผิด account=user1 fail 116 ครั้งวันนี้" แล้วให้ลูกค้าเดินไปแก้ password ที่เครื่องนั้นเอง เพราะถ้าเราไปแก้ให้ มันจะวนไม่จบ ต้นตอคือ device ค้าง ไม่ใช่ฝั่ง server
6. Audit domain — account ไหน active หรือ inactive
ก่อนจะลบ mail account เก่า ผมจะดูก่อนว่ามันมี success ใน N วันที่ผ่านมาหรือไม่ จะได้ไม่ลบของที่ยังใช้อยู่
DOM=customer.com
SINCE='2026-05-18' # วันเริ่มต้นที่จะนับ
LOG=/var/log/exim/mainlog
# Mail accounts ที่ configure ไว้
cat /etc/virtual/$DOM/passwd | awk -F: '{print $1"@'$DOM'"}' | sort > /tmp/conf
# Accounts ที่มี success AUTH ตั้งแต่ $SINCE
awk -v s="$SINCE" '$1 >= s' $LOG \
| grep -oE 'A=[a-z_]*:[^ ]+@'$DOM \
| sed 's/^A=[a-z_]*://' | sort -u > /tmp/act
echo "-- INACTIVE accounts (ไม่ login เลยตั้งแต่ $SINCE) --"
comm -23 /tmp/conf /tmp/act
echo ""
echo "-- ACTIVE accounts --"
cat /tmp/act
ตรวจ /etc/virtual/$DOM/aliases ก่อนเสมอ ถ้า account อยู่ในรูป catch-all หรือ forwarding ต้องเก็บไว้ อย่าดูแค่ว่าไม่มี login แล้วลบทิ้ง เพราะบางบัญชีมีไว้รับ forward อย่างเดียวไม่เคย login
7. Server-wide stuck client scan
อันนี้ผมใช้ตอนเช้าก่อน office เปิดเครื่อง เพื่อดูทั้ง server ว่ามีกี่ account ที่มี stuck client ค้างอยู่ จะได้แก้เชิงรุกก่อนลูกค้าโทรมา
LOG=/var/log/exim/mainlog
tmpok=$(mktemp); tmpfail=$(mktemp)
# ดึงคู่ (IP, account) จาก success ทั้งหมด
grep -a -E 'A=[a-z_]*:[^ ]+@[a-z0-9.-]+' $LOG \
| sed -nE 's/.*\[([0-9.]+)\].*A=[a-z_]*:([^ ]+).*/\1|\2/p' \
| sort -u > $tmpok
# ดึงคู่ (IP, account) จาก fail
grep -a 'authenticator failed' $LOG \
| sed -nE 's/.*\[([0-9.]+)\].*set_id=([^ )]+).*/\1|\2/p' \
| sort -u > $tmpfail
# จับ IP ที่: success บาง account + fail account อื่นใน domain เดียวกัน = stuck mixed
for ip in $(awk -F'|' '{print $1}' $tmpok | sort -u); do
for acct in $(grep "^$ip|" $tmpfail | awk -F'|' '{print $2}'); do
dom=${acct#*@}
[ "$(grep -c "^$ip|[^@]*@$dom$" $tmpok)" -eq 0 ] && continue
ok=$(grep -F "$ip" $LOG | grep -c "A=[a-z_]*:$acct")
fail=$(grep -F "$ip" $LOG | grep 'authenticator failed' | grep -c "set_id=$acct")
[ "$fail" -eq 0 ] && continue
printf '%-35s %-16s OK=%-4s FAIL=%s\n' "$acct" "$ip" "$ok" "$fail"
done
done | sort -k4 -t= -rn
rm -f $tmpok $tmpfail
ผลลัพธ์ที่ได้คือรายการ (account, IP, ok, fail) ที่ IP นั้นเคย login account อื่นใน domain เดียวกันได้สำเร็จ ซึ่งเป็นสัญญาณชัดเจนว่ามี stuck client ใน office นั้น
8. False-positive check — IP ถูก block ผิดหรือเปล่า
ลูกค้าโทรมาบ่นว่าส่งเมลไม่ได้ทั้งสำนักงาน ก่อนตอบ ผมจะดูก่อนว่า IP ที่ถูก block เป็น customer จริงหรือ attacker
TODAY='May 23' # syslog format ไม่ใช่ ISO
LOG=/var/log/exim/mainlog
grep "^$TODAY " /var/log/lfd.log \
| grep 'Blocked in csf' \
| grep -oE '([0-9]+\.){3}[0-9]+' | sort -u | while read ip; do
cc=$(geoiplookup $ip 2>/dev/null | grep -oE '[A-Z]{2},' | head -1 | tr -d ',')
ok=$(grep -ac "A=[a-z_]*:.*\[$ip\]" $LOG)
trig=$(grep "$ip" /var/log/lfd.log | tail -1 \
| grep -oE '\((smtpauth|credscan|distattack|sshd|modsec)\)' | tr -d '()')
note=''
[ "$cc" = "TH" ] && note='[!]TH'
[ "$ok" -gt 0 ] && note="$note success=$ok"
printf '%-16s %-4s ok=%-3s %-12s %s\n' "$ip" "${cc:-??}" "$ok" "${trig:--}" "$note"
done
mainlog ใช้รูปแบบ ISO เช่น 2026-05-23 แต่ lfd.log ใช้ syslog เช่น May 23 ถ้าใช้ format ผิดข้ามไฟล์ จะ grep ได้ผลเปล่าๆ โดยไม่มี error ให้เห็น เป็นจุดที่หลายคนพลาด
ตีความ
[!]TH+ success มากกว่า 0 = ลูกค้าจริง ให้ปลดบล็อกแล้ว whitelist 24 ชั่วโมง- ประเทศไม่ใช่ TH + ok=0 + trigger เป็น smtpauth = attacker brute force ปล่อย block ไว้
- ประเทศไม่ใช่ TH + ok=0 + trigger เป็น credscan = scanner หรือ bot ปล่อย block ไว้
ปลดบล็อกแล้ว whitelist 24 ชั่วโมง เมื่อยืนยันแล้วว่าเป็น customer
IP=203.0.113.x
csf -dr "$IP" # ลบจาก csf.deny
csf -tr "$IP" # ลบจาก csf.tempban
csf -ta "$IP" 86400 "customer office — manual" # whitelist 24h
ถ้ายังมี ghost device ใน LAN เดารหัสอยู่ การลบออกจาก deny อย่างเดียวจะทำให้มันถูก block ใหม่ภายในไม่กี่นาที การ csf -ta ใส่ allow ชั่วคราวมีอายุ ทำให้ลูกค้าทำงานต่อได้ระหว่างที่เราตามไปแก้ device เคสนี้ผมเจอบ่อยมาก
9. Whitelist behavior — ทำไม log fail ยังขึ้นแม้ใส่ whitelist แล้ว
คำถามคลาสสิกจากลูกค้า ใส่ whitelist แล้ว ทำไม log ยังเตือนรหัสผิด คำตอบคือ whitelist ของ CSF (csf -a, csf -ta, csf.allow, csf.ignore) ทำงานอยู่ที่ชั้น iptables กับ lfd เท่านั้น การ AUTH ที่ผิดยังคงเดินทางต่อ
- ยังถึง exim
- ยังได้ response
535 Incorrect authentication data - ยังเขียน entry ลง mainlog
- lfd ยังนับเข้า threshold แต่ไม่สั่ง
csf -d
สรุปคือถ้าอยากให้ log หยุดจริงๆ ต้องไปแก้ที่ฝั่ง client เปลี่ยนรหัสบนเครื่อง หรือลบ account ออก เพราะ whitelist เป็นแค่ firewall layer ไม่ใช่ auth layer
ความต่างระหว่าง csf.allow กับ csf.ignore
iptables + lfd
- ปล่อยทุก packet ผ่าน
- lfd ไม่ block IP นี้
- เทียบเท่า csf -ta แต่ถาวร
lfd เท่านั้น
- iptables ยัง drop ได้ถ้ามี rule อื่น
- แค่ lfd ไม่สั่ง csf -d
- ไม่เปิด packet ทั้งหมดเหมือน allow
ส่วน csf -ta IP TTL เทียบเท่า csf.allow แต่มี TTL ให้ auto-expire ตามเวลา
ดู whitelist ทั้งหมดที่ใช้อยู่
csf -t | grep -i allow # temp allow (csf -ta)
grep -v '^#' /etc/csf/csf.allow | grep -v '^$' # permanent
grep -v '^#' /etc/csf/csf.ignore | grep -v '^$' # lfd-only ignore
10. Pattern reference — ดูอะไรแล้วบอกได้ว่าเครื่องอะไร
ส่วนนี้ผมเก็บไว้เป็น cheat sheet ของตัวเอง อ่านบรรทัดเดียวก็เดาประเภทเครื่องได้คร่าวๆ
HELO hostname patterns
H=(DESKTOP-XXXXX)หรือH=(LAPTOP-XXXXX)= Windows + OutlookH=(iPhone)/H=(iPad)= iOS MailH=(Macbook-Pro.local)= macOS MailH=(android-XXXX)= Android K-9 หรือ Gmail appH=ppp-203-0-113-x.isp.example= rDNS default ของ ISP (ไม่ได้ตั้ง HELO ในเครื่อง)H=(localhost)/H=(WIN-XXXX)= VPS, bot, spammer เป็นส่วนใหญ่H=(mail-XXX.google.com)= Gmail SMTP relay (legit)
Authenticator patterns
A=plain:= client ส่วนใหญ่ (AUTH PLAIN)A=login:= Outlook (AUTH LOGIN)A=cram_md5:= client เก่ามาก น่าสงสัย- fail ทั้ง plain และ login ในวินาทีเดียว = client ลอง 2 mechanism นับเป็น 2 fail
LFD trigger reasons (จาก /var/log/lfd.log)
(smtpauth)= SMTP auth fail ครบ N ครั้งใน X วินาที (defaultLF_SMTPAUTH=10/300s)(distattack)= account เดียวกัน fail จากหลาย IP (defaultLF_DISTATTACK=2IPs)(credscan)= HTTP probe ไฟล์อย่าง wp-config, .env, .git/config (ban ทันที 24 ชั่วโมง)(modsec)= ModSecurity rule fired N ครั้ง (defaultLF_MODSEC=10/300s)BFM:dovecot1=N= DirectAdmin Brute Force Monitor ฝั่ง IMAP/POPBFM:exim1=N= DA BFM ฝั่ง SMTP
DirectAdmin BFM เรียก csf -d ตรงๆ ไม่ผ่าน LFD ดังนั้น hook unblock อัตโนมัติที่ผูกไว้กับ LFD จะไม่ทำงานกับ block ของ BFM ผมเลยใส่ sweep cron เป็น safety net เพิ่มอีกชั้น กันลูกค้าค้าง block ข้ามคืนโดยไม่มีใครรู้
11. One-liners ที่ใช้บ่อย — เก็บไว้ในกระเป๋า
Top 10 IP ที่ fail วันนี้
awk -v d="$(date +%F)" '$1==d' /var/log/exim/mainlog \
| grep 'authenticator failed' \
| sed -nE 's/.*\[([0-9.]+)\].*/\1/p' | sort | uniq -c | sort -rn | head
Top 10 account ที่ถูกเดารหัสวันนี้
awk -v d="$(date +%F)" '$1==d' /var/log/exim/mainlog \
| grep 'authenticator failed' \
| grep -oE 'set_id=[^ )]+' | sort | uniq -c | sort -rn | head
รวม success เทียบกับ fail วันนี้
LOG=/var/log/exim/mainlog
TODAY=$(date +%F)
ok=$(awk -v d="$TODAY" '$1==d' $LOG | grep -c 'A=[a-z_]*:')
fail=$(awk -v d="$TODAY" '$1==d' $LOG | grep -c 'authenticator failed')
echo "today: success=$ok fail=$fail"
ตรวจว่า IP อยู่ใน whitelist ครบทุกประเภทหรือยัง
IP=203.0.113.x
echo "-- csf.allow (permanent) --"; grep -F "$IP" /etc/csf/csf.allow
echo "-- csf -t ALLOW (temp) --"; csf -t | grep -i allow | grep -F "$IP"
echo "-- csf.ignore (lfd-only) --"; grep -F "$IP" /etc/csf/csf.ignore
สรุป workflow ตามอาการ
ผมใช้ลำดับนี้เป็น flowchart ในหัวเวลาลูกค้าโทรเข้ามา
- ลูกค้าบ่นว่าส่งเมลไม่ได้ ไปที่หัวข้อ 2 Per-account
- IP โดน block ซ้ำๆ ไปที่หัวข้อ 3 Per-IP
- สงสัย device ใน office รหัสผิด ไปที่หัวข้อ 4 และ 5 เรื่อง LAN กับ stuck client
- ลูกค้าถามว่า account ไหนใช้หรือไม่ใช้ ไปที่หัวข้อ 6 Domain audit
- อยาก audit ทั้ง server ไปที่หัวข้อ 7 Server-wide stuck scan
- ลูกค้าบ่นว่า block ผิด ไปที่หัวข้อ 8 False-positive check
- ลูกค้าถามว่าใส่ whitelist แล้วทำไม log ยังขึ้น ไปที่หัวข้อ 9 Whitelist behavior
ทั้งหมดนี้ผมก็ลองผิดลองถูกมาเรื่อยๆ จนตกผลึกเป็นท่ามาตรฐานในหัว เห็นว่าน่าจะเป็นประโยชน์เลยเอามาเขียนรวมไว้ ใครมีท่าเพิ่ม regex ที่ดีกว่านี้ script เก็บ stat อัตโนมัติ หรือทำเป็น dashboard เลย ก็ลงคอมเมนต์บอกได้ จะลองเอาไปปรับใช้ ใครถนัด postfix หรือ iptables ก็ปรับ regex ตามระบบตัวเองได้ ตามสะดวก หลักการเหมือนกันหมด
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



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