Progress belajar
Modul 58 dari 73
0% 0/73 modul selesai
Setelah selesai, tandai modul ini agar progres kursus tetap rapi.
Progress disimpan lokal di browser ini.
Manajemen Secrets
yang Aman
Secret yang bocor bisa mengubah bug kecil menjadi insiden keamanan, fraud pembayaran, dan audit finding.
Kenapa Secrets Selalu Jadi Risiko Produksi
Credential sensitif harus diperlakukan seperti akses langsung ke sistem.
Di React, secret yang masuk bundle frontend langsung dianggap publik. Di Laravel, secret biasanya hidup di .env. Di Go, prinsipnya sama, hanya loader config-nya sering kita tulis sendiri.
Secret bukan sekadar string konfigurasi. DB_URL bisa memberi akses ke database pelanggan, JWT_SECRET bisa memalsukan sesi login, PAYMENT_KEY bisa memverifikasi atau memanipulasi payment flow, dan AWS credentials bisa membuka akses ke banyak resource cloud. Karena itu, secret harus keluar dari kode, keluar dari git, dibatasi per environment, dan diaudit aksesnya.
Satu kali hardcode secret = audit finding permanen di git history.
Dalam proyek online shop skincare, secrets menyentuh bagian paling sensitif: data customer, token login, webhook pembayaran, object storage untuk gambar produk, dan database order. Modul ini fokus ke pola aman yang bisa kamu pakai dari local development sampai production AWS.
- React memakai env saat build, Laravel membaca
.envlewat framework. - Banyak developer terbiasa ada layer framework yang menyembunyikan detail config.
- Go biasanya membaca env sendiri lewat
os.LookupEnvatau library kecil. - Keuntungannya, aturan fail-fast dan fallback production bisa dibuat sangat jelas.
Apa Itu Secret di Backend
Secret adalah nilai yang jika bocor memberi akses, kemampuan impersonasi, atau kontrol sistem.
Tidak semua config adalah secret, tetapi semua secret adalah config yang harus dijaga.
Nilai sensitif yang memberi akses atau otorisasi ke sistem lain, misalnya password database, signing key JWT, server key payment gateway, API key email provider, private key, atau access key cloud.
DB_URL
Berisi host, username, password, database name, dan opsi TLS untuk PostgreSQL.
JWT_SECRET
Dipakai menandatangani token. Jika bocor, attacker bisa membuat token yang terlihat valid.
PAYMENT_KEY
Dipakai verifikasi signature webhook dan komunikasi dengan payment gateway.
AWS credentials
Untuk production, gunakan IAM role. Access key statis hanya untuk skenario terbatas dan tidak boleh masuk git.
Config non-secret contohnya HTTP_ADDR, APP_ENV, LOG_LEVEL, atau PUBLIC_CDN_BASE_URL. Nilai seperti ini tetap perlu benar, tetapi kebocorannya tidak langsung memberi akses ke database atau gateway pembayaran.
Laravel memberi helper env() dan config cache. Go tidak punya global helper bawaan untuk aplikasi, jadi kita buat satu fungsi Load yang membaca env, memvalidasi nilai wajib, lalu mengembalikan struct config.
Environment Variables di Proses Go
Env var hidup di proses, bukan di bundle, dan Go membacanya secara eksplisit.
Environment variable adalah pasangan key-value yang diwariskan OS atau orchestrator ke proses saat ia dijalankan. Inilah saluran utama config dan secret masuk ke backend Go.
Saat container atau shell menjalankan binary Go, sistem operasi menyalin sekumpulan env var ke proses tersebut. Di dalam Go, kamu membacanya lewat package os: os.Getenv mengembalikan nilai (atau string kosong jika tidak ada), os.LookupEnv mengembalikan (value, ok bool) sehingga kamu bisa membedakan “tidak diset” dari “diset tapi kosong”, dan os.Environ mengembalikan seluruh daftar KEY=value. Untuk secret wajib, pola ok bool penting agar aplikasi bisa fail-fast saat startup, bukan menjalankan query database dengan password kosong.
Di Node, process.env.DB_URL bernilai undefined saat absen. Di Laravel, env('DB_URL', 'default') punya fallback bawaan. Di Go, os.Getenv hanya memberi string kosong tanpa membedakan absen, jadi untuk secret wajib pakai value, ok := os.LookupEnv("DB_URL") lalu periksa ok agar gagal cepat dan jelas.
process.env.Xdi Nodeundefinedsaat absen, mudah lolos diam-diam.env('X')Laravel punya default dan cache, detail loading tersembunyi framework.
os.Getenv("X")selalustring,""saat absen, tanpa penanda khusus.os.LookupEnv("X")memberiok boolsehingga fail-fast secret wajib jadi eksplisit.
Di React/Vite/Next, env berawalan VITE_ atau NEXT_PUBLIC_ di-inline ke bundle saat build sehingga menjadi PUBLIK di browser. Env backend Go tak pernah ter-bundle, ia hidup di proses saat runtime. Jangan pernah menaruh PAYMENT_KEY atau JWT_SECRET di env frontend, nilai itu akan terbaca siapa pun yang membuka devtools.
Ada tiga lapis tempat nilai bisa berasal, dan penting membedakannya: env var proses (diwariskan langsung dari shell, Docker, atau IAM), file .env (dibaca library lalu dimuat menjadi env var, hanya untuk dev), dan secret store (Secrets Manager atau SSM, diambil via API di production). Aplikasi yang benar membaca dari env var proses sebagai sumber tunggal, dan dua lapis lain hanya mengisi env var itu sebelum aplikasi berjalan.
flowchart LR ENV["Env var proses"] -->|os.LookupEnv| APP["Go API"] DOTENV["File .env (dev saja)"] -.->|godotenv.Load mengisi env| ENV STORE["Secret store (SM / SSM)"] -.->|GetSecretValue (production)| APP
Gambar 1. Tiga sumber nilai. Aplikasi tetap membaca env var proses, file .env dan secret store hanya mengisi nilainya sebelum app jalan.
Local Development dengan .env dan godotenv
`.env` boleh untuk lokal, tetapi tidak boleh masuk repository.
Untuk development lokal, .env praktis. Untuk keamanan, repository hanya menyimpan .env.example tanpa nilai asli.
Pola yang aman adalah: .env dipakai di mesin developer dan CI secret store, .env.example berisi nama variabel dan placeholder, .gitignore menolak file env asli. Di Go, aplikasi tidak harus membaca file .env secara langsung. Untuk kenyamanan dev, kita pakai github.com/joho/godotenv (v1.5.1) yang memuat isi .env menjadi env var proses, lalu aplikasi tetap membaca environment variable standar lewat os.
.gitignore.env .env.* !.env.example
.env.exampleAPP_ENV=local HTTP_ADDR=:8080 DB_URL=postgres://postgres:postgres@localhost:5432/skincare_dev?sslmode=disable JWT_SECRET=replace-with-local-dev-secret PAYMENT_KEY=replace-with-local-payment-key AWS_REGION=ap-southeast-1
Kalau .env pernah masuk git, menghapus file pada commit berikutnya belum cukup. Secret harus di-rotate karena nilainya sudah ada di history.
Cara memuat .env di dev adalah memanggil godotenv.Load() di awal main, sebelum config.Load, dan hanya saat environment lokal. Perilaku default godotenv.Load() tidak menimpa env var yang sudah diset OS, jadi di production (yang env-nya datang dari IAM atau orchestrator) panggilan ini aman dilewati, dan kalaupun terpanggil ia tidak akan menabrak nilai asli. Pola guard APP_ENV membuatnya tegas: muat .env hanya saat APP_ENV masih kosong atau bernilai local.
cmd/api/main.go (potongan dev loader)import ( "os" "github.com/joho/godotenv" ) // loadDotenvForDev memuat .env hanya saat development lokal. // Di production env sudah diset orchestrator/IAM, jadi dilewati. func loadDotenvForDev() { if env := os.Getenv("APP_ENV"); env != "" && env != "local" { return } // Load tidak menimpa env yang sudah ada; absennya .env bukan error fatal di dev. _ = godotenv.Load() }
Di Node dan Laravel, file .env sering dimuat otomatis oleh framework. Di Go tidak ada keajaiban itu: kamu memanggil godotenv.Load() sendiri, eksplisit, di main. Karena eksplisit, gampang membatasinya hanya untuk dev dan tidak pernah ikut ke production.
- .env.example placeholder saja, boleh commit
- .gitignore wajib menolak .env asli
- cmd/
- api/
- main.go panggil config.Load saat startup
- internal/
- config/
- config.go loader env dan Secrets Manager
- go.mod
Production dengan AWS Secrets Manager
Production butuh secret store, bukan file yang disalin manual ke server.
AWS Secrets Manager menjadi tempat terpusat untuk menyimpan, mengambil, me-rotate, dan mengaudit secret production.
AWS Secrets Manager mengambil nilai terenkripsi melalui API GetSecretValue, yang mengembalikan salah satu dari SecretString (teks, biasanya JSON) atau SecretBinary (data biner). Kita menyimpan secret aplikasi sebagai JSON multi-key dalam satu SecretString agar satu kali panggil API cukup membawa semua nilai (DB_URL, JWT_SECRET, PAYMENT_KEY) dan loader tinggal json.Unmarshal. Dokumentasi resminya juga menyebut CloudTrail membuat log saat action ini dipanggil, dan permission minimum untuk membaca secret adalah secretsmanager:GetSecretValue, plus kms:Decrypt bila memakai customer-managed KMS key. Lihat GetSecretValue dan best practices Secrets Manager.
Untuk Roadmap 7, cukup pahami pola konsumsinya. Detail provision AWS, IAM role ECS, VPC endpoint, dan deployment akan diperdalam di Roadmap 8.
aws/secrets/skincare-api-prod.json{ "DB_URL": "postgres://app_user:strong-password@rds.example.ap-southeast-1.rds.amazonaws.com:5432/skincare?sslmode=require", "JWT_SECRET": "long-random-signing-secret", "PAYMENT_KEY": "payment-gateway-server-key" }
Untuk aplikasi kecil sampai menengah, satu secret JSON per service dan environment membuat loading config lebih sederhana. Untuk organisasi besar, pecah secret berdasarkan ownership dan izin akses.
Secrets Manager bukan satu-satunya pilihan. AWS Systems Manager (SSM) Parameter Store dengan tipe SecureString juga menyimpan nilai terenkripsi KMS dan dibaca via GetParameter. Untuk config sederhana yang jarang berubah, Parameter Store lebih murah. Secrets Manager unggul saat kamu butuh rotation bawaan, integrasi rotation database, dan ukuran secret lebih besar.
- Murah, cocok untuk config non-rahasia atau secret sederhana yang jarang berputar.
- Tidak punya rotation bawaan, kamu kelola sendiri. Enkripsi via KMS, dibaca
GetParameter.
- Berbayar per secret, tetapi menyediakan managed rotation dan integrasi RDS.
- Cocok untuk credential database, payment key, dan secret yang wajib di-rotate berkala.
flowchart TD
DEV["Local developer"] -->|env dari .env lokal| APP["Go API"]
ECS["ECS task role"] -->|IAM role temporary credentials| SM["AWS Secrets Manager"]
SM -->|GetSecretValue AWSCURRENT| APP
APP -->|DB_URL| RDS[("PostgreSQL RDS")]
APP -->|PAYMENT_KEY| PAY["Payment Gateway"]
CT["AWS CloudTrail"] -. audit access .-> SMGambar 2. Local cukup env, production mengambil secret dari Secrets Manager dengan IAM role dan diaudit CloudTrail.
Pattern LoadConfig yang Aman
Satu pintu config membuat startup fail-fast dan mencegah secret tersebar di kode.
Handler, service, dan repository tidak boleh membaca env sendiri-sendiri. Baca sekali di startup, validasi, lalu inject config yang sudah bersih.
Pattern di bawah membaca env untuk local development. Jika AWS_SECRETS_MANAGER_SECRET_ID diset, loader mengambil secret JSON dari AWS Secrets Manager. Ini cocok untuk container production, karena task cukup punya IAM role dengan izin membaca secret tertentu.
flowchart TD
START["config.Load(ctx)"] --> Q{"AWS_SECRETS_MANAGER_SECRET_ID diset?"}
Q -->|ya| SM["GetSecretValue + json.Unmarshal"]
Q -->|tidak| ENV["Baca env (dev, opsi godotenv)"]
SM --> V["validate()"]
ENV --> V
V -->|ada yang kosong| FAIL["Error fail-fast, os.Exit(1)"]
V -->|lengkap| OK["Config bersih siap di-inject"]Gambar 3. Percabangan Load(). Production lewat Secrets Manager, dev lewat env, keduanya bermuara ke validate() yang fail-fast.
internal/config/config.gopackage config import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "github.com/aws/aws-sdk-go-v2/aws" awscfg "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" ) type Config struct { AppEnv string HTTPAddr string AWSRegion string DBURL string JWTSecret string PaymentKey string SecretsManagerID string } type secretPayload struct { DBURL string `json:"DB_URL"` JWTSecret string `json:"JWT_SECRET"` PaymentKey string `json:"PAYMENT_KEY"` } func Load(ctx context.Context) (Config, error) { cfg := Config{ AppEnv: getenvDefault("APP_ENV", "local"), HTTPAddr: getenvDefault("HTTP_ADDR", ":8080"), AWSRegion: getenvDefault("AWS_REGION", "ap-southeast-1"), } if secretID, ok := lookupNonEmpty("AWS_SECRETS_MANAGER_SECRET_ID"); ok { cfg.SecretsManagerID = secretID payload, err := loadSecretPayload(ctx, cfg.AWSRegion, secretID) if err != nil { return Config{}, err } cfg.DBURL = strings.TrimSpace(payload.DBURL) cfg.JWTSecret = strings.TrimSpace(payload.JWTSecret) cfg.PaymentKey = strings.TrimSpace(payload.PaymentKey) } else { var err error if cfg.DBURL, err = requiredEnv("DB_URL"); err != nil { return Config{}, err } if cfg.JWTSecret, err = requiredEnv("JWT_SECRET"); err != nil { return Config{}, err } if cfg.PaymentKey, err = requiredEnv("PAYMENT_KEY"); err != nil { return Config{}, err } } if err := cfg.validate(); err != nil { return Config{}, err } return cfg, nil } func loadSecretPayload(ctx context.Context, region, secretID string) (secretPayload, error) { awsConfig, err := awscfg.LoadDefaultConfig(ctx, awscfg.WithRegion(region)) if err != nil { return secretPayload{}, fmt.Errorf("load aws config: %w", err) } client := secretsmanager.NewFromConfig(awsConfig) out, err := client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ SecretId: aws.String(secretID), }) if err != nil { return secretPayload{}, fmt.Errorf("get secret value: %w", err) } if out.SecretString == nil { return secretPayload{}, errors.New("secret value is not a JSON string") } var payload secretPayload if err := json.Unmarshal([]byte(*out.SecretString), &payload); err != nil { return secretPayload{}, fmt.Errorf("decode secret JSON: %w", err) } return payload, nil } func (c Config) validate() error { missing := make([]string, 0, 3) if strings.TrimSpace(c.DBURL) == "" { missing = append(missing, "DB_URL") } if strings.TrimSpace(c.JWTSecret) == "" { missing = append(missing, "JWT_SECRET") } if strings.TrimSpace(c.PaymentKey) == "" { missing = append(missing, "PAYMENT_KEY") } if len(missing) > 0 { return fmt.Errorf("missing required secrets: %s", strings.Join(missing, ", ")) } return nil } func requiredEnv(key string) (string, error) { value, ok := lookupNonEmpty(key) if !ok { return "", fmt.Errorf("%s is required", key) } return value, nil } func lookupNonEmpty(key string) (string, bool) { value, ok := os.LookupEnv(key) if !ok { return "", false } value = strings.TrimSpace(value) return value, value != "" } func getenvDefault(key, fallback string) string { value, ok := lookupNonEmpty(key) if !ok { return fallback } return value }
Karena Load dipanggil sekali saat startup, tidak ada caching di sini, dan memang tidak perlu. Caching client-side (mis. aws-secretsmanager-caching-go) baru relevan jika kamu membaca secret berulang kali selama runtime, bukan untuk pola baca-sekali seperti ini.
cmd/api/main.gopackage main import ( "context" "log/slog" "net/http" "os" "time" "github.com/kamu/skincare-backend/internal/config" ) func main() { loadDotenvForDev() // ctx ini khusus bootstrap: timeout untuk LoadDefaultConfig + GetSecretValue. // Tidak dipakai ListenAndServe, jadi cancel() di akhir bootstrap aman. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cfg, err := config.Load(ctx) if err != nil { slog.Error("load config", "error", err) os.Exit(1) } logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger.Info("starting api", "env", cfg.AppEnv, "addr", cfg.HTTPAddr) mux := http.NewServeMux() mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) if err := http.ListenAndServe(cfg.HTTPAddr, mux); err != nil { logger.Error("server stopped", "error", err) os.Exit(1) } }
slog.Info("config", "cfg", cfg) terlihat praktis, tetapi bisa mencetak DB_URL, JWT_SECRET, atau PAYMENT_KEY. Log hanya metadata aman seperti env dan addr.
Laravel punya helper global config('database.url') dan config:cache agar nilai stabil di runtime. Go sengaja tidak menyediakan helper config global: kamu membaca sekali di Load(), lalu meneruskan struct Config immutable ke constructor service dan repository. Eksplisit ini bukan kekurangan, ia membuat dependensi config terlihat di signature, mudah diuji, dan mustahil dibaca diam-diam dari tempat tak terduga.
Rotation Tanpa Downtime
Secret harus bisa diganti tanpa mematikan bisnis.
Rotation adalah proses mengganti credential di secret store dan target service, lalu membuat aplikasi memakai versi baru.
AWS mendefinisikan rotation sebagai pembaruan credential pada secret dan database atau service terkait. Secrets Manager mendukung managed rotation untuk secret tertentu dan rotation dengan Lambda untuk skenario lain. Untuk database production, strategi yang aman biasanya menjaga secret lama tetap valid sebentar saat secret baru mulai dipakai.
Untuk PostgreSQL, buat user atau password baru yang bisa mengakses database dengan permission sama atau lebih kecil.
Update secret di Secrets Manager sehingga AWSCURRENT mengarah ke nilai baru, sementara nilai lama masih bisa dipakai selama masa transisi.
Deploy container baru yang memanggil Load saat startup, mengambil secret baru, lalu menerima traffic setelah health check lulus.
Biarkan instance lama keluar dari load balancer, tutup pool lama, lalu cabut credential lama setelah tidak ada traffic.
Secrets Manager menandai versi secret dengan staging label. Versi yang sedang dibuat berlabel AWSPENDING, versi aktif yang dibaca aplikasi berlabel AWSCURRENT, dan versi lama yang baru saja digeser berlabel AWSPREVIOUS. Fase AWSPREVIOUS inilah yang membuat rotation aman: credential lama masih valid sebentar saat aplikasi berpindah ke yang baru, sehingga tidak ada koneksi yang putus mendadak.
stateDiagram-v2 [*] --> AWSPENDING: buat credential baru AWSPENDING --> AWSCURRENT: promosikan jadi versi aktif AWSCURRENT --> AWSPREVIOUS: rotation berikutnya menggeser AWSPREVIOUS --> Deprecated: credential lama dicabut Deprecated --> [*]
Gambar 4. Lifecycle satu versi secret lewat staging label, fase AWSPREVIOUS menjaga transisi tanpa downtime.
sequenceDiagram participant Ops as Operator participant SM as Secrets Manager participant AppOld as API lama participant AppNew as API baru participant DB as PostgreSQL Ops->>DB: buat credential baru Ops->>SM: update secret AWSCURRENT AppOld->>DB: tetap pakai credential lama sementara Ops->>AppNew: rolling deploy AppNew->>SM: GetSecretValue AppNew->>DB: connect dengan credential baru Ops->>AppOld: drain dan stop instance lama Ops->>DB: revoke credential lama
Gambar 5. Rotation tanpa downtime menghindari pemutusan koneksi mendadak dengan fase transisi.
Ini berlabuh ke pgxpool dari Roadmap 3. Pool yang sudah terbuka memegang koneksi yang dibuat dengan credential lama, dan koneksi itu tidak otomatis berganti saat secret berubah di Secrets Manager. Karena Load hanya jalan saat startup, cara paling sederhana dan paling tahan banting adalah rolling deploy: instance baru memanggil Load, membuat pgxpool dengan credential baru, lalu instance lama drain dan tutup pool-nya. Hot reload pool saat runtime bisa dibuat, tetapi kompleksitasnya jauh lebih tinggi dan jarang sepadan.
Jika secret database berubah, pool yang sudah terbuka tidak otomatis berubah. Cara paling sederhana adalah rolling deploy. Hot reload config bisa dibuat, tetapi kompleksitasnya lebih tinggi.
Least Privilege untuk Secret
Setiap service hanya boleh membaca secret yang benar-benar dibutuhkan.
Least privilege berarti izin cukup untuk bekerja, bukan izin sebesar mungkin agar mudah sementara.
Prinsip memberi permission minimum yang diperlukan untuk menjalankan tugas tertentu, pada resource tertentu, dengan kondisi yang relevan.
Untuk skincare API, service API perlu membaca prod/skincare/api. Worker invoice mungkin perlu secret email provider, tetapi tidak perlu payment key. Admin backoffice mungkin butuh konfigurasi berbeda. Jangan pakai satu IAM role superuser untuk semua container.
aws/iam/skincare-api-secrets-policy.json{ "Version": "2012-10-17", "Statement": [ { "Sid": "ReadOnlySkincareAPISecret", "Effect": "Allow", "Action": ["secretsmanager:GetSecretValue"], "Resource": "arn:aws:secretsmanager:ap-southeast-1:123456789012:secret:prod/skincare/api-*" }, { "Sid": "DecryptOnlyViaSecretsManager", "Effect": "Allow", "Action": ["kms:Decrypt"], "Resource": "arn:aws:kms:ap-southeast-1:123456789012:key/11111111-2222-3333-4444-555555555555", "Condition": { "StringEquals": { "kms:ViaService": "secretsmanager.ap-southeast-1.amazonaws.com" } } } ] }
kms:Decrypt hanya dibutuhkan jika secret memakai customer-managed KMS key. Jika memakai AWS managed key aws/secretsmanager, izin KMS eksplisit biasanya tidak perlu diberikan ke role aplikasi.
API service
Baca DB_URL, JWT_SECRET, dan PAYMENT_KEY untuk melayani request dan webhook.
Worker email
Baca secret SMTP atau SES, tetapi tidak perlu membaca payment gateway key.
Migration job
Baca DB_URL migration, tetapi tidak perlu JWT signing secret.
Audit Akses Secret
Kamu perlu tahu siapa membaca secret, kapan, dan dari role mana.
Audit bukan hanya mencari pelaku setelah insiden. Audit membantu menemukan akses yang terlalu luas sebelum menjadi insiden.
AWS Secrets Manager terintegrasi dengan CloudTrail untuk audit dan monitoring. Untuk catatan berkelanjutan, AWS merekomendasikan membuat trail yang mengirim log ke S3, karena event history di console bukan catatan permanen. Saat GetSecretValue dipanggil, CloudTrail mencatat event API, principal, region, waktu, dan metadata request. Jangan menaruh nilai rahasia di parameter request atau log aplikasi.
flowchart LR
Role["ECS task role"] -->|GetSecretValue| SM["Secrets Manager"]
SM -->|management event| CT["CloudTrail"]
CT --> S3[("S3 audit bucket")]
S3 --> SIEM["Security monitoring"]
SIEM --> Alert["Alert akses anomali"]Gambar 6. Akses secret harus meninggalkan jejak audit yang bisa ditinjau tim security dan platform.
Kirim CloudTrail ke S3 bucket audit, bukan hanya mengandalkan event history sementara di console.
Monitor GetSecretValue, PutSecretValue, UpdateSecretVersionStage, dan perubahan policy secret.
Beri alarm jika role yang tidak biasa membaca secret production atau terjadi lonjakan request secret.
CloudTrail membantu audit API call, tetapi aplikasi tetap harus memastikan secret value tidak pernah masuk log aplikasi, error response, panic dump, atau tracing attribute.
Hands-on: Checklist Secrets untuk Skincare API
Latihan ringan untuk merapikan config sebelum masuk Roadmap 8.
Target hands-on ini adalah membuat aplikasi gagal start jika secret belum lengkap, dan aman saat dijalankan di local maupun production.
.env.exampleIsi nama variabel yang dibutuhkan tanpa nilai production, lalu pastikan .env asli masuk .gitignore.
Tambah go get github.com/joho/godotenv, panggil loadDotenvForDev() di awal main dengan guard APP_ENV.
internal/configHandler dan service menerima config dari constructor, bukan memanggil os.Getenv sendiri.
Jika DB_URL, JWT_SECRET, atau PAYMENT_KEY kosong, aplikasi keluar dengan error jelas sebelum menerima traffic.
Set AWS_SECRETS_MANAGER_SECRET_ID di environment staging, lalu pastikan aplikasi mengambil nilai dari Secrets Manager.
Pastikan tidak ada secret value tercetak di log startup, log error, maupun panic handler.
Terminalgrep -R "JWT_SECRET\|PAYMENT_KEY\|postgres://" . \ --exclude-dir=.git \ --exclude=.env.example
TerminalAPP_ENV=local \ HTTP_ADDR=:8080 \ DB_URL='postgres://postgres:postgres@localhost:5432/skincare_dev?sslmode=disable' \ JWT_SECRET='local-only-secret' \ PAYMENT_KEY='local-payment-key' \ go run ./cmd/api
Setelah hands-on, source code boleh dibaca publik tanpa membuka akses ke database, token signing, payment gateway, atau AWS account.
Jebakan Umum Developer JS/PHP
Kebiasaan kecil dari stack lama bisa menjadi celah besar di backend Go.
Masalah secret biasanya bukan karena algoritma sulit, tetapi karena kebiasaan convenience yang terbawa ke production.
Memakai os.Getenv langsung di banyak tempat
Sulit diuji, sulit diaudit, dan rawan fallback diam-diam. Gunakan satu Load lalu inject config.
Menganggap env frontend sebagai secret
Env yang masuk bundle React bukan secret. Jangan pernah menaruh payment server key di frontend.
Commit .env lalu hanya menghapusnya
Git history tetap menyimpan nilai lama. Solusinya rotate secret dan bersihkan history jika kebijakan tim mengharuskan.
Memakai satu AWS access key untuk semua service
Di production, gunakan IAM role per workload. Hindari long-lived access key dalam container.
Memanggil godotenv.Load di production
godotenv hanya untuk dev. Beri guard APP_ENV agar production tidak pernah membaca file .env di disk.
Melog seluruh struct config
Config struct nyaman untuk debug, tetapi field secret harus dianggap tidak boleh dicetak.
Tidak punya rencana rotation
Secret pasti perlu diganti. Rancang sejak awal agar rotation tidak berarti downtime.
- Laravel biasa menjalankan
config:cacheagar config stabil selama runtime. - Nilai env tidak dibaca sembarangan setelah aplikasi berjalan.
- Go idiomatik membaca config saat startup, lalu menyuntikkannya ke dependency.
- Untuk perubahan secret, gunakan rolling deploy atau mekanisme reload yang eksplisit.
Ringkasan & Poin Penting
Manajemen secrets adalah pagar produksi untuk database, auth, payment, dan AWS resource.
Yang Wajib Menempel
- Secret mencakup
DB_URL,JWT_SECRET,PAYMENT_KEY, private key, API key provider, dan AWS credentials. - Env var hidup di proses dan dibaca eksplisit:
os.LookupEnv(adaok bool) untuk secret wajib, bukanos.Getenvyang menyamarkan absen. Env backend selalu runtime, tak pernah ter-bundle sepertiVITE_/NEXT_PUBLIC_di React. - Secret tidak boleh hardcode di kode dan tidak boleh commit ke git.
.envhanya untuk local (muat viagodotenvdengan guardAPP_ENV),.env.examplehanya placeholder. - Production memakai secret store seperti AWS Secrets Manager (atau SSM Parameter Store untuk config sederhana yang lebih murah), dengan IAM role dan permission minimum.
- Config Go sebaiknya dibaca dari satu fungsi
Load, divalidasi saat startup, lalu di-inject ke dependency. - Rotation tanpa downtime butuh fase transisi: credential baru aktif, aplikasi rolling deploy, koneksi lama drain, credential lama dicabut.
- Least privilege memisahkan akses secret per service. API, worker, migration job, dan admin tidak harus punya secret yang sama.
- Audit akses secret dilakukan lewat CloudTrail, tetapi nilai secret tetap tidak boleh masuk log aplikasi.
Di proyek online shop skincare, pola ini melindungi database customer, token login, webhook pembayaran, dan integrasi AWS. Setelah ini, modul berikutnya di Roadmap 7 menutup sisi production safety lain sebelum kita masuk Roadmap 8, yaitu Docker, CI/CD, dan deployment AWS yang benar-benar menjalankan pola secret ini di environment produksi.
Progress disimpan lokal di browser ini.