Rearchitecturing TipMe for Anti-DDoS

ช่วงหลังๆ มา TipMe โดน DDoS บ่อยขึ้น ซึ่งโดนไปแต่ละครั้งก็ต้อง rearchitect แก้ไปเรื่อยๆ เพราะเราไม่อยากบอกว่าโดนยิง ปิดบริการจนกว่าคนยิงจะเลิก

Gen 1 – Cloud Armor

Architecture ของ TipMe มันคือ Client > Google Cloud Load Balancer > ingress-nginx > gunicorn ซึ่งไม่มีกัน DDoS อะไรตรงไหนเลย นอกจากพวก Layer 4 attack ที่ Google LB absorb ให้แค่นั้น เหลือแค่ L7 ที่ต้องจัดการ

พอโดน DDoS เข้ามาแรกๆ ก็เอา Cloud Armor มาปิด แต่ Cloud Armor รุ่นแรกๆ ในมุมการป้องกัน DDoS คือแย่มาก เรียกว่ามันเป็น IP Firewall เลยก็ว่าได้ (Google Cloud LB ไม่มี IP firewall ให้แบบ AWS ALB) แถมคิดเงินตาม rule change ด้วย เลยเปิดเฉพาะระหว่างมี attack เท่านั้น

ครั้งแรกๆ ที่โดนยิง มันเป็น DoS ไม่ใช่ DDoS คือเหมือนคนกด F5 รัวๆ นี่แหละ ระบบก็ล่ม ก็ ban IP กันไปเป็นอันใช้ได้ เพราะ IP มันโผล่ชัดเจนใน Load balancer log แต่พอโดน DDoS ของจริงแล้ว request มันมาหลากหลายมาก แล้ว log ไม่ค่อยมีข้อมูลอื่น ก็จนปัญญาไม่รู้จะจับอย่างไร…

(เข้าใจว่าปัจจุบัน Cloud Armor ดีขึ้นมาหน่อย แต่คิดว่าคงไม่ใช้แล้ว)

Gen 2 – Cloudflare

มีช่วงถัดมาโดนยิงบ่อยขึ้นบ้าง ก็เลยตัดสินใจย้ายเว็บมาใช้ Cloudflare Free เต็มตัว ที่เดิมไม่ได้ใช้เพราะเว็บใช้ WebSocket ค่อนข้างเยอะกลัวจะโดนคิดเงิน แต่พอโดนยิงบ่อยๆ ก็คิดว่าช่างมันย้ายๆ แล้วกัน

สิ่งที่พบกับ Cloudflare Free คือเอาจริงๆ มันไม่ได้กันอะไรเลย คนชอบบอกว่าใช้ Cloudflare กันโดนยิง จริงๆ ไม่ใช่ ผมเปิด ticket ไปถามก็รู้ว่ามันมี threshold เลขนึงถ้าคุณยิงเข้ามาเกินเลขนี้มันจะ block ให้ แต่ถ้ายิงมาไม่ถึงมันไม่ block เพราะไม่รู้ว่ามันเป็น traffic จริงๆ หรือคุณจัด flash sale อยู่ ซึ่งเลขนี้ผมมีโซนที่เป็น Enterprise อยู่ เขาก็บอกเลขให้ (แพลนอื่นๆ เขาไม่บอก) พอเห็นเลขแล้วก็รู้เลยว่าถ้ายิงแบบ DoS มาน่าจะโดนบล็อก แต่ถ้า botnet ใหญ่ๆ แล้วยิงเบาๆ แบบนี้หลุดแน่นอน จะเอาให้อยู่สงสัยต้องใช้ e2-medium 4-5 เครื่องถึงจะรับโหลดที่เขาไม่ block เข้ามาไหว ซึ่งเปิด server เข้าสู้มันไม่ได้ทำให้เกิดรายได้ผมก็ไม่อยากจะไปใช้วิธีนี้

ทีนี้ฟีเจอร์จริงๆ ของ Cloudflare คือ firewall rule ซึ่งเราต้องเขียน rule เอง (อีกแล้ว) หลักๆ ที่มีคนแจกกันในเน็ตก็คือ ISP ไม่พึงประสงค์ พวกนี้แบนไปเลย แต่ที่จะไม่มีแจกก็คือเอา pattern ที่มีคนยิงจริงมาตั้ง block แบบนี้จะไม่หลุดเข้ามาเลย (ผมว่ามันเป็น security by obscurity หน่อยๆ ถ้าแกะได้ว่า rule ผมเขียนยังไงก็ bypass ได้ง่ายๆ)

Gen 2.5 – Cloudflare Pro

