เผลอ push API key ขึ้น Git ทำอะไรก่อน: เปลี่ยนกุญแจ ล้างประวัติ กันซ้ำ
API token หลุดขึ้น GitHub ลบ commit ไม่ช่วย ต้องเปลี่ยนกุญแจก่อน แล้วค่อยตรวจ log ล้างประวัติด้วย git filter-repo และตั้ง gitleaks กับ push protection กันครั้งหน้า
- ขั้นแรกคือเพิกถอนและออกกุญแจใหม่ ไม่ใช่ลบ commit เพราะบอตเจอกุญแจภายในไม่กี่นาที
- ดู log การใช้งานของกุญแจเก่าย้อนไปถึงเวลาที่ push เพื่อรู้ว่ามีใครเอาไปใช้หรือยัง
- ล้างประวัติด้วย git filter-repo --sensitive-data-removal แล้วตรวจซ้ำด้วย git log -S ก่อน force push
- กันซ้ำสามชั้น: .env กับ .gitignore, gitleaks ใน pre-commit และ push protection ของ GitHub
เผลอ commit ไฟล์ .env หรือแปะ API token ลงโค้ดแล้ว push ขึ้น GitHub เป็นเรื่องที่เกิดกับทุกทีม สิ่งที่ทำให้เรื่องเล็กกลายเป็นเรื่องใหญ่คือ ลำดับ คนส่วนใหญ่รีบลบ commit ก่อน ทั้งที่กุญแจถูกอ่านไปแล้วตั้งแต่นาทีแรก บทความนี้เรียงขั้นตอนตามลำดับที่ควรทำจริง จากฝั่งเจ้าของระบบ
ทดสอบบน Git 2.45 และ git-filter-repo 2.47.0 (ลองกับ repo ทดลองที่มีกุญแจปลอม) คำสั่ง gitleaks ตรวจกับซอร์สของ v8.30.1 ส่วนตัวเลขงานวิจัยตรวจกับรายงานต้นฉบับ
ทำไมลบ commit ไม่ได้ช่วย
พอ push แล้ว ค่านั้นไม่ได้อยู่ที่เดียวอีกต่อไป เอกสารของ GitHub เองบอกว่าแม้จะเขียนประวัติใหม่ ข้อมูลเก่ายังค้างอยู่ใน clone และ fork ของคนอื่น ใน pull request ที่อ้างถึง commit นั้น และเปิดดูได้ด้วย SHA ตรง ๆ ผ่าน cached view จนกว่า GitHub Support จะเคลียร์ให้
ที่หนักกว่าคือความเร็วของฝั่งคนหา
| แหล่ง | ผลที่พบ |
|---|---|
| Orca Security, Honeypotting in the Cloud 2023 | AWS key ที่วางไว้บน GitHub ถูกใช้ภายใน 2 นาที |
| Comparitech, GitHub honeypot | AWS key ปลอมถูกเจอและเริ่มถูกใช้ภายใน 1 นาที |
| GitGuardian, State of Secrets Sprawl 2025 | ปี 2024 พบความลับใหม่ใน public commit บน GitHub 23.8 ล้าน รายการ และ 70% ของที่หลุดในปี 2022 ยังใช้งานได้อยู่ |
ตัวเลขชุดสุดท้ายบอกว่าปัญหาจริงไม่ใช่ “หลุด” แต่คือ “หลุดแล้วไม่มีใครเปลี่ยน”
กุญแจที่ push ขึ้น public repo แล้ว ให้ถือว่าคนอื่นมีสำเนาไปแล้ว งานแรกคือทำให้สำเนานั้นใช้ไม่ได้
ขั้นที่ 1: เพิกถอนและออกกุญแจใหม่ (ทำทันที)
เข้าหน้าจัดการ key ของผู้ให้บริการนั้น (cloud, payment, email API, LLM API) แล้ว
- สร้างกุญแจใหม่ก่อน ใส่ลงที่เก็บความลับของระบบ (ตัวแปรใน CI, secret manager, ไฟล์
.envบนเซิร์ฟเวอร์) แล้ว deploy ให้ระบบใช้ตัวใหม่ - เพิกถอนกุญแจเก่า (revoke / delete / disable) ไม่ใช่แค่ “หยุดใช้”
- ถ้ากุญแจเก่าเป็นตัวที่ระบบ production ใช้อยู่และสลับทันทีไม่ได้ ยอมให้ระบบสะดุดไม่กี่นาทีดีกว่าปล่อยกุญแจที่หลุดไว้อีกหลายชั่วโมง
ถ้า repo เป็น public GitHub มีโปรแกรม secret scanning partner ที่ส่งแจ้งเตือนไปหาผู้ให้บริการที่ร่วมโปรแกรมโดยตรง และผู้ให้บริการอาจเพิกถอนให้เองหรือติดต่อเรา แต่ อย่ารอ ให้ระบบนี้ทำแทน ไม่ใช่ทุกเจ้าร่วม และไม่ใช่ทุกรูปแบบกุญแจที่ตรวจเจอ
ถ้ากุญแจตัวเดียวใช้หลายงาน ตอนนี้คือเวลาที่เห็นราคาของมัน ต้องเปลี่ยนทุกระบบพร้อมกัน วิธีแยกกุญแจตามหน้าที่ไว้ล่วงหน้าเล่าไว้ใน แยกกุญแจตามหน้าที่
ขั้นที่ 2: ดู log ว่ามีใครใช้ไปแล้วหรือยัง
เพิกถอนแล้วยังต้องรู้ว่าในช่วงที่กุญแจหลุด มีใครทำอะไรไปบ้าง
- เวลาเริ่มต้น คือเวลาที่ push commit นั้น (ดูจาก
git logหรือหน้า commit บน GitHub) ไม่ใช่เวลาที่เราเห็น - ดู log ฝั่งผู้ให้บริการ เช่น audit log ของ cloud, หน้า usage/billing ของ API, ประวัติการเรียกของ key นั้น หา IP หรือ region ที่ไม่ใช่ของเรา, การสร้าง resource ใหม่ (เครื่อง, user, key ใหม่), ยอดใช้งานที่พุ่งขึ้น
- ถ้ากุญแจมีสิทธิ์สร้างกุญแจอื่นได้ ให้ไล่ดูว่ามี key หรือ user ใหม่ที่เราไม่ได้สร้างหรือไม่ เพราะการเพิกถอนตัวเดิมไม่ได้ลบตัวที่ถูกสร้างต่อ
- จดเวลาและสิ่งที่พบไว้ ถ้าเป็นข้อมูลลูกค้าอาจมีหน้าที่ต้องแจ้งตามกฎหมาย
ขั้นที่ 3: ล้างประวัติด้วย git filter-repo
ทำหลังจากกุญแจใช้ไม่ได้แล้วเท่านั้น จุดประสงค์คือไม่ให้ค่าเก่าค้างใน repo, ไม่ให้สแกนเนอร์เตือนซ้ำ และไม่ให้มีคนก๊อปไปใช้เป็นตัวอย่าง GitHub แนะนำ git-filter-repo ตั้งแต่เวอร์ชัน 2.47 ซึ่งมีโหมด --sensitive-data-removal (ติดตั้งด้วย pip install git-filter-repo)
ทำใน clone ใหม่ ไม่ใช่โฟลเดอร์ที่ทำงานอยู่
git clone https://github.com/example/app.git app-clean
cd app-clean
# กรณีที่ 1: ลบไฟล์ .env ออกจากทุก commit
git filter-repo --sensitive-data-removal --invert-paths --path .env
# กรณีที่ 2: กุญแจอยู่ในไฟล์โค้ด ให้แทนค่าเป็นข้อความอื่น
# replacements.txt หนึ่งบรรทัดต่อหนึ่งค่า รูปแบบ ค่าเดิม==>ค่าใหม่
echo 'sk_test_EXAMPLE1234567890abcdef==>REMOVED' > ../replacements.txt
git filter-repo --sensitive-data-removal --replace-text ../replacements.txt
ไฟล์ replacements.txt มีกุญแจตัวจริงอยู่ข้างใน วางไว้นอก repo และลบทิ้งเมื่อเสร็จ ถ้าไฟล์เคยถูกย้ายหรือเปลี่ยนชื่อ ต้องใส่ --path ให้ครบทุกชื่อ
ตรวจก่อน push ทุกครั้ง ในการทดลองของเรา คำสั่งแรกทำให้ commit เปลี่ยน 2 จาก 3 และคำสั่งตรวจด้านล่างไม่เจออะไรเลย
git log --all --oneline -S "sk_test_EXAMPLE" # ต้องไม่มีผลลัพธ์
git log --all --oneline -- .env # ต้องไม่มีผลลัพธ์
git grep "sk_test_EXAMPLE" $(git rev-list --all) # ต้องไม่มีผลลัพธ์
แล้วค่อย push ทับ
git push --force --mirror origin
ตอนจบ filter-repo จะพิมพ์ First Changed Commit(s) ออกมา เก็บไว้ใช้ขั้นต่อไป
- pull request ที่อ้างถึง commit เก่าอยู่ใน
refs/pull/*ซึ่ง push ทับไม่ได้ ต้องเปิดเรื่องกับ GitHub Support พร้อมชื่อ repo, จำนวน PR ที่เกี่ยวข้อง และ First Changed Commit ให้เขาลบ cached view และเก็บกวาด commit ที่ไม่มีใครอ้างถึง - เพื่อนร่วมทีม ต้องลบ clone เก่าแล้ว clone ใหม่ ถ้าใครยัง
git pullแล้วgit pushจาก clone เก่า ประวัติที่มีกุญแจจะกลับมาทันที - hash ของทุก commit หลังจุดที่แก้จะเปลี่ยน PR ที่เปิดค้างอยู่และลายเซ็น commit จะได้รับผลกระทบ บอกทีมก่อนทำ
ขั้นที่ 4: กันไม่ให้เกิดซ้ำ
ชั้นที่ 1: กุญแจอยู่ใน .env และ .env อยู่ใน .gitignore โค้ดอ่านค่าจาก environment เท่านั้น และ commit ไฟล์ตัวอย่างที่ไม่มีค่าจริงแทน
echo ".env" >> .gitignore
git rm --cached .env # ถ้าเคย track ไปแล้ว .gitignore อย่างเดียวไม่ช่วย
cp .env .env.example # แล้วแก้ค่าใน .env.example ให้เป็นค่าว่าง
git add .gitignore .env.example
git commit -m "stop tracking .env"
ชั้นที่ 2: gitleaks ใน pre-commit สแกนเฉพาะสิ่งที่กำลังจะ commit ถ้าเจอ commit จะไม่ผ่าน ใช้ผ่านเครื่องมือ pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
pip install pre-commit
pre-commit install # ติดตั้ง hook ลง .git/hooks ของเครื่องนี้
hook นี้รัน gitleaks git --pre-commit --redact --staged --verbose ให้เอง ถ้าอยากสแกนประวัติทั้ง repo ครั้งเดียวใช้ gitleaks git -v ข้อควรรู้คือ hook อยู่ในเครื่องแต่ละคน ข้ามได้ด้วย --no-verify และคนที่ยังไม่ได้รัน pre-commit install จะไม่มี จึงควรรัน gitleaks ใน CI ด้วยอีกชั้น
ชั้นที่ 3: push protection ของ GitHub ฝั่ง server บล็อก push ที่มีรูปแบบกุญแจที่รู้จัก สำหรับบัญชีผู้ใช้ GitHub เปิดเป็นค่าเริ่มต้น เมื่อ push ไป public repo ระดับ repository ต้องเปิดเองและต้องมี GitHub Secret Protection คนที่มีสิทธิ์เขียนกดข้ามได้โดยต้องให้เหตุผล และทุกครั้งที่ข้ามระบบจะสร้าง alert กับส่งอีเมลให้ผู้ดูแล
สามชั้นนี้จับกุญแจที่มีรูปแบบชัด รหัสผ่านทั่วไปหรือ token ที่ไม่มี prefix อาจหลุดรอด กติกาที่กันได้จริงคือไม่มีค่าลับในโค้ดเลยตั้งแต่ต้น
ข้อผิดพลาดที่เจอบ่อย
- ล้างประวัติก่อน เปลี่ยนกุญแจทีหลัง ระหว่างนั่งอ่านคู่มือ filter-repo กุญแจยังใช้งานได้อยู่
- ทำ repo เป็น private แล้วคิดว่าจบ สำเนาที่ถูก clone ไปแล้วไม่หายไปด้วย
- เพิ่ม
.envใน.gitignoreแต่ไม่git rm --cachedGit ยัง track ไฟล์ที่เคย add ไปแล้ว - push ทับแล้วไม่บอกทีม clone เก่าของใครสักคนจะ push ประวัติเดิมกลับมา
- ลืม log ฝั่งผู้ให้บริการ เพิกถอนแล้วแต่ไม่รู้ว่ามี key ใหม่ที่ผู้บุกรุกสร้างค้างไว้
- ใส่กุญแจจริงใน issue, wiki หรือ CI log ที่เหล่านี้ไม่ได้อยู่ในประวัติ git filter-repo ไม่ช่วย ต้องลบและเปลี่ยนกุญแจเหมือนกัน
ถ้าทีมใช้แพ็กเกจจากภายนอกเยอะ อ่านต่อเรื่อง กันมัลแวร์จาก npm และ pip ซึ่งเป็นอีกทางที่กุญแจในเครื่อง dev หลุดออกไปได้
แหล่งอ้างอิง
- Removing sensitive data from a repository — GitHub Docs
- About push protection — GitHub Docs
- About secret scanning for partners — GitHub Docs
- gitleaks — GitHub
- The State of Secrets Sprawl 2025 — GitGuardian
- 2023 Honeypotting in the Cloud Report — Orca Security
- It takes hackers 1 minute to find and abuse credentials exposed on GitHub — Comparitech
คำถามที่พบบ่อย
ทำ repo เป็น private ทันที หรือลบ repo ทิ้ง พอไหม
ไม่พอ ถ้า repo เคยเป็น public แม้ไม่กี่นาที ต้องถือว่ากุญแจหลุดแล้ว อาจมีคน clone หรือ fork ไปแล้ว และสแกนเนอร์อัตโนมัติอ่านไปแล้ว ทางเดียวที่ปิดความเสี่ยงได้จริงคือเพิกถอนกุญแจที่ผู้ให้บริการ
repo เป็น private ตั้งแต่แรก ต้องทำทั้งหมดนี้ไหม
ควรเปลี่ยนกุญแจเหมือนกัน คนที่มีสิทธิ์อ่าน repo, CI, เครื่องที่ clone ไว้ และ backup ทุกที่มีกุญแจนั้นอยู่ ส่วนการล้างประวัติทำเพื่อไม่ให้สแกนเนอร์เตือนซ้ำและไม่ให้หลุดรอบสองถ้าวันหนึ่ง repo กลายเป็น public
ใช้ git rebase หรือ git commit --amend ลบได้ไหม
ได้เฉพาะเมื่อ commit นั้นยังไม่ได้ push ถ้า push แล้ว ค่าใน commit เก่ายังอยู่ใน clone, fork และ cache ของคนอื่น การแก้ประวัติจึงเป็นแค่การทำความสะอาด ไม่ใช่การกู้ความลับคืน
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