Environment Variables & Secrets — DevOps

Config Berbeda per Environment, Tapi Kode Sama Aplikasi perlu tahu: URL database production, API key Stripe, secret JWT. Nilai-nilai ini berbeda per…

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 .env berisi JWT_SECRET ikut ter-commit. Kamu langsung menjalankan git rm --cached .env, commit, dan push. Apakah orang yang sempat menyalin JWT_SECRET itu 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

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

Gak di git, gak di image, gak di log

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"}

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

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.

Yang akan kamu pelajari