ไฟล์ .env ส่วนใหญ่อยู่ใน .gitignore อยู่แล้ว แต่ secret มักหลุดเข้า git ทางอื่น เช่น ใส่ค่าจริงเป็น default ใน config/*.php ไว้ลองก่อน วาง key ลงใน test หรือ seeder หรือ commit ไฟล์ dump ที่มี token ติดมา พอรู้ตัวแล้วลบออกใน commit ถัดไป ค่านั้นก็ยังอยู่ใน history ให้ทุกคนที่ clone repo ไปเปิดดูได้
gitleaks ช่วยหาค่าพวกนี้ให้ก่อนที่คนอื่นจะเจอ
gitleaks คืออะไร
gitleaks เป็นเครื่องมือ command line ใช้ license MIT เขียนด้วย Go ติดตั้งได้จาก Homebrew หรือโหลดไฟล์ binary จากหน้า release ไปใช้ได้เลย
ข้างในมีชุดกฎ (rule) กว่าสองร้อยข้อ แต่ละข้อจับรูปแบบของ secret จากบริการหนึ่ง เช่น AWS access key, GitHub token หรือ Stripe key ส่วนค่าที่ไม่มีรูปแบบเฉพาะจะไปเข้ากฎ generic-api-key ซึ่งดูสองอย่างประกอบกัน คือชื่อตัวแปรมีคำอย่าง key, token, secret หรือ password และค่าที่อยู่ข้าง ๆ ดูสุ่มพอ วัดจาก entropy ของตัวอักษร
มีสองคำสั่งหลัก
# อ่านทุก commit ใน history ของ repo
gitleaks git -v .
# อ่านเฉพาะไฟล์ในโฟลเดอร์ตอนนี้ ไม่ดู history
gitleaks dir -v .
gitleaks git อ่านเฉพาะบรรทัดที่ เพิ่มเข้ามา ในแต่ละ commit secret ตัวหนึ่งจึงโผล่เป็น finding ครั้งเดียว ที่ commit ซึ่งใส่มันเข้ามา แม้ไฟล์นั้นจะถูกแก้อีกหลายรอบหลังจากนั้น
ตั้งให้สแกนทั้ง history ทุกวันใน GitHub Actions
ทางที่ง่ายที่สุดดูเหมือนจะเป็น gitleaks-action แต่ action ตัวนี้ต้องใช้ license key เมื่อ repo อยู่ใต้ organization (ขอฟรีได้ที่ gitleaks.io) ถ้าไม่อยากเพิ่ม secret อีกตัวมาดูแล ก็ติดตั้ง CLI เองใน workflow ได้ ใช้เวลาไม่กี่บรรทัด
name: security-scan
on:
schedule:
- cron: '0 19 * * *' # 02:00 เวลาไทย
workflow_dispatch:
permissions:
contents: read
env:
GITLEAKS_VERSION: 8.30.1
jobs:
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # ต้องได้ history ครบ
persist-credentials: false
- name: Install gitleaks
run: |
set -euo pipefail
base="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}"
tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
curl -fsSLO "$base/$tarball"
curl -fsSLO "$base/gitleaks_${GITLEAKS_VERSION}_checksums.txt"
grep " ${tarball}\$" "gitleaks_${GITLEAKS_VERSION}_checksums.txt" | sha256sum -c -
tar -xzf "$tarball" gitleaks
- name: Scan the whole history
run: >-
./gitleaks git --no-banner --redact --exit-code 1 --log-opts="--all"
--report-format json --report-path gitleaks-report.json .
- name: Upload the redacted report
if: always()
uses: actions/upload-artifact@v7
with:
name: gitleaks-report
path: gitleaks-report.json
retention-days: 7
if-no-files-found: ignore
ค่าที่ตั้งไว้มีเหตุผลแบบนี้
- รันตามเวลา ไม่รันทุก push เพราะสแกนทั้ง history ทุกครั้ง และ gitleaks เวอร์ชันใหม่อาจมีกฎใหม่ที่ไปเจอ secret ใน commit เก่า ๆ ซึ่ง push แต่ละครั้งไม่มีทางรู้ รอบละวันก็พอ
fetch-depth: 0ให้ checkout ดึง history มาครบ ค่า default ของ checkout ดึงมาแค่ commit ล่าสุด gitleaks ก็จะเห็นแค่ commit เดียว- ตรวจ checksum ก่อนแตกไฟล์ เพราะเครื่องมือที่อ่าน secret ทั้ง repo ควรเป็นตัวที่เราแน่ใจว่าโหลดมาถูกตัว
--redactปิดค่าจริงใน log และใน report ให้เหลือแค่REDACTEDเพราะ log ของ GitHub Actions คนที่อ่าน repo ได้ก็เปิดดูได้ และ report ก็เก็บไว้แค่ 7 วัน
เมื่อเจอ secret workflow จะจบด้วย exit code 1 ให้ GitHub ส่งอีเมลแจ้งว่า run ไม่ผ่าน เราโหลดไฟล์ gitleaks-report.json จากหน้า run มาดูว่าเจออะไรที่ไหน
ดูให้แน่ก่อนว่าเป็น secret จริงไหม
finding ทุกตัวบอกไฟล์ บรรทัด commit กฎที่จับได้ และ fingerprint ซึ่งเป็นรหัสของ finding ตัวนั้น หน้าตาแบบนี้
Finding: CLOUDFLARE_WEB_ANALYTICS_TOKEN', 'REDACTED'
Secret: REDACTED
RuleID: generic-api-key
Entropy: 3.709868
File: config/cloudflare.php
Line: 29
Commit: 1bf7aa7372b076d485a1a775f7be3b64f55d697e
Date: 2026-10-16T20:12:01Z
Fingerprint: 1bf7aa7372b076d485a1a775f7be3b64f55d697e:config/cloudflare.php:generic-api-key:29
จากตรงนี้มีได้สองทาง
ถ้าเป็น secret จริง เช่น key ของ payment gateway หรือรหัส SMTP ให้เปลี่ยน key ที่ต้นทางก่อนเรื่องอื่น ออก key ใหม่ ใส่ใน .env ของเซิร์ฟเวอร์ แล้วยกเลิกตัวเก่า การลบ commit หรือเขียน history ใหม่ไม่พอ เพราะค่าเดิมอาจอยู่ใน clone ของเพื่อนร่วมทีมหรือใน fork ไปแล้ว และ GitHub ยังเปิด commit เดิมให้ดูได้จาก hash แม้เราจะ force push ทับไปแล้วก็ตาม เมื่อ key เก่าใช้ไม่ได้แล้ว ค่อยใส่ fingerprint ลง .gitleaksignore ให้ CI กลับมาผ่าน
ถ้าเป็นค่าที่ไม่ใช่ความลับ เช่น key ตัวอย่างใน test หรือค่าที่ตั้งใจให้ทุกคนเห็นอยู่แล้ว ก็ใส่ fingerprint ได้เลย พร้อมเขียนสั้น ๆ ว่าทำไมไม่นับเป็นความลับ
ตัวอย่างจากเว็บนี้ คือ token ของ Cloudflare Web Analytics
finding ข้างบนมาจาก repo ของเว็บนี้เอง ใน config/cloudflare.php มีบรรทัดนี้
'web_analytics_token' => env('CLOUDFLARE_WEB_ANALYTICS_TOKEN', 'ab924a2d…'),
ชื่อตัวแปรมีคำว่า TOKEN และค่าเป็นตัวอักษรสุ่ม 32 ตัว ตรงกับกฎ generic-api-key พอดี แต่ token ตัวนี้คือ token ของ beacon ใน Cloudflare Web Analytics ซึ่ง Cloudflare ให้เราวาง script ไว้ในทุกหน้า HTML ใครกด View Source ก็เห็นค่าเดียวกันใน data-cf-beacon อยู่แล้ว และ token นี้ใช้ได้อย่างเดียวคือส่งจำนวนคนเข้าชมเข้าไป อ่านรายงานหรือแก้ค่าอะไรใน Cloudflare ไม่ได้ จึงไม่ใช่ secret
ส่วนเลข account ID กับ site tag ที่อยู่ในไฟล์เดียวกัน gitleaks ไม่จับ เพราะชื่อไม่มีคำที่บอกว่าเป็นความลับ
เรื่องนี้ดูเล็ก แต่ workflow แดงทุกวันอยู่สิบวัน ก่อนเราจะเข้าไปดู ซึ่งอันตรายกว่า finding ตัวนี้เอง เพราะพอเห็นว่าแดงจนชินตา วันที่มี secret ตัวจริงหลุดเข้ามา เราก็จะไม่เห็นความต่าง scan ที่ fail เพราะ false positive จึงควรเคลียร์ให้เร็วพอ ๆ กับ finding จริง
ข้าม finding ด้วย fingerprint ใน .gitleaksignore
gitleaks อ่านไฟล์ .gitleaksignore ที่ root ของ repo ใส่ fingerprint บรรทัดละตัว บรรทัดที่ขึ้นต้นด้วย # เป็นคอมเมนต์
# Findings gitleaks may skip, by fingerprint (commit:file:rule:line), never by value.
# Cloudflare Web Analytics beacon token: public by design, rendered into every guest page's data-cf-beacon.
1bf7aa7372b076d485a1a775f7be3b64f55d697e:config/cloudflare.php:generic-api-key:29
fingerprint ประกอบด้วย commit, ไฟล์, กฎ และเลขบรรทัด จึงข้ามได้แค่ finding ตัวนั้นตัวเดียว ถ้าวันหลังมีคนวาง key จริงลงไฟล์เดียวกัน gitleaks ก็ยังจับได้ตามปกติ วิธีนี้แคบกว่าการ allowlist ทั้งไฟล์ใน .gitleaks.toml ซึ่งจะปิดตาไฟล์นั้นไปตลอด และไม่ต้องเขียนค่าของ secret ลงไปใน repo อีกรอบด้วย
ก่อน push ให้รันบนเครื่องตัวเองดูก่อนว่าผ่าน
gitleaks git --no-banner --redact --log-opts="--all" .
INF 412 commits scanned.
INF scanned ~18107225 bytes (18.11 MB) in 8.26s
INF no leaks foundมีสองเรื่องที่ควรรู้ไว้
- fingerprint ผูกกับ commit ถ้าแก้บรรทัดนั้นใน commit ใหม่ gitleaks จะเห็นเป็นการเพิ่มบรรทัดใหม่ และขึ้น finding ใหม่ด้วย fingerprint ใหม่ ถ้าไม่อยากเจอซ้ำ จะย้ายค่าไปไว้ใน
.envเลยก็ได้ แต่ commit เก่ายังมีค่านี้อยู่ fingerprint เดิมจึงต้องอยู่ต่อ - คอมเมนต์
gitleaks:allowท้ายบรรทัดก็ใช้ข้ามได้ แต่ได้ผลเฉพาะ commit ที่มีคอมเมนต์นั้นแล้ว commit เก่าที่ใส่ค่าเข้ามาก่อนยังเป็น finding อยู่ เหมาะกับค่าใหม่ที่เรารู้ตั้งแต่ตอนเขียนว่าไม่ใช่ความลับ เช่น key ปลอมใน test
ถ้าเพิ่งเริ่มใช้ gitleaks กับ repo เก่าที่เจอ finding เป็นสิบตัว ลองดู --baseline-path ซึ่งรับ report เก่าเข้าไป แล้วรายงานเฉพาะ finding ที่ไม่มีใน report นั้น เราจะไล่เคลียร์ของเก่าทีละตัวได้ โดย CI จับของใหม่ไปพร้อมกัน
กันไว้ตั้งแต่ก่อน commit
scan รายวันจับได้หลังจาก secret เข้า history ไปแล้ว ถ้าอยากกันตั้งแต่ต้นทาง ให้รัน gitleaks กับไฟล์ที่ stage ไว้ก่อนทุก commit
gitleaks git --pre-commit --staged -v .
จะใส่คำสั่งนี้ใน .git/hooks/pre-commit เอง หรือใช้ผ่าน pre-commit ตามที่ README ของ gitleaks มีตัวอย่างไว้ก็ได้ แต่ hook อยู่บนเครื่องของแต่ละคน ใครไม่ได้ติดตั้งก็ไม่มีผล scan ใน CI จึงยังต้องมีไว้เป็นด่านสุดท้าย
ส่วนค่าที่เป็นความลับจริงของแต่ละ environment ควรอยู่ใน .env ไม่ใช่ใน config/*.php ถ้าอยากเก็บ .env ของ production ไว้ใน repo ด้วย Laravel มี env:encrypt ให้เข้ารหัสก่อน commit ลองอ่านต่อได้ที่ วิธีเก็บ .env ของ production ไว้ใน git แบบเข้ารหัส ครับ






