Belajar Vault - Integrasi Vault dengan Aplikasi Web (Node.js, Go, Python, Laravel)
Episode 16 of 26

Belajar Vault - Integrasi Vault dengan Aplikasi Web (Node.js, Go, Python, Laravel)

Dua jalur integrasi aplikasi ke Vault: langsung lewat SDK resmi dengan AppRole, atau tidak langsung dengan injeksi secret ke environment. Kita bedah kode nyata di Python, Node.js, Go, dan Laravel beserta pola aman retry, renewal, dan fallback.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

Setelah di episode 14 dan 15 sebelumnya kita membahas Vault Agent — pendekatan tidak langsung yang merender secret ke file sehingga aplikasi bahkan tidak perlu tahu keberadaan Vault — pada episode kali ini kita akan melihat sisi sebaliknya: integrasi langsung di mana aplikasi memanggil API Vault sendiri menggunakan SDK resmi, dan bagaimana melakukannya dengan aman.

Kapan kalian butuh pendekatan ini? Ketika aplikasi membutuhkan secret yang dinamis — misalnya kredensial database yang di-generate per request, atau saat kalian perlu memakai Transit untuk enkripsi data di aplikasi. Dalam skenario seperti ini, aplikasi harus menjadi client Vault yang sesungguhnya. Tantangannya bukan sekadar "bisa memanggil API", melainkan bagaimana melakukannya dengan benar: login lewat AppRole, menangani renewal, dan tetap berfungsi saat Vault sedang down. Ini pelajaran yang akan membedakan kalian dari developer yang sekadar menyalin contoh dari blog.

Pembahasan Utama

Dua Teknik Integrasi: Langsung vs Tidak Langsung

Sebelum masuk ke kode, mari kita tegaskan peta integrasi agar tidak bingung:

AspekDirect IntegrationIndirect Integration
MekanismeAplikasi memanggil Vault API via SDKSecret di-inject ke env / file
Contoh toolhvac (Python), node-vault (Node.js), vault/api (Go)Vault Agent, CI/CD secrets injection
Perubahan kodeYa, wajib menulis kode integrasiTidak ada — kode aplikasi bersih
Secret dinamisMendukung penuhTerbatas
Ketergantungan saat Vault downAplikasi harus punya fallbackFile/env sudah tersedia

Intinya: pilih langsung jika butuh dinamis, pilih tidak langsung jika hanya butuh statis — dan ingat, keduanya bisa digabung dalam satu sistem, misalnya konfigurasi statis lewat agent, sementara token database dinamis diambil langsung lewat SDK.

Persiapan: Role & Secret ID AppRole

Semua contoh kode di episode ini memakai AppRole sebagai auth method — standar otentikasi mesin-ke-mesin yang sudah kita bahas di episode 10. Mari siapkan role bernama web-app yang hanya boleh membaca path yang dibutuhkannya:

web-app-policy.hcl
path "secret/data/myapp" {
  capabilities = ["read"]
}
 
path "transit/encrypt/my-key" {
  capabilities = ["create", "update"]
}
Setup AppRole role & secret ID
vault policy write web-app web-app-policy.hcl
vault auth enable approle
 
vault write auth/approle/role/web-app \
  secret_id_ttl=24h \
  token_ttl=1h \
  token_max_ttl=24h \
  policies="web-app"
 
vault read -field=role_id auth/approle/role/web-app/role-id
vault write -f -field=secret_id auth/approle/role/web-app/secret-id

Important

Di produksi, jangan pernah menulis role_id dan secret_id secara hardcoded di kode atau file konfigurasi yang ter-commit ke Git. Kirim keduanya lewat environment variable saat proses deploy, atau gunakan mekanisme aman seperti response wrapping (episode 13) dan AWS/GCP metadata service bila aplikasi berjalan di cloud.

Python: hvac untuk Dynamic Database Credentials

Python memakai library resmi hvac. Kasus paling umum: mengambil dynamic credentials database dari secrets engine database — kredensial yang di-generate on-demand dan punya TTL pendek. Pertama install:

Install hvac
pip install hvac
Pythonvault_client.py
import os
import time
import hvac
 
client = hvac.Client(url=os.environ["VAULT_ADDR"])
 
# 1. Login via AppRole
client.auth.approle.login(
    role_id=os.environ["VAULT_ROLE_ID"],
    secret_id=os.environ["VAULT_SECRET_ID"],
)
 
