GitHub ปลอดภัยไหม สิ่งที่คุณเปิดเผยจริง ๆ
GitHub ปลอดภัยพอสำหรับงานส่วนใหญ่ แต่ควรรู้ว่าอะไรเป็น public โดยค่าเริ่มต้น repo private ซ่อนอะไรไม่ได้ ทำไมลบไฟล์ไม่ลบคีย์ออกจากประวัติ git และ token ที่หลุดทำอะไรได้
GitHub ปลอดภัยไหม คำตอบสั้นก่อน
GitHub ปลอดภัยพอสำหรับงานเกือบทุกแบบ รวมถึงโค้ดที่ใช้ทำเงินจริงของบริษัท ความเสี่ยงที่เกิดขึ้นจริงแทบไม่เคยมาจากตัวแพลตฟอร์มถูกเจาะ มันมาจากสิ่งที่คุณ push ขึ้นไปเอง และจากบัญชีที่ตั้งค่าไว้หลวม ๆ
สามอย่างที่ทำให้นักพัฒนาเจ็บตัวบ่อยที่สุดคือ คีย์หรือรหัสผ่านที่ติดไปกับ commit, ความเข้าใจผิดว่า repo แบบ private เท่ากับความลับ และ personal access token (PAT, โทเคนสำหรับเข้าถึงบัญชีแทนรหัสผ่าน) ที่มีสิทธิ์กว้างกว่างานที่มันต้องทำ ทั้งสามข้อเป็นเรื่องการตั้งค่าฝั่งคุณ ไม่ใช่เรื่องที่ต้องรอให้ GitHub แก้ให้
หน้านี้ไล่ทีละข้อว่าอะไรเป็นสาธารณะโดยค่าเริ่มต้น อะไรที่ private ซ่อนให้ไม่ได้ ลำดับที่ถูกต้องเมื่อคีย์หลุด และเส้นแบ่งจริงที่ทำให้บางองค์กรต้องรัน Git server ของตัวเอง คำสั่ง git ทุกอันในหน้านี้เป็นตัวอย่างให้คุณลองรันบนเครื่องและ repo ของคุณเอง ไม่ใช่สคริปต์สำเร็จที่ copy ทั้งชุดแล้วยิงใส่งานจริง
repo แบบ public เปิดเผยอะไรบ้าง
เมื่อ repo เป็น public ทุกคนบนอินเทอร์เน็ตอ่านได้โดยไม่ต้องล็อกอิน และสิ่งที่เปิดเผยกว้างกว่าที่คนส่วนใหญ่คิด เพราะ git เก็บประวัติทั้งหมด ไม่ใช่แค่ไฟล์เวอร์ชันล่าสุด
- โค้ดทุกไฟล์ ทุก branch ทุก tag และทุก commit ที่เคยมีอยู่ รวมถึงไฟล์ที่คุณลบไปแล้ว
- ชื่อและอีเมลของผู้เขียนทุก commit เพราะ git ฝังสองค่านี้ไว้ใน object ของ commit ตั้งแต่ตอน commit
- ข้อความ commit ทั้งหมด ซึ่งมักมีเลข ticket ชื่อลูกค้า ชื่อเครื่องภายใน หรือคำอธิบายช่องโหว่ที่คุณเพิ่งแก้
- issue, pull request, comment, release note, wiki และไฟล์ที่แนบไว้ในนั้น
- ผลรันของ GitHub Actions พร้อม log ซึ่งเห็นทุกค่าที่ workflow พิมพ์ออกมา ยกเว้นค่าที่ถูก mask
- เว็บจาก GitHub Pages ของ repo นั้น ถ้าเปิดใช้ไว้ เว็บที่ออกมาเป็นสาธารณะเสมอในแผนฟรี ต่อให้ repo ต้นทางเป็น private ก็ตาม ก่อนใช้เป็นเว็บจริงลองอ่าน ข้อจำกัดของการให้ GitHub โฮสต์เว็บไซต์ของคุณ ประกอบ
อีเมลใน commit เป็นข้อที่คนตกใจที่สุด เปิด URL ของ commit ใด ๆ แล้วเติม .patch ต่อท้าย เช่น https://github.com/OWNER/REPO/commit/COMMIT_SHA.patch บรรทัดที่ขึ้นต้นด้วย From: จะแสดงชื่อและอีเมลที่ตั้งไว้ในเครื่องที่ commit ถ้านั่นคืออีเมลบริษัทหรืออีเมลส่วนตัวที่คุณไม่อยากให้บอตเก็บไป เปลี่ยนไปใช้อีเมล noreply ของ GitHub ซึ่งหาได้ในหน้า Settings แล้วส่วน Emails
git config user.email
git config --global user.email "12345678+USERNAME@users.noreply.github.com"ค่านี้มีผลกับ commit ใหม่เท่านั้น commit เก่าที่ push ไปแล้วยังถืออีเมลเดิมอยู่ เพราะอีเมลเป็นส่วนหนึ่งของข้อมูลที่ถูกแฮชเป็น SHA ของ commit การแก้ย้อนหลังคือการเขียนประวัติใหม่ ซึ่งเป็นงานเดียวกับหัวข้อล้างคีย์ด้านล่าง
อีกสองอย่างที่เป็นสาธารณะโดยการออกแบบ ไม่ใช่การรั่ว: https://github.com/USERNAME.keys คืน SSH public key ทุกอันที่คุณเพิ่มไว้ในบัญชี และเติม .gpg แทนก็คืน GPG public key public key ไม่ใช่ความลับ แต่มันบอกคนนอกได้ว่าคุณใช้คีย์ชนิดไหน มีกี่อัน และอันไหนเก่าจนควรถอดออก
โค้ด public ถูกเก็บสำเนาไปเร็วแค่ไหน
สมมติฐานที่ปลอดภัยคือ เร็วกว่าที่คุณจะกลับมาลบทัน เหตุผลอยู่ที่กลไก ไม่ใช่ที่โชค
git clone คัดลอก object ทั้งหมดของ repo ในคำขอเดียว ไม่ใช่การไล่อ่านทีละไฟล์ ใครที่อยากได้ประวัติทั้งชุดจึงใช้เวลาเท่ากับความเร็วเน็ตของเขา ไม่ใช่เท่ากับขนาดความตั้งใจ และ GitHub ยังเปิด feed สาธารณะของกิจกรรมทั้งเว็บไว้ที่ https://api.github.com/events ซึ่งบอกว่าใคร push อะไรเข้า repo ไหนแบบเกือบเรียลไทม์ บอตที่ไล่หาคีย์ในโค้ดสาธารณะทำงานจาก feed นี้ตรง ๆ มันไม่ต้องรอ search engine มา index หน้าเว็บของคุณ
มีงานวิจัยด้านความปลอดภัยหลายชิ้นที่วัดเวลาจากการ push คีย์ขึ้น repo สาธารณะถึงเวลาที่คีย์ถูกนำไปใช้ ตัวเลขที่ตีพิมพ์ต่างกันตามวิธีวัดและตามชนิดคีย์ อยู่ในระดับนาทีถึงชั่วโมง ข้อสรุปที่นำไปใช้ได้ไม่ขึ้นกับตัวเลข: คีย์ที่เคยปรากฏใน repo สาธารณะ ให้ถือว่าถูกเปิดเผยแล้ว และต้องหมุนใหม่ ไม่ใช่แค่ลบ
นอกจากบอต ยังมีสำเนาที่เกิดขึ้นตามปกติของระบบนิเวศ: fork ของคนอื่น, mirror ที่คนตั้งไว้เอง, โปรเจกต์เก็บถาวรโค้ดโอเพนซอร์ส, เครื่องมือค้นโค้ดของเจ้าอื่น และชุดข้อมูลสำหรับฝึกโมเดล สำเนาเหล่านี้ไม่ได้อยู่ใต้บัญชีคุณ คุณจึงสั่งลบไม่ได้
repo แบบ private ซ่อนอะไรได้ และซ่อนอะไรไม่ได้
private ทำงานหนึ่งอย่างได้ดี: คนที่ไม่ได้รับเชิญอ่านไม่ได้ และ search engine มองไม่เห็น ชื่อ repo กับคำอธิบายก็ไม่ปรากฏบนโปรไฟล์ สิ่งที่มันไม่ได้ทำมีดังนี้
- collaborator ทุกคนที่คุณเชิญเห็นทั้ง repo รวมถึงประวัติทั้งหมด GitHub ไม่มีการจำกัดสิทธิ์เป็นรายโฟลเดอร์ ให้สิทธิ์อ่านคือให้อ่านทุกอย่างที่เคยมี
- ถ้า repo อยู่ใต้ organization เจ้าของ organization และคนที่มีสิทธิ์ admin เข้าถึงได้ ต่อให้ไม่ได้ถูกเพิ่มชื่อเป็น collaborator และในบัญชีระดับ enterprise เจ้าของ enterprise ก็อยู่ในกลุ่มนี้ ข้อนี้สำคัญเวลาคุณเอา repo ส่วนตัวไปฝากไว้ใต้องค์กรของลูกค้า
- fork network เก็บข้อมูลร่วมกัน GitHub เขียนไว้ตรง ๆ ว่าข้อมูล Git จาก repository ใดใน network หนึ่ง เข้าถึงได้จาก repository อื่นใน network เดียวกัน รวมถึง upstream และยังเข้าถึงได้แม้ fork นั้นถูกลบไปแล้ว แปลว่า commit ที่คุณ push ผิดเข้า fork แล้วลบ fork ทิ้ง ยังเปิดอ่านได้ถ้ามีคนรู้ SHA ของมัน
- การลบ repo สาธารณะไม่ได้ลบ network ถ้ายังมี public fork ที่ใช้งานอยู่ fork นั้นจะกลายเป็น upstream ใหม่ และ commit ก็ยังอยู่ต่อ
- การเปลี่ยนจาก public เป็น private ไม่เรียกสำเนาที่คนอื่นโคลนไปแล้วกลับมา มันหยุดคนใหม่ ไม่ย้อนอดีต
- GitHub เองเข้าถึงข้อมูลได้ตามเงื่อนไขการให้บริการ เช่น เมื่อคุณเปิดเคสกับ support หรือเมื่อมีคำสั่งตามกฎหมาย นี่ไม่ใช่การรั่ว แต่ถ้าสัญญากับลูกค้าห้ามโค้ดอยู่บนระบบของบุคคลที่สาม
privateไม่ช่วยข้อนี้เลย
สิ่งที่ยังเป็นสาธารณะแม้ repo ทั้งหมดของคุณเป็น private คือโปรไฟล์: ชื่อ, รูป, ผู้ติดตาม, organization ที่คุณตั้งเป็น public member และกราฟการทำงานรายวัน ถ้าคุณเปิดตัวเลือกแสดง private contribution กราฟจะบอกจำนวนการทำงานของคุณ แต่ไม่บอกว่า repo ไหน ถ้าอยากเทียบว่าแผนฟรีให้สิทธิ์อะไรไว้ตั้งแต่ต้น ดู บัญชี GitHub แบบฟรีให้อะไรมาบ้าง ประกอบ
และก่อนจะไปต่อ ขอแยกคำสองคำให้ชัด: git คือระบบควบคุมเวอร์ชันที่รันบนเครื่องคุณ GitHub คือบริการที่รับฝาก repo และเพิ่มเครื่องมือรอบ ๆ ให้ ความปลอดภัยที่เราคุยกันในหน้านี้เกือบทั้งหมดเป็นเรื่องของชั้นที่สอง ถ้าเส้นแบ่งนี้ยังพร่า อ่าน ความต่างระหว่าง Git, GitHub และ Git server ของตัวเอง ก่อน แล้วกลับมา
ลบไฟล์ออกแล้ว ทำไมคีย์ยังอยู่ในประวัติ git
เพราะ commit แต่ละอันเก็บภาพรวมของไฟล์ทั้งชุดในเวลานั้น การลบไฟล์คือการสร้าง commit ใหม่ที่ภาพนั้นไม่มีไฟล์ดังกล่าว ตัวเนื้อไฟล์เดิมยังเป็น object ที่ commit เก่าอ้างถึงอยู่ จึงยังเดินทางไปกับทุกสำเนาของ repo และยังอ่านได้ตรง ๆ
git log --all --full-history -- config/secrets.yml
git show COMMIT_SHA:config/secrets.ymlคำสั่งแรกไล่หา commit ทุกอันบนทุก branch ที่เคยแตะไฟล์นั้น คำสั่งที่สองพิมพ์เนื้อไฟล์ ณ commit นั้นออกมา ถ้าคุณเห็นคีย์ตอบกลับมาบนหน้าจอ คนอื่นที่โคลน repo ไปก็เห็นเหมือนกัน บน GitHub เอง URL ของ commit เก่ายังเปิดได้ตามปกติ การ push commit ที่ลบไฟล์ไม่ได้ทำให้หน้านั้นหาย
ข้อที่พลาดกันบ่อยคือคิดว่า git rm หรือการเพิ่มชื่อไฟล์ลงใน .gitignore ย้อนหลังช่วยได้ .gitignore บอก git ว่าอย่าเริ่มติดตามไฟล์ใหม่ มันไม่แตะไฟล์ที่ถูกติดตามไปแล้ว ไฟล์ที่ commit ไปครั้งเดียวจะอยู่ในประวัติจนกว่าจะมีการเขียนประวัติใหม่
คีย์หลุดขึ้น GitHub แล้วต้องทำอะไรก่อน
ลำดับสำคัญกว่าความเร็ว เพิกถอนและหมุนคีย์ก่อน แล้วค่อยล้างประวัติ เหตุผลเป็นกลไกล้วน ๆ : การเขียนประวัติใหม่ไม่ได้ทำให้คีย์ใช้งานไม่ได้ ตราบใดที่ผู้ให้บริการยังยอมรับคีย์นั้น สำเนาที่ถูกโคลนไปก่อนหน้าก็ยังใช้เข้าระบบคุณได้เหมือนเดิม เอกสารของ GitHub เองก็ระบุลำดับนี้ไว้ว่าถ้าข้อมูลที่ต้องลบเป็นความลับอย่างรหัสผ่านหรือโทเคน ขั้นแรกคือเพิกถอนหรือหมุนมันเสียก่อน
ลำดับที่ใช้ได้จริงเมื่อคีย์หลุด
- เพิกถอนคีย์ที่ฝั่งผู้ให้บริการทันที เช่น ลบ access key ใน AWS, roll คีย์ใน Stripe, ลบ PAT ในหน้า Settings ของ GitHub
- ออกคีย์ใหม่ แล้วเอาไปใส่ที่เก็บความลับ ไม่ใช่ใส่กลับเข้าไฟล์ใน repo
- ไล่ log การใช้งานของคีย์เก่าย้อนหลังถึงวันที่ commit นั้นถูก push ไม่ใช่ย้อนถึงวันที่คุณเพิ่งรู้ตัว
- หา commit ทุกอันที่มีคีย์ด้วย
git log --all --full-history - ล้างประวัติด้วย
git filter-repoแล้ว force push - ถ้าจำเป็น เปิดเคสกับ GitHub Support เพื่อขอให้ล้าง view และ reference ที่ถูกแคชไว้ฝั่งเซิร์ฟเวอร์
- ปิดรอยเดิม:
.gitignore, ที่เก็บความลับที่ใช้จริง, และ push protection
เครื่องมือล้างประวัติที่ GitHub แนะนำคือ git filter-repo บน Ubuntu 24.04 ลง package ของ distro ได้ตรง ๆ ด้วย sudo apt install git-filter-repo หรือใช้ pipx install git-filter-repo เมื่อต้องการเวอร์ชันใหม่กว่าที่ distro ให้ ข้อควรรู้: pip install git-filter-repo ตรง ๆ บน Ubuntu 24.04 จะถูกปฏิเสธด้วยข้อความ error: externally-managed-environment เพราะ Python ของระบบถูกทำเครื่องหมายว่าจัดการโดย apt ไม่ใช่โดย pip
git filter-repo --version
git clone https://github.com/OWNER/REPO.git
cd REPO
git filter-repo --invert-paths --path config/secrets.yml
git push --force --mirror origin--invert-paths --path แปลว่าเก็บทุกอย่าง ยกเว้น path ที่ระบุ ถ้าคีย์ฝังอยู่กลางไฟล์ที่ต้องเก็บไว้ ใช้ --replace-text ../passwords.txt แทน โดยไฟล์นั้นมีข้อความที่ต้องแทนบรรทัดละรายการ และในเวอร์ชัน 2.47 ขึ้นไปยังมี --sensitive-data-removal ซึ่งทำให้ผลลัพธ์ที่พิมพ์ออกมามีรายการ commit แรกที่เปลี่ยน ในรูปแบบที่ GitHub Support ขอ ตรวจเวอร์ชันที่คุณมีก่อนใช้แฟล็กนี้ ไม่ใช่เดา
สิ่งที่จะเกิดขึ้นและควรรู้ไว้ก่อน: git push --force --mirror จะฟ้องว่า push ref ที่ขึ้นต้นด้วย refs/pull/ ไม่ได้ อาการนี้เป็นเรื่องปกติ เพราะ GitHub ตั้ง ref ของ pull request ไว้เป็นอ่านอย่างเดียว ส่วนผลข้างเคียงที่ใหญ่กว่าคือ SHA ของ commit เปลี่ยนทั้งสาย ทุกคนในทีมต้องโคลนใหม่ และ pull request ที่ยังเปิดอยู่จะเสียการอ้างอิง ทำงานนี้ตอนที่ตกลงกับทีมแล้ว ไม่ใช่ตอนตกใจ
อย่าคาดหวังว่า Support จะล้างให้ทุกกรณี เอกสารของ GitHub ระบุว่าทีมจะช่วยลบข้อมูลที่เป็นความลับเฉพาะกรณีที่ประเมินว่าความเสี่ยงลดไม่ได้ด้วยการหมุนคีย์ นั่นคือเหตุผลที่ข้อ 1 มาก่อนข้อ 5 เสมอ
ถ้าคุณใช้ผู้ช่วยเขียนโค้ดหรือเอเจนต์ที่ commit ให้ ความเสี่ยงข้อนี้เพิ่มขึ้นแบบเงียบ ๆ เพราะมันอ่านไฟล์ในโปรเจกต์ทั้งหมดรวมถึง .env แล้วอาจเขียนค่าเหล่านั้นลงไฟล์อื่นเอง วิธีจัดการเป็นเรื่องของการวางขอบเขต ไม่ใช่การขอร้อง อ่าน วิธีกันไม่ให้ความลับหลุดเข้าไปในเอเจนต์ AI และเก็บโทเคนจริงไว้ใน ตัวจัดการรหัสผ่าน Vaultwarden บน VPS ของคุณเอง แทนการวางไว้ในโฟลเดอร์โปรเจกต์
secret scanning กับ push protection จับอะไรได้ จับอะไรไม่ได้
GitHub มีสองกลไกที่ชื่อคล้ายกันแต่ทำงานต่างเวลา และควรแยกให้ออก
secret scanning ทำงานหลังข้อมูลขึ้นไปแล้ว บน repo สาธารณะมันทำงานอัตโนมัติและไม่มีค่าใช้จ่าย โดยสแกนประวัติ Git ทั้งหมดบนทุก branch ไม่ใช่เฉพาะ commit ใหม่ เมื่อเจอ มันแจ้งเตือนเจ้าของ repo และแจ้งผู้ให้บริการที่เป็นพาร์ตเนอร์ให้ทราบด้วย ในหลายกรณีผู้ให้บริการจะเพิกถอนคีย์นั้นให้เอง
push protection ทำงานก่อนข้อมูลขึ้นไป มันปฏิเสธ push ที่มีคีย์ตั้งแต่ต้นทาง สำหรับบัญชีผู้ใช้ GitHub เปิดไว้ให้โดยค่าเริ่มต้นเวลา push เข้า repo สาธารณะ ส่วนระดับ repository นั้นปิดอยู่โดยค่าเริ่มต้น ต้องให้ผู้ดูแลเปิดเอง ซึ่งเป็นจุดที่ทีมส่วนใหญ่พลาด: repo ส่วนตัวของบริษัทคุณอาจไม่มีการกันนี้อยู่เลย เวลาถูกบล็อก คุณจะเห็นข้อความจากฝั่งเซิร์ฟเวอร์ที่ขึ้นต้นประมาณนี้
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - Push cannot contain secretsและมันข้ามได้ ใครที่มีสิทธิ์เขียนสามารถ bypass โดยระบุเหตุผลหนึ่งในสามแบบ คือใช้ทดสอบ, เป็นการตรวจจับผิดพลาด, หรือจะแก้ภายหลัง เมื่อ bypass GitHub จะสร้าง alert บันทึกเหตุการณ์ลง audit log และส่งอีเมลถึงเจ้าของบัญชีและเจ้าขององค์กร ดังนั้นการข้ามไม่เงียบ แต่ก็ไม่ได้ถูกห้าม
สิ่งที่ทั้งสองกลไกจับไม่ได้ ต้องพูดให้ชัด แพตเทิร์นที่เปิดใช้ให้ฟรีคือความลับที่มีรูปแบบชัดเจนของผู้ให้บริการที่ร่วมโปรแกรม เช่นโทเคนที่ขึ้นต้นด้วยคำนำหน้าเฉพาะตัว ส่วนการตรวจจับความลับแบบไม่มีรูปแบบ เช่นรหัสผ่านฐานข้อมูลที่คุณตั้งเอง หรือ connection string ที่คุณประกอบขึ้นเอง เป็นความสามารถในชุดที่ต้องจ่ายเงิน ซึ่งข้อมูลนี้เป็นไปตามหน้าเอกสารของ GitHub เมื่อเดือนกันยายน 2026 และราคาของแผนเปลี่ยนได้ ให้ตรวจที่หน้าราคาก่อนวางแผนงบ
สรุปเชิงปฏิบัติ: ถือว่า push protection เป็นตะแกรงกันความผิดพลาดที่พบบ่อย ไม่ใช่กำแพง สิ่งที่มันไม่เห็นมีทั้งรหัสผ่านที่คุณคิดเอง คีย์ HMAC ภายในองค์กร ไฟล์ดัมป์ฐานข้อมูลที่มีข้อมูลลูกค้า และไฟล์คอนฟิกที่มีชื่อ host ภายในกับพอร์ตที่เปิดอยู่ ความลับสามอย่างหลังไม่มีรูปแบบให้จับได้เลย
บัญชีของคุณ: 2FA, passkey และรหัสกู้คืน
บัญชีที่ถูกยึดเปิดทาง repo ส่วนตัวทุกอันที่บัญชีนั้นเข้าถึงได้ในครั้งเดียว ตั้งแต่ต้นปี 2024 GitHub บังคับให้บัญชีที่ส่งโค้ดบน GitHub.com เปิดการยืนยันตัวตนสองขั้น (2FA) อยู่แล้ว สิ่งที่ยังเป็นทางเลือกของคุณคือใช้วิธีไหน
เลือก passkey หรือกุญแจฮาร์ดแวร์เป็นอันดับแรก รองลงมาคือแอป TOTP ที่สร้างรหัสหกหลัก และเลี่ยง SMS ให้ได้มากที่สุด เหตุผลของข้อสุดท้ายคือ SMS ผูกกับหมายเลขโทรศัพท์ ไม่ใช่กับเครื่องของคุณ ผู้โจมตีที่โน้มน้าวผู้ให้บริการมือถือให้ย้ายเบอร์ไปซิมใหม่ได้ ก็รับรหัสของคุณได้ทั้งหมด
เก็บรหัสกู้คืนที่ GitHub ให้มาตอนเปิด 2FA ไว้แบบออฟไลน์ ถ้าเครื่องที่ถือ TOTP หายไปพร้อมกับรหัสกู้คืนที่เก็บไว้ในเครื่องเดียวกัน คุณจะเข้าบัญชีตัวเองไม่ได้ และกระบวนการขอคืนใช้เวลาเป็นวัน
อีกสองหน้าที่ควรเปิดดูทุกไตรมาส หน้า security log ของบัญชีบอกทุกเหตุการณ์ที่สำคัญ รวมถึงการสร้างโทเคนและการเพิ่ม SSH key และหน้า Applications บอกว่ามี OAuth app หรือ GitHub App ใดที่คุณอนุญาตไว้ แอปที่คุณกดยอมรับเมื่อสองปีก่อนเพื่อทดลองอะไรอย่างหนึ่ง ยังถือสิทธิ์อยู่จนวันนี้ ถ้าคุณไม่ได้ถอนมันเอง
personal access token ควรตั้ง scope และวันหมดอายุแบบไหน
โทเคนคือส่วนที่รั่วง่ายที่สุด เพราะมันถูกวางไว้ในไฟล์คอนฟิก ในตัวแปรสภาพแวดล้อมของ CI และในเครื่องของคนอื่น GitHub มีสองแบบ และความต่างนั้นใหญ่กว่าที่ชื่อบอก
โทเคนแบบ classic ใช้ scope แบบกว้าง scope ชื่อ repo ตามเอกสารของ GitHub ให้สิทธิ์เต็มทั้ง repo สาธารณะและส่วนตัว รวมถึงอ่านและเขียนโค้ด, commit status, คำเชิญ, collaborator, deployment status และ webhook สิทธิ์นี้ไม่ได้จำกัดที่ repo ใด repo หนึ่ง มันครอบทุก repo ที่บัญชีคุณเข้าถึงได้ รวมถึง repo ขององค์กรที่คุณเป็นสมาชิก โทเคนใบเดียวที่ตั้งไว้ให้สคริปต์ deploy จึงเท่ากับกุญแจของทุกอย่างที่คุณมองเห็น
โทเคนแบบ fine-grained จำกัดได้สองชั้น: เลือก repo เป็นรายตัว และเลือกสิทธิ์เป็นรายหมวด สคริปต์ที่ต้องอ่านโค้ดเพื่อ build ควรได้แค่ repo นั้นหนึ่งอันกับสิทธิ์ Contents แบบอ่าน ไม่ใช่มากกว่านั้น ใช้แบบนี้เป็นค่าเริ่มต้น และตั้งวันหมดอายุให้สั้นที่สุดที่งานยังเดินได้ หลีกเลี่ยงตัวเลือกไม่มีวันหมดอายุ เพราะโทเคนที่ไม่หมดอายุคือโทเคนที่ไม่มีใครกลับมาทบทวน
กฎเพิ่มอีกสองข้อที่ช่วยได้จริง ข้อแรก ถ้าเซิร์ฟเวอร์ต้องการเพียงดึงโค้ดของ repo เดียว ใช้ deploy key แบบอ่านอย่างเดียวแทน PAT เพราะ deploy key ผูกกับ repo นั้นเท่านั้น โดยธรรมชาติของมัน ไม่ต้องอาศัยวินัยของคนตั้งค่า ข้อสอง อย่าให้ scope workflow กับโทเคนที่ไม่ได้มีหน้าที่แก้ไฟล์ workflow เพราะตามเอกสารของ GitHub scope นี้คือสิทธิ์เพิ่มและแก้ไฟล์ workflow ของ GitHub Actions ซึ่งเป็นสิทธิ์ที่เปลี่ยนโค้ดให้รันอัตโนมัติบนเครื่องของคนอื่นได้
สำหรับ CI ที่ต้องถือความลับหลายใบ การเก็บใน repository secret ตรง ๆ ใช้ได้กับทีมเล็ก แต่พอเริ่มมีหลายทีมและหลายสภาพแวดล้อม การมีที่เก็บกลางที่ออกความลับอายุสั้นให้จะจัดการง่ายกว่า ตัวเลือกและข้อแลกเปลี่ยนอยู่ใน ตัวจัดการความลับที่คุณโฮสต์เอง
token ที่ถูกขโมยไปถึงอะไรได้ผ่าน GitHub Actions
นี่คือส่วนที่คนประเมินต่ำที่สุด โทเคนที่เขียน repo ได้ ไม่ได้แปลว่าอ่านและเขียนโค้ดได้เท่านั้น มันแปลว่าสั่งให้โค้ดรันได้ เพราะ workflow ที่ตั้ง trigger ไว้ที่ push จะเริ่มทำงานเองเมื่อมี commit ใหม่ และ workflow นั้นเข้าถึงความลับที่ผูกกับ repo หรือ environment นั้นได้
กลไกการนำความลับออกไปก็ตรงไปตรงมา GitHub ปิดบังค่าของความลับใน log ให้ แต่การปิดบังทำงานกับข้อความที่ตรงกันเท่านั้น โค้ดที่เข้ารหัสค่านั้นเป็น base64 ก่อนพิมพ์ หรือส่งมันออกไปที่ปลายทางภายนอก ไม่ถูกปิดบัง การปิดบัง log จึงเป็นการกันความผิดพลาด ไม่ใช่การกันคนที่ตั้งใจ
รายละเอียดหนึ่งที่มักช่วยชะลอความเสียหายได้: โทเคน classic ที่มีแค่ scope repo แก้ไฟล์ใน .github/workflows ไม่ได้ ต้องมี scope workflow ด้วย แต่อย่าวางใจกับข้อนี้มากเกินไป เพราะผู้โจมตีไม่ต้องแก้ไฟล์ workflow ก็ได้ ถ้า workflow เรียก npm run build หรือ make ผู้ที่แก้สคริปต์ใน repo ได้ก็เปลี่ยนสิ่งที่ถูกรันได้แล้ว โดยไม่ต้องแตะไฟล์ workflow เลย
สองจุดที่ทำให้ผลกระทบลามออกนอก GitHub จุดแรกคือ self-hosted runner คือเครื่องของคุณเองที่รับงานจาก Actions งานที่รันบนนั้นคือโค้ดจาก repo ที่รันบนเครื่องในเครือข่ายคุณ และ runner แบบถาวรจะเก็บร่องรอยของงานก่อนหน้าไว้ ให้ใช้ runner แบบใช้ครั้งเดียวแล้วทิ้ง และอย่าเปิดรับ workflow จาก fork บนเครื่องเหล่านี้ จุดที่สองคือ OIDC ซึ่งให้ workflow ขอ credential อายุสั้นจากคลาวด์ได้ ถ้าเงื่อนไขความเชื่อถือฝั่งคลาวด์เขียนกว้าง เช่นยอมรับทุก repo ในองค์กร โทเคนที่ถูกขโมยใบเดียวก็เอื้อมถึงบัญชีคลาวด์ได้
การตั้งค่าที่ควรทำในหน้า Actions ของ repo มีไม่กี่ข้อ ตั้งสิทธิ์เริ่มต้นของ GITHUB_TOKEN เป็นอ่านอย่างเดียวแล้วเพิ่มสิทธิ์เป็นราย workflow เท่าที่ต้องการ บังคับให้ต้องมีคนอนุมัติก่อนรัน workflow จาก contributor ภายนอก ใส่ผู้ตรวจที่ต้องอนุมัติให้ environment ที่ถือความลับของ production และจำกัดว่า action จากภายนอกตัวไหนรันได้ ที่เหลือคือสุขอนามัยปกติ: ลบโทเคนที่ไม่ได้ใช้ และเปิด security log ดูว่ามีโทเคนใบใหม่ที่คุณไม่ได้สร้างไหม
เมื่อไหร่ที่ควรย้ายไปรัน Git server ของตัวเอง
คำตอบที่ตรงไปตรงมาคือ ไม่ใช่เพราะ GitHub ไม่ปลอดภัย ในทางปฏิบัติ GitHub ดูแลระบบของตัวเองดีกว่าที่ทีมเล็กส่วนใหญ่จะดูแลเซิร์ฟเวอร์ตัวเองได้ และการย้ายออกเพื่อความปลอดภัยแบบลอย ๆ มักได้ผลลัพธ์ที่แย่กว่า เพราะคุณรับงานแพตช์, สำรองข้อมูล, ความพร้อมใช้งาน และระบบยืนยันตัวตนมาทำเองทั้งหมด
เหตุผลที่ฟังขึ้นจริงมีอยู่ไม่กี่ข้อ ข้อแรกคือข้อผูกพัน สัญญากับลูกค้าหรือกฎของอุตสาหกรรมระบุว่าซอร์สโค้ดต้องไม่อยู่บนระบบของบุคคลที่สาม หรือต้องอยู่ในเขตแดนที่กำหนด ข้อนี้ไม่มีการตั้งค่าใดบน GitHub ที่แก้ได้ ข้อสองคือการควบคุมการลบอย่างแท้จริง อย่างที่เห็นในหัวข้อ fork network ข้างบน คุณลบข้อมูลบางส่วนด้วยตัวเองไม่ได้ ต้องพึ่ง support ข้อสามคือสภาพแวดล้อมที่ตัดขาดจากอินเทอร์เน็ต ซึ่งบริการภายนอกไม่ตอบโจทย์ตั้งแต่ต้น
ถ้าเหตุผลของคุณอยู่ในสามข้อนั้น ขั้นต่อไปคือเลือกซอฟต์แวร์และขนาดเครื่อง ตัวเลือกกับสิ่งที่ต้องดูแลต่อสรุปไว้ใน ตัวเลือก Git server ที่โฮสต์บน VPS ของคุณเอง และเมื่อเซิร์ฟเวอร์เป็นของคุณ งานที่ GitHub ทำให้เงียบ ๆ อยู่ก็กลายเป็นงานคุณ เริ่มจากการรู้ว่าเครื่องคุณมีช่องโหว่ที่ประกาศแล้วอยู่ไหม ซึ่งทำได้ตามขั้นตอนใน วิธีตรวจเซิร์ฟเวอร์ของคุณกับ CVE ที่ประกาศแล้ว
สำหรับคนอื่นเกือบทั้งหมด คำตอบคืออยู่บน GitHub ต่อ แล้วทำห้าอย่างนี้ให้ครบ: เปิด 2FA ด้วย passkey หรือ TOTP, ใช้โทเคนแบบ fine-grained ที่มีวันหมดอายุ, เปิด push protection ในระดับ repository ไม่ใช่พึ่งค่าเริ่มต้นของบัญชี, เก็บความลับไว้นอก repo ตั้งแต่วันแรก และซ้อมลำดับการหมุนคีย์ก่อนที่จะต้องใช้มันจริง
FAQ
GitHub ปลอดภัยพอสำหรับโค้ดของบริษัทไหม
ปลอดภัยพอสำหรับงานเชิงพาณิชย์ส่วนใหญ่ ความเสี่ยงหลักไม่ได้อยู่ที่แพลตฟอร์ม แต่อยู่ที่ความลับที่ถูก commit ขึ้นไป โทเคนที่มีสิทธิ์กว้างเกินจำเป็น และบัญชีที่ไม่มี 2FA ที่แข็งพอ เหตุผลที่ควรย้ายออกจริงมีอยู่ข้อเดียวคือข้อผูกพันทางสัญญาหรือทางกฎหมายที่ห้ามเก็บโค้ดไว้บนระบบของบุคคลที่สาม ถ้าข้อนั้นไม่ใช่กรณีของคุณ ให้ลงแรงกับการตั้งค่าบัญชีและการจัดการความลับ ซึ่งได้ผลมากกว่าการย้ายเซิร์ฟเวอร์
repo แบบ private ใครเห็นได้บ้าง
collaborator ทุกคนที่ถูกเชิญเห็นทั้ง repo รวมถึงประวัติทั้งหมด เพราะ GitHub ไม่จำกัดสิทธิ์เป็นรายโฟลเดอร์ ถ้า repo อยู่ใต้ organization เจ้าขององค์กรและผู้มีสิทธิ์ admin เข้าถึงได้ด้วย และในบัญชี enterprise เจ้าของ enterprise ก็เช่นกัน นอกจากนั้นข้อมูล Git ใน fork network เดียวกันเข้าถึงข้ามกันได้ตามที่ GitHub ระบุไว้ แม้ fork จะถูกลบแล้ว การเปลี่ยน repo จาก public เป็น private ก็ไม่เรียกสำเนาที่คนอื่นโคลนไปแล้วกลับมา
ลบไฟล์ที่มีคีย์ออกจาก repo แล้ว ยังต้องเปลี่ยนคีย์ไหม
ต้องเปลี่ยน และต้องเปลี่ยนก่อนล้างประวัติ การลบไฟล์สร้าง commit ใหม่ที่ไม่มีไฟล์นั้น แต่เนื้อไฟล์เดิมยังถูก commit เก่าอ้างถึง ลองรัน git log --all --full-history -- PATH แล้วต่อด้วย git show COMMIT_SHA:PATH คุณจะเห็นคีย์กลับมาบนหน้าจอ และสำเนาที่คนอื่นโคลนไปก่อนหน้าก็ยังมีมันอยู่ ให้เพิกถอนและออกคีย์ใหม่ที่ฝั่งผู้ให้บริการก่อน แล้วค่อยล้างประวัติด้วย git filter-repo ตามด้วย git push --force --mirror origin
push protection กันคีย์หลุดได้ทั้งหมดไหม
ไม่ได้ มันจับความลับที่มีรูปแบบชัดเจนของผู้ให้บริการที่ร่วมโปรแกรมเป็นหลัก รหัสผ่านฐานข้อมูลที่คุณตั้งเอง connection string ที่คุณประกอบเอง ข้อมูลลูกค้า และไฟล์คอนฟิกที่มีชื่อเครื่องภายใน ไม่มีรูปแบบให้จับ นอกจากนี้ในระดับ repository ฟีเจอร์นี้ปิดอยู่โดยค่าเริ่มต้นและต้องให้ผู้ดูแลเปิด ส่วนคนที่มีสิทธิ์เขียนยัง bypass ได้โดยระบุเหตุผล ซึ่ง GitHub จะสร้าง alert บันทึก audit log และส่งอีเมลแจ้งเจ้าของ
ควรใช้ personal access token แบบ classic หรือ fine-grained
ใช้ fine-grained เป็นค่าเริ่มต้น เพราะเลือกได้ทั้ง repo เป็นรายตัวและสิทธิ์เป็นรายหมวด ส่วน scope repo ของโทเคนแบบ classic ให้สิทธิ์เต็มกับทุก repo สาธารณะและส่วนตัวที่บัญชีคุณเข้าถึงได้ รวมถึง repo ขององค์กรที่คุณเป็นสมาชิก ตั้งวันหมดอายุให้สั้นที่สุดที่งานยังเดินได้ และถ้าเซิร์ฟเวอร์ต้องการเพียงดึงโค้ดของ repo เดียว ใช้ deploy key แบบอ่านอย่างเดียวแทน เพราะมันผูกกับ repo นั้นเท่านั้นโดยธรรมชาติ