Environment Variables & Secrets — Security

Mengelola Secrets dengan Aman Secrets (API keys, database credentials, encryption keys) harus disimpan terpisah dari kode. Menyimpan secrets di kode sumber…

Secret adalah nilai yang membuka akses: API key, password database, kunci untuk menandatangani token. Tempatnya bukan di kode. Bayangin PIN kartu ATM: kamu gak menuliskannya di kartu, karena kartunya dibawa ke mana-mana dan bisa difotokopi. Kode juga begitu. Dia disalin ke git, ke laptop teman setim, ke server CI, bahkan sebagian ke browser setiap pengunjung. Jadi secret disimpan terpisah, di environment variable (variabel yang disediakan sistem saat program jalan), dan kode cuma menyebut namanya.

🧪 Tebakan cepat: di proyek Vite kamu menulis VITE_STRIPE_SECRET_KEY=sk_live_abc123 di .env, dan .env sudah masuk .gitignore. Apakah secret itu aman? Tebak dulu, jawabannya ada di bagian "Prefix VITE_ dan NEXT_PUBLIC_ artinya publik".

Kode membaca nama, nilainya dari luar

// BURUK: secret ditulis di kode, ikut ter-commit ke git
const API_KEY = "sk-abc123xyz";

// BAIK: kode cuma tahu namanya, nilainya disediakan saat program jalan
const API_KEY = process.env.OPENAI_API_KEY;

Saat develop, nilainya ditaruh di file .env di root proyek:

# .env (cuma di laptopmu, jangan di-commit)
STRIPE_SECRET_KEY=sk_live_abc123
OPENAI_API_KEY=sk-abc123xyz

Node 20.6 ke atas bisa membacanya langsung dengan node --env-file=.env server.js; di versi lama pakai package dotenv. Di server produksi, nilainya diisi lewat pengaturan hosting atau secret manager, bukan lewat file yang dikirim bareng kode.

.gitignore dan .env.example

# .gitignore
node_modules
dist
.env
# .env.example (di-commit: daftar nama, tanpa nilai)
STRIPE_SECRET_KEY=
OPENAI_API_KEY=

Kalau .env sudah terlanjur ter-commit, .gitignore gak menolong: file yang sudah dilacak git tetap dilacak. Keluarkan dari pelacakan dengan git rm --cached .env, commit, lalu ganti (rotate) semua key di dalamnya, karena nilai lamanya tetap ada di history.

Prefix VITE_ dan NEXT_PUBLIC_ artinya publik

Di Vite, cuma variabel berawalan VITE_ yang bisa dibaca kode frontend lewat import.meta.env. Saat npm run build, Vite menempelkan nilainya sebagai teks biasa ke file JavaScript yang diunduh setiap pengunjung. Next.js melakukan hal yang sama untuk NEXT_PUBLIC_. Ini proyek yang ada di Lab:

// src/checkout.js (jalan di browser)
fetch("https://api.stripe.com/v1/payment_intents", {
  method: "POST",
  headers: { Authorization: `Bearer ${import.meta.env.VITE_STRIPE_SECRET_KEY}` },
});
$ npm run build
$ grep -rnoE "sk_live_[0-9A-Za-z]+" dist/
dist/assets/index-D7nb3Uqz.js:1:sk_live_abc123

Secret-nya ada di bundle dan bisa dibaca siapa pun lewat DevTools. Itu jawaban tebakannya: gak aman. .gitignore cuma menjaga file .env dari git, bukan dari build.

Perbaikannya: secret gak boleh pakai prefix itu, dan panggilan yang butuh secret pindah ke server. Browser memanggil server-mu sendiri, dan server yang memegang key:

// src/checkout.js (browser): panggil server sendiri
fetch(import.meta.env.VITE_API_URL + "/pay", { method: "POST", body: JSON.stringify({ orderId }) });

// server/pay.js (server): secret tetap di server
fetch("https://api.stripe.com/v1/payment_intents", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` },
});

Yang memang boleh publik: URL API (VITE_API_URL), Stripe publishable key (pk_live_...), Supabase anon key. Yang gak boleh: apa pun dengan kata secret, private, password, atau service role di namanya.

Laravel: env() di config, config() di kode

Di Laravel, env('API_KEY') membaca environment variable dari .env. Aturannya: panggil env() cuma di file dalam folder config/, lalu kode lain membaca lewat config():

// config/services.php
'openai' => ['key' => env('OPENAI_API_KEY')],

// di controller
$key = config('services.openai.key');

Setelah php artisan config:cache (biasa dijalankan saat deploy), .env gak dibaca lagi, dan env() di luar folder config/ mengembalikan null.

Cek dulu sebelum jalan: validateEnv

Server yang jalan dengan variabel kosong baru error saat variabel itu dipakai, misalnya di tengah pembayaran. Lebih baik dicek begitu server mulai:

function validateEnv(requiredVars) {
  const missing = requiredVars.filter((name) => !process.env[name]);
  return { valid: missing.length === 0, missing };
}

process.env.DB_HOST = "localhost";
process.env.DB_PASSWORD = "secret";

console.log("All set:", JSON.stringify(validateEnv(["DB_HOST", "DB_PASSWORD"])));
console.log("Missing API_KEY:", JSON.stringify(validateEnv(["DB_HOST", "DB_PASSWORD", "API_KEY"])));
All set: {"valid":true,"missing":[]}
Missing API_KEY: {"valid":false,"missing":["API_KEY"]}

⚠️ Jebakan yang sering ketemu

TL;DR: Kode cuma menyebut nama secret (process.env.OPENAI_API_KEY, config('services.openai.key') di Laravel), nilainya datang dari environment. Simpan nilai lokal di .env yang masuk .gitignore, commit .env.example tanpa nilai, dan kalau .env sempat ter-commit, jalankan git rm --cached .env lalu rotate semua key-nya. Prefix VITE_ dan NEXT_PUBLIC_ artinya nilainya ditempel ke bundle browser, jadi secret disimpan tanpa prefix dan dipakai di server. Cek variabel wajib saat server mulai dengan filter dan !process.env[name], lalu cetak dengan JSON.stringify.

Yang akan kamu pelajari