← กลับ
SecurityDevOpsBest Practice

จัดการ Environment Variables ยังไงไม่ให้ข้อมูลหลุด

เผลอ commit .env ขึ้น GitHub มาก่อนใช่ไหม เก็บ secret ยังไงให้ปลอดภัยตั้งแต่เครื่อง dev ถึง production

2025-12-10อ่าน 6 นาทีใหม่

เรื่องที่เกิดบ่อยกว่าที่คิด

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 หลุดไปแล้วต้องทำยังไง

ขั้นเร่งด่วน:

  1. revoke key ทันที — ไปที่ provider (AWS, Stripe, ฯลฯ) ลบหรือ disable key
  2. สร้าง key ใหม่ + update ในที่ที่ใช้
  3. ตรวจ log ว่ามีการใช้ผิดปกติช่วงที่หลุดไหม
  4. ถ้าเป็น 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 ครั้งเดียวตอนเริ่มโปรเจค ปลอดภัยตลอดอายุงาน

← ดูบทความอื่น