HashiCorp Vault — Complete Implementation Guide
In this tutorial, you will learn about HashiCorp Vault. We cover key concepts, practical examples, and best practices to help you master this topic.
HashiCorp Vault is a centralized secrets management system that provides encrypted storage, dynamic secret generation, access control, audit logging, and automated rotation for secrets across all environments.
What You'll Learn
By the end of this tutorial, you will know how to set up Vault for application secrets, authenticate applications, read static and dynamic secrets, manage leases, and use Vault Agent for sidecar injection.
Why It Matters
Vault is the industry standard for secrets management. It provides a single source of truth for secrets with encryption, access control, and audit trails that meet compliance requirements.
Real-World Use
DodaTech runs Vault in production across 200 Microservices. Each service authenticates with its Kubernetes service account and retrieves database credentials dynamically with 24-hour leases.
HashiCorp Vault Learning Path
flowchart LR
A[Secrets Management] --> B[HashiCorp Vault]
B --> C[Authentication]
B --> D[Static Secrets]
B --> E[Dynamic Secrets]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Vault Authentication Methods
Applications must authenticate to Vault before reading secrets.
class VaultAuthMethods {
static list() {
return [
{
method: "Token",
description: "Simple token-based auth",
useCase: "Development and testing",
security: "Low - tokens can be leaked"
},
{
method: "Kubernetes",
description: "Authenticate via Kubernetes service account",
useCase: "Applications running in Kubernetes",
security: "High - ties to pod identity"
},
{
method: "AppRole",
description: "Role-based auth with RoleID and SecretID",
useCase: "Non-Kubernetes services",
security: "Medium - requires secure SecretID delivery"
},
{
method: "AWS IAM",
description: "Authenticate via AWS IAM roles",
useCase: "Applications running on AWS",
security: "High - ties to AWS instance identity"
}
];
}
}
// Kubernetes authentication example
const vault = require("node-vault")({
apiVersion: "v1",
endpoint: process.env.VAULT_ADDR || "http://vault:8200"
});
async function kubernetesAuth() {
const jwt = require("fs").readFileSync(
"/var/run/secrets/kubernetes.io/serviceaccount/token",
"utf8"
);
const result = await vault.kubernetesLogin({
role: "my-app",
jwt: jwt
});
vault.token = result.auth.client_token;
console.log("Authenticated to Vault via Kubernetes");
}
// kubernetesAuth();
console.log("Vault auth methods available:", VaultAuthMethods.list().length);
Reading Static Secrets
Static secrets are stored directly in Vault and retrieved by authenticated applications.
class VaultStaticSecrets {
constructor(vaultClient) {
this.vault = vaultClient;
}
async getSecret(path) {
try {
const result = await this.vault.read(path);
const data = result.data;
// KV v2 returns data.data
const secret = data.data || data;
console.log(`Read secret from ${path}`);
return secret;
} catch (err) {
if (err.response?.statusCode === 404) {
throw new Error(`Secret not found at ${path}`);
}
if (err.response?.statusCode === 403) {
throw new Error(`Permission denied for ${path}`);
}
throw err;
}
}
async writeSecret(path, data) {
await this.vault.write(path, { data });
console.log(`Wrote secret to ${path}`);
}
async listSecrets(path) {
try {
const result = await this.vault.list(path);
return result.data.keys;
} catch {
return [];
}
}
}
const vaultClient = { token: "test" };
const secrets = new VaultStaticSecrets(vaultClient);
console.log("Static secrets module ready");
Dynamic Database Credentials
Vault can generate temporary database credentials on demand.
class VaultDynamicCredentials {
constructor(vaultClient) {
this.vault = vaultClient;
this.currentLease = null;
}
async getDatabaseCredentials(dbName = "postgres") {
const path = `database/creds/${dbName}`;
try {
const result = await this.vault.read(path);
const creds = result.data;
this.currentLease = {
username: creds.username,
password: creds.password,
leaseId: result.lease_id,
leaseDuration: result.lease_duration,
renewable: result.renewable,
obtainedAt: Date.now()
};
console.log(
`Generated DB credentials: ${creds.username} ` +
`(valid for ${result.lease_duration}s)`
);
return this.currentLease;
} catch (err) {
throw new Error(`Failed to generate database credentials: ${err.message}`);
}
}
async renewLease() {
if (!this.currentLease || !this.currentLease.renewable) {
throw new Error("No renewable lease to renew");
}
const result = await this.vault.write("sys/leases/renew", {
lease_id: this.currentLease.leaseId,
increment: this.currentLease.leaseDuration
});
this.currentLease.obtainedAt = Date.now();
this.currentLease.leaseDuration = result.data.lease_duration;
console.log(`Lease renewed, valid for ${result.data.lease_duration}s`);
}
async scheduleRenewal() {
if (!this.currentLease) return;
const renewInterval = (this.currentLease.leaseDuration / 2) * 1000;
console.log(`Scheduling renewal every ${renewInterval / 1000}s`);
setInterval(async () => {
try {
await this.renewLease();
} catch (err) {
console.error("Lease renewal failed:", err.message);
}
}, renewInterval);
}
}
const dynCreds = new VaultDynamicCredentials(vaultClient);
dynCreds.getDatabaseCredentials("postgres").then(creds => {
console.log("Credentials:", creds.username);
});
// Generated DB credentials: v-token-postgres-abc123... (valid for 3600s)
Vault Agent Sidecar
Vault Agent runs as a sidecar to authenticate and retrieve secrets for applications.
# vault-agent-config.hcl
vault {
address = "http://vault:8200"
}
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "my-app"
token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
}
}
sink "file" {
config = {
path = "/tmp/vault-token"
}
}
}
template {
contents = "{{ with secret \"database/creds/postgres\" }}DB_USERNAME={{ .Data.username }}\nDB_PASSWORD={{ .Data.password }}{{ end }}"
destination = "/etc/secrets/db-credentials.env"
}
// Application reads the template output
const fs = require("fs");
function loadVaultAgentSecrets() {
const secretFile = "/etc/secrets/db-credentials.env";
if (!fs.existsSync(secretFile)) {
console.log("Waiting for Vault Agent to generate secrets...");
return null;
}
const content = fs.readFileSync(secretFile, "utf8");
const lines = content.split("\n").filter(l => l.includes("="));
const secrets = {};
lines.forEach(line => {
const [key, ...rest] = line.split("=");
secrets[key] = rest.join("=");
});
return secrets;
}
const secrets = loadVaultAgentSecrets();
if (secrets) {
console.log("Loaded secrets from Vault Agent template");
}
Vault Policy and Access Control
Vault policies control which paths each application can access.
class VaultPolicyManager {
static generateAppPolicy(appName, paths) {
const rules = paths.map(p => ({
path: p.path,
capabilities: p.capabilities || ["read"],
description: p.description || ""
}));
const policy = `
path "${appName}/*" {
capabilities = ["list"]
}
${rules.map(r => `path "${r.path}" {\n capabilities = [${r.capabilities.map(c => `"${c}"`).join(", ")}]\n}`).join("\n\n")}
`;
return policy;
}
static leastPrivilegeExample() {
return {
"database/creds/postgres": ["read"],
"secret/data/my-app/*": ["read", "list"],
"transit/encrypt/my-app-key": ["create", "update"],
"transit/decrypt/my-app-key": ["create", "update"]
};
}
}
const policy = VaultPolicyManager.generateAppPolicy("my-app", [
{ path: "database/creds/postgres", capabilities: ["read"], description: "Dynamic DB credentials" },
{ path: "secret/data/my-app/config", capabilities: ["read"], description: "App config" }
]);
console.log("Vault policy generated");
Common Mistakes
Using root tokens in production -- Root tokens have unlimited access and should never be used for applications. Use tokens with least-privilege policies.
Not renewing leases -- Dynamic secret leases expire. Applications must renew leases or request new credentials before the lease expires.
Storing Vault tokens in environment variables -- Vault tokens should be stored in files with restricted permissions, not environment variables that can be leaked in Process listings.
Not enabling audit logging -- Vault audit logs provide the audit trail needed for compliance. Enable audit logging on all Vault clusters.
Hardcoding Vault paths -- Vault paths can change between environments. Make the secret path configurable via environment variables.
Practice Questions
What authentication method should a Kubernetes application use to access Vault? Kubernetes auth method. The application authenticates using its service account JWT token.
What is the difference between static and dynamic secrets in Vault? Static secrets are stored in Vault and retrieved directly. Dynamic secrets are generated on demand with a lease and expire automatically.
Why should database credentials be dynamic in Vault? Each application instance gets unique, temporary credentials. If compromised, the credentials expire automatically within hours.
Challenge: Implement a Vault client that auto-renews leases and handles re-authentication.
class AutoRenewingVaultClient {
constructor(config) {
this.addr = config.addr;
this.role = config.role;
this.token = null;
this.leaseTimers = new Map();
}
async authenticate() {
const jwt = require("fs").readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8");
const response = await fetch(`${this.addr}/v1/auth/kubernetes/login`, {
method: "POST",
body: JSON.stringify({ role: this.role, jwt })
});
const data = await response.json();
this.token = data.auth.client_token;
this.scheduleTokenRenewal(data.auth.lease_duration);
}
scheduleTokenRenewal(duration) {
setTimeout(() => this.authenticate(), (duration / 2) * 1000);
}
async read(path) {
if (!this.token) await this.authenticate();
const response = await fetch(`${this.addr}/v1/${path}`, {
headers: { "X-Vault-Token": this.token }
});
return response.json();
}
}
FAQ
Mini Project
Build a Vault integration module that authenticates via Kubernetes, reads static and dynamic secrets, manages lease renewal, and handles Vault unavailability with cached secrets.
class VaultIntegration {
constructor(vaultAddr) {
this.addr = vaultAddr;
this.cache = new Map();
}
async getSecret(path) {
if (this.cache.has(path)) {
return this.cache.get(path);
}
const value = await this.fetchFromVault(path);
this.cache.set(path, value);
setTimeout(() => this.cache.delete(path), 300000);
return value;
}
async fetchFromVault(path) {
// Vault API call
return { path, value: "secret-value" };
}
}
const vault = new VaultIntegration("http://vault:8200");
vault.getSecret("database/creds/postgres").then(v => console.log("Vault integration ready"));
What's Next
Now that you understand HashiCorp Vault, learn about AWS Secrets Manager. Then explore Kubernetes ConfigMaps and Secrets.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro