TipMe Tech Stack 2019

เห็นช่วงนี้หลายๆ ที่ share stack กัน ก็เลยอยากเล่าบ้าง…

Design

แต่ละที่ stack มี tradeoff แตกต่างกันไป ก่อนจะเข้าใจ stack ก็เลยจะต้องเล่าถึง design goals ก่อนว่าจะทำอะไร

  1. เราพยายามไม่ใช้ microservice ให้มากที่สุด เพราะเขียนคนเดียวไม่มีปัญญาดูหลาย service (และใน environment ที่เขียนหลายคนเราก็เห็นแล้วว่า service บางตัวก็ไม่ได้ถูกดูแลจนเราพอใจ)
  2. คุม budget เป็นเรื่องสำคัญค่อนข้างมาก เนื่องจากว่าเราพอมีไอเดียแล้วว่า market cap ของวงการนี้มันมีเท่าไร และไม่ได้มีเงินให้เผาเหมือน startup ก็เลยจะต้องรัดเข็มขัดอยู่ตลอดเวลา
  3. เนื่องจากว่ามันเป็น environment ที่ไม่มีใครค้านอะไรใน design เราได้นอกจากด้านบน ก็เลยพยายามจะใช้เทคโนโลยี cutting edge อยู่เสมอ

Frontend

jQuery

ย้อนกลับไปตอนที่สร้างเว็บ เวลานั้นเราตั้งคำถามว่าเราจะใช้ jQuery แล้ว iterate ได้เร็ว หรือใช้ React แล้วได้โค้ดที่สวยงามแต่ iterate ได้ช้า แน่นอนคำตอบเราคือใช้ jQuery สิ! ถึงเวลานี้ก็ยังคิดว่าคิดถูก 100%

เราว่าสิ่งที่คน Design พลาดกันเยอะคือเราไปมอง stack บริษัทเทพๆ (หรือโดนเค้า hype มา) แล้วบอกว่าเราจะทำแบบนั้น แต่นั่นคือ Design ที่เวิร์คกับเค้าใน scale นั้น มันไม่ได้แปลว่ามันจะเวิร์คใน scale ของเรา

React

React มาพร้อมกับเพื่อนของมันคือ Babel, Webpack และ MobX ซึ่งปัจจุบันโค้ด TipMe ฝั่งที่เป็น jQuery ก็ใช้ ES6 ผ่าน Babel และ build ด้วย Webpack

ในปี 2017 เราก็เอารายได้ส่วนหนึ่งบริจาคให้ Babel และ webpack Open Collective (เสียดายว่าเปิดบริษัทแล้วจะทำอะไรแบบนี้ก็ยากขึ้น เพราะมันไม่มีใบเสร็จหักค่าใช้จ่าย)

เหตุผลที่เอา React เข้ามาคือตอนที่ทำระบบ Overlay ใหม่ก็เริ่มมองเห็นอนาคตว่าเร็วๆ นี้ เว็บอาจจะได้เปลี่ยนเป็น React ทั้งเว็บ (สุดท้ายก็ยังไม่ได้ทำเพราะมีอย่างอื่นสำคัญกว่า และจะต้องเสียเงินเยอะมากในการเปลี่ยน) เลยเลือกเป็น React เลย

วิธีที่เราใช้ React ก็จะไม่ค่อยเหมือนชาวบ้านเท่าไรนัก แต่ก็ไม่ใช่ท่าแปลก คือตัวหน้าเว็บจะเป็น Django Template แล้วส่วนที่เป็น interactive ก็ค่อยเอา React ไป enhance โดยใช้ MobX เป็น state store

พอใช้ MobX แล้วเราก็เริ่มค้นพบว่า inheritance ทำให้โค้ดอ่านเข้าใจง่ายขึ้นมาก ก็เลยเอามาใช้กับ controller component ด้วย ตอนนี้เริ่มเจอปัญหาเพราะ inheritance กับ decorator (HOC) ไม่ค่อยจะกินเส้นกันเท่าไรนัก อาจจะต้องเริ่มถอดออกเพราะ community จะมาทางนี้

