2 คะแนน โดย GN⁺ 2024-06-02 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในความเสี่ยงการรั่วไหลของข้อมูลบน AWS ปัญหา การเข้าถึง S3 bucket โดยไม่ได้รับอนุญาต ปรากฏซ้ำๆ และด้วยการออกแบบ API แบบเก่ากับพฤติกรรมยกเว้นบางอย่าง จึงตัดสินแบบง่ายๆ ว่า “สาธารณะ/ไม่สาธารณะ” ได้ยาก
  • งานหลายอย่างของ S3 ไม่ได้เรียกผ่าน AWS endpoint ทั่วไป แต่เรียกผ่าน URL ของ bucket โดยตรง และหากนโยบาย bucket ผิดพลาด แม้แต่คำขอ curl ที่ไม่ยืนยันตัวตนก็อาจทำงานที่เสี่ยงอันตรายได้
  • การบล็อกแค่ s3:ListBucket อย่างเดียวไม่ได้แปลว่าปลอดภัย เพราะเส้นทางอย่าง ListBucketVersions, ListMultipartUploads, fetch-owner อาจเปิดเผย object key และตัวระบุบัญชี ได้
  • ผู้อัปโหลดสามารถมีผลต่อคุณสมบัติของออบเจ็กต์ เช่น storage class, tag, Object Lock, และ header บางส่วนที่เกี่ยวกับ redirect จึงต้องมีการควบคุมเพิ่มเติมอย่างเงื่อนไข IAM และนโยบาย lifecycle
  • หากดูแค่ ACL กับการตั้งค่าบล็อก public access แล้วสรุปว่าเป็น private อาจพลาดช่องทางอื่น เพราะผู้ใช้อินเทอร์เน็ตยังเข้าถึงออบเจ็กต์ใน S3 ได้ผ่าน CloudFront distribution หรือ Cognito identity pool

การออกแบบ API แบบเก่าของ S3 และการเรียกแบบ anonymous

  • S3 เป็นหนึ่งในบริการยุคแรกของ AWS จึงมีความเสถียรและผ่านการทดสอบมามาก แต่ก็ยังมีร่องรอยจากก่อนยุคที่รูปแบบการออกแบบถูกทำให้เป็นมาตรฐาน ทำให้มี รูปแบบ API ต่างจากบริการ AWS อื่น
  • API บางส่วนของ S3 ใช้ endpoint ทั่วไปอย่าง s3.us-east-2.amazonaws.com แต่หลายงานต้องส่งคำขอไปยัง URL ของ bucket เป้าหมาย โดยตรง
    • ตัวอย่างการดึงรายการ bucket คือ GET / ในรูปแบบ Host: [bucketname].s3.amazonaws.com
    • การดู tag ของ bucket ก็ต้องส่ง GET /?tagging ไปยังโฮสต์ของ bucket นั้น
  • บริการ AWS จำนวนมากอย่าง EC2 หรือ DynamoDB ใช้ endpoint ทั่วไป และส่ง resource เป้าหมายผ่าน HTTP header หรือพารามิเตอร์เป็นเรื่องปกติ
  • เนื่องจาก S3 bucket รองรับทั้ง public access และ authenticated access จึงไม่ชัดเจนเสมอไปว่า API ใดบ้างที่เรียกได้โดยไม่ต้องยืนยันตัวตน
  • หากตัวอย่างนโยบาย bucket อนุญาต Principal: "*" และ Action: "s3:*" กับ resource ของ bucket ก็อาจลบ bucket ได้แม้ด้วยคำขอที่ไม่ยืนยันตัวตน
  • งานบางอย่างไม่รองรับคำขอแบบ anonymous และจะคืนข้อผิดพลาดอย่าง s3:GetBucketOwnershipControls does not support Anonymous requests!
  • คำขอ API แบบ anonymous จะถูกบันทึกใน CloudTrail ภายใต้บัญชี anonymous
    • หากเป็นคำขอที่ไม่ยืนยันตัวตน จะระบุไม่ได้ว่าใครเป็นคนลบ bucket หรือใครเป็นคนตรวจสอบการตั้งค่าการเข้ารหัสหรือสถานะ logging
  • เส้นทางอย่าง /?logging, /?tagging, /?encryption สามารถทดสอบได้จากเบราว์เซอร์ด้วย
  • บางงานอย่าง GetObjectTorrent ยังมีเอกสารอยู่ แต่ไม่สามารถใช้งานได้อีกแล้ว

การบล็อกแค่ ListBucket ยังป้องกันการเปิดเผย object key ได้ไม่ดีพอ

  • การดาวน์โหลดออบเจ็กต์จาก S3 ต้องรู้ key ของแต่ละออบเจ็กต์ก่อน ซึ่ง key ทำหน้าที่คล้ายพาธของไฟล์
  • คำขอ GET ไปยัง root bucket อาจคืนเนื้อหาใน bucket ได้ตามเงื่อนไข จึงทำให้การปฏิเสธ s3:ListBucket ดูเหมือนเป็นการป้องกันที่พบบ่อย
  • แม้ใช้ ACL แบบ public-read ควบคู่กับนโยบายปฏิเสธ s3:ListBucket ก็ยังมีช่องทางให้ได้ object key อยู่
    • GET /?versions หรือ s3:ListBucketVersions จะคืน metadata ของเวอร์ชันออบเจ็กต์ใน bucket
    • GET /?uploads หรือ s3:ListMultipartUploads จะคืนรายการ multipart upload ที่กำลังดำเนินอยู่
  • เอกสารของ HeadBucket มีข้อความเกี่ยวกับการตรวจสอบการมีอยู่ของ bucket และสิทธิ์การเข้าถึง แต่ในทางปฏิบัติจริงมันใช้ตรวจว่ามีสิทธิ์ทำงาน ListBucket หรือไม่
  • หากตรวจแค่ว่ามีการปฏิเสธ ListBucket หรือไม่ ก็อาจพลาดความเป็นไปได้ที่ object key ของ S3 จะรั่วไหล

