ZenVault: Automasi AWS Secrets di API Go Minimalis
β€’7 min read

ZenVault: Automasi AWS Secrets di API Go Minimalis

Tutorial External Secrets Operator (ESO) + AWS Secrets Manager di k3d. Plus, lesson learned kenapa pindah dari GCP WIF ke AWS demi efisiensi infra.

golang
kubernetes
devops
aws
docker
k3d
external-secrets
cert-manager
zerossl
backend
Share

Oke, jadi ceritanya gue lagi pengen eksplor sesuatu yang udah lama ada di wishlist: gimana caranya bikin API yang bisa ambil rahasia dari cloud (API key, password, dsb) secara otomatis, tanpa hardcode, dan tanpa nulis kode SDK cloud di dalam source code?

Hasilnya lahirlah ZenVault β€” sebuah "lab" yang menggabungkan:

  • 🐹 Go sebagai bahasa API, dikompilasi jadi binary statis
  • πŸ‹ Docker dengan base image scratch (literally image kosong, nggak ada OS-nya)
  • ☸️ k3d buat simulasi cluster Kubernetes di lokal
  • πŸ” External Secrets Operator (ESO) sebagai "kurir" yang ngambil secret dari cloud ke Kubernetes
  • ☁️ AWS Secrets Manager buat nyimpen rahasianya
  • πŸ”’ cert-manager + ZeroSSL buat otomatisasi sertifikat HTTPS

Spoiler: perjalanannya nggak mulus. Ada drama GCP, ada WIF yang gagal, ada Org Policy yang kayak firewall manusia. Tapi endingnya manis. Let's dive in.


Awal Mula: PRD dan Rencana Besar

Sebelum nulis kode, gue bikin PRD (Product Requirements Document) dulu buat mastiin arahnya jelas. Intinya:

  1. Bikin API Go yang cuma baca env var APP_DEBUG_KEY
  2. Secret itu nggak boleh hardcode β€” harus ditarik otomatis dari cloud
  3. Image Docker harus ringan, target < 15MB
  4. TLS otomatis via ZeroSSL

Simplenya gini alurnya:

Cloud Secret β†’ ESO β†’ K8s Secret β†’ Pod Env β†’ API Response

Keliatan elegan. Tapi eksekusinya... mari kita bahas.


Fase 1: Bikin API-nya Dulu

Kode main.go intentionally super sederhana. Cuma dua endpoint:

// /healthz β€” buat ngecek masih hidup apa nggak
// /v1/debug β€” return APP_DEBUG_KEY dari environment

os.Getenv("APP_DEBUG_KEY")

Nggak ada import "github.com/aws/aws-sdk-go". Nggak ada credentials di kode. Beneran zero. Nanti yang ngurusin itu Kubernetes-nya.

Buat Docker image-nya, gue pakai multi-stage build:

  • Stage 1: Compile Go binary pakai golang:alpine
  • Stage 2: Copy binary ke FROM scratch β€” literally image kosong tanpa OS

Hasilnya? πŸŽ‰ 5.96MB. Di bawah target 15MB. Mantap.


Fase 2: Drama GCP β€” Org Policy yang Nge-block Segalanya

Rencananya modal awal pakai Google Secret Manager (GSM). Gue udah bikin ServiceAccount zenvault-eso, kasih role secretmanager.secretAccessor. Tinggal download JSON key-nya dan masukin ke Kubernetes.

Eh, ternyata...

ERROR: iam.disableServiceAccountKeyCreation: enforced: true

Org Policy di level Workspace Google nge-blokir pembuatan JSON key buat semua ServiceAccount di project. Bahkan Owner project nggak bisa bypass ini. Ini bukan bug, ini fitur keamanan yang memang disengaja β€” tapi buat lab kita, ini jadi tembok besar.

Usaha Pertama: Workload Identity Federation (WIF)

Gue nggak nyerah gitu aja. Solusi resminya untuk auth tanpa JSON key di GCP adalah Workload Identity Federation (WIF) β€” cara autentikasi "keyless" yang pakai OIDC token dari cluster Kubernetes.

Setup WIF itu lumayan kompleks:

  1. Bikin Workload Identity Pool di GCP
  2. Upload openid-configuration dan jwks.json dari cluster k3d ke GCS Bucket publik (buat jadi OIDC Discovery Endpoint)
  3. Daftarin GCS Bucket URL itu sebagai OIDC Provider di WIF
  4. Bikin IAM binding antara Kubernetes ServiceAccount dan GCP ServiceAccount
  5. Re-create cluster k3d dengan flag --service-account-issuer baru

Semua berhasil di-setup. Provider terdaftar. IAM binding aktif. Cluster selesai dibuat ulang.

Tapi pas ESO dicoba connect ke GCP...

failed to lookup workload identity:
unable to get project id: Get "http://169.254.169.254/computeMetadata/v1/project/project-id":
dial tcp 169.254.169.254:80: connect: connection refused

πŸ’€ GCE Metadata Server. IP 169.254.169.254 itu IP khusus yang hanya ada di VM Google Cloud (GKE, Compute Engine). Di laptop kita yang pakai k3d? Nggak ada. Provider gcpsm di ESO ternyata hardcoded nyari IP itu kalau mode Workload Identity aktif.

Lesson learned ini yang paling mahal: WIF itu bagus dan aman, tapi dia "designed for cloud" bukan untuk lokal.


Pivot: Pindah ke AWS

