ก่อน Commit ไฟล์ .env ผมจะเช็กอะไรใน Git และ Gitea

ถ้ามี source code, ไฟล์ .env และ deployment token อยู่ใน project เดียวกัน เราควรแยกอะไรออกจาก Git ตั้งแต่แรก และต้องทำอย่างไรเมื่อค่าลับถูก commit ไปแล้ว

ทีมขนาดเล็กตรวจเส้นทางของโค้ดจากเครื่องนักพัฒนาไปยัง Gitea โดยแยกกุญแจและ token ไว้ในตู้ที่ปลอดภัย

เวลาเริ่ม project ใหม่ เรามักมี source code อยู่ folder หนึ่ง แล้วก็มีไฟล์ .env วางอยู่ข้าง ๆ กัน ข้างในอาจเป็น database password, API key หรือ token สำหรับ deploy ซึ่งโปรแกรมต้องใช้จริง ถ้าไม่มีไฟล์นี้ application ก็รันไม่ได้

คำถามคือ ในเมื่อมันเป็นส่วนหนึ่งที่ทำให้ระบบทำงาน แล้วทำไมถึงไม่ควรเก็บเข้า Git ไปพร้อม code เลย

ถ้าเป็นผม ผมจะยังไม่กด git add . จนกว่าจะตอบได้ว่าค่าจริงจะไปอยู่ที่ไหน เพราะ Git จำของเก่งกว่าที่หน้าจอ repository แสดงให้เราเห็นมาก ลบ .env แล้ว commit ใหม่ ไฟล์อาจหายจากหน้าปัจจุบัน แต่ค่าข้างในยังอยู่ใน commit เก่า, branch, tag หรือ clone บนเครื่องอื่นได้ งานที่ดูเหมือนแก้ด้วยการลบหนึ่งไฟล์จึงอาจจบด้วยการเปลี่ยน password หลายระบบแทนครับ

ผมอยากให้ Git รู้โครงสร้าง แต่ไม่รู้ค่าจริง

ภาพประกอบ: โต๊ะทำงานมี source code และไฟล์ config ตัวอย่างอยู่ฝั่งหนึ่ง ส่วน credential จริงถูกเก็บแยกในตู้ล็อก

สิ่งที่ผมชอบเก็บใน repository คือไฟล์อย่าง .env.example ซึ่งบอกว่า application ต้องการตัวแปรชื่ออะไร เช่น DATABASE_URL หรือ DEPLOY_TOKEN พร้อมคำอธิบายสั้น ๆ แต่ไม่ใส่ค่าที่เปิดระบบจริง คนที่ clone project จะรู้ว่าต้องเตรียมอะไร โดยไม่ต้องเห็น credential ของ production

ส่วน .env ที่มีค่าจริงควรอยู่ในที่ที่เหมาะกับวิธีรันระบบ อาจเป็น secret store ของ deployment platform, secret ของ Gitea Actions หรือไฟล์ใน server ที่จำกัดสิทธิ์ไว้ จุดนี้ไม่จำเป็นต้องเริ่มด้วยระบบใหญ่เสมอไป สำหรับ project เล็ก ผมเลือกวิธีที่ทีมเข้าใจและเปลี่ยนค่าได้จริงก่อน ดีกว่ามี secret manager ตัวใหญ่แต่สุดท้ายทุกคนยังส่ง password กันใน chat เพราะใช้งานยาก

จากนั้นจึงใส่ .env และไฟล์ private key ที่เกิดในเครื่องลง .gitignore แต่มีข้อควรจำที่สำคัญมาก: .gitignore ช่วยเฉพาะไฟล์ที่ Git ยังไม่ได้ track ถ้าเคย commit ไปแล้ว การเพิ่มชื่อภายหลังไม่ได้ลบไฟล์ออกจาก history ตามที่เอกสาร Git อธิบายไว้

ผมจึงชอบเปิดดู staged changes ก่อน commit โดยเฉพาะครั้งแรกที่เพิ่ม config หรือ workflow ใหม่ ใช้เวลาไม่กี่วินาที แต่ช่วยเห็นได้ว่าไฟล์ที่กำลังเข้า Git มีมากกว่าที่ตั้งใจหรือเปล่า และถ้าต้องมีค่าตัวอย่าง อย่าเอา key จริงมาเปลี่ยนสองตัวอักษรแล้วเรียกว่า sample ครับ สร้างค่าที่ไม่มีสิทธิ์กับระบบจริงไปเลยจะสบายใจกว่า

ตั้งเป็น Private แล้ว ผมยังไม่ถือว่าเป็นที่เก็บ Password

การตั้ง repository ใน Gitea เป็น private เหมาะกับ code ภายในและช่วยจำกัดคนที่เปิดดูได้ ผมตั้งแน่นอนถ้า project ไม่ได้ต้องการเปิดสาธารณะ แต่จะไม่ใช้เหตุผลนี้เพื่อเก็บ secret ลง Git เพราะสมาชิกที่มีสิทธิ์ยัง clone ได้ เครื่องของสมาชิกยังมีสำเนา และ token ก็ยังใช้งานได้แม้เราจะลบไฟล์ออกภายหลัง