ค่าใช้จ่ายและการเปิดเผยข้อมูลจาก multipart upload ที่ยังไม่เสร็จ

  • multipart upload เริ่มต้นด้วย create-multipart-upload และอัปโหลดแต่ละส่วนด้วย upload-part
  • multipart upload ที่ยังไม่เสร็จตรวจดูได้ไม่ง่ายจากเว็บคอนโซล และตรวจได้ด้วย /?uploads หรือ aws s3api list-multipart-uploads --bucket [bucket-name]
  • หากคำขอ complete ส่งไม่สำเร็จ Amazon S3 จะไม่ประกอบชิ้นส่วนและจะไม่สร้างออบเจ็กต์
    • ชิ้นส่วนที่อัปโหลดไว้จะยังคงอยู่ในบัญชีจนกว่า multipart upload จะเสร็จสมบูรณ์หรือถูกยกเลิก
    • ชิ้นส่วนที่เก็บไว้นี้มีค่าใช้จ่ายด้าน storage ของ S3
  • ผู้เขียนยังหาวิธีดาวน์โหลดชิ้นส่วนของออบเจ็กต์ก่อน complete ไม่พบ แต่สามารถลบได้
  • AWS แนะนำให้ใช้งาน lifecycle rule เพื่อลบการอัปโหลดที่ไม่เสร็จหลังผ่านไปตามจำนวนวันที่กำหนด
  • เมื่อแสดงรายการ multipart upload ที่ยังไม่เสร็จด้วย /?uploads จะคืนค่า ARN ของ principal ที่เริ่มอัปโหลดมาด้วย
    • หากมองว่า identifier อย่าง account ID หรือ ARN ไม่ใช่ข้อมูลอ่อนไหว ก็อาจไม่ถือเป็นปัญหา
    • แต่หากไม่ต้องการเปิดเผย identifier ที่เป็นประโยชน์ต่อผู้โจมตี ก็อาจมองว่านี่คือการรั่วไหลได้

ACL และการตรวจสอบบัญชีผ่านอีเมล

  • เอกสาร ACL ของ S3 ยังมีร่องรอยจากยุคที่บัญชี AWS ถูกระบุด้วยอีเมลของ root user
  • การทำงาน PutBucketACL สามารถระบุ grantee ด้วยอีเมลได้
    • ใช้ Type: AmazonCustomerByEmail และ EmailAddress
  • หากไม่มีบัญชี AWS ที่ผูกกับอีเมลนั้น จะเกิดข้อผิดพลาด UnresolvableGrantByEmailAddress
    • ข้อความ error จะมีลักษณะว่า “ที่อยู่อีเมลที่ให้มาไม่ตรงกับบัญชีใดเลยในระบบ”
  • พฤติกรรมนี้ทำให้ตรวจสอบได้ว่าอีเมลใดอีเมลหนึ่งมีบัญชี AWS ที่ลงทะเบียนอยู่หรือไม่

storage class และ metadata ของออบเจ็กต์ที่ผู้อัปโหลดเลือกได้

  • storage class ของ S3 ถูกใช้กับระดับออบเจ็กต์ ไม่ใช่ระดับ bucket
  • ไม่มีการตั้งค่าที่ล็อก storage class ที่ต้องการไว้ทั้ง bucket และผู้ที่อัปโหลดสามารถกำหนด storage class ของออบเจ็กต์ได้
    • ตัวอย่างคือ aws s3 cp "my.txt" "s3://mybucket/myobject.txt" --storage-class [CLASS]
  • ผู้อัปโหลดจึงสามารถมีผลต่อค่าใช้จ่ายต่อ GB สำหรับการเก็บและการเข้าถึงที่เจ้าของ bucket ต้องจ่ายได้ ภายในชุดค่าที่ระบบกำหนดไว้ล่วงหน้า
  • ใน IAM policy สามารถใช้ condition key s3:x-amz-storage-class เพื่อจำกัด storage class ที่อนุญาตได้
    • ตัวอย่างนโยบายอนุญาตเฉพาะ STANDARD สำหรับ s3:PutObject
  • หากตั้งค่านโยบาย lifecycle ก็สามารถย้ายออบเจ็กต์ทั้งหมดไปยัง storage class ที่กำหนดได้หลังผ่านไประยะเวลาหนึ่ง
  • ในการอัปโหลดด้วย pre-signed URL นั้น AWS Signature Version 4 กำหนดให้ต้องเซ็นทุก header ที่ขึ้นต้นด้วย X-Amz-
    • storage class ถูกกำหนดผ่าน header x-amz-storage-class
    • หากแอปพลิเคชันไม่ได้ถูกทำพลาดอย่างร้ายแรง ก็ยังไม่เห็นวิธีชัดเจนที่จะบิดเบือนค่านี้ได้ทันที