# 2. Ambil dynamic DB credentials (biasanya di-call saat inisialisasi koneksi)
creds = client.secrets.database.generate_credentials(name="web-role")
username = creds["data"]["username"]
password = creds["data"]["password"]
lease_id = creds["lease_id"]
print(f"Got DB creds: {username} (lease: {lease_id})")
 
# 3. Perpanjang lease sebelum habis (TTL default role, misal 1 jam)
client.sys.renew_lease(lease_id=lease_id, increment=3600)

Perhatikan poin penting di kode di atas:

  • Login dilakukan sekali di awal proses, bukan per request.
  • generate_credentials memanggil database/creds/web-role — Vault membuat user & password sementara di database secara instan.
  • renew_lease memperpanjang umur kredensial. Untuk pemakaian jangka panjang, jalankan renewal dalam background thread sesuai TTL, atau cukup generate ulang saat koneksi dibuka lagi — itu alasan utama dynamic secret aman.

Warning

Jangan cache kredensial database dinamis lebih lama dari TTL-nya. Jika aplikasi menyimpan password dalam variabel global dan memakainya setelah TTL berakhir, koneksi akan gagal dengan password authentication failed yang membingungkan. Aturannya: simpan selama koneksi itu hidup, lalu generate ulang.

Node.js: node-vault untuk KV & Transit Encryption

Untuk Node.js, library yang paling populer adalah node-vault. Kita akan melakukan dua hal: membaca secret KV v2 dan memakai Transit untuk enkripsi data sensitif. Install dahulu:

Install node-vault
npm install node-vault
vault-client.js
const vault = require("node-vault")({
  apiVersion: "v1",
  endpoint: process.env.VAULT_ADDR,
});
 
async function main() {
  // 1. Login via AppRole
  await vault.approleLogin({
    role_id: process.env.VAULT_ROLE_ID,
    secret_id: process.env.VAULT_SECRET_ID,
  });
 
  // 2. Baca KV v2 (data.data berisi payload, metadata di sampingnya)
  const { data } = await vault.read("secret/data/myapp");
  const dbHost = data.data.DB_HOST;
  const dbPass = data.data.DB_PASSWORD;
  console.log(`DB_HOST=${dbHost}`);
 
  // 3. Enkripsi data sensitif via Transit (EaaS - episode 6)
  const plaintext = Buffer.from("NIK: 3171xxxxxxxxxx").toString("base64");
  const enc = await vault.write("transit/encrypt/my-key", { plaintext });
  console.log(`Ciphertext: ${enc.data.ciphertext}`);
 
  const dec = await vault.write("transit/decrypt/my-key", {
    ciphertext: enc.data.ciphertext,
  });
  const original = Buffer.from(dec.data.plaintext, "base64").toString("utf8");
  console.log(`Decrypted: ${original}`);
}
 
main().catch((err) => {
  console.error("Vault call failed:", err.message);
  process.exit(1);
});

Note

Untuk KV v2, node-vault mengembalikan response API mentah — jadi data payload berada di data.data, sama seperti di CLI. Banyak developer pemula salah mengakses data.DB_HOST dan bingung kenapa hasilnya undefined. Ingat struktur respons Vault: data (level API) → data (payload KV v2) → key-value asli.

Go: vault/api untuk Aplikasi Berperforma Tinggi

Untuk aplikasi Go, pakailah library resmi github.com/hashicorp/vault/api. Pattern-nya sama: login AppRole, set token ke client, lalu baca secret:

vault_client.go
package main
 
import (
	"fmt"
	"os"
 
	vaultapi "github.com/hashicorp/vault/api"
)
 
func main() {
	config := vaultapi.DefaultConfig()
	config.Address = os.Getenv("VAULT_ADDR")
 
	client, err := vaultapi.NewClient(config)
	if err != nil {
		panic(err)
	}
 
	// 1. Login via AppRole
	secret, err := client.Logical().Write("auth/approle/login", map[string]interface{}{
		"role_id":   os.Getenv("VAULT_ROLE_ID"),
		"secret_id": os.Getenv("VAULT_SECRET_ID"),
	})
	if err != nil {
		panic(err)
	}
	client.SetToken(secret.Auth.ClientToken)
 
	// 2. Baca secret KV v2
	s, err := client.Logical().Read("secret/data/myapp")
	if err != nil {
		panic(err)
	}
	payload := s.Data["data"].(map[string]interface{})
	fmt.Println("DB_HOST:", payload["DB_HOST"])
}

Tip