ถ้าถามเราเราก็ยังคิดว่า controller component ควรใช้ inheritance เพราะ React มันไม่มีวิธีสวยๆ ที่ทำให้ controller (นอกจาก root) ไม่ใช่ component

Developer tools

เครื่องมืออื่นๆ ที่ใช้ก็มี

  • Gulp – โค้ดใช้ Gulp build มานานมากเพราะต้อง compress CSS, JavaScript ด้วย ก่อนที่จะมี Webpack เข้ามา ช่วงต้นปีที่แล้ว Gulp จะมี custom plugin อันนึงที่เขียนเองแล้วบอกได้ว่า file นี้ include React vendor chunk หรือเปล่า แต่พอ Webpack 4 ออกก็เลิกใช้ไปใช้ Code split ตัวใหม่แทน
  • Storybook – เราทำ storybook ไว้สำหรับทุก Component อยู่ แต่ก็ไม่ได้อัพเดตนานแล้วเพราะไม่ค่อยได้แตะโค้ดตรงนั้น

Backend

Django

เฟรมเวิร์คที่เราชื่นชอบคือ Django ทั้งเว็บก็เลยอยู่บน Django monolithic เลย ตั้งแต่ระบบตกแต่ง ส่งเอกสาร ไปจนถึงบัญชี

แน่นอนว่าต้องใช้ Django REST Framework ด้วย สำหรับ API Endpoint ต่างๆ ในช่วงแรกๆ คิดว่าเราไม่ใช่ proper API ไม่จำเป็นต้องใช้ แต่ตอนหลังก็มา port เป็น DRF อยู่ดี ทำให้โค้ดส่วนที่เป็น validation ถูกแยกออกไปเป็นสัดส่วนชัดเจน

ตอนที่ย้าย DRF นี่ก็เป็นส่วนหนึ่งของ Great refactoring of 2017 ที่เราอ่าน Two scoops of Django แล้วก็ list มาว่าจะต้อง refactor อะไรบ้าง ถึงทุกวันนี้ก็ยังไม่หมด เพราะบางอันก็ยากมาก เช่น redesign ส่วน payment ใหม่หมด

Celery

เนื่องจากเราดีลกับระบบ 3rd party เยอะมาก ดังนั้น Celery จึงหลีกเลี่ยงไม่ได้ ปัญหาหลักตอนนี้คือมีบาง operation ที่ไม่ threadsafe อยู่เราจึงจะเปิด Celery ได้แค่ตัวเดียว แต่ก็ยังไม่เป็นปัญหาเท่าไรนักเพราะ operation ที่ทำอยู่ไม่ค่อยหนัก

ในปี 2017 เราบริจาครายได้ส่วนหนึ่งให้ python-social-auth, Celery, PyPy และในปี 2018 เราเป็น Python Foundation Supporting Member

Go

แม้เราจะยันไม่ยอมเป็น microservice อยู่นานมาก แต่พอเราจะต้องเก็บบัตรประชาชนลูกค้าแล้วก็เลยต้องยอมแยก service มาเพื่อแยก access level ให้ชัดเจน ถ้า app ตัวนอกถูกเจาะก็จะต้องไม่สามารถทะลุ Access Control เข้ามาได้ เลยเลือกใช้ Go เพราะมันกินทรัพยากรระบบน้อยที่สุด เหมาะสำหรับ service ที่ idle อยู่เฉยๆ เป็นส่วนมาก จะกินแรมเยอะก็ไม่เหมาะ

อีก service หนึ่งที่ใช้ Go คือ QR generator ที่ qr.tipme.in.th ซึ่งแต่เดิมเป็นส่วนหนึ่งของ monolithic แล้วก็คิดว่าควรจะแยกออกมา มันไม่ได้เกี่ยวพันกับระบบที่เหลือเลย

หลังจากแยกมาแล้วก็พบว่าเราเอา service นี้ไปลองเล่นอะไรได้เยอะมาก เช่น Cloud Run ก็ลองด้วย QR service มาแล้ว