tag, Object Lock และ redirect ก็อยู่ในขอบเขตที่ผู้อัปโหลดมีผลได้

  • คุณสมบัติหลายอย่างที่เกี่ยวข้องกับออบเจ็กต์ใน S3 ถูกควบคุมได้โดยผู้อัปโหลด
  • สามารถกำหนด object tag ได้ตอนอัปโหลด
    • ตัวอย่างคือ --tagging "AllYourTags=AreBelong&To=Us"
  • ระบบที่ทำ automation ตามค่า tag อาจได้รับผลจากค่า tag ที่ผู้อัปโหลดสร้างขึ้น
  • Object Lock สามารถตั้งการเก็บรักษาออบเจ็กต์และ legal hold ได้ หาก bucket เปิดใช้ object locking ไว้
    • ตัวอย่างคำสั่งใช้ --object-lock-retain-until-date "2099-01-01T00:00:00+0000", --object-lock-legal-hold-status "ON", --object-lock-mode "COMPLIANCE"
  • ใน bucket ที่เปิดใช้ static website hosting สามารถทำ open redirect ได้ผ่านการตั้งค่าของไฟล์ที่อัปโหลด
  • ควรระวังรายการ header ทั้งหมดที่ PutObject รองรับด้วย
    • pre-signed URL มีข้อจำกัดอยู่
    • แต่ในสถาปัตยกรรมที่พึ่งพา Cognito identity และ IAM policy นั้น สามารถเซ็นคำขอด้วยบริบท Cognito ที่ยืนยันตัวตนแล้วได้

การเปิดเผยเจ้าของ bucket และตัวระบุบัญชี

  • หากต้องการตรวจว่าบัญชี ID ใดเป็นเจ้าของ bucket ที่เข้าถึงได้หรือไม่ สามารถใส่ header x-amz-expected-bucket-owner ในคำขอ ListBucket ได้
    • หากใส่ account ID ผิด จะได้ AccessDenied
    • หากใส่ account ID ถูกและผู้เรียกมีสิทธิ์ ListBucket ก็จะได้การตอบกลับตามปกติ
  • หากใช้พารามิเตอร์ fetch-owner=true ของ API ListBucket แต่ละ key ในผลลัพธ์จะมี element Owner เพิ่มเข้ามา
  • ID ภายใน Owner คือสตริงเลขฐานสิบหกยาว 64 ตัวอักษรที่เอกสาร AWS เรียกว่า canonical user ID
    • มันคือรูปแบบที่ทำให้อ่านยากของ AWS account ID
  • หากใส่ canonical user ID ลงใน Principal แบบ CanonicalUser ใน IAM policy แล้วบันทึก จากนั้นรีเฟรช ระบบจะตีความกลับเป็น AWS account ID
  • ListBucketVersions และ ListMultipartUploads ก็ทำงานคล้ายกันแม้ไม่ใช้ fetch-owner

S3 object key ดูเหมือนชื่อไฟล์ แต่ทำงานไม่เหมือนกัน

  • object key ของ S3 แยกแยะตัวพิมพ์เล็กและใหญ่
  • แม้ชื่อจะดูเหมือนกัน หากตัวพิมพ์ต่างกันก็อัปโหลดเป็นหลายออบเจ็กต์ได้
  • หากแอปพลิเคชันปฏิบัติต่อ object key ของ S3 เหมือนชื่อไฟล์ที่ไม่สนตัวพิมพ์เล็กใหญ่ ก็อาจเกิดปัญหาได้
    • ตัวอย่างแอปพลิเคชันเก็บรหัสผ่านผู้ใช้ไว้ในไฟล์บน S3 และใช้ชื่อผู้ใช้เป็นชื่อไฟล์
    • ตอนสมัครสมาชิกจะเช็กแค่ว่ามีไฟล์นั้นอยู่หรือไม่ แต่ตอนเปลี่ยนรหัสผ่านจะเปลี่ยนชื่อผู้ใช้เป็นตัวพิมพ์เล็กก่อนแล้วค่อยเขียนลงไฟล์
    • ดังนั้นแม้มี jeff อยู่แล้ว ก็ยังสมัคร JEFF ได้ และผู้ใช้ JEFF ก็อาจเปลี่ยนรหัสผ่านจนเขียนทับไฟล์ของ jeff ได้
  • object key ของ S3 ใช้อักขระ UTF-8 ใดๆ ก็ได้
    • อักขระบางตัวอาจทำให้เกิดปัญหาในบางแอปพลิเคชันและบางโปรโตคอล
    • ช่องว่าง slash และเครื่องหมายเปอร์เซ็นต์ก็เป็น object key ที่ถูกต้องได้เช่นกัน