Di aplikasi produksi, jangan panic saat Vault error. Gunakan pattern retry dengan exponential backoff di inisialisasi, dan jika Vault benar-benar tidak tersedia, aplikasi bisa fallback ke nilai dari environment variable yang telah disuntikkan saat deploy. Aplikasi yang crash total hanya karena Vault sedang maintenance adalah kegagalan desain.

Renewal Terjadwal untuk Dynamic Credentials di Go

Untuk dynamic database credentials yang dipakai koneksi berumur panjang, pola terbaik adalah menjalankan renewal di goroutine terpisah yang berjalan sepanjang umur proses. Contoh implementasinya:

renewer.go
package main
 
import (
	"log"
	"time"
 
	vaultapi "github.com/hashicorp/vault/api"
)
 
// renewCredential memakai LifetimeWatcher untuk merenew lease secara berkala
// selama aplikasi hidup, dan berhenti ketika proses dimatikan.
func renewCredential(client *vaultapi.Client, leaseID string) {
	watcher, err := client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
		Secret: &vaultapi.Secret{
			LeaseID:       leaseID,
			Renewable:     true,
			LeaseDuration: 3600,
		},
		Increment: 3600,
	})
	if err != nil {
		log.Fatal(err)
	}
 
	stopCh := make(chan struct{})
	go func() {
		time.Sleep(24 * time.Hour)
		close(stopCh) // hentikan watcher saat aplikasi shutdown
	}()
	go watcher.Start()
	defer watcher.Stop()
 
	for {
		select {
		case <-stopCh:
			client.Sys().Revoke(leaseID)
			return
		}
	}
}

Important

Perhatikan Stop + Revoke pada akhir proses. Ketika aplikasi dimatikan, lease dynamic credentials harus di-revoke agar user database sementara ikut dihapus dari database — inilah yang menjaga prinsip short-lived credentials (episode 5) tetap berjalan: kredensial hanya hidup selama aplikasi memakainya, tidak lebih.

PHP / Laravel: Opsi yang Tersedia

HashiCorp belum menyediakan SDK resmi untuk PHP, sehingga ekosistem Laravel mengandalkan library komunitas. Opsi yang umum dipakai:

LibraryKarakteristik
xenolope/VaultPHP client dengan dukungan AppRole & KV read, cukup populer di komunitas
Laravel config + Vault AgentPola tidak langsung: .env di-render agent, Laravel membacanya seperti biasa
Custom HTTP clientMemanggil REST API Vault langsung dengan Guzzle untuk kebutuhan khusus

Karena tidak ada SDK resmi, banyak tim Laravel lebih memilih pola indirect integration (Vault Agent atau injeksi env di CI/CD) — Laravel secara native membaca variabel dari environment, jadi selama secret sudah ada di env, aplikasi tidak perlu tahu dari mana asalnya:

config/database.php (pola tidak langsung)
'connections' => [
    'mysql' => [
        'driver'    => 'mysql',
        'host'      => env('DB_HOST', '127.0.0.1'),
        'database'  => env('DB_DATABASE', 'forge'),
        'username'  => env('DB_USERNAME', 'forge'),
        'password'  => env('DB_PASSWORD', ''),
        // Laravel tidak peduli asal secret - env() sudah cukup
    ],
],

Namun untuk kebutuhan langsung (misal membaca secret saat bootstrap, atau memakai Transit di aplikasi Laravel), kalian bisa memakai library xenolope/Vault. Contoh penggunaan dengan AppRole:

app/Providers/VaultServiceProvider.php
use Xenolope\Vault\Client as VaultClient;
use GuzzleHttp\Client as HttpClient;
 
public function boot(): void
{
    $http = new HttpClient([
        'base_uri' => env('VAULT_ADDR', 'http://127.0.0.1:8200'),
    ]);
 
    $vault = new VaultClient($http);
 
    // 1. Login via AppRole
    $vault->authenticate(
        'approle',
        [
            'role_id'   => env('VAULT_ROLE_ID'),
            'secret_id' => env('VAULT_SECRET_ID'),
        ]
    );
 
    // 2. Baca secret KV v2 -> simpan ke config runtime
    $secret = $vault->get('secret/data/myapp');
    config(['database.connections.mysql.password' =>
        $secret['data']['data']['DB_PASSWORD']]);
}

Note

Pola di atas mengisi config Laravel satu kali saat bootstrap. Karena library komunitas tidak mengelola renewal otomatis, untuk secret ber-lease sebaiknya tim Laravel memakai pola tidak langsung (agent) — atau menyimpan kredensial dinamis dengan cache TTL di dalam proses. Pilih yang paling sederhana untuk kebutuhan kalian.