TypeScript

TypeScript ค่อนข้างเป็นของใหม่ ในฝั่ง frontend เลยยังไม่ได้แปลงมาใช้ ใน backend จะมีใช้เพราะเราใช้ socket.io ทำ realtime

แต่เดิม realtime เราใช้ Django Channels ซึ่งมันก็ทำให้ก้อน monolithic ทำ WebSocket ได้ด้วย แต่พอใช้ๆ ไป เค้าก็ออกมาบอกว่าจะเปลี่ยน design ใหม่แล้วและจะไม่ compatible กับของเดิม พอลอง port ดูพบว่าใช้ effort มาก ก็เลยตัดสินใจว่าจะย้ายออก

แต่ย้ายออกก็ไม่ยาก เราใช้เวลาวางแผนอยู่ปีหนึ่งจึงจะนึกออกว่าจะเอาออกมาได้ยังไง เนื่องจาก design เดิมผูกติดกับ authentication system อยู่มาก และมันรู้ว่า user connect เข้ามาแล้วจะส่ง message ต้อนรับให้ด้วย

จากโจทย์นี้ วิธีที่ง่ายที่สุดคือก็เมื่อมีคน join แล้วให้ยิงข้อความเข้า message queue จากนั้นตัวแอพก็ read message มาแล้วตอบกลับไปว่าให้ส่งว่าอะไร ปัญหาก็คือเราพยายามหนีจาก message queue อยู่ เนื่องจากว่า RabbitMQ ใช้ทรัพยากรระบบมาก และ Cloud Pubsub ก็เคยมีกรณี latency 5 วินาที ทำเว็บเราล่มไปแล้ว

วิธีที่ทำอยู่คือให้ client ต่อเข้า WS service ก่อน แล้วจึงยิงไปบอก Main server ว่าต่อแล้ว จากนั้น main server จึงค่อยบอก WS ว่าให้ join ห้องได้และส่งข้อความแรกให้ไปด้วย วิธีนี้ทำให้บน server กลายเป็น one way communication

Sentry

Sentry เป็นโปรแกรมจับ Error และเป็น monitoring หลักที่เราดูเลย รายละเอียดเล่าบ่อยมากแล้ว ดูจากสไลด์แล้วกัน

Infrastructure

gRPC

ระหว่าง service ข้างในคุยกันเป็น gRPC ทั้งหมด ตอนนี้รู้สึกว่าคิดผิด ยังมองอยู่ว่าจะย้ายไป Twirp หรือลง Linkerd ดี แต่ Istio นี่ทดลองหลายหนแล้วยังไม่เวิร์ค

รายละเอียดตามสไลด์นี้

Kubernetes

ทั้งหมดทั้งปวงที่เล่ามารันอยู่บน Kubernetes แต่ก็มีส่วนที่รันอยู่ด้านนอกเช่นกัน (ของที่มี state กับต้องการ Fix IP)

เราคิดว่าระบบมี hard dependency กับ Kubernetes อยู่ เช่น เรา design ระบบให้ crash ตอน error เพื่อให้ Kubernetes restart ให้ จะได้ไม่ต้อง handle retry เอง และเราใช้ CronJob ด้วย

ก็เกิดคำถามว่าถ้าคนเลิกใช้ Kubernetes แล้วจะทำอย่างไร ตอนนี้คิดว่ายังไม่ต้องหาคำตอบเพราะน่าจะเป็นอนาคตอีกสิบปี

Cloudflare

จำไม่ได้ว่าย้ายมา Cloudflare ตอนไหน แต่ก็น่าจะหลังมาใช้ Google Cloud ซึ่งเราเอาไปใช้ serve static เป็นหลักก่อน เนื่องจากเราใช้ WebSocket มากแล้วกลัวจะเกิน usage quota ของแพลนฟรี (Cloudflare เสียเงินน่าจะเป็นอย่างถัดไปที่ซื้อ แต่ต้องคิด budget ดูก่อน)