แม้จะดูเหมือน “private bucket” ก็ยังอาจมีเส้นทางเข้าถึงได้

  • แม้จะปิด ACL ตั้ง resource policy ให้แคบ และเปิด block public access อยู่ bucket ก็อาจยังเข้าถึงจากสาธารณะได้
  • เส้นทางที่พบบ่อยที่สุดคือ Amazon CloudFront distribution
    • หากวาง CDN ไว้หน้า S3 bucket โดยทั่วไปก็มีเจตนาให้ส่งคอนเทนต์สู่อินเทอร์เน็ต
    • เครื่องมือความปลอดภัยอาจตัดสินว่าไม่เป็น public หาก resource policy ของ bucket จำกัดไว้ที่ CloudFront
  • ในตัวอย่าง get-bucket-policy-status คืนค่า IsPublic: false
    • หากส่งคำขอไปยัง bucket โดยตรงจะได้ AccessDenied
    • แต่หากส่งคำขอเดียวกันไปยังโดเมนของ CloudFront distribution จะได้เนื้อหาของออบเจ็กต์กลับมา
  • Cognito identity pool ก็อาจเปิดเผย bucket ที่มี resource policy แบบจำกัดได้เช่นกัน
    • หลังล็อกอินสำเร็จ Cognito จะมอบ temporary AWS credentials ของ role ที่ตั้งไว้ล่วงหน้า
    • หาก role นั้นมีสิทธิ์ s3:ListBucket และ s3:GetObject ผู้ใช้ก็จะเรียก S3 API ได้
  • มีการตั้งค่า Cognito สองแบบที่อาจนับว่าเป็น public access
    • Self-registration: หากผู้ใช้อินเทอร์เน็ตสมัครและล็อกอินเข้าแอปได้ ก็แทบจะเท่ากับเป็น public access
    • Guest access: มอบตัวระบุเฉพาะและ AWS credentials ให้ผู้ใช้ที่ไม่ได้ยืนยันตัวตน
  • ตัวอย่าง guest access คือรับ IdentityId ผ่าน get-id จากนั้นรับ temporary credentials ผ่าน get-credentials-for-identity แล้วจึงรัน aws s3 ls ด้วยโปรไฟล์นั้น
  • ทั้ง CloudFront และ Cognito identity pool ถูกใช้งานจริงบนอินเทอร์เน็ตอยู่บ่อยครั้ง แต่เป็นเส้นทาง public access ที่เครื่องมือความปลอดภัยมักแสดงผลน้อยกว่าความเป็นจริง