Daripada stuck, kita pivot. AWS Secrets Manager jadi pilihan pengganti. Alasannya simpel: AWS pakai IAM User dengan Access Key + Secret Key yang bisa dipakai di mana saja β€” nggak perlu infrastruktur cloud khusus.

Setup AWS dalam 5 Langkah

1. Bikin secret di AWS:

aws secretsmanager create-secret \
  --name "zenvault/app_debug_key" \
  --secret-string "nilai-rahasianya" \
  --region ap-southeast-1

2. Bikin IAM User + policy khusus (least privilege β€” cuma bisa baca dua secret itu):

aws iam create-user --user-name zenvault-eso
aws iam create-access-key --user-name zenvault-eso

3. Simpen ke Kubernetes Secret:

kubectl create secret generic aws-secret \
  --from-literal=access-key="AKIAXXXXXXX" \
  --from-literal=secret-access-key="xxxxxxxx" \
  --namespace external-secrets

4. Deploy ClusterSecretStore pakai provider AWS:

spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-southeast-1
      auth:
        secretRef:
          accessKeyIDSecretRef:
            name: aws-secret
            key: access-key

5. Apply semua manifest:

kubectl apply -f manifests/

Hasilnya langsung:

clustersecretstore.external-secrets.io/aws-store   Valid   ReadWrite   True βœ…

Test endpoint:

curl -k https://127.0.0.1/v1/debug -H "Host: playlab.my.id"
> {"app":"zenvault","app_debug_key":"nilai-rahasianya"}

πŸŽ‰ Bekerja sempurna. Secret dari AWS muncul di response API. Tanpa satu baris kode AWS SDK.


Fase TLS: ZeroSSL dan cert-manager

Untuk TLS, kita pakai cert-manager dengan ACME provider ZeroSSL (alternatif Let's Encrypt yang lebih enterprise-friendly).

Trik yang perlu dikenali: ClusterIssuer hanya bisa baca secret di namespace cert-manager, bukan default. Jadi HMAC Key ZeroSSL (yang kita simpan via ESO) harus punya ExternalSecret tersendiri di namespace yang benar.

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: zerossl-eab-secret
  namespace: cert-manager # ← kunci ada di sini
spec:
  data:
    - secretKey: ZEROSSL_EAB_HMAC
      remoteRef:
        key: zenvault/zerossl_eab_hmac

Setelah fix ini: ClusterIssuer langsung berstatus:

The ACME account was registered with the ACME server βœ…

Tapi order sertifikat domain playlab.my.id? Masih pending. Karena buat complete HTTP-01 challenge, ZeroSSL perlu akses internet ke server kita β€” dan laptop di balik WiFi/NAT nggak bisa direach dari luar. Ini expected behavior di lokal, bukan bug kita.


GCP vs AWS: Head-to-Head

Aspek πŸ”΅ GCP 🟠 AWS
Auth Method Service Account JSON Key IAM Access Key
Hambatan di Lokal Org Policy blokir SA Key Tidak ada
WIF Support Teknis iya, tapi ESO-nya stuck di GCE N/A
ESO Integrasi gcpsm (tergantung GCE) aws (fleksibel)
Setup Speed Lambat (troubleshooting WIF) Cepat dan mulus
Rekomendasi Production di GKE Local dev + Multi-cloud

Lessons Learned

1. Org Policy itu Real

Di dunia kerja nyata, banyak hal yang "secara teknis bisa" tapi diblokir oleh kebijakan perusahaan. Kemampuan beradaptasi β€” bukan ngotot β€” yang menentukan seberapa cepat kamu deliver.

2. Cloud-native Auth β‰  Universal

WIF itu solusi yang bagus. Tapi dia didesain buat ekosistemnya sendiri (GKE). Kalau kamu mau eksperimen di lokal, static credentials masih jadi raja.

3. Kubernetes Operator = Superpower

Dengan ESO, kamu bisa inject secret dari cloud ke dalam Pod tanpa menyentuh kode aplikasinya sama sekali. Ini bukan cuma security improvement β€” ini perubahan paradigma cara kerja.

4. ClusterIssuer dan Namespace itu Penting

ClusterIssuer di cert-manager itu cluster-scoped, tapi dia baca secret dari namespace tertentu. Satu detail kecil ini butuh satu percobaan tersendiri buat nyadarinya.

5. HTTP-01 vs DNS-01 ACME Challenge

Untuk dev lokal tanpa IP publik, HTTP-01 nggak akan pernah berhasil. Solusinya: pakai DNS-01 (dengan API Cloudflare/Route53) atau mkcert untuk self-signed cert lokal.


Kesimpulan

ZenVault bukan cuma tentang "cara bikin API kecil." Ini tentang cara berfikir tentang keamanan, automasi, dan adaptasi di lingkungan cloud-native.

Pipeline akhirnya:

AWS Secrets Manager
  └─ ESO ClusterSecretStore
      └─ Kubernetes Secret
          └─ Pod Env Variable
              └─ GET /v1/debug β†’ {"app_debug_key": "..."}  βœ…

Aplikasi Golang-nya literally nggak tau dia nyambung ke AWS. Dia cuma baca environment variable β€” sisanya urusan Kubernetes.

Dan itulah yang disebut Zero-ops Secret Management.

Source code ada di: github.com/stayrelevantid/zenvault

Enjoyed this article? Share it!

Share

Diskusi & Komentar

Artikel Terkait