ช่วงปีที่แล้วมีช่วงที่โดน DDoS บ่อยมากจนปวดหัว ก็เลยตัดสินใจย้ายโดเมนทั้งหมดขึ้น Cloudflare เพราะปกติจะโดนแค่ DoS ซึ่งใช้ Cloud Armor กันก็ได้ แต่รอบนั้น IP มันเยอะเขียน rule ไม่ทัน

Google Cloud

ทุกบริการเราอยู่บน Google Cloud ซึ่งก็ใช้หลายบริการเหมือนกัน คือ

  • Google Kubernetes Engine
  • Google Compute Engine (มีเครื่องที่ไม่ใช่ Kubernetes ด้วย)
  • Google Cloud Storage
  • Google Container Builder
  • Google Cloud Function ไว้แจ้งเตือนตอน Container Builder build เสร็จแล้ว
  • Google Cloud Vision
  • Stackdriver Logging
  • Stackdriver Monitoring

ก็ไม่รู้สึกว่า Stackdriver Monitoring มันดีสักเท่าไร ยังสงสัยว่า Google เค้า Monitoring ด้วยไอ้นี่ได้จริงหรอ (ถามคนรู้จักที่เป็น Google SRE เค้าบอกว่าไม่) แต่ที่ใช้เพราะมันเกือบได้ฟรี

CircleCI

ตัว Monolithic จะ build ด้วย Container Builder แต่พอใช้ๆ ไปพบว่ามัน build git submodule ยุ่งยากมาก ต้องมี service account เพิ่ม แล้วเราก็ต้องไป secure มันอีก ตอนนั้นพวก service เกิดใหม่เลย build บนเครื่องอย่างเดียว จนกระทั่งปีนี้มาลองหาดูแล้วก็พบว่า CircleCI มันดีมาก

เคยแกะ CircleCI ดู พบว่ามันใช้ GCE นี่แหละ โซน us-central ก็สงสัยว่าทำไมมัน build ไวกว่า Container Builder อีก….

Debian

ที่ชอบ GCP อีกอย่างคือ Google ค่อนข้าง prefer Debian เหมือนเรา เลือกมาเป็น default และ support ค่อนข้างดี ต่างกับ AWS ที่จะหา image Debian ต้องไปหาเบอร์ AMI มาใส่

เหตุผลที่ชอบ Debian ก็มาจากประสบการณ์ homelab แล้วเราก็พบว่า Ubuntu มัน upgrade version ทีไรก็พังทุกที พอมาใช้ Debian แล้ว มันไม่ค่อยออกใหม่ แถมตอนอัพก็ไม่พัง เลยแบน Ubuntu ตั้งแต่นั้นมา

อ้อ สำหรับเครื่อง Kubernetes แน่นอนว่าเป็น Container Optimized OS

Helm

Helm เป็นอะไรที่ไม่ชอบมากๆ แต่ถูกบังคับใช้เพราะ service หลายตัวมันใช้ จะไปนั่งเขียน manifest เองก็ขี้เกียจ เลยต้องยอมๆ มัน

Kapitan

สำหรับ Kubernetes templating solution ที่เราชอบก็คือ Kapitan จาก Deepmind เลย มันใช้ Jsonnet ในการ config ทำให้สามารถใส่ลูกเล่นได้พอสมควร

ทีแรกที่เอา Kapitan มาใช้ใช้คู่กับ Ksonnet แล้วรำคาญมากเพราะมันเขียนยุ่งยาก เลยโยนทิ้งไปแล้วกลับไปเขียน Kubernetes YAML ธรรมดา จนกระทั่งบ่นลง Twitter แล้วเค้ามาเห็นเลยชวนให้ไปนั่งคุยใน Slack เค้าก็คิดว่าปัญหาเราคือ Ksonnet นี่แหละ ที่ Deepmind ก็ไม่ได้ใช้ แต่เขียนเหมือน Manifest ธรรมดาๆ มากกว่า พอเราลองทำตามดูแล้วก็พบว่ามันง่ายดีแต่ทรงพลังมาก

Ansible

