Secret adalah nilai yang memberi akses: password database, JWT_SECRET, API key. Di server, mengurus secret mirip mengurus kunci cadangan rumah. Kamu gak menempelkannya di pintu depan (git), gak mencetaknya di brosur (bundle frontend atau image Docker), kamu menyimpannya di kotak terkunci (file 600), dan kalau kunci itu sempat hilang, kamu ganti gemboknya (rotasi), bukan cuma mencari kuncinya. Cara mendeteksi kebocoran dan menanganinya sebagai insiden ada di pelajaran Environment Variables & Secrets, track Security. Di sini sisi operasinya, di web-1.
🧪 Tebakan cepat: repo catatan-api sempat publik sehari, dan
.envberisiJWT_SECRETikut ter-commit. Kamu langsung menjalankangit rm --cached .env, commit, dan push. Apakah orang yang sempat menyalinJWT_SECRETitu masih bisa membuat token admin palsu? Tebak dulu, jawabannya ada di bagian "Rotasi: ganti gemboknya".
Config, secret, dan public
Semua nilai yang beda antar environment dibaca dari environment variable, tapi penanganannya beda:
| Variabel | Jenis | Kenapa |
|---|---|---|
PORT, NODE_ENV, LOG_LEVEL |
config | beda per server, tapi gak rahasia |
DATABASE_URL, JWT_SECRET |
secret | memberi akses; DATABASE_URL berisi password |
VITE_API_URL (frontend catatan-web) |
public | awalan VITE_ mengirim nilainya ke browser |
- Awalan
VITE_,NEXT_PUBLIC_, danPUBLIC_artinya nilainya ditulis ke bundle JavaScript yang diunduh browser, jadi siapa pun bisa membacanya. Variabel berawalan publik selalu public, apa pun namanya, dan gak boleh berisi secret. - Sisanya, nama yang mengandung
SECRET,PASSWORD,TOKEN,KEY, atauCREDENTIALadalah secret. Yang lain config.
Di server: satu file 600, dibaca systemd
Semua secret catatan-api ada di satu file, 600 milik root (pelajaran File System & Permissions), dan dibaca systemd lewat EnvironmentFile= (pelajaran Systemd & Service Management):
deploy@web-1:~$ ls -l /etc/catatan-api.env
-rw------- 1 root root 156 Oct 8 17:20 /etc/catatan-api.env
deploy@web-1:~$ sudo cat /etc/catatan-api.env
DATABASE_URL=postgres://catatan:83hQLRdx8M1klbAQvNcZPwBn@localhost:5432/catatan
JWT_SECRET=0b08ce8c4914826707aa575513330eaf0e3783035d7eab9656197a23af0f6382
- Ubah isinya dengan
sudoedit /etc/catatan-api.env, lalusudo systemctl restart catatan-api. systemd membaca file ini sekali, saat menyalakan service. Proses yang sedang jalan gak tahu file-nya berubah. - Nilai biasa boleh ditulis langsung di unit file (
Environment=NODE_ENV=production), tapi jangan secret: unit file biasanya644, dansystemctl show catatan-api -p Environmentboleh dijalankan user mana pun.
Gak di git, gak di image, gak di log
- Git:
.envmasuk.gitignore. Yang di-commit adalah.env.example: nama variabelnya saja, tanpa nilai rahasia, supaya teman setim tahu apa yang harus diisi. Catatan:.gitignoregak berlaku untuk file yang sudah di-track. Keluarkan dulu dengangit rm --cached .env. - Image Docker (dibahas mulai pelajaran Apa itu Docker): jangan
ENV JWT_SECRET=...atauCOPY .envdi Dockerfile. Isinya tersimpan di layer image selamanya, dan ikut ke mana pun image itu dikirim. Berikan secret saat container dijalankan, misalnyadocker run --env-file /etc/catatan-api.env. - Log: jangan
console.log(process.env)atau mencetak config lengkap saat start. Log dibaca lebih banyak orang daripada file600.
Secret di CI/CD
Pipeline yang men-deploy catatan-api juga butuh secret, misalnya SSH key untuk masuk ke web-1. Di GitHub Actions, secret disimpan terenkripsi di pengaturan repo dan dibaca dengan ${{ secrets.DEPLOY_SSH_KEY }}. Untuk cloud provider seperti AWS, lebih baik lagi OIDC:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: ap-southeast-1
Dengan OIDC, GitHub membuktikan identitas workflow-nya ke AWS, lalu AWS memberi credential sementara yang cuma berlaku selama run itu. Gak ada AWS_ACCESS_KEY_ID permanen yang disimpan, jadi gak ada yang bisa bocor. GitHub Actions dibahas lengkap di pelajaran-pelajaran CI/CD nanti.
Rotasi: ganti gemboknya
git rm --cached .env cuma mengubah commit berikutnya. Commit lama di GitHub tetap menyimpan JWT_SECRET, dan yang lebih penting, server masih memakainya. Jadi ya, token palsu tetap diterima. Itu jawaban tebakannya. Yang menghentikannya cuma rotasi: ganti secret-nya, supaya nilai yang bocor gak berguna lagi.
deploy@web-1:~$ openssl rand -hex 32
4681783844dbb75403ab029609e5318d0bbcd6ed92fd6aa73605922c6414b322
deploy@web-1:~$ sudoedit /etc/catatan-api.env
deploy@web-1:~$ sudo systemctl restart catatan-api
Lalu dari luar, dengan token palsu dan token lama milik Budi:
$ curl -s -o /dev/null -w "%{http_code}\n" -X DELETE https://catatan.example.com/catatan/1 -H "Authorization: Bearer $TOKEN_PALSU"
401
$ curl -s https://catatan.example.com/catatan -H "Authorization: Bearer $TOKEN_BUDI"
{"error":"Token tidak valid"}
openssl rand -hex 32membuat 32 byte acak, ditulis sebagai 64 karakter hex.- Langkahnya selalu tiga: secret baru, tulis ke file, restart. Tanpa restart, proses lama masih memakai secret lama.
- Efek sampingnya: semua token lama ditolak, termasuk milik Budi, jadi semua user harus login lagi. Itu harga yang wajar untuk secret yang bocor.
- Rotasi juga dilakukan saat ada anggota tim yang keluar, dan secara terjadwal (misalnya tiap 90 hari). Untuk password database, ubah password-nya di PostgreSQL dan di file, lalu langsung restart, supaya jeda gagal login-nya cuma beberapa detik.
- Setelah rotasi, baru bersihkan riwayat git kalau perlu (misalnya dengan
git filter-repo). Urutannya selalu rotasi dulu.
Di latihan: mengelompokkan env var
classifyEnvVar(name, value) mengembalikan { name, category, shouldGitignore }. Cek dengan urutan ini: nama diawali PUBLIC_, NEXT_PUBLIC_, atau VITE_ (startsWith) jadi "public". Kalau bukan, nama yang mengandung SECRET, PASSWORD, TOKEN, KEY, atau CREDENTIAL (includes) jadi "secret". Selain itu "config". shouldGitignore bernilai true cuma untuk "secret".
⚠️ Jebakan yang sering ketemu
- Mengira menghapus dari git sudah cukup. Secret yang pernah ter-push harus dianggap bocor. Rotasi dulu, bersihkan riwayat belakangan.
- Mengganti file tanpa restart. Proses yang jalan masih memakai nilai lama sampai di-restart.
- Secret di
Environment=unit file. Siapa pun di server bisa membacanya lewatsystemctl show. PakaiEnvironmentFile=dengan file600. - Secret di variabel berawalan publik.
VITE_STRIPE_SECRET_KEYtetap dikirim ke browser. Secret cuma boleh ada di backend. - Nilai cadangan di kode.
process.env.JWT_SECRET || "rahasia"membuat app tetap jalan dengan secret yang tertulis di git. Lebih baik gagal saat start kalau secret-nya kosong.
TL;DR: Pisahkan config (gak rahasia), secret (memberi akses: nama berisi SECRET, PASSWORD, TOKEN, KEY, CREDENTIAL), dan public (berawalan VITE_, NEXT_PUBLIC_, PUBLIC_, pasti sampai ke browser). Di web-1, semua secret ada di /etc/catatan-api.env (600, root) yang dibaca systemd lewat EnvironmentFile=, bukan di Environment=, bukan di git (.env di .gitignore, .env.example yang di-commit), bukan di image Docker, bukan di log. Di CI pakai secrets repo, dan OIDC untuk cloud supaya gak ada credential permanen. Secret yang bocor dihentikan dengan rotasi: openssl rand -hex 32, sudoedit file-nya, systemctl restart, lalu semua user login lagi.