ปีนี้มี Streamer รายใหญ่เข้ามาซึ่งคนดูน่าจะเป็นสาย tech เยอะ เลยมีพวกลองของพอสมควร botnet ที่ยิงเข้ามาก็จะเป็นอีก pattern นึงไม่เหมือนสมัยก่อน

พอ pattern มันเปลี่ยน rule ที่ block อยู่เดิมมันเอาไม่อยู่ แต่ก็ไม่รู้จะเขียน rule มากันยังไง พอดีผมมี zone enterprise อยู่ก็จะเห็น firewall log มันมีกราฟให้ดู ซึ่ง free ไม่มี ก็เลยยอมซื้อ Cloudflare Pro เพื่อให้มี dashboard แบบเดียวกัน ทีนี้พอมันมี attack เข้ามาเราก็เห็น pattern ไปตั้ง block ได้ง่ายขึ้นมาก

ปัญหาของ Pro ที่ต่างกับ Enterprise คือมัน Match by Regex ไม่ได้ แล้ว TipMe URL มันเป็น vanity URL คือ https://tipme.in.th/username ทำให้ใช้ string contains อย่างเดียวมา match หน้าโดเนทไม่ได้เลย ก่อนจะยอมไปซื้อฟีเจอร์นี้ใน Business plan ก็เริ่มคิดว่าเราน่าจะถึงเวลา rearchitect กันก่อน

Gen 3 – Caching

รบร้อยครั้งชนะร้อยครั้งก็ไม่สู้ชนะได้โดยไม่ต้องรบ

เอาจริงๆ ชนะโดยไม่ต้องรบคือไม่สร้างศัตรู แต่ผมว่าเป็นไปไม่ได้ ก็ต้องรับมืออีกทางหนึ่งคือ Caching หรือการให้ Cloudflare รับหน้าแทนให้

จริงๆ การทำ Cache ก็มีความเสี่ยงสูงเพราะใน Cloudflare ถ้าจะ cache HTML ต้องตั้ง cache level = cache everything เท่านั้น แล้ว behavior มันจะเปลี่ยนไป คือถ้าไม่บอกมันว่าห้าม cache มันจะ cache by default ก็ไม่รู้ทำไมเขาออกแบบระบบให้มันงงๆ แบบนี้ ผมเคย config เว็บอื่นผิดแล้วมัน cache อันที่ไม่ควร cache ทำให้ personal info หลุดมาแล้ว (login แล้วเห็นเป็น user คนอื่น) ก็เลยรู้สึกว่าเสี่ยงสูงมากที่จะไปทางนี้ แต่ก็จำเป็นต้องทำ

ครั้งนี้ทดลองโดยการ install middleware ไว้ใน origin ที่จะส่ง Cache-Control: private, max-age=0 ทุกหน้าเลยเหมือนที่ PHP ทำให้ by default อยู่แล้ว ทดลอง test ดูก็พบว่า Cloudflare ไม่ cache จริง ก็คิดว่าทางนี้ไปได้ถูกทาง เลยไปเพิ่มให้หน้าโดเนทมี cache ได้สัก 1 นาที แล้วก็เพิ่มระบบว่าเวลา reconfig หน้าโดเนทแล้วมันจะยิงไป purge cache ของหน้านั้นให้ ซึ่งก็เป็นข้อดีของ Cloudflare ที่ purge ฟรีและไม่จำกัด ไม่เหมือน Google CDN หรือ Cloudfront ที่คิดเงินค่า invalidation ด้วย

ผลของวิธีนี้ก็ effective พอสมควร มีบาง DDoS ที่ไม่ได้รับผลกระทบเลย (โดนจับที่ Firewall rule หรือ cache ไปหมด) แต่ก็มี botnet ใหม่โผล่เข้ามาที่ยิง random path เลยเพื่อ bust cache ก็เลยเพิ่ม rule ดักจับ cache busting เข้าไปอีก

แต่ว่าหน้าที่โดนยิงเยอะที่สุดคือหน้าแรกซึ่งเดาว่าเป็น random internet traffic เลยแหละไม่ได้รู้จักอะไรกัน แล้วหน้านี้มันแสดงผลต่างกันระหว่างเวลาที่ login กับไม่ได้ login ซึ่งต้องใช้ฟีเจอร์ bypass cache on cookie แต่ว่ามันเป็น enterprise feature เลยไม่สามารถทำได้ ก็ต้องมองเป้าถัดไปว่าถ้าจะแก้ปัญหาที่ใหญ่ที่สุดนี้เราต้อง rearchitect แล้วในระหว่างนี้ก็ใช้วิธีคือเขียน firewall rule ปิดที่หน้าเสี่ยงสูงไว้ก่อน จะยอม false positive ก็ได้ขอให้อย่าให้ยิงหลุดเข้ามา