สำหรับเครื่องที่ไม่ใช่ Kubernetes ก็ยังจำเป็นจะต้องใช้ Ansible manage อยู่ แต่เนื่องจากเครื่องพวกนั้นไม่ได้ scale ออก ก็เลยจะไม่ได้ถึงขนาดจะต้องใช้ Packer ทำ image ด้วย

nginx

ระบบเรามี nginx อยู่หลายหน้าที่มาก อันแรกสุดคือเป็น ingress controller สำหรับ Kubernetes ซึ่งการที่มันสามารถใส่ snippet ได้ทำให้ปรับแต่งได้เยอะมาก

อีกหน้าที่หนึ่งคือเรามี nginx ที่เป็น proxy ขาออก เนื่องจาก partner หลายๆ เจ้าต้องการ fix IP ก็เลยจะให้วิ่งออกทาง nginx ตัวนี้ แล้วก็เอาไปรันในเครื่อง g1 ก็จะไม่เสียเงินเยอะมากเมื่อเทียบกับซื้อ Cloud NAT

Gunicorn

สัปดาห์นี้เพิ่งย้ายออกจาก Django Channels เป็น Gunicorn หมาดๆ เลย โดยลองใช้ aiohttp worker ดูด้วย

Sendgrid

ตอนเปิดใหม่ๆ เลี่ยงไม่ส่งอีเมลได้อยู่สักพัก ก็จนมีคนทักว่าคุณไม่มีข้อมูลติดต่อลูกค้าเลย ก็เลยทำระบบยึนยันอีเมลขึ้นมา แต่ก็ไม่ได้ใช้ทำอะไรจนกระทั่งตอนปิด True Wallet แล้วต้องส่งเมลหาลูกค้าทุกคน

ตอนนั้นก็พบว่า Sendgrid Marketing แพงมาก ต้องไปใช้เจ้าไหนจำไม่ได้แล้ว ยิงผ่าน SES ของเราอีกที

Haraka

พอเปิด True Wallet ตัวใหม่แล้วก็พบว่าต้องทำแจ้งเตือนโดเนททางเมลให้ลูกค้า แต่จะยิงผ่าน Sendgrid ก็จะเกินโควต้าฟรีแน่นอน (ถึงจะได้โควต้าสูงกว่าปกติแล้วก็ตาม) เลยตัดสินใจจะยิงเมลเอง ส่วนเมลไหนที่สำคัญและไม่ใช่ bulk ก็จะไปใช้ Sendgrid เหมือนเดิม

Mail server ที่ใช้คือ Haraka เขียนด้วย Node.js และใช้ใน Craiglist มาก่อน เราใช้เป็น mail server @whs.in.th นานแล้ว ก็เลยไว้ใจ ทีนี้มันยิงเมลออกทาง Google Cloud ไม่ได้อยู่แล้ว วิธีที่ทำคือเครื่องขาออกจะมี Haraka ซึ่งจะ relay มาเครื่องที่รันบล็อคนี้อีกที (มี TLS + whitelist กันเพื่อความชัวร์) เครื่องนี้ก็จะ sign แล้วส่งออกไปอีกที มี custom plugin เล็กน้อยเพื่อ strip routing ภายในออก

หลังๆ มารู้สึกว่า IP reputation ของเราดีกว่า Sendgrid เสียอีก เพราะลูกค้าหลายคนบอกว่าเมลยึนยันตก spam ไปแล้ว

Backoffice

Crisp

Support เพิ่งย้ายมาใช้ Crisp หลังจากไปลอง Freshdesk แล้วก็โดนเมลมาขายเรื่อยๆ

ปัญหาหลักที่ต้องการแก้คือลูกค้าเคยชินกับการ support ทาง Facebook แต่ระบบ support ส่วนมากเป็น ticket-based ไม่เหมาะสักเท่าไร จนมาใช้ Crisp ซึ่งมัน build ทุกอย่างเป็นแชต ก็ชอบมากๆ

