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.

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.
Sebelum masuk ke kode, mari kita tegaskan peta integrasi agar tidak bingung:
| Aspek | Direct Integration | Indirect Integration |
|---|---|---|
| Mekanisme | Aplikasi memanggil Vault API via SDK | Secret di-inject ke env / file |
| Contoh tool | hvac (Python), node-vault (Node.js), vault/api (Go) | Vault Agent, CI/CD secrets injection |
| Perubahan kode | Ya, wajib menulis kode integrasi | Tidak ada — kode aplikasi bersih |
| Secret dinamis | Mendukung penuh | Terbatas |
| Ketergantungan saat Vault down | Aplikasi harus punya fallback | File/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.
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:
path "secret/data/myapp" {
capabilities = ["read"]
}
path "transit/encrypt/my-key" {
capabilities = ["create", "update"]
}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-idImportant
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 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:
pip install hvacimport 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:
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.
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:
npm install node-vaultconst 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.
Untuk aplikasi Go, pakailah library resmi github.com/hashicorp/vault/api. Pattern-nya sama: login AppRole, set token ke client, lalu baca secret:
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.
Untuk dynamic database credentials yang dipakai koneksi berumur panjang, pola terbaik adalah menjalankan renewal di goroutine terpisah yang berjalan sepanjang umur proses. Contoh implementasinya:
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.
HashiCorp belum menyediakan SDK resmi untuk PHP, sehingga ekosistem Laravel mengandalkan library komunitas. Opsi yang umum dipakai:
| Library | Karakteristik |
|---|---|
xenolope/Vault | PHP client dengan dukungan AppRole & KV read, cukup populer di komunitas |
Laravel config + Vault Agent | Pola tidak langsung: .env di-render agent, Laravel membacanya seperti biasa |
| Custom HTTP client | Memanggil 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:
'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:
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.
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:
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:
| Kesalahan | Gejala | Solusi |
|---|---|---|
Hardcoded role_id/secret_id di kode | Secret bocor ke Git | Inject via env / secrets manager |
| Menyimpan token Vault dalam variabel global | Token kadaluarsa dan renewal tidak terjadwal | Login sekali di init + renewal terpusat |
| Tidak ada renewal lease dynamic secret | Koneksi DB mendadak gagal setelah TTL | Renew lease / generate ulang per koneksi |
| Tidak ada retry saat Vault down | Aplikasi crash saat Vault restart | Implementasikan backoff + fallback |
Salah mengakses level data.data KV v2 | Nilai undefined / None | Ingat struktur respons KV v2 |
| Memanggil Vault pada setiap request | Latensi & beban API tinggi | Cache nilai dengan TTL di dalam aplikasi |
| Logging response secret ke console/log | Secret terekam di log system | Jangan 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.
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!