Runbook: ScamGuard Admin Portal (Production) — ฉบับ Canonical
เกณฑ์เริ่มต้นในการสั่ง rollback ปรับได้โดย SRE พร้อมบันทึกเหตุผล : - False-positive rate 15% จากงาน review ย้อนหลัง 100 งานติดกัน, หรือ - Inference P95 25 วินาที ภาพ 1080p ต่อเนื่อง 15 นาที, หรือ - Worker OOM/killed ≥ 3 ครั้งใน 1…
Runbook: ScamGuard Admin Portal (Production) — ฉบับ Canonical
เอกสารนี้คือ runbook ฉบับ canonical เดียว (
wiki/runbook.md);Document/admin/runbook.mdเป็นเพียง pointer มาที่นี่ ห้ามแก้ไขแยกสองฉบับ สัญลักษณ์<...>คือค่าที่ผู้ปฏิบัติต้องแทนด้วยค่าจริงหน้างาน (เช่น<JOB_ID>,<ADMIN_EMAIL>,<IP>) ห้ามรันคำสั่งทั้งที่ยังมีวงเล็บมุม กฎเหล็ก: backup ก่อนเขียนทุกครั้ง, ระบุเป้าหมายทีละตัว (ทีละ job/session/admin), ตรวจ verify หลังทำทุกขั้น
0. การเข้าถึงเซิร์ฟเวอร์ (SSH)
- เชื่อมต่อ:
ssh <USER>@<HOST> -p <PORT>(ตัวอย่าง<USER>=deploy,<HOST>ดูใน inventory ภายใน — ห้ามใส่ host จริงในเอกสารนี้) - ยืนยันตัวตนเครื่อง: ตรวจ fingerprint ครั้งแรกกับ inventory, ตรวจ
whoami && hostnameตรงกับเครื่องเป้าหมายก่อนรันคำสั่งเขียนทุกครั้ง - ตัวแปรฐานข้อมูลอ่านจาก
.envฝั่งเซิร์ฟเวอร์เท่านั้น ห้าม hardcode รหัสผ่านในคำสั่ง/เอกสาร (psql "$DATABASE_URL" -c "...")
1. วิธีปิดบัญชี Super Admin (Revoke Super Admin — ทีละบัญชี)
- สำรองแถวก่อนแก้:ภาษา bash
psql "$DATABASE_URL" -c "COPY (SELECT * FROM admins WHERE email = '<ADMIN_EMAIL>') TO '/tmp/admin_backup_<ADMIN_EMAIL_SAFE>.csv' CSV HEADER;" - ปิดสิทธิ์ + ตัดเซสชันของบัญชีนั้นบัญชีเดียวใน transaction เดียว:ภาษา bash
psql "$DATABASE_URL" -c "BEGIN; UPDATE admins SET is_superadmin = FALSE, updated_at = now() WHERE email = '<ADMIN_EMAIL>'; DELETE FROM admin_sessions WHERE admin_id = (SELECT id FROM admins WHERE email = '<ADMIN_EMAIL>'); COMMIT;" - Verify:
psql "$DATABASE_URL" -c "SELECT email, is_superadmin FROM admins WHERE email = '<ADMIN_EMAIL>';"ต้องได้is_superadmin = fและSELECT count(*) FROM admin_sessions WHERE admin_id = (SELECT id FROM admins WHERE email = '<ADMIN_EMAIL>');ต้องได้0 - Rollback: นำ CSV ข้อ 1 กลับเข้า (
COPY ... FROM) หรือUPDATE admins SET is_superadmin = TRUE WHERE email = '<ADMIN_EMAIL>';แล้วบันทึกเหตุผลลง audit
2. วิธี Rollback Model (เกณฑ์ตัวเลข + transaction)
เกณฑ์เริ่มต้นในการสั่ง rollback (ปรับได้โดย SRE พร้อมบันทึกเหตุผล):
- False-positive rate > 15% จากงาน review ย้อนหลัง 100 งานติดกัน, หรือ
- Inference P95 > 25 วินาที (ภาพ 1080p) ต่อเนื่อง 15 นาที, หรือ
- Worker OOM/killed ≥ 3 ครั้งใน 1 ชั่วโมง
ขั้นตอน:
- ผ่าน Admin Portal: เมนู AI Models → ค้นหาเวอร์ชันก่อนหน้า → กด Rollback (วิธีหลัก)
- กรณีหน้าเว็บใช้ไม่ได้ — หา ID รุ่นก่อนหน้าแบบ deterministic แล้วสลับใน transaction เดียว:ภาษา bash
psql "$DATABASE_URL" -c "SELECT id, version, deployed_at FROM model_versions WHERE status='active' ORDER BY deployed_at DESC LIMIT 1;" psql "$DATABASE_URL" -c "SELECT id, version, deployed_at FROM model_versions WHERE status='inactive' ORDER BY deployed_at DESC LIMIT 1;" psql "$DATABASE_URL" -c "BEGIN; UPDATE model_versions SET status='inactive' WHERE id = '<ACTIVE_ID>'; UPDATE model_versions SET status='active' WHERE id = '<PREVIOUS_ID>'; COMMIT;" - Verify:
SELECT id, version, status FROM model_versions WHERE id IN ('<ACTIVE_ID>','<PREVIOUS_ID>');ต้องมี active exactly 1 แถว; ยิง smoke inference 1 ภาพแล้วตรวจคะแนนอยู่ในช่วงที่คาด - Rollback ของ rollback: สลับ
<ACTIVE_ID>/<PREVIOUS_ID>กลับด้วยคำสั่งเดียวกัน
3. วิธีจัดการ Export Job ที่ค้างหรือล้มเหลว (ทีละ job — ห้าม restart backend ทั้ง service)
- ตรวจดิสก์:
df -h /var/lib/postgresql /tmp - ระบุ job ที่ค้างทีละตัว (ไม่แตะ job อื่น):ภาษา bash
psql "$DATABASE_URL" -c "SELECT id, status, updated_at FROM export_jobs WHERE status='running' AND updated_at < now() - interval '1 hour' ORDER BY updated_at;" - หยุดเฉพาะ worker ของ export (ห้าม
systemctl restart scamguard-backend— จะล้าง RAM ของงานอื่นทั้งหมด):systemctl stop scamguard-export-workerแล้วตรวจsystemctl status scamguard-export-worker - สำรองแถว job นั้นก่อนแก้:
psql "$DATABASE_URL" -c "COPY (SELECT * FROM export_jobs WHERE id = '<JOB_ID>') TO '/tmp/export_job_<JOB_ID>.csv' CSV HEADER;" - ทำเครื่องหมาย failed ทีละ job ใน transaction:
psql "$DATABASE_URL" -c "BEGIN; UPDATE export_jobs SET status='failed', error_message='Manually aborted (<TICKET>)' WHERE id = '<JOB_ID>' AND status='running'; COMMIT;" - Verify:
SELECT id, status FROM export_jobs WHERE id = '<JOB_ID>';ต้องได้failed; สตาร์ท worker กลับsystemctl start scamguard-export-workerแล้วตรวจ status
4. วิธีรับมือเหตุการณ์ละเมิดความปลอดภัย (Incident Response — ระบุเป้าหมายทีละตัว)
- บล็อก IP ทีละ address (ตัวอย่าง nftables; เลือกอย่างใดอย่างหนึ่งให้ตรง OS หน้างาน):
Verify:ภาษา bashnft add rule ip filter input ip saddr <IP> drop # หรือระดับ Nginx: เพิ่ม "deny <IP>;" ใน server block แล้วรัน "nginx -t && systemctl reload nginx" # หรือ AWS WAF: เพิ่ม <IP>/32 เข้า IP set ของ WebACL (ผ่าน console/CLI ตามบัญชีที่ใช้จริง)nft list ruleset | grep <IP>หรือnginx -tผ่าน + ทดสอบจาก IP นั้นถูกปฏิเสธ - เพิกถอนเซสชันเฉพาะบัญชีที่กระทบทีละบัญชี (ห้าม
TRUNCATE admin_sessions— จะเตะแอดมินทุกคนโดยไม่สำรอง):Verify:ภาษา bashpg_dump "$DATABASE_URL" -t admin_sessions -f /tmp/admin_sessions_$(date +%F_%H%M).sql psql "$DATABASE_URL" -c "DELETE FROM admin_sessions WHERE admin_id IN (SELECT id FROM admins WHERE email IN ('<ADMIN_EMAIL_1>','<ADMIN_EMAIL_2>'));"SELECT count(*) FROM admin_sessions WHERE admin_id IN (...);ต้องได้0; ไฟล์ backup ข้อบนต้องมีขนาด > 0 - รัน Backup ฐานข้อมูล (ดูข้อ 5) ก่อน เก็บหลักฐานอื่นต่อ
- ตรวจ
audit_logทันที: audit log เป็น append-only (ล็อก UPDATE/DELETE ระดับ policy) จึงเหมาะใช้อ้างอิงย้อนหลัง — แต่ห้ามอ้างว่าเชื่อถือได้ 100% ให้ cross-check กับ log ฝั่ง proxy/app เสมอ
5. การกู้คืนระบบและฐานข้อมูล (พร้อมเกณฑ์สำเร็จ)
- Backup:
bash server/scripts/backup.sh(เข้ารหัสด้วย OpenSSL) - Restore:
bash server/scripts/restore.sh <backup-file.enc> - เกณฑ์สำเร็จ (ต้องผ่านทุกข้อจึงถือว่าเสร็จ):
- สคริปต์จบด้วย exit code
0 - ตรวจ checksum ไฟล์ backup ตรงกับค่าที่บันทึกตอนสร้าง (
sha256sum) - หลัง restore:
SELECT count(*) FROM scans;(และตารางหลัก) ตรงกับจำนวนที่บันทึกไว้ก่อน backup ±0 - ยิง
GET /healthได้ HTTP 200 และล็อกอินแอดมินทดสอบผ่าน - ทดสอบ restore ลง staging อย่างน้อยไตรมาสละ 1 ครั้งแล้วบันทึกผล
- สคริปต์จบด้วย exit code