นอกจากนี้ราคายังโหดมากเพราะ Unlimit plan แค่เหมาจ่าย $95 ใช้ได้ทุกอย่าง ไม่จำกัดจำนวนคน แถมเรายังได้ส่วนลดอีกเพราะเป็นบริษัทเปิดใหม่ คุ้มค่าแก่การอวยมากๆ

Crisp ยังมีระบบ Support site รวม article ต่างๆ ไว้ด้วย และ Status page ซึ่งแปลกใจมาก เท่าที่ใช้มา Status page อาจจะฟีเจอร์สู้ statuspage.io ไม่ได้ แต่ถ้าคิดว่ามันเป็นของแถมแล้วมันก็เวิร์คมากเลย ตอนนี้ก็เลยย้าย text ยุบยับในเว็บบางส่วนไปอยู่บน Crisp แทน

ฟีเจอร์ที่คาดหวังจาก Crisp ตอนนี้คือ custom slash command จะได้ integrate กับตัวเว็บเพิ่มไปได้อีก

GTK

ใช่แล้ว GTK บนลินุกซ์นั่นแหละ เราเอามาทำ Desktop app สำหรับพิมพ์ใบเสร็จส่งให้บัญชี รันได้ทั้งใน Windows และ Linux เขียนด้วย Go

Asana

พวก feature ต่างๆ จะ note ไว้ใน Asana ติดมาจากที่วงใน ใช้ง่ายดีและไม่มีอะไร distract ให้เสียเวลาเหมือน Trello (ไปนั่งลากการ์ดเล่น) หรือ issue tracker (ใส่ field เยอะๆ เล่น)

G Suite

สุดท้าย เมลติดต่องานต่างๆ ก็ใช้ G Suite รวมถึงทำเอกสารต่างๆ ด้วย

เก็บไฟล์บัตรประชาชนยังไงดี?

ช่วงต้นปีที่ผ่านมามีข่าวครึกโครมอันหนึ่งที่บริษัทชื่อดัง ทำ bucket ที่เก็บบัตรประชาชนลูกค้าเป็นสาธารณะ และมีผู้ไปพบเข้า

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

Threat Model

ก่อนจะทำอะไรก็ต้องนึกถึง threat model ก่อน เพราะถ้า regulation ต้องการว่าไฟล์ถูก encrypt นั้น เนื่องจากเราใช้ Google Cloud Storage ก็จะทำ Encryption ให้อยู่แล้วโดยไม่สามารถปิดได้ (ต่างจาก AWS ที่ค่า default คือปิด)

แต่ข้อกังวลสำหรับผมไม่ใช่ regulation เพราะเรายังไม่มีใครมาตรวจส่วนนี้ สำหรับเคสนี้ผมคิดว่าปัญหาจะอยู่ที่

  1. เผลอทำ bucket หลุดไป ข้อมูลไม่ควรจะสามารถอ่านได้
  2. Application server ถูก hack แล้วใช้สิทธิ์ของ app มาอ่านได้ (เพราะ app มันต้องอ่านได้ด้วย ตอนที่ admin เข้าไปตรวจสอบ)
  3. เนื่องจากเราจะใช้ signed url มีความเสี่ยงที่ signed url จะหลุดไป
  4. admin แอบเซฟรูปไป อันนี้คิดว่าคงไม่น่าจะเป็นปัญหาในตอนนี้ เนื่องจากทีมเรามีจำกัด

Design

ปัญหาที่ผมค่อนข้างกังวลคือข้อ 2 เนื่องจากแอพเป็น monolithic จึงการันตีได้ว่าทั้งระบบปลอดภัยได้ยาก และไม่มีแผนที่จะแยกเป็น microservice ให้ยุ่งยากด้วย เพราะคนเขียนมีคนเดียวไม่จำเป็นจะต้องพยายามทำอะไรให้รองรับทีมขนาดใหญ่ได้ในตอนนี้