ก่อนเชิญคนเข้า repository ผมจะดูง่าย ๆ ว่าเขาต้องอ่าน code, push code หรือเปลี่ยน settings จริงหรือไม่ คนที่มาช่วย review ไม่จำเป็นต้องจัดการ repository ทุกอย่าง และคนที่ออกจาก project ไปแล้วก็ไม่ควรอยู่ใน team ต่อเพียงเพราะยังไม่มีใครเข้าไปเอาออก ถ้ารายชื่อเริ่มยาวจนจำไม่ได้ว่าใครทำอะไร

Private จึงมีประโยชน์ แต่ทำหน้าที่เป็นรั้วมากกว่าตู้เก็บกุญแจ ผมอยากให้สองเรื่องนี้แยกกันตั้งแต่ต้น จะได้ไม่ต้องมาถกกันตอน secret หลุดแล้วว่า “แต่ repo เราไม่ได้ public นะ”

Token ที่สะดวกวันนี้ อาจทำให้เปลี่ยนยากวันหลัง

จุดที่ผมจะระวังต่อคือ token ของ automation เพราะมันขยายตัวได้ง่ายมาก เริ่มจากสร้าง personal access token หนึ่งค่าไว้ให้ script ใช้ พอใช้ได้ก็นำค่าเดิมไปใส่ IDE, CI และงาน deploy อีกสองสามตัว สุดท้ายไม่มีใครอยาก rotate เพราะไม่แน่ใจว่าจะทำอะไรพังบ้าง

Gitea รองรับ access token ที่กำหนด scope อ่านหรือเขียนแยกตามกลุ่ม API ได้ ผมจะสร้าง token ตามงาน เช่น build, publish package หรือ deploy แล้วให้สิทธิ์เท่าที่ต้องใช้ งานที่มีหน้าที่อ่าน repository เพื่อ build ไม่ควรได้สิทธิ์ดูแลระบบเพียงเพราะ admin token ตั้งค่าแล้วผ่านแน่นอน

วิธีนี้มีงานเพิ่มตรงที่ต้องดูแล token หลายค่า ผมยอมรับข้อเสียนี้ เพราะเวลาค่าหนึ่งหลุด เราปิดเฉพาะงานนั้นได้ และเวลาต้องเปลี่ยนก็ไม่ต้องลุ้นว่าระบบที่ไม่เกี่ยวข้องจะหยุดตามไปด้วย อย่างน้อยควรรู้ว่า token นี้ใช้ที่ไหน ใครดูแล และจะ revoke จากหน้าใด โดยไม่คัดลอกค่าจริงไปใส่ inventory อีกชุด

ถ้าใช้ Gitea Actions ค่าจริงสามารถเก็บเป็น secret ระดับ user, organisation หรือ repository แล้วเรียกผ่าน secrets context ได้ ซึ่งเหมาะกว่าการเขียน token ลง workflow YAML แต่ยังต้องดูว่าใครแก้ workflow ได้ เพราะคนที่แก้ขั้นตอนของ job อาจทำให้ workflow เรียกใช้ secret ที่เปิดให้มันอยู่แล้วได้

Scanner ควรอยู่ตรงที่หยุดเราได้ทัน

ภาพประกอบ: โค้ดเดินผ่านจุดตรวจสามชั้นบนเส้นทางจาก laptop ไปยัง pull request และ Gitea Actions

ผมไม่ค่อยอยากฝากความหวังไว้กับการจำเพียงอย่างเดียว เพราะวันที่รีบแก้ production คือวันที่ไฟล์แปลก ๆ หลุดเข้า commit ได้ง่ายที่สุด Secret scanner อย่าง Gitleaks ช่วยตรวจไฟล์, staged changes และ Git history ได้ แต่จะวางตรงไหนก็มีผลกับประสบการณ์ใช้งาน

ถ้าติดไว้ก่อน commit คนเขียนจะรู้เร็วและแก้ง่าย แต่แต่ละคนอาจไม่ได้ติดตั้งเหมือนกัน ถ้าวางใน CI ตอนเปิด pull request ทีมจะได้กติกากลาง แต่ secret ก็เข้า Git ไปหนึ่งขั้นแล้ว ผมจึงชอบมีจุดตรวจใกล้เครื่องสำหรับ feedback เร็ว และมี CI เป็นด่านกลางก่อน merge ไม่จำเป็นต้องเริ่มด้วย scanner หลายตัวที่เตือนเรื่องเดียวกันจนทีมเลิกอ่านผล

ใน Gitea เราสามารถใช้ protected branch เพื่อจำกัด direct push, force push, กำหนด approval และรอ status check ก่อน merge ตัว branch protection ไม่ได้รู้เองว่าบรรทัดไหนเป็น password แต่ช่วยทำให้ผล scanner มีโอกาสหยุด code ก่อนเข้าหลัก ถ้า admin bypass ทุกอย่างเป็นนิสัย ต่อให้ตั้ง rule ไว้สวยแค่ไหน สุดท้ายก็กลับไปพึ่งความระวังของคนเหมือนเดิม

