Setiap aplikasi membawa sekumpulan nilai yang tidak boleh terbaca oleh publik: password database, API key layanan pihak ketiga, dan signing key. Masalahnya, nilai-nilai tersebut sering berakhir sebagai string constant di dalam source code karena praktis. Padahal kode hidup di version control: secret yang pernah di-commit akan tertinggal di git history selamanya, bahkan setelah barisnya dihapus. Chapter ini membahas cara menyimpan konfigurasi dan secret dengan aman — dibaca dari environment variable, dimuat ke struct dengan default aman, divalidasi sejak startup, dan tidak pernah bocor ke log maupun pesan panic.
Secret di Kode adalah Kebocoran yang Menunggu Waktu #
Pola berikut terlihat praktis, tetapi membawa tiga masalah sekaligus:
const dbPassword = "s3cr3t-prod" // JANGAN seperti ini
db, err := sql.Open("pgx", "postgres://app:"+dbPassword+"@db.internal:5432/toko")Pertama, secret ikut tersalin ke setiap fork, backup, dan clone. Kedua, git history menyimpan setiap versi file — menghapus baris di commit berikutnya tidak menghapus jejaknya. Ketiga, rotasi menjadi sulit karena mengganti secret berarti mengubah kode dan deploy ulang. Prinsip 12-factor menegaskan: konfigurasi yang berbeda antar lingkungan (development, staging, produksi) disimpan di environment, bukan di kode.
Membaca Konfigurasi dari Environment Variable #
Package os menyediakan function os.Getenv untuk membaca environment variable. Pemanggilan mengembalikan string kosong "" bila variabel belum diset, sehingga pemanggil selalu perlu menangani kondisi tersebut:
port := os.Getenv("APP_PORT")
token := os.Getenv("APP_API_TOKEN") // "YOUR_API_TOKEN" milik lingkungan, bukan kodeNilai disuntikkan saat menjalankan program, sehingga binary yang sama dapat berjalan di tiga lingkungan dengan tiga set secret berbeda.
APP_PORT=3000 APP_API_TOKEN=YOUR_API_TOKEN_1234567890abcdef go run config-demo.goFile .env untuk Development, .env.example untuk Repository #
Mengetik export panjang di setiap terminal melelahkan, sehingga tim memakai file .env berisi daftar variabel. Aturannya satu: .env masuk .gitignore dan tidak pernah di-commit; yang dibagikan ke repository justru .env.example, templat tanpa nilai rahasia:
# .env.example — aman di-commit, templat untuk developer baru
APP_PORT=8080
APP_LOG_LEVEL=info
APP_DATABASE_URL=postgres://app:[email protected]:5432/toko
APP_API_TOKEN=YOUR_API_TOKENDeveloper baru menyalin templat menjadi .env dan mengisi nilai asli dari manajer secret tim; nilai contoh seperti YOUR_API_TOKEN sengaja tidak berguna di luar lingkungan tim.
Config Struct dengan Load() dan Validasi Fail-Fast #
Konfigurasi yang tersebar di puluhan pemanggilan os.Getenv sulit diaudit. Praktik yang lebih rapi: satu struct menampung seluruh konfigurasi, satu function Load() membaca environment, menerapkan default aman, lalu memvalidasi nilai wajib sebelum aplikasi melayani request:
package main
import (
"fmt"
"os"
"strings"
)
// Config menyimpan konfigurasi aplikasi yang dibaca dari environment variable.
type Config struct {
Port string
LogLevel string
DatabaseURL string
APIToken string
}
// envOr membaca environment variable; bila kosong, kembalikan nilai default.
func envOr(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
// Load membangun Config dari environment variable dengan default aman
// dan validasi fail-fast untuk nilai wajib.
func Load() (Config, error) {
cfg := Config{
Port: envOr("APP_PORT", "8080"),
LogLevel: envOr("APP_LOG_LEVEL", "info"),
}
url := strings.TrimSpace(os.Getenv("APP_DATABASE_URL"))
if url == "" {
return cfg, fmt.Errorf("variabel APP_DATABASE_URL wajib diisi")
}
cfg.DatabaseURL = url
cfg.APIToken = strings.TrimSpace(os.Getenv("APP_API_TOKEN"))
if len(cfg.APIToken) < 20 {
return cfg, fmt.Errorf("APP_API_TOKEN terlalu pendek: minimal 20 karakter")
}
return cfg, nil
}
// redact menyamarkan nilai sensitif sebelum dicetak ke layar atau log.
func redact(s string) string {
if len(s) <= 8 {
return "***"
}
return s[:8] + "***"
}
func main() {
// Skenario 1: environment kosong → Load gagal cepat saat startup.
fmt.Println("== Skenario 1: environment kosong ==")
_, err := Load()
if err != nil {
fmt.Println("Load gagal:", err)
}
// Skenario 2: environment lengkap → Load berhasil.
// Di produksi, nilai-nilai ini diisi oleh environment, bukan oleh kode.
os.Setenv("APP_DATABASE_URL", "postgres://app:[email protected]:5432/toko")
os.Setenv("APP_API_TOKEN", "YOUR_API_TOKEN_1234567890abcdef")
os.Setenv("APP_PORT", "3000")
fmt.Println()
fmt.Println("== Skenario 2: environment lengkap ==")
cfg, err := Load()
if err != nil {
fmt.Println("Load gagal:", err)
return
}
fmt.Println("Port :", cfg.Port)
fmt.Println("LogLevel :", cfg.LogLevel)
fmt.Println("DatabaseURL :", redact(cfg.DatabaseURL))
fmt.Println("APIToken :", redact(cfg.APIToken))
}Output yang Dihasilkan:
== Skenario 1: environment kosong ==
Load gagal: variabel APP_DATABASE_URL wajib diisi
== Skenario 2: environment lengkap ==
Port : 3000
LogLevel : info
DatabaseURL : postgres***
APIToken : YOUR_API***Mari kita telaah baris demi baris program di atas:
- Baris 9-15 (
type Config): struct mengumpulkan empat konfigurasi dalam satu tipe, sehingga seluruh kebutuhan aplikasi terlihat dari satu tempat. - Baris 17-23 (
envOr): helper membaca environment variable denganstrings.TrimSpace; nilai kosong dianggap tidak diset dan default diberlakukan. - Baris 27-31: dua nilai opsional (
APP_PORT,APP_LOG_LEVEL) mendapat default aman — aplikasi tetap jalan dengan nilai yang wajar. - Baris 33-43: dua nilai wajib divalidasi fail-fast —
APP_DATABASE_URLharus terisi danAPP_API_TOKENminimal 20 karakter; kegagalan mengembalikan error yang menyebut nama variabelnya. - Baris 46-52 (
redact): menyamarkan nilai sensitif menjadi awalan 8 karakter plus***sebelum mencetak — perhatikan output di atas tidak pernah menampilkan secret utuh. - Baris 64-66 (
os.Setenv): program mensimulasikan environment produksi dari dalam kode agar demo deterministik; di aplikasi nyata, baris ini tidak ada dan nilai datang dari lingkungan.
Pola Load() (Config, error) ini juga mengikuti konvensi error terstruktur pada section Error Handling
: pemanggil memutuskan cara bereaksi, misalnya menghentikan startup dengan log.Fatal di main.
Fail-Fast Lebih Baik daripada Gagal Tengah Malam #
Tanpa validasi startup, konfigurasi kosong baru terasa saat request pertama menyentuh database — jam 2 pagi. Dengan validasi di Load(), kesalahan muncul pada detik ke-0 proses berjalan, lengkap dengan nama variabel yang salah. Aturan praktisnya: nilai wajib divalidasi sejak awal, nilai opsional diberi default aman.
Rotasi Secret dan Dampaknya ke Konfigurasi #
Secret yang dipakai selamanya memberi penyerang waktu tak terbatas untuk menebaknya. Rotasi berkala — mengganti API key tiap beberapa bulan, atau segera saat ada indikasi kebocoran — memangkas jendela risiko. Karena secret hidup di environment, rotasi cukup memperbarui nilai di manajer secret lalu memulai ulang proses; kode dan binary tidak berubah. Inilah alasan praktis mengapa nilai rahasia harus keluar dari kode.
Referensi #
- Dokumentasi package os
— function
GetenvdanSetenvuntuk environment variable. - Go Security — praktik keamanan resmi di ekosistem Go.
Konfigurasi kini berada di luar kode, tervalidasi sejak startup, dan tersamarkan di output. Bab terakhir dari lapisan keamanan ini menoleh ke arah yang jarang diaudit: dependency pihak ketiga yang kita unduh — dan alat resmi Go untuk memeriksa celah di dalamnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
config-demo.go
Unduh berkaspackage main
import (
"fmt"
"os"
"strings"
)
// Config menyimpan konfigurasi aplikasi yang dibaca dari environment variable.
type Config struct {
Port string
LogLevel string
DatabaseURL string
APIToken string
}
// envOr membaca environment variable; bila kosong, kembalikan nilai default.
func envOr(key, fallback string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return fallback
}
// Load membangun Config dari environment variable dengan default aman
// dan validasi fail-fast untuk nilai wajib.
func Load() (Config, error) {
cfg := Config{
Port: envOr("APP_PORT", "8080"),
LogLevel: envOr("APP_LOG_LEVEL", "info"),
}
url := strings.TrimSpace(os.Getenv("APP_DATABASE_URL"))
if url == "" {
return cfg, fmt.Errorf("variabel APP_DATABASE_URL wajib diisi")
}
cfg.DatabaseURL = url
cfg.APIToken = strings.TrimSpace(os.Getenv("APP_API_TOKEN"))
if len(cfg.APIToken) < 20 {
return cfg, fmt.Errorf("APP_API_TOKEN terlalu pendek: minimal 20 karakter")
}
return cfg, nil
}
// redact menyamarkan nilai sensitif sebelum dicetak ke layar atau log.
func redact(s string) string {
if len(s) <= 8 {
return "***"
}
return s[:8] + "***"
}
func main() {
// Skenario 1: environment kosong → Load gagal cepat saat startup.
fmt.Println("== Skenario 1: environment kosong ==")
_, err := Load()
if err != nil {
fmt.Println("Load gagal:", err)
}
// Skenario 2: environment lengkap → Load berhasil.
// Di produksi, nilai-nilai ini diisi oleh environment, bukan oleh kode.
os.Setenv("APP_DATABASE_URL", "postgres://app:[email protected]:5432/toko")
os.Setenv("APP_API_TOKEN", "YOUR_API_TOKEN_1234567890abcdef")
os.Setenv("APP_PORT", "3000")
fmt.Println()
fmt.Println("== Skenario 2: environment lengkap ==")
cfg, err := Load()
if err != nil {
fmt.Println("Load gagal:", err)
return
}
fmt.Println("Port :", cfg.Port)
fmt.Println("LogLevel :", cfg.LogLevel)
fmt.Println("DatabaseURL :", redact(cfg.DatabaseURL))
fmt.Println("APIToken :", redact(cfg.APIToken))
}