ลักษณะการใช้งานของข้อมูลบัตรประชาชนคือ

  1. User submit ข้อมูลมา
  2. Admin เข้าไปดูรูปและยึนยันว่าเป็นบัตรประชาชนที่ถูกต้อง อาจจะมีการอ่านข้อความที่ต้องใช้เก็บไว้
  3. หลังจากนั้นแล้วไม่มีใครควรจะดูได้อีก
  4. หากบริษัทถูกเรียกถาม ควรจะสามารถเรียกคืนมาให้ตรวจได้

จะเห็นว่าจากข้อ 3 แล้วทำให้เราจัดได้ว่าข้อมูลนี้เป็น cold storage หลังแอดมินยึนยันข้อมูลแล้ว

ระบบที่เรา Design ออกมาก็เลยเป็นดังนี้

(ขออภัยกับ UML ด้วย ไม่ค่อยชินกับ tool ที่ใช้วาดเท่าไร)

  1. พอ admin ยึนยันบัตรประชาชนแล้ว app จะ call ไปหา secure service เราเรียกมันว่า sealerd
  2. sealerd จะถือ Google Cloud service account token อีกชุดหนึ่ง แยกไปจากของ app ปกติ
  3. sealerd ทำดังต่อไปนี้
    1. Download file มาแล้ว encrypt ด้วย public key ของบริษัท
    2. แก้ไข ACL ของไฟล์นั้นใน Google Cloud Storage ให้สามารถเข้าได้เฉพาะ service account ของ sealerd (แม้แต่ผมก็เข้าไม่ได้ ต้อง assume role เป็น sealerd เท่านั้น)
  4. app จะ mark ว่าไฟล์เข้ารหัสแล้ว เพื่อประโยชน์ในการแสดงผล UI

พูดง่ายๆ ก็คือเรามี security 2 ชั้นคือ

  1. File level access control ทั้งหมดจะถูกล้างเสมอ
  2. ถ้ามีคนเอาไฟล์ไปได้ ไฟล์ก็จะถูกเข้ารหัสด้วย key ซึ่งกุญแจถอดรหัสเราเก็บไว้ offline เว็บเราไม่สามารถถอดรหัสได้

จาก threat model แล้วก็จะพบว่า

  1. ถ้า bucket เผลอเปิดเป็น public เราจะมี encryption ป้องกันไม่ให้อ่านไฟล์ได้
  2. สิทธิ์การใช้ของ application server จะถูกยกเลิกหลังไฟล์ถูก seal (ทั้งนี้การถอนสิทธิ์ใน Google Cloud Storage เป็น eventual consistent แอพอาจจะยังอ่านได้อยู่อีกประมาณ 1 นาที)
  3. เราตั้งให้ Signed URL มีวันหมดอายุอยู่แล้ว และเนื่องจากเรายกเลิกสิทธิ์ของ Application server หลัง seal แล้ว Signed URL ที่มีจึงใช้ไม่ได้

Implementation

คำถามถัดมาคือ แล้วจะ implement อย่างไร?

ภาษา

ภาษาแรกในหัวผมที่จะใช้เขียนคือ Rust แต่หลายๆ ครั้งที่พยายามจะใช้ผมพบว่า Rust มี library ไม่ค่อยดีพอเลยยังไม่กล้าเสี่ยง

ถัดมาคือ Python กับ JavaScript ที่ใช้อยู่แล้ว แต่เนื่องจาก encryption library มักเป็น synchronous และ blocking ด้วย ทั้ง Python และ JavaScript พวกนี้มี Global Lock ก็เลยไม่น่าจะเหมาะที่จะเอามาใช้ เพราะต้องลุ้นอีกทีว่า library ที่ใช้มันไปทำข้างนอก interpreter lock หรือไม่

อีกเหตุผลคือผมอยากจะใช้ Tink ที่ยังไม่รองรับภาษาเหล่านี้

สุดท้ายคือ Go ซึ่งผมคิดว่า Tink น่าจะรองรับ ก็เลยเลือกใช้ Go

RPC

ท่า RPC ก็เป็นเรื่องที่ต้องตัดสินใจเช่นกัน ตัวเลือกก็จะมี

  1. JSON on REST (HTTP) ง่ายๆ ใครๆ ก็ใช้
  2. gRPC
  3. Cap’nproto
  4. Flatbuffer

