Web Artisan
Beranda

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.

Roadmap 7 · Production Safety

Manajemen Secrets
yang Aman

Secret yang bocor bisa mengubah bug kecil menjadi insiden keamanan, fraud pembayaran, dan audit finding.

Bahasa: Go 1.26~70 menit baca
01

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.

🧾Catatan audit

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.

JS/PHP: env sering terasa otomatis
  • React memakai env saat build, Laravel membaca .env lewat framework.
  • Banyak developer terbiasa ada layer framework yang menyembunyikan detail config.
Go: config eksplisit
  • Go biasanya membaca env sendiri lewat os.LookupEnv atau library kecil.
  • Keuntungannya, aturan fail-fast dan fallback production bisa dibuat sangat jelas.
02

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.

secret

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.

🌉Jembatan: `.env` Laravel vs env Go

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.

03

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.

🌉Jembatan: `process.env` (Node) & `env()` (Laravel) vs `os.LookupEnv` (Go)

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.

JS/PHP: env terasa magis
  • process.env.X di Node undefined saat absen, mudah lolos diam-diam.
  • env('X') Laravel punya default dan cache, detail loading tersembunyi framework.
Go: env eksplisit lewat `os`
  • os.Getenv("X") selalu string, "" saat absen, tanpa penanda khusus.
  • os.LookupEnv("X") memberi ok bool sehingga fail-fast secret wajib jadi eksplisit.
⚠️Env backend selalu runtime, env React build-time

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.

04

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.example
APP_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
⚠️Jangan commit `.env`

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() }
🌉Jembatan: `dotenv` (Node) & `vlucas/phpdotenv` (Laravel) vs `godotenv` (Go)

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.

File config yang aman untuk repository
  • .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
05

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" }
💡Simpan sebagai satu secret JSON

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.

SSM Parameter Store (SecureString)
  • Murah, cocok untuk config non-rahasia atau secret sederhana yang jarang berputar.
  • Tidak punya rotation bawaan, kamu kelola sendiri. Enkripsi via KMS, dibaca GetParameter.
AWS Secrets Manager
  • 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 .-> SM

Gambar 2. Local cukup env, production mengambil secret dari Secrets Manager dengan IAM role dan diaudit CloudTrail.

06

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.go
package 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 }
🗂️Kapan caching secret baru perlu

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.go
package 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) } }
⚠️Jangan log config mentah

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.

🌉Jembatan: `config()` global Laravel vs config inject Go

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.

07

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.

Buat credential baru

Untuk PostgreSQL, buat user atau password baru yang bisa mengakses database dengan permission sama atau lebih kecil.

Simpan sebagai versi baru

Update secret di Secrets Manager sehingga AWSCURRENT mengarah ke nilai baru, sementara nilai lama masih bisa dipakai selama masa transisi.

Rolling restart aplikasi

Deploy container baru yang memanggil Load saat startup, mengambil secret baru, lalu menerima traffic setelah health check lulus.

Drain koneksi lama

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.

💡Koneksi pool perlu strategi

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.

08

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.

least privilege

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 permission bersyarat

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.

09

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.

Aktifkan trail permanen

Kirim CloudTrail ke S3 bucket audit, bukan hanya mengandalkan event history sementara di console.

Filter event sensitif

Monitor GetSecretValue, PutSecretValue, UpdateSecretVersionStage, dan perubahan policy secret.

Alert akses anomali

Beri alarm jika role yang tidak biasa membaca secret production atau terjadi lonjakan request secret.

⚠️Audit log bukan tempat 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.

10

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.

Buat .env.example

Isi nama variabel yang dibutuhkan tanpa nilai production, lalu pastikan .env asli masuk .gitignore.

Pasang godotenv untuk dev

Tambah go get github.com/joho/godotenv, panggil loadDotenvForDev() di awal main dengan guard APP_ENV.

Pindahkan env read ke internal/config

Handler dan service menerima config dari constructor, bukan memanggil os.Getenv sendiri.

Tambahkan validasi startup

Jika DB_URL, JWT_SECRET, atau PAYMENT_KEY kosong, aplikasi keluar dengan error jelas sebelum menerima traffic.

Simulasikan production

Set AWS_SECRETS_MANAGER_SECRET_ID di environment staging, lalu pastikan aplikasi mengambil nilai dari Secrets Manager.

Cek log

Pastikan tidak ada secret value tercetak di log startup, log error, maupun panic handler.

Terminal
grep -R "JWT_SECRET\|PAYMENT_KEY\|postgres://" . \ --exclude-dir=.git \ --exclude=.env.example
Terminal
APP_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
Tujuan akhir

Setelah hands-on, source code boleh dibaca publik tanpa membuka akses ke database, token signing, payment gateway, atau AWS account.

11

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: config cache
  • Laravel biasa menjalankan config:cache agar config stabil selama runtime.
  • Nilai env tidak dibaca sembarangan setelah aplikasi berjalan.
Go: config struct immutable
  • Go idiomatik membaca config saat startup, lalu menyuntikkannya ke dependency.
  • Untuk perubahan secret, gunakan rolling deploy atau mekanisme reload yang eksplisit.
12

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 (ada ok bool) untuk secret wajib, bukan os.Getenv yang menyamarkan absen. Env backend selalu runtime, tak pernah ter-bundle seperti VITE_/NEXT_PUBLIC_ di React.
  • Secret tidak boleh hardcode di kode dan tidak boleh commit ke git. .env hanya untuk local (muat via godotenv dengan guard APP_ENV), .env.example hanya 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.