เรื่องที่เกิดบ่อยกว่าที่คิด
GitHub มีระบบ scan secret อัตโนมัติ ทุกวันมี API key, AWS access key, database password หลุดขึ้น public repo เป็นพันๆ ตัว
วิธีที่ผู้โจมตีใช้: bot scan repo ใหม่ทุกๆ นาที พบ key ก็เอาไปใช้ทันที AWS key ที่หลุดอาจถูกใช้เปิด EC2 ขุด crypto จนเรียกบิลหลายแสนภายในไม่กี่ชั่วโมง
ปัญหานี้เกิดจากความเผลอเล็กๆ:
- เผลอ
git add .แล้ว.envติดไปด้วย - copy ค่าจริงลง code เพื่อทดสอบ แล้วลืมลบก่อน commit
- ส่ง screenshot ที่มี key ไปในแชต
- เก็บ secret ใน
next.config.jsหรือvite.config.jsที่ commit ขึ้น git
หลักการพื้นฐาน
กฎ 1: secret ห้ามอยู่ใน git history เด็ดขาด
ครั้งเดียวที่หลุดเข้า history แล้ว ลบยากมาก ต้อง rewrite history (git filter-repo) แล้ว force push ทุก clone ต้อง re-clone
กฎ 2: secret ที่ใช้ใน frontend ก็คือ public
ทุกอย่างใน NEXT_PUBLIC_* หรือ VITE_* ผู้ใช้เห็นได้หมด API key ที่ไปอยู่ใน browser ถือว่า public
กฎ 3: dev key, staging key, production key ต้องคนละตัวเสมอ
แยก key คนละตัว ถ้า dev key หลุด ก็ revoke ตัวเดียวจบ ไม่กระทบ production
ตั้งค่าให้ถูกตั้งแต่เริ่มโปรเจค
.gitignore ต้องมี
# secrets
.env
.env.local
.env.*.local
.env.development
.env.production
# อื่นๆ
node_modules
.next
dist
.DS_Store
ตรวจสอบว่าใช้:
git check-ignore -v .env
# ควรขึ้นบรรทัด .gitignore:1:.env .env
ทำ .env.example ที่ commit ได้
# .env.example (commit ได้)
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
JWT_SECRET=changeme
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxx
NEXT_PUBLIC_API_URL=http://localhost:3000/api
ใส่ name + format ตัวอย่าง ไม่ใส่ค่าจริง คนที่ clone repo ครั้งแรกแค่ cp .env.example .env แล้วเติมค่าจริงเอง
Validate env ตอนแอป start
ใช้ library อย่าง zod หรือ @t3-oss/env-nextjs:
// lib/env.ts
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
NODE_ENV: z.enum(['development', 'production', 'test']),
})
export const env = envSchema.parse(process.env)
ตอน app start ถ้าขาด env ตัวไหนหรือ format ผิด crash ทันที — ดีกว่าไป crash ตอน user คนแรกใช้
การตรวจ secret ก่อน commit
ติดตั้ง gitleaks หรือ git-secrets แล้วเปิด pre-commit hook:
# ติดตั้ง gitleaks (Mac)
brew install gitleaks
# scan repo ทั้งหมด
gitleaks detect --source . --verbose
# scan แค่ที่กำลังจะ commit
gitleaks protect --staged --verbose
ใช้คู่กับ husky + lint-staged ให้รันอัตโนมัติทุก commit:
// package.json
{
"scripts": {
"prepare": "husky"
},
"lint-staged": {
"*": "gitleaks protect --staged --redact"
}
}
เก็บ secret ใน CI/CD
GitHub Actions
ตั้งใน Settings → Secrets and variables → Actions แล้วเรียกผ่าน ${{ secrets.NAME }}:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
run: npm run deploy
ห้ามทำ:
# ❌ พิมพ์ secret ลง log
- run: echo "Token is ${{ secrets.GITHUB_TOKEN }}"
# ❌ ใส่ secret ใน workflow file ตรงๆ
- env:
API_KEY: sk_live_actual_key
GitHub มี mask อัตโนมัติ ถ้า secret โผล่ใน log จะแสดงเป็น *** แต่ก็ไม่ได้ป้องกัน 100% (ถ้า encode base64 แล้วพิมพ์ก็หลุด)
เก็บ secret บน server production
วิธีง่ายสุด: ไฟล์ .env บน server
# สร้างไฟล์บน server เท่านั้น (ไม่ commit)
sudo nano /var/www/myapp/.env
# ตั้ง permission ให้แค่ owner อ่าน
sudo chown deploy:deploy /var/www/myapp/.env
sudo chmod 600 /var/www/myapp/.env
ใช้ได้แต่ติดตามไม่ได้ว่าใครแก้อะไรเมื่อไหร่
วิธีดีกว่า: Secret Manager
โปรเจคที่ใหญ่ขึ้นแนะนำใช้ tool พวกนี้:
- Doppler — UI ดี ใช้ง่าย sync secret ลงทุก env, มี free tier
- Infisical — open source, self-host ได้, ฟรี
- AWS Secrets Manager — ถ้าใช้ AWS อยู่แล้ว, มีค่าใช้จ่ายต่อ secret
- HashiCorp Vault — enterprise grade, complex แต่ทรงพลังมาก
ตัวอย่าง Doppler:
# ติดตั้ง CLI
curl -Ls --tlsv1.2 --proto "=https" \
--retry 3 https://cli.doppler.com/install.sh | sh
doppler login
doppler setup
# รัน app โดย Doppler inject env
doppler run -- npm start
ข้อดี: rotate key ได้ใน UI ทุก service ที่ใช้ key นั้นได้ค่าใหม่ทันทีโดยไม่ต้อง deploy ใหม่
ถ้า secret หลุดไปแล้วต้องทำยังไง
ขั้นเร่งด่วน:
- revoke key ทันที — ไปที่ provider (AWS, Stripe, ฯลฯ) ลบหรือ disable key
- สร้าง key ใหม่ + update ในที่ที่ใช้
- ตรวจ log ว่ามีการใช้ผิดปกติช่วงที่หลุดไหม
- ถ้าเป็น repo public → secret อาจถูก scan เก็บไว้แล้ว key เก่าใช้ไม่ได้แน่นอน
ลบจาก git history (ไม่ค่อยจำเป็นถ้า revoke key แล้ว แต่ทำได้):
# ใช้ git-filter-repo (ดีกว่า filter-branch)
pip install git-filter-repo
git filter-repo --path .env --invert-paths --force
# force push (ทุก clone ต้อง re-clone)
git push origin --force --all
แต่จำไว้: secret ที่เคยหลุด ถือว่าหลุดถาวร ต้อง revoke เท่านั้น ลบจาก git ไม่ใช่การแก้
เช็คลิสต์ส่วนตัว
- [ ]
.gitignoreมี.env*ตั้งแต่ commit แรก - [ ] มี
.env.exampleใน repo - [ ] ใช้ zod หรือเครื่องมืออื่น validate env ตอน start
- [ ] ติดตั้ง gitleaks pre-commit hook
- [ ] ไม่มี secret อยู่ใน frontend code
- [ ] dev / staging / production key คนละตัว
- [ ] รู้ว่าจะ revoke key แต่ละตัวยังไงถ้าหลุด
ทำ checklist ครั้งเดียวตอนเริ่มโปรเจค ปลอดภัยตลอดอายุงาน