What went right?

เมื่อกลางปีไปพูดให้น้องม. ปลายที่โรงเรียนฟังเรื่องสายอาชีพ

เราก็สังเกตอย่างนึงว่า จริงๆ แล้วเวลามีคนอยากให้แนะนำว่าจะใช้ชีวิตในม. ปลาย/มหาลัยยังไง คำตอบของคนที่มาพูดหลายคนไม่ใช่ What went right แต่เป็น What I regret

ซึ่งมันก็อาจจะดี เพราะต้องมีประสบการณ์ถึงจะบอกได้ว่าในช่วงชีวิตที่ผ่านมามันเสียดายอะไร

แต่เรารู้สึกว่าคนฟังอาจจะไม่ได้ relate ด้วย เช่น เวลามีคนถาม เราจะบอกว่าตอนจบมาตกใจว่าความฝันตั้งแต่เด็กว่าจะเป็นโปรแกรมเมอร์ มันไม่มีตำแหน่งงานชื่อโปรแกรมเมอร์นะ มันแยกเป็นอีกหลายสิบแบบ ต้องเลือกเอาสักแบบ

คนฟังก็อาจจะรู้สึกว่าเอาแค่ให้มีงานทำก่อนถึงจะเลือกได้ขนาดนั้น

มันอาจจะเป็นเหตุผลที่ว่าทำไมคนอายุเยอะๆ ชอบเล่าเรื่องเก่าๆ (ขนาดเราอายุไม่เยอะก็ยังชอบเลย) ทั้งๆ ที่คนฟังไม่ค่อยอยากฟังหรอก

What went right?

ก็เลยสงสัยว่า ถ้าอย่างนั้นแล้วเวลาต้องพูดเรื่องนี้ เราควรจะพูดอะไรดี? What went right ของเราคือ

  • Get a Homelab! ที่บ้านสนับสนุนให้เรามี Homelab มาตั้งแต่ม. 1 แล้วมันทำให้เกิดนวัตกรรมเยอะมากกก (อย่างน้อยๆ project NSC 2 ตัว) ไปจนถึงวันที่เราย้าย server ออกจากบ้านไปบน VPS แล้วมันก็ยังเคย host ทั้ง Kyou (NSC 15 winner), TipMe

ถ้าบอกว่าเปลืองไฟ เราว่า ROI server เรา 100 เท่าเลยนะ

  • จ่ายเงินเล่น cloud ซะเถอะ มันไม่แพงอย่างที่คิด

ก่อนย้าย TipMe มา cloud เราจ่ายค่า S3 เดือนนึงไม่ถึง 30 บาท แล้วพอมันตัองจ่ายอยู่แล้ว เวลาอยากเล่น EC2 หรืออื่นๆ มันเลยไม่มี friction ในการเริ่มเท่าไร

แต่ที่สนุกสุดคงเป็นตอนฝึกงาน ทั้งเราตอนฝึกงานและน้องฝึกงานที่เราเคยดูบอก CTO เหมือนกันว่าการเล่น cloud โดยไม่ต้องจ่ายเงินเองนี่มันเทพมาก

  • Work on Open Source

เอาจริงๆ เราว่างาน open source contribution เราค่อนข้างน้อย คือสองสามเดือนทำที แต่ก็คงเป็น spirit ของ open source ล่ะคือ เรามีปัญหา เค้ามีทางแก้ แต่เค้าไม่ได้แก้ตรงที่เราคัน เราก็ patch ให้มันหายคัน

แต่การอยู่ในวงการ open source สอนอะไรเราเยอะมาก เราเริ่มมาจากโมบอร์ด SMF ซึ่งมันก็จะเห็นโค้ดคนอื่นว่าเค้า implement อย่างไร มี pattern อะไร มันก็ influence สไตล์เรามาพอสมควร (ถ้าเห็นเขียน comment แบบอ่านไม่รู้เรื่องแต่ใส่ cultural reference ก็น่าจะติดมาจาก SMF นี่แหละ)

มันสอนให้เราไม่มอง library เป็น black box แต่เป็นส่วนหนึ่งของโค้ดเรา และ design advice ที่ดีที่สุดที่เราเคยได้ยินมาคือ “ให้คิดเสมอว่าโค้ดที่เขียนอยู่จะถูก open source เมื่อไรก็ได้”

เพราะมีหลายคนไม่กล้า open source งานตัวเองเพราะว่าอายโค้ด พอคิดแบบนี้แล้วก็จะไม่กล้าเขียนอะไรน่าอายตั้งแต่แรก

  • Blog

รู้สึกว่าสกิลการเขียนเราอ่านไม่ค่อยรู้เรื่อง ครูภาษาไทยเคยบอกเรามีสไตล์ของตัวเอง เหมือนคนอ่านหนังสือมาเยอะ (??) แต่เวลากลับไปอ่านงานเก่าๆ ก็รู้สึกว่าพัฒนาขึ้นมาเยอะ

พอเขียนบล็อคแล้วมันเป็นการโปรโมท เขียนบล็อคส่วนตัวก็โปรโมทตัวเอง เขียนบล็อคบริษัทก็ชวนคนมาบริษัท แม้แต่ว่าเขียนบล็อค TipMe ที่เขียนก็คงเพราะอยากให้คนใช้มีความเชื่อมั่นในระบบเรา

ข้อเสียของการเขียนเยอะๆ คือมันก็โชว์โง่ได้เหมือนกันอย่างที่เป็นดราม่าอยู่ในปีที่ผ่านมา ในบล็อคนี้เองถ้าขุดของเก่าๆ ก็มีอันที่ไม่ค่อยอยากให้อ่านแล้วเหมือนกัน บางอันก็ hide ทิ้งไปแล้ว ก็ยังดีว่าเขียนสมัยเด็กๆ อันไหนไม่ดีคนอ่านก็มองว่าเป็นความคิดเด็กๆ ไปได้ (เคยขุด exteen blog สมัยป. 4-6 มาอ่าน อันนั้นคือลบได้เลย ไม่อยากอ่านสักเรื่อง)

แต่อายุเท่าไรก็มีความคิดแบบเด็กๆ ล่ะ เรียกว่าความคิดที่ยังไม่มีประสบการณ์ก็ได้ เพียงแต่พอคนนับถือเรามากขึ้น (ไม่ว่าด้วยอายุ หรือประสบการณ์) เวลาพลาดมันก็อาจจะมี impact ที่มากขึ้นไปด้วย หลายปีมานี้เลยลด public post ลง แล้วเขียนลง friend only บ้าง ลงบล็อคแล้วไม่ลงใน public บ้าง (ใคร subscribe blog ก็ได้กำไรไป)

มีคนบอกว่าเวลาไปคุ้ยงานเก่าแล้วรู้สึกจูนิเบียว ไม่ต้องแปลกใจ มันแสดงว่าคุณเก่งขึ้นเยอะแล้ว


พอมา List ดูแล้วก็เริ่มรู้สึกว่า จริงๆ list พวกนี้ไปเล่าให้คนอื่น เค้าก็คงไม่ relate อยู่ดี เพราะเค้าไม่ได้มีความสนใจเหมือนเรา

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

ช่วงต้นปีที่ผ่านมามีข่าวครึกโครมอันหนึ่งที่บริษัทชื่อดัง ทำ 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 ของระบบเราแล้ว วิธีที่อธิบายมาในบทความนี้จึงน่าจะเป็นทางเลือกที่ค่อนข้างเหมาะสม