Pola Aman: Retry, Renewal & Fallback

Kesalahan terbesar bukan pada kode integrasi itu sendiri, melainkan pada cara aplikasi berperilaku saat Vault bermasalah. Tiga pola yang wajib dimiliki aplikasi yang terhubung langsung ke Vault:

Pythonpola_aman.py
import os
import time
import hvac
 
 
def get_vault_client(max_retries: int = 5) -> hvac.Client:
    """Login dengan retry + exponential backoff."""
    client = hvac.Client(url=os.environ["VAULT_ADDR"])
    for attempt in range(max_retries):
        try:
            client.auth.approle.login(
                role_id=os.environ["VAULT_ROLE_ID"],
                secret_id=os.environ["VAULT_SECRET_ID"],
            )
            return client
        except Exception:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # 1, 2, 4, 8, 16 detik
    raise RuntimeError("Vault unreachable")
 
 
def read_secret_or_fallback(client: hvac.Client, path: str) -> dict:
    """Baca secret; jika Vault down, fallback ke env yang di-inject saat deploy."""
    try:
        resp = client.secrets.kv.v2.read_secret_version(path=path)
        return resp["data"]["data"]
    except Exception:
        return {
            "DB_HOST": os.environ.get("DB_HOST"),
            "DB_PASSWORD": os.environ.get("DB_PASSWORD"),
        }

Tiga prinsip dari kode di atas:

  1. Retry with backoff — jangan menyerah pada percobaan pertama; Vault kadang sedang restart atau baru saja di-unseal.
  2. Renewal terpusat — satukan logika renew lease di satu tempat, jangan tersebar di tiap modul.
  3. Fallback to env — nilai secret disediakan juga lewat environment saat deploy, sehingga aplikasi tetap bisa degraded but alive ketika Vault down. Kombinasi langsung + fallback adalah pola yang paling sering dipakai di perusahaan besar.

Kesalahan Umum Integrasi SDK

KesalahanGejalaSolusi
Hardcoded role_id/secret_id di kodeSecret bocor ke GitInject via env / secrets manager
Menyimpan token Vault dalam variabel globalToken kadaluarsa dan renewal tidak terjadwalLogin sekali di init + renewal terpusat
Tidak ada renewal lease dynamic secretKoneksi DB mendadak gagal setelah TTLRenew lease / generate ulang per koneksi
Tidak ada retry saat Vault downAplikasi crash saat Vault restartImplementasikan backoff + fallback
Salah mengakses level data.data KV v2Nilai undefined / NoneIngat struktur respons KV v2
Memanggil Vault pada setiap requestLatensi & beban API tinggiCache nilai dengan TTL di dalam aplikasi
Logging response secret ke console/logSecret terekam di log systemJangan log nilai data; hanya log sukses/gagal

Caution

Banyak insiden kebocoran secret terjadi bukan di kode, tapi di log. Pastikan library HTTP tidak mencatat body request/response yang berisi secret, dan jangan pernah console.log(secret) saat debugging. Gunakan format log yang menandai [REDACTED] untuk field sensitif.

Penutup

Pada episode 16 ini kita telah membahas dua teknik integrasi: langsung dengan SDK resmi (hvac di Python untuk dynamic database credentials, node-vault di Node.js untuk KV & Transit, vault/api di Go) serta pilihan untuk Laravel/PHP yang mengandalkan library komunitas atau pola tidak langsung. Kita juga mempraktikkan pola aman: retry dengan backoff, renewal terpusat, dan fallback ke environment variable saat Vault tidak tersedia.

Inti pelajaran episode ini: integrasi langsung memberi aplikasi kekuatan penuh atas secret dinamis, tetapi tanggung jawab keamanannya ada di tangan kalian — AppRole harus dikelola ketat, token tidak boleh dibocorkan, dan perilaku saat Vault down harus dirancang sejak awal.

Di episode 17 selanjutnya kita akan masuk ke dunia Kubernetes: K8s Auth Method, Vault Agent Sidecar Injector, dan Vault Secrets Operator (VSO) — bagaimana cluster Kubernetes berotentikasi ke Vault dan menerima secret secara otomatis. Pastikan tetap semangat!

Belajar Vault - Integrasi Vault dengan Aplikasi Web (Node.js, Go, Python, Laravel) | Belajar Secret Management dengan HashiCorp Vault