Gen 4 – Go Static!

วันก่อนไปลองเล่น Astro มา ตอนแรกก็อ่านไม่เข้าใจว่ามันทำอะไร แต่ก็พบว่าไอเดียมันตรงกับที่อยากได้มากๆ คือเว็บเป็น static generate แบบ Gatsby แต่ว่าเวลา rehydrate ไม่ได้ทำทั้งหน้า ทำเฉพาะส่วนที่จำเป็นต้องทำเท่านั้นทำให้ bundle size เล็กลง ก็เลยคิดว่างั้นเราจะ port เว็บเดิมแบบ 1-1 เลยจาก Django Template มาเป็น Astro + React

ข้อดีของการ port 1-1 เลยคือมัน rollback ง่ายเพราะหน้าเหมือนเดิม ไม่ต้องกลัวว่า ship feature ใหม่ไปแล้วลูกค้างง ใช้ๆ อยู่หายไป และไม่เกิด scope creep เท่าไรนักซึ่งเป็นปัญหาใน project redesign ที่ผ่านๆ มาแล้วไม่ได้ออกสักที

ปัญหาก็คือหน้าเว็บหลักมันเป็น Django ไปแล้ว ถ้าเราเอา frontend ไปไว้บน static hosting ก็ต้องย้ายไปเปิด subdomain ใหม่ซึ่งลำบากแน่นอนและทำให้เกิด CORS request เยอะ จะใช้ท่า strangler pattern ที่ proxy path เก่าเข้า API, path ใหม่เข้า frontend ก็จะติดปัญหาว่าถ้าโดนยิง DDoS origin ก็รับไม่ไหวอยู่ดี

ก็มองๆ ไปเห็น Cloudflare Workers ซึ่งมันรันที่ edge และ scale ได้ไม่จำกัด เลยตัดสินใจไปทางนั้น โดยที่หน้าเว็บ Astro จะทำงานบน Cloudflare Pages แล้วบน domain หลักมี Cloudflare Worker route ระหว่างเข้า origin กับเข้า pages แถมมันยังช่วยทำลายข้อจำกัด Astro อีกอย่างหนึ่งด้วยคือ URL ต้องเป็น static เท่านั้น โดยเราให้ Workers map URL หลายๆ อันเข้าที่ static URL อันเดียวได้

ในส่วนของหน้าเว็บก็จะช่วยให้ cache บางอย่างได้ดีขึ้นด้วย เช่นเว็บมีระบบประกาศทั้งเว็บ (site-wide announcement) เดิมทีเวลาแก้ไขประกาศต้อง purge cache ทั้งเว็บ แต่เนื่องจากเว็บเป็น static แล้วเลยต้อง rearchitect ให้ระบบประกาศเป็น API และ fetch จาก client side แทนทำให้ประกาศมีระยะเวลา cache ต่างจากของหน้าเว็บหลักได้

Client side is still the future

หลังๆ มาผมเริ่มเห็นกลุ่ม frontend กลับไปมองว่า web application มันไม่ควรจะ split frontend-backend แล้วเพราะ single page application มันโหลดช้า แล้วกลับไปสู่ยุคคลาสสิกว่าใช้ JavaScript ให้น้อยลงและเว็บทำงานต่อไปได้

ผมมองว่าเว็บ TipMe มัน hold out ผ่านยุค SPA มาได้นะ ถ้าแกะดูส่วน overlay ก็จะเห็นว่ามันเป็น island architecture มาตั้งแต่ launch ฟีเจอร์นี้หลายปีก่อนแล้ว แต่วันนี้ผมกลับมาพูดว่าทำเว็บแบบโบราณมันไม่ใช่อนาคตที่เราอยากได้ หนึ่งคือ interactivity ต่างๆ มันทำยาก เสียเวลากว่าทำใน client side เยอะเพราะต้องเก็บ intermediate state และสองคือมันยังเป็นท่าที่ cost effective มากกว่าโดยเฉพาะถ้าเว็บจะโดนยิงบ่อยๆ แบบ TipMe

ซึ่ง Island architecture ยุคใหม่กำลังมา และผมคิดว่าเราทำเว็บกึ่ง SPA ยุคใหม่มันทำให้เราได้ข้อดีทั้งสองข้อ โดยที่ฝั่ง user ไม่ได้ต้องรอโหลดเว็บนานๆ เหมือนสมัย SPA อีกแล้ว