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:

go
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:

go
port := os.Getenv("APP_PORT")
token := os.Getenv("APP_API_TOKEN") // "YOUR_API_TOKEN" milik lingkungan, bukan kode

Nilai disuntikkan saat menjalankan program, sehingga binary yang sama dapat berjalan di tiga lingkungan dengan tiga set secret berbeda.

bash
APP_PORT=3000 APP_API_TOKEN=YOUR_API_TOKEN_1234567890abcdef go run config-demo.go

File .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:

bash
# .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_TOKEN

Developer 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:

go
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:

text
== 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:

  1. Baris 9-15 (type Config): struct mengumpulkan empat konfigurasi dalam satu tipe, sehingga seluruh kebutuhan aplikasi terlihat dari satu tempat.
  2. Baris 17-23 (envOr): helper membaca environment variable dengan strings.TrimSpace; nilai kosong dianggap tidak diset dan default diberlakukan.
  3. Baris 27-31: dua nilai opsional (APP_PORT, APP_LOG_LEVEL) mendapat default aman — aplikasi tetap jalan dengan nilai yang wajar.
  4. Baris 33-43: dua nilai wajib divalidasi fail-fast — APP_DATABASE_URL harus terisi dan APP_API_TOKEN minimal 20 karakter; kegagalan mengembalikan error yang menyebut nama variabelnya.
  5. Baris 46-52 (redact): menyamarkan nilai sensitif menjadi awalan 8 karakter plus *** sebelum mencetak — perhatikan output di atas tidak pernah menampilkan secret utuh.
  6. 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 #

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 berkas
go
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))
}