Scanner เองก็มีทั้ง false positive และของที่ตรวจไม่เจอ ผมจะทำ allowlist แบบเจาะจงกับค่าทดสอบที่อธิบายได้ แทนการปิด rule กว้าง ๆ เพื่อให้ pipeline กลับมาเขียวเร็ว เพราะสีเขียวที่เกิดจากการไม่ตรวจไม่ได้ช่วยให้ปลอดภัยขึ้น

ถ้าเผลอ Commit แล้ว ผมจะเปลี่ยนกุญแจก่อนล้างประวัติ

เมื่อเห็น secret จริงอยู่ใน commit สิ่งแรกที่ผมจะทำไม่ใช่รีบลบไฟล์หรือ force-push แต่ไป revoke หรือ rotate ที่ระบบต้นทางก่อน ถ้าเป็น API key ก็ปิด key นั้น ถ้าเป็น database password ก็เปลี่ยนค่า ถ้าเป็น deployment token ก็ยกเลิก token เดิม เป้าหมายคือทำให้สิ่งที่มองเห็นอยู่ใช้เปิดระบบต่อไม่ได้

ภาพประกอบ: ทีมรับมือ credential รั่วโดยปิดกุญแจเดิม ตรวจร่องรอย และประสานการทำความสะอาด Git history

หลังจากนั้นค่อยดู log ตามสิทธิ์ของ credential ว่ามีการใช้งานที่ไม่คาดคิดหรือไม่ แล้วตามหาว่าค่านั้นไปอยู่ที่ใดบ้าง นอกจาก branch หลักยังอาจอยู่ใน tag, pull request, CI log, artifact หรือ clone ของคนในทีม ถ้ามี workflow ที่ยังใช้ค่าเดิม ก็ต้องเปลี่ยนไปใช้ค่าใหม่ผ่านช่องทาง secret ที่ถูกต้อง

การ rewrite history อาจยังจำเป็น แต่ผมจะทำหลังจาก credential เดิมใช้ไม่ได้แล้ว และต้องบอกคนที่มี clone หรือ fork ให้เข้าใจตรงกัน เพราะการเขียนประวัติใหม่ทำให้ commit hash เปลี่ยน กระทบ branch, tag และ signature ได้ ถ้าคนหนึ่งยังถือ history เก่าแล้ว push กลับมา ของที่เพิ่งลบก็กลับเข้ามาอีก

ตรงนี้เป็นเหตุผลที่ผมไม่ค่อยชอบคำแนะนำสั้น ๆ ว่า “เผลอ commit secret ก็ลบ commit ทิ้ง” มันแก้ภาพที่เห็นใน Git แต่ไม่ได้ปิดกุญแจ ไม่ได้ดูว่ามีใครใช้ไปแล้วหรือยัง และไม่ได้จัดการสำเนาที่ออกจากเครื่องเราไปก่อนหน้านั้น

ถ้าเริ่ม Repository ใหม่ ผมจะทำแค่ไหนก่อน

ผมจะยังไม่ซื้อระบบ secret management ขนาดใหญ่เพียงเพราะเริ่ม repository ใหม่ สิ่งที่อยากให้จบก่อนคือมี .env.example ที่ไม่มีค่าจริง, .gitignore ที่ใส่ตั้งแต่แรก, repository visibility ที่เหมาะสม, สิทธิ์คนไม่กว้างเกินงาน, token แยกตามหน้าที่ และ CI ที่ตรวจ secret ก่อน merge

จากนั้นลองตาม deployment token หนึ่งค่าตั้งแต่สร้างจนถึงตอนใช้งาน ดูว่ามันถูกเก็บที่ไหน workflow ไหนเรียกได้ ใครเปลี่ยนได้ และถ้าต้องปิดตอนนี้จะไปกดตรงไหน ถ้าตอบได้ครบ ผมคิดว่า setup เริ่มดูแลต่อได้แล้ว ถ้าตอบไม่ได้ การเพิ่ม scanner อีกตัวอาจยังไม่ช่วยเท่ากับการกลับไปจัดทางเดินของ token ให้ชัด

สำหรับ repository ที่มีอยู่แล้ว ลองเริ่มจากเปิด .gitignore, หน้า Actions secrets และรายชื่อ collaborator ขึ้นมาพร้อมกันครับ เราไม่ต้องแก้ทุกอย่างในวันเดียว แค่เลือก secret ที่สำคัญหนึ่งค่าแล้วตามให้จบก่อนว่าอยู่ตรงไหน ผ่านอะไร และปิดอย่างไร ก็เห็นจุดที่ควรแก้มากกว่าการอ่าน checklist ยาว ๆ แล้วไม่รู้ว่าจะเริ่มบรรทัดไหน

อ่านต่อที่เกี่ยวข้อง

แหล่งข้อมูลอ้างอิง