1 ความคิดเห็น

 
GN⁺ 2024-06-02
ความคิดเห็นจาก Hacker News
  • มีหลายประเด็นที่น่าสนใจ แต่ยากที่จะเห็นด้วยกับการมองว่าเป็นเรื่องน่าบ่นที่ระบบไฟล์ แยกแยะตัวพิมพ์ใหญ่-เล็ก
    ผมมองว่ามันควรเป็นแบบนั้นอยู่แล้ว และกลับรู้สึกรำคาญมากกว่าที่ macOS ไม่ทำแบบนั้น

    • ไม่เข้าใจว่า “มันควรเป็นแบบนั้นอยู่แล้ว” เพราะอะไร Windows ก็ไม่ได้แยกแยะตัวพิมพ์ใหญ่-เล็ก ดังนั้น S3 ก็ไม่ได้ขัดกับธรรมเนียมที่แทบจะเป็นสากลอะไร
      การแยกแยะตัวพิมพ์ใหญ่-เล็กในชื่อไฟล์อาจรู้สึกเหนือความคาดหมายแม้แต่สำหรับผู้ใช้ที่ไม่ใช่สายเทคนิค ถ้ามีคนบอกว่าส่ง “Book Draft 1.docx” มาให้ แต่ในกล่องเมลมี “Book draft 1.docx” อยู่ ปกติคนเราคงไม่พูดว่า “เหมือนจะส่งไฟล์คนละไฟล์มานะครับ?”
      ในการเขียน ตัวพิมพ์ใหญ่-เล็กก็มักไม่เปลี่ยนความหมาย “Hi, how are you?” กับ “hi, how are you?” มีความหมายเดียวกัน และกรณีที่ตัวพิมพ์ใหญ่เปลี่ยนความหมายก็มีแค่ประมาณการแยกคำนามเฉพาะกับคำนามทั่วไป ซึ่งเป็นเรื่องที่ไม่ค่อยสำคัญในชื่อไฟล์
    • จากมุมมองการนำไปใช้งานเชิงเทคนิค จุดที่ว่า 'A' กับ 'a' เป็นอักขระคนละตัว นั้นถูกกำหนดไว้อย่างชัดเจนใน ASCII, Unicode และอื่น ๆ
      ไม่ว่าความชอบส่วนตัวจะเป็นอย่างไร ก็ยากจะเข้าใจที่นักพัฒนาหรือผู้ดูแลระบบจะประหลาดใจหรือหงุดหงิดกับการที่ระบบไฟล์แยกแยะตัวพิมพ์ใหญ่-เล็ก ถ้าจำเป็น นักพัฒนาก็สามารถทำ abstraction ให้ผู้ใช้ปลายทางได้เหมือนผลการค้นหา
    • ผมเป็นผู้เขียนเอง นี่ไม่ใช่การบ่น แต่ใกล้เคียงกับการสังเกตมากกว่า ไม่ใช่เรื่องดี/เลวแบบเด็ดขาด แต่เป็นปัจจัยที่ต้องพิจารณาเวลาออกแบบแอปพลิเคชัน
    • ไม่แน่ใจว่าการให้ชื่อไฟล์ แยกแยะตัวพิมพ์ใหญ่-เล็ก มีข้อดีตรงไหนกันแน่ ในทางกลับกัน มันเปิดช่องให้เกิดความผิดพลาดทั่วไปมากมายที่แต่เดิมไม่ควรเกิดขึ้นได้
      มันก็ไม่ได้ช่วยให้อ่านง่ายขึ้นด้วยการบังคับใช้สไตล์โค้ดเหมือนการเขียนโค้ด ในการเขียนโปรแกรมเอง ก่อนที่ IDE จะฉลาดพอจะจับการพิมพ์ชื่อตัวแปรผิดได้ มันก็มักเป็นสาเหตุของบั๊กอยู่บ่อย ๆ หนึ่งในข้อดีของ Pascal คือไม่ต้องใส่ใจตัวพิมพ์ใหญ่-เล็ก ต่างจาก C
    • macOS รักษารูปแบบตัวพิมพ์ใหญ่-เล็ก ไว้ ผมเองมองว่าเป็นวิธีที่ผสมข้อดีของทั้งสองฝั่งได้ดี
      คุณใช้สไตล์ที่ต้องการกับชื่อไฟล์ได้ และรูปแบบนั้นจะถูกเก็บรักษาไว้ แต่เวลาค้นหาหรือประมวลผลก็ไม่ต้องจำสไตล์นั้นให้แม่น เพราะการค้นหาไม่แยกแยะตัวพิมพ์ใหญ่-เล็ก
  • การแยกแยะตัวพิมพ์ใหญ่-เล็กยังถือว่าง่ายกว่า สิ่งที่ไม่ค่อยเป็นสัญชาตญาณยิ่งกว่าคือ พาธของ S3 เป็นของปลอม
    S3 ยอมรับการอัปโหลด “/builds/1/installer.exe” และแสดงรายการภายใน /builds ให้ดูด้วย แต่จริง ๆ แล้วมันอัปโหลดคีย์เดียวชื่อ '/builds/1/installer.exe' ซึ่งมี '/' อยู่ในชื่อ
    ดังนั้น “/builds/1//installer.exe” กับ “/builds//1/installer.exe” ก็อัปโหลดได้ และเป็นไฟล์คนละตัวโดยสมบูรณ์ มันเป็นแค่ชื่อคีย์เท่านั้น ไม่มีไดเรกทอรีจริง

    • ใช่ แต่มีข้อยกเว้นถ้าใช้ S3 Directory buckets [1] ตัวใหม่ ซึ่งกลับยิ่งทำให้ภาพรวมสับสนขึ้นไปอีก
      [1] https://docs.aws.amazon.com/AmazonS3/latest/userguide/direct...
    • ต้องไม่พลาดด้วยว่า "/" เป็นแค่อักขระคั่นพาธเริ่มต้นเท่านั้น ถ้าคุณต้องการ "/" ในชื่อไฟล์ ก็สามารถใช้อักขระอื่นที่ต้องการเป็นตัวคั่นได้: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObje...
    • ไม่รู้ว่า ไดเรกทอรีจริง ต่างจาก prefix โดยเนื้อแท้อย่างไร นอกจากการตีความพาธให้อยู่ในรูปแบบมาตรฐานบางอย่าง เช่น รวม / ที่ซ้ำกัน
    • วิธีแบบ prefix สร้างบั๊กเยอะจริง ๆ ผมเข้าใจว่าทำไม AWS ถึงทำแบบนั้น และจริง ๆ แล้วก็เป็นแนวทางที่ฉลาด แต่ก็ยังมีนักพัฒนาจำนวนมากติดกับ
      ปีนี้ระบบ production ของเราก็เจอบั๊กประหลาด และต้องใช้คน 5 คนถึงจะหาเจอ สาเหตุคือมีออบเจ็กต์ที่มีชื่อตามตัวอักษรว่า “/” และซอฟต์แวร์พยายามจัดการมันเหมือนเป็นพาธ ไม่ใช่ไฟล์
  • ยากที่จะไว้วางใจใช้ S3 หรือบริการ AWS อื่น ๆ ไม่มีอะไรที่เป็นสัญชาตญาณเลย มีชิ้นส่วนที่เคลื่อนไหวมากเกินไป และมีเอกสารที่ต้องอ่านมากเกินไป
    ถึงทำอย่างนั้นแล้ว ก็ยังอาจเผลอเปิดทุกอย่างให้ทั้งโลกเห็นได้เหมือนในบทความต้นฉบับ ผมขอใช้บริการที่เรียบง่ายจริง ๆ อย่าง Hetzner Storage Boxes หรือ DigitalOcean Spaces ดีกว่า

    • ผมชอบ DigitalOcean Spaces แต่ที่นั่นก็มีความแปลกเฉพาะตัวที่น่ารำคาญเหมือนกัน
      ล่าสุดพบว่าเมื่ออัปโหลดไฟล์วิดีโอที่ใหญ่กว่าสองสาม MB ผ่าน pipe ค่า Location ที่ส่งกลับมาจะตก https:// ไป ดังนั้นทุกครั้งที่อัปโหลดไฟล์ ต้องตรวจว่า Location ขึ้นต้นด้วย https หรือไม่ และถ้าไม่มีก็ต้องเติมเอง
      แน่นอนว่าใน issue ของ S3 Node client บน GitHub ก็บอกว่า “ดูเหมือนเป็นบั๊กของ DigitalOcean นะ” ส่วนในฟอรัมของ DigitalOcean ก็บอกว่า “ดูเหมือนเป็นบั๊กของ S3 Node client นะ”
    • วิธีที่ DigitalOcean จัดการกับ ค่าลับ นั้นน่ากลัวสำหรับทุกคน รู้ไหมว่าถ้าใช้ Container Registry แล้วตั้งค่าให้ K8S เข้าถึงอัตโนมัติ บริการนั้นจะสร้าง secret ที่เข้าถึง Spaces ได้ทั้งหมด?
    • ผมหยุดทำงานพัฒนาบนคลาวด์ไปหลายปีและทำงานฝั่งไคลเอนต์เป็นหลัก เพิ่งกลับมาเมื่อไม่นานนี้ แล้วก็แปลกใจกับความซับซ้อนที่สะสมขึ้นมา และ ภาระทางความคิด ที่จำเป็นต่อการสร้างโซลูชันที่แน่นหนาบน public cloud
      ฟีเจอร์และความแปลกเฉพาะจำนวนมากที่เดิมทีออกแบบมาเพื่อช่วยในบางสถานการณ์พิเศษ ตอนนี้กลายเป็นส่วนหนึ่งของโปรโตคอลทั่วไปไปแล้ว ดูเหมือนว่าเป็นผลจากการพยายามทำให้ไม่มีลูกค้าธุรกิจรายใดต้องหันหลังจากไป
  • การลบอ็อบเจกต์หลายหมื่นล้านรายการก็ต้องระวังเช่นกัน หากเรียก API ลบโดยตรง อาจมีค่าใช้จ่ายสูงได้
    แทนที่จะทำแบบนั้น สามารถตั้งค่า กฎวงจรชีวิต ให้เวลาหมดอายุเป็น now สำหรับไวลด์การ์ดหรือทั้งบัคเก็ตได้ฟรี จากนั้นการคิดค่าบริการสตอเรจจะหยุดทันที และ AWS จะจัดการลบให้เอง

    • พูดให้เคร่งครัดคือ การเรียกเพื่อลบนั้นฟรี สิ่งที่มีค่าใช้จ่ายคือ การเรียกดูรายการ เพื่อหาอ็อบเจกต์ ถ้าทางทฤษฎีคุณรู้จากแหล่งอื่นว่ามีอ็อบเจกต์ใดอยู่บ้าง ก็ทำได้ฟรี
    • ผลของกฎวงจรชีวิตไม่ได้เกิดขึ้นทันที เพราะถูกนำไปใช้ผ่านงานแบบแบตช์ที่รันวันละครั้ง ดังนั้นการลบจึงไม่ได้เกิดขึ้นในทันที
    • เพราะ AWS สามารถเลือกเวลาลบจริงได้ โดยในเมตาดาตาจะระบุว่าอ็อบเจกต์ถูกลบแล้ว และ AWS อาจจัดการลบในช่วงเวลาที่มีการใช้งานต่ำ
      วิธีนี้ยังช่วยหลีกเลี่ยงไม่ให้เซิร์ฟเวอร์ S3 API โดนถล่มด้วยจำนวนคำขอต่อวินาทีได้ด้วย
  • เรื่องที่ multipart upload ที่ล้มเหลวจะค้างอยู่แบบมองไม่เห็น และถ้าไม่ตั้งค่า lifecycle อย่างชัดเจนก็ยังถูกคิดค่าสตอเรจด้วยนั้น แย่มากจริง ๆ
    ผมนึกว่า S ใน “Simple” หมายถึงความเรียบง่ายเสียอีก

    • ใช่ เรื่องนั้นแย่จริง โทษ ahenry@ ซึ่งเป็น S3 GM ตอนนั้นได้เลย
      ข้อเสนอของผมคือให้พาร์ตของอัปโหลดที่ยังไม่เสร็จค้างอยู่ได้แค่ 24 ชั่วโมงหลังจากมีกิจกรรมล่าสุด และไม่คิดค่าสตอเรจในช่วงนั้นด้วย แต่ ahenry@ ปฏิเสธ
    • ปัญหานี้ทำให้เราเสียเงินไปหลายพันดอลลาร์
      บนเซิร์ฟเวอร์เก่ามากเครื่องหนึ่ง มีสคริปต์ cron ที่เริ่ม multipart upload ทุกคืนทำงานมาเกือบ 10 ปีแล้ว ใช้สำหรับดันแบ็กอัปเข้าไปในบัคเก็ต แต่บัคเก็ตนั้นเก็บคอนเทนต์ที่ผู้ใช้อัปโหลดด้วย จึงดูเหมือนเป็นเรื่องปกติที่ขนาดจะค่อย ๆ โตขึ้นทุกวัน
      สคริปต์อยู่ในสถานะ “ไม่ทำงาน” เราจึงไม่ได้พึ่งพาข้อมูลแบ็กอัปนั้น ไฟล์ก็ไม่ปรากฏใน S3 และขนาดบัคเก็ตก็เพิ่มขึ้นอย่างสม่ำเสมอแต่ไม่มากเกินไป แต่พอดูเมื่อฤดูใบไม้ผลินี้ พบว่ามี multipart upload ที่ยังไม่เสร็จเกือบ 3TB ถูกเก็บอยู่
      แน่นอนว่าผมรู้ว่าเรื่องเล่านี้เต็มไปด้วยแนวปฏิบัติที่ไม่ดี
    • S ตัวนั้นคือ S ของ วิธีง่าย ๆ ที่ทำให้ค่าใช้จ่ายพุ่ง
    • ชื่อ “Simple” ถูกตั้งขึ้นในยุคที่ทางเลือกคือการจัดการฝูงเซิร์ฟเวอร์ที่มีดิสก์เอง เวลาเปลี่ยนทุกสิ่ง
    • ผมเองก็เคยเหยียบกับระเบิดเรื่องค่าสตอเรจมาแล้ว โชคดีที่แค่ไม่กี่เซนต์ แต่คอนโซลแสดงข้อมูลที่เกี่ยวข้องได้แย่มากจนค่อนข้างโมโห
  • การถกเถียงเรื่องแยกแยะ/ไม่แยกแยะตัวพิมพ์ใหญ่น้อย โดยรวมแล้วให้ความรู้สึกว่า ยึดภาษาอังกฤษเป็นศูนย์กลาง มากเกินไป
    พูดอีกอย่างคือ โดยเฉพาะใน IT การถกเถียงเรื่องภาษามักยึดภาษาอังกฤษเป็นศูนย์กลางมากเกินไปบ่อยครั้ง

    • ผมกลับมองว่าการยึดภาษาอังกฤษเป็นศูนย์กลางนั้นเป็นเรื่องดีเสียอีก เพราะ ASCII จัดการง่ายกว่า Unicode มาก
      ในฐานะคนที่ภาษาอังกฤษไม่ใช่ภาษาแม่ ผมขอบอกว่าในการเขียนโปรแกรมก็มีแนวคิดและองค์ประกอบมากเกินพออยู่แล้ว ไม่ควรเพิ่มความซับซ้อนด้วยการต้องคำนึงถึงอีก 101 ภาษา
      Unicode และเขตเวลาเป็นตัวอย่างหลัก ๆ ของสิ่งที่พยายามคำนึงถึงภาษาและวัฒนธรรมที่หลากหลายขึ้นในการเขียนโปรแกรม แต่ผลลัพธ์คือสร้างความเจ็บปวดมากที่สุดให้ทุกคน รวมถึงโปรแกรมเมอร์ที่ไม่ใช้ภาษาอังกฤษด้วย
      ผมไม่อยากเขียนโปรแกรมด้วยภาษาแม่ของตัวเอง ยิ่งถ้าต้องแลกกับการที่เวลาเขียนโปรแกรมต้องคำนึงถึงภาษาหลักทั้งหมดด้วยแล้ว ยิ่งไม่อยาก การถกเถียงด้าน IT จะยึดภาษาอังกฤษเป็นศูนย์กลางก็ไม่เป็นไร ความหลากหลายคือความซับซ้อน และภาษาอังกฤษไม่ใช่ภาษาที่ใครเป็นเจ้าของ แต่เป็นเพียงเครื่องมือที่ผู้คนใช้สื่อสารกัน
      ภาษากลางนี้ทำให้ผมสามารถแสดงความคิดของตนต่อผู้คนมากมายในอินเดีย จีน ญี่ปุ่น อเมริกาใต้ และที่อื่น ๆ ได้ ทันทีที่พวกเขาตัดสินใจพูดเป็นภาษาอังกฤษ พวกเขาก็เป็นเจ้าของภาษาอังกฤษด้วย ไม่จำเป็นต้องลากการเมืองเรื่องความหลากหลายเข้ามาใน IT ปล่อยให้เป็นเรื่องเชิงเทคนิคจะดีกว่า
    • พูดถึงวัฒนธรรมที่ไม่ใช่ภาษาอังกฤษแล้ว ในภาษาญี่ปุ่น ระบบที่ไม่แยกแยะตัวพิมพ์ใหญ่น้อยจะแยก ฮิรางานะกับคาตาคานะ หรือไม่?
      ในบางแง่ ตัวอักษรพยางค์สองชุดนี้ให้ความรู้สึกเหมือนตัวพิมพ์ใหญ่/ตัวพิมพ์เล็กของอักษรละติน
  • ยังมีอีกหลายเรื่อง
    Multipart upload ไม่สามารถทำจากหลายเครื่องที่ใช้ instance credentials ได้ เพราะ principal ต่างกันจึงเข้าถึง multipart upload ของกันและกันไม่ได้ หากต้องการประกอบ multipart upload เดียวจากหลายเครื่อง จำเป็นต้องใช้ IAM user จริง
    คำขอ LIST ไม่ได้แค่ช้า แต่ถ้าทำเป็นจำนวนมากก็แพงมากด้วย มีทางเลี่ยงอย่าง “bucket inventory” แต่ก็ไม่สะดวกและไม่ถูก
    การสร้าง bucket ใช้ DNS ภายใน จึงไม่มีความสม่ำเสมอแบบ read-after-write ดังนั้นบางครั้งหลังสร้าง bucket ทันทีอาจยังเข้าถึงไม่ได้ หรือก่อนรอให้การเปลี่ยนแปลงกระจายตัวเพียงพอ ก็อาจลบ bucket ที่เพิ่งสร้างไม่ได้ ดู https://github.com/julik/talks/blob/master/euruko-2019-no-su...
    คุณสามารถสร้าง object ชื่อ “foo” และ object ชื่อ “foo/bar” พร้อมกันได้ นั่นทำให้โครงสร้างกลายเป็นแบบที่ไฟล์ทับไดเรกทอรี ส่งผลให้ไม่สามารถย้ายข้อมูลใน bucket ไปเป็นโครงสร้างไฟล์ระบบได้
    S3 แยกตัวพิมพ์เล็ก-ใหญ่ จึงสามารถสร้าง object ที่ย้ายไปเป็นโครงสร้างไฟล์ระบบไม่ได้ได้ Rails file storage เคยพังหนักบน macOS เพราะสมมติว่า storage แยกตัวพิมพ์เล็ก-ใหญ่ และถูกแก้ให้ใช้ identifier เป็นตัวพิมพ์เล็กเสมอ
    การตั้งค่า S3 ส่วนใหญ่อนุญาต GET แต่ไม่อนุญาต HEAD ดูเหมือนเป็นวิธีป้องกันการสำรวจว่า object มีอยู่หรือไม่ แต่ก็ไม่แน่ใจ อย่างไรก็ตาม flow ที่เป็นมิตรกับ cache ซึ่งใช้คำขอ HEAD เพื่อตรวจสอบขนาด object จะไม่ทำงาน โดยเฉพาะกับ pre-signed URL ต้องเลี่ยงด้วย GET ที่กำหนด Range ให้เล็กมาก เช่น ดึงแค่ byte แรกแทน
    ถ้าสร้าง pre-signed URL จำนวนมาก อาจเพิ่มความเร็วในการสร้างได้ 10–40 เท่า: https://github.com/WeTransfer/wt_s3_signer
    คุณยังต้องจ่ายค่า storage สำหรับ multipart upload ที่ยังไม่เสร็จด้วย ต้องระวังเป็นพิเศษถ้าโครงสร้างของคุณเปิดให้ผู้ใช้เริ่ม upload แบบนี้ได้ มีการตั้งค่าสำหรับลบ multipart upload ที่ยังไม่เสร็จหลังผ่านไประยะหนึ่งโดยอัตโนมัติ ดังนั้นถ้าไม่อยากลำบากก็ควรเปิดไว้
    ในทางกลับกัน S3 เคยเป็นสิ่งปฏิวัติวงการ และทุกวันนี้ก็ยังเป็นผลิตภัณฑ์ที่ยอดเยี่ยมในหลายระดับ เพียงแต่ว่ายิ่งมีฟีเจอร์มาก ก็ยิ่งมีกับดักมากเช่นกัน

    • สิ่งที่ทำให้ผมติดอยู่เมื่อไม่กี่สัปดาห์ก่อนคือข้อจำกัด ขนาด chunk เริ่มต้นขั้นต่ำ 5MiB ของ multipart upload: https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts...
      ผมสร้าง pipeline หลังประมวลผล CSV แบบ streaming ใน Elixir โดยใช้ Stream.transform(https://hexdocs.pm/elixir/Stream.html#transform/3) เพื่อแก้ไขและแทรก column โมดูล AWS และ CSV ของ Elixir จัดการข้อมูล streaming ขาเข้าได้ แต่ถ้าปริมาณรวมของ stream ขาออกน้อยกว่า 5MiB โมดูล AWS จะใช้ multipart upload ทำให้ S3 แจ้ง error ซึ่งน่าเศร้า
  • ยังมีอีกปัญหาที่น่าสนใจซึ่งผมกับเพื่อนร่วมงานใช้เวลาวิเคราะห์และวินิจฉัยอยู่หลายวัน S3 จะทิ้งคำขอถัดไปอย่างเงียบ ๆ หลังจาก TCP connection เดียวส่ง HTTP request 100 ครั้ง
    https://github.com/aws/aws-sdk-go/issues/2825

    • ไม่ได้ทิ้งอย่างเงียบ ๆ จริง ๆ แต่ส่ง header ว่าปิด TCP connection แล้ว
      นี่เป็น pattern ที่พบบ่อยเมื่ออยากได้ keep-alive เพื่อ performance แต่ก็อยากป้องกันไม่ให้ client เกาะอยู่นานเกินไปจนสร้าง hotspot บน load balancer
  • อีกประเด็นคือ S3 ใน standard storage class มี latency สูง จึงไม่เหมาะกับการเสิร์ฟเว็บ
    หลายคนคิดว่าสามารถ host resource ของเว็บไซต์ เช่น รูปภาพหรือฟอนต์ จาก S3 โดยตรงได้ แต่ user experience อาจแย่ลง
    “applications can achieve consistent small object latencies (and first-byte-out latencies for larger objects) of roughly 100–200 milliseconds.”
    ที่มา: https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimi...

    • ส่วนใหญ่ใช้ S3 เป็น origin ของ AWS CloudFront สำหรับการส่งมอบเนื้อหา
      หากใช้ CloudFront signed cookies ก็สามารถให้ผู้ใช้บางรายเข้าถึง CDN ได้เฉพาะเนื้อหาใน S3 ที่เป็นของตนเองได้ด้วย ค่อนข้างเจ๋งทีเดียว
    • หากต้องการเสิร์ฟ web assets โดยทั่วไปจะใช้ S3 ร่วมกับ CloudFront
      สามารถ cache assets ที่ถูกเข้าถึงบ่อยเพื่อลด latency และลดค่าใช้จ่ายได้ค่อนข้างมาก
    • S3 ไม่ได้ถูก optimize สำหรับการเสิร์ฟเว็บไซต์โดยตรง แต่ถูก optimize สำหรับการจัดเก็บและดึงข้อมูลปริมาณแทบไม่จำกัดอย่างทนทาน
  • การที่ uploader เป็นคนกำหนดกฎนี่ค่อนข้างโหด หมายความว่าหากเว็บไซต์ตั้งค่าไว้หละหลวม ผู้ใช้ที่มีแรงจูงใจมากพออาจทำให้ user content ถูก upload ไปยัง Amazon Glacier แล้วภายหลังถูกเสิร์ฟจากที่นั่นได้หรือเปล่า?

    • เป็นไปได้ อย่างไรก็ตาม หากกังวลเรื่องนี้หรือหลายรายการอื่น ๆ สามารถใช้ IAM ควบคุมการเข้าถึงฟังก์ชันดังกล่าวได้
      https://docs.aws.amazon.com/service-authorization/latest/ref...
      โดยเฉพาะ condition keys อยู่ที่นี่ และจะเห็น key ที่ใช้ควบคุมการเข้าถึง storage class หรือ tagging เป็นต้น
      https://docs.aws.amazon.com/service-authorization/latest/ref...