ส่วนตัวอยากลอง Cap’nproto/Flatbuffer แต่รู้สึกว่า RPC library มันยังไม่ค่อยดี ไว้โอกาสหน้าอาจจะ port ดูเล่นๆ

เหลือแค่ gRPC กับ REST ก็เลยเลือก gRPC

เหตุผลที่เลือกคือ

  1. มันมี type ชัดเจน ใน Go จะเขียนง่าย
  2. มันเร็วมาก ที่วงในผมเขียน integration test ให้ start gRPC server ใน python แล้วยิง test request เร็วอย่างกับยิงใน process เดียวกัน
  3. ไม่ต้องกังวลเรื่อง compatibility เพราะไม่มี clientside ยิงมาอยู่แล้ว เลยไม่จำเป็นจะต้องใช้ HTTP

Encryption

Project นี้รู้สึกว่าจะพลาดที่สุดก็ตรงนี้แหละ ผมเสียเวลาไป 3-4 วันในการ design implement ระบบที่ secure ที่สุดเท่าที่จะนึกออก พอเขียนจริงเลยไม่ได้ research ละเอียดว่าภาษาอะไรที่จะใช้ implement แล้วเหมาะสม

มารู้สึกตัวครั้งแรกคือตอนที่เขียน gRPC ให้ Go เสร็จแล้วกำลังจะเขียนส่วนที่เข้ารหัส ก็เจอกับ Surprise แรกคือ Tink ไม่มีใน Go… อ้าว!!

ก็เลยมาที่ plan B คือใช้ Sealed box เหมือนที่ผ่านๆ มา

Surprise ที่สองคือ Go มี nacl box ในตัวก็จริง แต่ไม่มี libsodium ทำให้ไม่มี sealed box…

สุดท้ายเลยต้องทำเหมือนใน JavaScript คือ implement nacl-sealed-box เสียเองเลย ซึ่งก็ไม่ยากเพราะ Go มี crypto primitive ให้หมดแล้ว แค่เอามาต่อกันให้เหมือนกับของ libsodium เป็นอันเรียบร้อย

Connect

เสร็จแล้วก็เขียนต่อจากฝั่งแอพ เนื่องจากเป็น gRPC ก็ทำให้การ implement ค่อนข้างง่าย แต่ตอน compile proto ก็อาจจะเหนื่อยหน่อย

สำหรับการ dev บนเครื่องเราก็ทำ version พิเศษที่ต่อกับ local filesystem และเพื่อความชัวร์จึงทำให้ option ในการเลือก driver นี้เป็น link time option จะได้ไม่พลาดบน production

สรุป

ในวงการ Cryptography มีกฎอยู่ข้อหนึ่งเรียกว่า Schneider’s Law บอกว่า

ใครๆ ก็สามารถคิดค้นระบบความปลอดภัยที่ที่เทพมากจนเค้าไม่สามารถคิดวิธีแหกได้
Any person can invent a security system so clever that she or he can’t think of how to break it

ผมว่าระบบอันนี้ค่อนข้าง fit กับ description นี้แล้ว คือผมคิดว่ามันแน่นหนาพอ และผมว่าถ้าไปเจอใครโม้เรื่องวิทยาการรหัสลับเสียดิบดีโดยไม่ได้มี disclaimer แบบนี้ ผมว่ามันน่ากลัวกว่านี้อีกเยอะ

จริงๆ แล้วคิดว่าในการใช้งานส่วนมากแล้วอาจจะแค่ download ไฟล์ที่อนุมัติแล้วเก็บเข้า offline cold storage ก็น่าจะปลอดภัยกว่าระบบนี้แล้ว แต่ก็จะ automate น้อยลง ความเสี่ยงในการสูญหายมากขึ้น ใน tradeoff ของระบบเราแล้ว วิธีที่อธิบายมาในบทความนี้จึงน่าจะเป็นทางเลือกที่ค่อนข้างเหมาะสม