system-design64 min read

How to Design HashiCorp Vault - Secrets Management Platform — A Senior+ Guide

How to Design HashiCorp Vault - Secrets Management Platform — A Senior+ Guide

A deep-dive into building a production-grade secrets management system with dynamic secrets, encryption as a service, identity-driven access, and zero-trust principles.

Article #221 Published: July 31, 2024 Category: System Design Reading Time: ~55 min

1. Introduction: Vault at Scale

In modern distributed systems, managing secrets—API keys, database credentials, TLS certificates, encryption keys—is one of the most critical and error-prone operational challenges. Hardcoding secrets in configuration files, embedding them in environment variables, or storing them in version control repositories are antipatterns that have led to some of the largest data breaches in history. HashiCorp Vault was purpose-built to solve this problem at enterprise scale, providing a unified platform for secrets management, encryption as a service, and identity-based access control.

Vault operates on three fundamental pillars that distinguish it from simpler secrets storage solutions. First, dynamic secrets ensure that no long-lived credentials exist anywhere in the system. Instead of sharing a single static database password across dozens of services, Vault generates unique, short-lived credentials on demand for each requesting entity. When a microservice needs to connect to a PostgreSQL database, it authenticates with Vault, receives a credentials pair with a configurable TTL, and when that TTL expires, the credentials are automatically revoked. This eliminates the blast radius of credential compromise entirely.

Second, encryption as a service through the Transit engine allows applications to encrypt and decrypt data without ever managing encryption keys themselves. Applications send plaintext to Vault and receive ciphertext, or vice versa. The encryption keys never leave Vault's security boundary. This is particularly powerful for applications that need to implement field-level encryption in databases, tokenization for PII, or envelope encryption for stored data. It decouples the responsibility of cryptographic management from application developers.

Third, identity-based access ties every secret retrieval to a verified identity. Whether that identity is a Kubernetes service account, an AWS IAM role, a human user authenticating via OIDC, or an Azure managed identity, Vault enforces fine-grained policies that determine exactly which secrets that identity can access, for how long, and under what conditions. This is the foundation of zero-trust architecture—no implicit trust, every access is authenticated and authorized.

At scale, enterprises like Capital One, Shopify, and GitLab run Vault clusters serving millions of secret requests per day across thousands of services. The platform handles everything from securing infrastructure credentials to issuing short-lived TLS certificates for service mesh environments, to providing encryption APIs for GDPR and HIPAA compliance. Vault's extensible architecture through secrets engines and auth methods means it can integrate with virtually any infrastructure provider, identity provider, or workflow.

Consider a typical enterprise scenario: a Kubernetes cluster running 500 microservices, each needing access to different database credentials, API keys, and TLS certificates. Without Vault, managing the lifecycle of these secrets—rotation, revocation, auditing—becomes a full-time operational burden. With Vault, a single operator can configure auth methods for the cluster, define policies that map service identities to the exact secrets they need, and let Vault handle the rest. Credentials are generated dynamically, rotated automatically, revoked when services are decommissioned, and every access is logged for compliance.

This guide is designed for senior engineers and architects who need to understand not just how Vault works, but how to design a complete secrets management platform around it. We will cover the internal architecture, every major subsystem, production deployment patterns, security hardening, and the trade-offs involved in each design decision. By the end, you will have the knowledge to architect, deploy, and operate Vault at any scale—from a single development cluster to a globally distributed enterprise deployment.

ConceptDescriptionExample
Dynamic SecretsShort-lived credentials generated on demandPostgreSQL user with 1-hour TTL
Encryption as a ServiceEncrypt/decrypt without managing keysTransit engine for field-level DB encryption
Identity-Based AccessFine-grained policies tied to verified identitiesK8s pod gets access only to its DB secrets
Audit LoggingComplete request/response loggingEvery secret access logged with actor identity
Lease ManagementAutomatic revocation of expired credentialsDB credentials revoked after TTL expiry

2. Core Architecture

HashiCorp Vault's architecture is built around a client-server model with several critical internal subsystems. Understanding these subsystems is essential for designing a reliable and secure deployment. At its core, Vault consists of a server process that manages a sealed storage backend, exposes a HTTP API, and enforces authentication and authorization for every request.

The Vault Server is the central component that handles all client requests. When a client sends an API request to Vault, the request passes through a pipeline of handlers: the Router determines which secrets engine or auth method should handle the request, the Barrier (a security envelope) ensures that data is encrypted before being written to storage and decrypted after being read, and the Backend handles the actual storage operations. The server exposes a single HTTP/HTTPS endpoint (default port 8200) and all interactions happen through this unified API.

The Storage Backend is where Vault persists its data. This can be an integrated storage system like Raft (recommended for production), or an external backend like Consul, PostgreSQL, DynamoDB, or Azure Storage. The storage backend is opaque to Vault—it only stores encrypted data. This is a critical security property: even if an attacker gains access to the raw storage, the data is encrypted by the barrier and is useless without the unseal keys.

The Auth Methods are responsible for authenticating clients. Vault supports numerous auth methods including Token, Kubernetes, AWS IAM, Azure, GCP, LDAP, OIDC, AppRole, Userpass, and more. Each auth method maps external credentials (like a Kubernetes service account token or an AWS-signed request) to an internal Vault identity that is then evaluated against policies.

The Secrets Engines are the pluggable backends that generate and manage secrets. Each secrets engine is mounted at a path and handles requests within that path. For example, a PostgreSQL secrets engine mounted at database/creds/prod generates dynamic database credentials when requested. A KV engine mounted at secret/ stores static key-value secrets. A Transit engine mounted at transit/ provides encryption-as-a-service.

graph TB Client[Client Application] -->|HTTP/HTTPS API| LB[Load Balancer] LB --> Vault1[Vault Server Node 1] LB --> Vault2[Vault Server Node 2] LB --> Vault3[Vault Server Node 3] Vault1 --> Barrier1[Security Barrier - AES-256-GCM] Vault2 --> Barrier2[Security Barrier - AES-256-GCM] Vault3 --> Barrier3[Security Barrier - AES-256-GCM] Barrier1 --> Router1[Request Router] Barrier2 --> Router2[Request Router] Barrier3 --> Router3[Request Router] Router1 --> Auth[Auth Methods] Router1 --> SE[Secrets Engines] Router1 --> Policy[Policy Engine] Router1 --> Audit[Audit Backend] SE --> DB[(Database)] SE --> Transit[(Transit Engine)] SE --> PKI[(PKI Engine)] SE --> KV[(KV Store)] Auth --> K8s[Kubernetes Auth] Auth --> AWS[AWS IAM Auth] Auth --> LDAP[LDAP Auth] Auth --> OIDC[OIDC Auth]

The Security Barrier is perhaps Vault's most important internal component. It is a cryptographic envelope that encrypts all data before writing to the storage backend and decrypts it after reading. The barrier uses AES-256-GCM encryption with a master key that is derived from the unseal keys through Shamir's Secret Sharing. When Vault starts, it is in a sealed state—the barrier is active and no data can be read or written. An administrator must provide a threshold of unseal keys to unseal Vault and make it operational.

The Router is the internal component that maps incoming request paths to the appropriate handler. When a request comes in for /v1/database/creds/my-role, the router determines that the request should be handled by the database secrets engine mounted at database/. The router also handles path normalization and permission checking through the policy engine.

The Audit Backend logs every authenticated request and response to Vault. This is essential for compliance and security monitoring. Vault supports file-based audit logging and syslog-based logging. Every audit log entry includes the request path, the requesting entity, the operation performed, and the response (with sensitive data redacted by default).

Understanding the request lifecycle is crucial for capacity planning and performance optimization. A typical request flows through: HTTP Ingress → TLS Termination → Request Parsing → Auth Method Validation → Policy Evaluation → Secrets Engine Handling → Response Encryption → HTTP Egress. Each of these steps has measurable latency, and at high scale, understanding this pipeline helps identify bottlenecks.

ComponentRoleCritical Details
HTTP APIUnified client interfacePort 8200, REST-based, JSON payloads
Security BarrierEncrypts all stored dataAES-256-GCM, Shamir key derivation
RouterMaps paths to handlersPath-based routing, permission checks
Auth MethodsAuthenticate clientsPluggable, supports 10+ methods
Secrets EnginesGenerate and manage secretsPluggable, path-mounted, isolated
Policy EngineAuthorizes operationsHCL-based policies, path-matching
Audit BackendLogs all operationsRequest/response logging, redaction
Storage BackendPersists encrypted dataRaft, Consul, DynamoDB, PostgreSQL

3. Auth Methods

Authentication in Vault is the process of verifying the identity of a client and mapping it to an internal Vault identity that can be evaluated against policies. Vault's auth method architecture is pluggable, allowing organizations to integrate with their existing identity infrastructure. Each auth method is mounted at a path and handles authentication requests within that path. A critical design principle is that Vault supports multiple auth methods simultaneously—a single Vault deployment might use Kubernetes auth for service workloads, LDAP for human administrators, and OIDC for CI/CD pipelines.

Token Auth is the underlying auth method that all other auth methods ultimately create. When any auth method successfully authenticates a client, it creates a Vault token with attached policies and metadata. Tokens have a configurable TTL, can be renewable, and support token hierarchical relationships (orphan tokens). Token auth is also used for inter-service communication where one service delegates a subset of its permissions to another.

Kubernetes Auth is one of the most widely used auth methods in cloud-native environments. It works by validating the Kubernetes service account token presented by a pod. Vault communicates with the Kubernetes API server to verify the token's validity and extracts the pod's namespace, service account name, and other metadata. This metadata is then used to map to Vault policies through role definitions. This is the foundation of secrets injection in Kubernetes—pods authenticate as themselves and receive only the secrets they are authorized to access.

AWS IAM Auth allows AWS resources to authenticate to Vault using their IAM credentials. The requesting entity (EC2 instance, Lambda function, ECS task) signs a GetCallerIdentity request using the AWS Signature V4 protocol. Vault verifies this signature against AWS's STS endpoint and extracts the IAM role ARN, account ID, and other metadata. This is particularly powerful because it requires no additional credentials—the IAM role attached to the AWS resource is its identity. Vault can also verify the unique AWS instance ID for EC2 instances, preventing confused-deputy attacks.

LDAP Auth integrates with existing LDAP directories (Active Directory, OpenLDAP) for human user authentication. Users provide their LDAP credentials, Vault validates them against the LDAP server, and maps LDAP groups to Vault policies. This is the standard approach for providing human access to Vault, as it leverages the existing user lifecycle management in the directory.

OIDC Auth supports authentication through any OpenID Connect provider (Okta, Azure AD, Google Workspace, Keycloak). This provides a modern SSO experience where users authenticate through their identity provider's login flow, and Vault receives the authenticated identity along with group claims. OIDC is increasingly preferred over LDAP for human authentication due to its support for MFA, conditional access policies, and modern authentication flows.

AppRole Auth is designed for machines and applications that need a simple authentication mechanism. It works by providing a role ID and a secret ID (both essentially static credentials). The secret ID can have a limited TTL and CIDR restrictions. While simpler than Kubernetes or AWS auth, AppRole is less secure because it relies on shared secrets. It is best used for legacy systems that cannot integrate with cloud-native or identity-based auth methods.

sequenceDiagram participant App as Client Application participant Vault as Vault Server participant IdP as Identity Provider Note over App,IdP: Kubernetes Auth Flow App->>Vault: POST /v1/auth/kubernetes/login (role, jwt) Vault->>IdP: Validate JWT with K8s API Server IdP-->>Vault: Token valid, pod metadata Vault->>Vault: Map service account to Vault policy Vault-->>App: Vault token with policies & TTL Note over App,IdP: AWS IAM Auth Flow App->>Vault: POST /v1/auth/aws/login (role, iam_http_request_method, ...) Vault->>IdP: Verify SigV4 signature via AWS STS IdP-->>Vault: Caller identity verified Vault->>Vault: Map IAM role to Vault policy Vault-->>App: Vault token with policies & TTL Note over App,IdP: OIDC Auth Flow App->>Vault: Redirect to /v1/auth/oidc/authorize Vault->>IdP: Redirect to OIDC provider IdP-->>App: User authenticates (MFA) App->>Vault: Callback with authorization code Vault->>IdP: Exchange code for tokens IdP-->>Vault: ID token with claims Vault->>Vault: Map OIDC groups to Vault policy Vault-->>App: Vault token with policies & TTL

When designing auth for a Vault deployment, it is essential to consider the principle of least privilege and defense in depth. Each auth method should be configured with the minimum necessary permissions. For Kubernetes auth, use bound_service_account_names and bound_service_account_namespaces to restrict which pods can authenticate. For AWS auth, use bound_iam_principal_arn to restrict which IAM roles can authenticate. For OIDC, use bound_audiences and bound_claims to restrict which tokens are accepted.

Another critical design consideration is auth method isolation. In multi-tenant environments, different namespaces may have their own auth methods with different configurations. This prevents a compromised auth method in one tenant from affecting others. Vault's namespace feature allows each tenant to have their own auth method mounts, policies, and secrets engines, providing complete isolation.

Auth MethodBest ForSecurity LevelComplexity
TokenInter-service communicationMediumLow
KubernetesK8s pod workloadsHighMedium
AWS IAMAWS resourcesHighMedium
LDAPHuman users (legacy)MediumMedium
OIDCHuman users (modern SSO)HighMedium
AppRoleLegacy machinesLow-MediumLow
AzureAzure resourcesHighMedium
GCPGCP resourcesHighMedium

4. Secrets Engines

Secrets engines are the pluggable backends in Vault that are responsible for generating, managing, and revoking secrets. Each secrets engine is mounted at a specific path and handles all requests within that path namespace. This path-based isolation is a powerful architectural pattern—it allows multiple instances of the same engine type to coexist with different configurations. For example, you can mount two separate PostgreSQL database engines at database/prod and database/staging, each connecting to different database clusters with different credentials.

The Key-Value (KV) secrets engine is the simplest and most commonly used engine. It stores static secrets as key-value pairs and supports two versions: KV v1 (no versioning) and KV v2 (with versioning, metadata, soft deletes, and check-and-set operations). KV v2 is mounted by default at secret/ and is ideal for storing configuration values, API keys, and other static secrets that do not need dynamic generation. Each version of a secret in KV v2 is immutable, providing an audit trail and the ability to roll back to previous versions.

The Database secrets engine generates dynamic database credentials for relational databases including PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, and others. Instead of sharing a static password, Vault creates a unique database user with a configurable TTL when a client requests credentials. When the TTL expires, the user is automatically revoked. This eliminates long-lived database credentials and provides complete audit trails of who accessed which database and when.

The PKI secrets engine is a certificate authority (CA) that can issue X.509 certificates. It can operate as a root CA or an intermediate CA, and supports configurable certificate templates, roles, and policies. The PKI engine is essential for implementing mutual TLS (mTLS) in service mesh environments, issuing short-lived certificates for internal services, and replacing long-lived certificates that are a common attack vector.

The Transit secrets engine provides encryption as a service. It manages encryption keys and provides a API for encrypting, decrypting, signing, verifying, and hashing data. The keys never leave Vault, and the engine supports multiple key types, key rotation, convergent encryption, and key wrapping. We will cover this in depth in the encryption section.

The AWS secrets engine generates dynamic AWS IAM credentials. It can create IAM users, assume IAM roles, or generate STS temporary credentials. The credentials are scoped with a configurable TTL and automatically revoked when expired. This eliminates the need to store AWS access keys in configuration files and provides per-request audit trails.

The SSH secrets engine provides secure SSH access through two modes: signed certificates (recommended) and OTP (one-time passwords). In signed certificate mode, Vault acts as a CA for SSH certificates. Users authenticate with Vault, Vault signs their public key and returns a short-lived certificate, and the SSH server validates the certificate against Vault's CA. This eliminates the need to distribute and manage SSH keys across infrastructure.

graph LR Client[Client] -->|Mount Path| Router[Router] Router -->|secret/| KV[KV Engine v2] Router -->|database/| DB[Database Engine] Router -->|pki/| PKI[PKI Engine] Router -->|transit/| Transit[Transit Engine] Router -->|aws/| AWS[AWS Engine] Router -->|ssh/| SSH[SSH Engine] KV -->|Read/Write| KVStore[(KV Storage)] DB -->|Generate| PG[(PostgreSQL)] DB -->|Generate| My[(MySQL)] PKI -->|Issue| Cert[(X.509 Certs)] Transit -->|Encrypt/Decrypt| Keys[(Key Material)] AWS -->|Create/Assume| IAM[(AWS IAM)] SSH -->|Sign/OTP| SSHServer[(SSH Servers)]

Each secrets engine operates within a lease framework. When a secret is generated, Vault creates a lease with a defined duration (TTL). The lease can be renewable (if the engine supports it), and when it expires, Vault revokes the secret. This lifecycle management is critical for security—it ensures that no secret lives longer than intended. The database engine, for example, creates a database user with the same TTL as the lease. When the lease expires, the user is dropped from the database.

When designing secrets engine usage, consider the mounting strategy carefully. Secrets engines should be mounted at logical paths that reflect the organization's structure. A common pattern is to mount by environment and service: database/prod/postgres, database/staging/postgres, aws/prod/readonly, aws/staging/readonly. This provides clear separation and makes policy writing intuitive. Another pattern is to mount by team or business unit, which works well with namespaces for multi-tenancy.

The mount configuration also includes important security settings. The default_lease_ttl and max_lease_ttl settings on a mount determine the default and maximum duration for secrets generated by that engine. Setting appropriate TTLs is a security trade-off—shorter TTLs mean more frequent rotations (better security) but also more load on Vault and the backend systems. Most production deployments use TTLs between 1 hour and 24 hours for dynamic database credentials, and between 1 hour and 8 hours for cloud provider credentials.

EngineTypeKey CapabilityTypical TTL
KV v2StaticVersioned key-value storageN/A (manual)
DatabaseDynamicShort-lived DB credentials1-24 hours
PKIDynamicX.509 certificate issuance1-72 hours
TransitServiceEncryption as a serviceN/A (keys persistent)
AWSDynamicIAM credential generation1-8 hours
SSHDynamicSSH certificate signing1-24 hours

5. Dynamic Secrets

Dynamic secrets represent the most significant architectural advantage Vault provides over traditional secrets management. Unlike static secrets—fixed username/password pairs stored in a configuration file—dynamic secrets are generated on demand, unique to each request, and automatically revoked after a configurable time-to-live (TTL). This paradigm shift fundamentally changes the security posture of an organization by eliminating long-lived credentials that represent persistent attack surfaces.

Consider the lifecycle of a static database credential: an administrator creates a database user with a password, stores it in Vault (or worse, in a config file), and distributes it to all services that need database access. That password might be rotated every 90 days (if the organization is diligent), but during those 90 days, the same credential is valid. If it leaks—through a compromised configuration file, a memory dump, or an insider threat—the attacker has 90 days of unrestricted access. With dynamic secrets, each service request generates a unique username/password pair with a TTL (perhaps 1 hour). Even if a credential leaks, the window of exploitation is limited to 1 hour, and the credential is automatically revoked after that.

The workflow for dynamic secrets follows a consistent pattern across all engines. First, the operator configures the secrets engine with connection information to the target system (for example, the PostgreSQL database connection string with administrative credentials). Next, the operator defines one or more roles that specify the template for generated credentials—for example, a PostgreSQL role might specify CREATE LOGIN ... WITH VALID UNTIL 'timestamp' with specific permissions. When a client requests credentials for a role, Vault connects to the target system using the configured connection, creates the credential according to the role template, and returns the credential to the client along with a lease ID and TTL.

Lease management is the mechanism that makes dynamic secrets self-cleaning. Every secret generated by Vault is associated with a lease. The lease has a duration (TTL) and can be renewable. When the lease expires, Vault automatically revokes the secret—for a database credential, this means dropping the user from the database; for an AWS credential, this means deleting the IAM user or requesting new temporary credentials. Clients can renew a lease before it expires if they need the credential for longer, but this renewal is bounded by the maximum lease TTL configured on the engine.

Here is a C# example of a client that retrieves dynamic PostgreSQL credentials from Vault, uses them, and handles lease renewal:

C#
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
using VaultSharp.V1.SecretsEngines.Database;
using VaultSharp.V1.Commons;

// Vault client configuration
var authMethod = new TokenAuthMethodInfo("s.xxxxx"); // Vault token
var vaultClientSettings = new VaultClientSettings("https://vault.internal:8200", authMethod)
{
    VaultToken = "s.xxxxx"
};

var vaultClient = new VaultClient(vaultClientSettings);

// Request dynamic database credentials
var credentials = await vaultClient.V1.Secrets.Database.GetCredentialsAsync(
    role: "app-production-role",
    mountPoint: "database"
);

// Extract the generated credentials
string username = credentials.Data.Username;  // e.g., "v-app-prod-aB3xK9mQ"
string password = credentials.Data.Password;  // e.g., "A1b2-C3d4-E5f6-G7h8"
int ttl = credentials.Data.LeaseDurationSeconds;

Console.WriteLine($"Generated credentials for role 'app-production-role':");
Console.WriteLine($"  Username: {username}");
Console.WriteLine($"  TTL: {ttl} seconds ({ttl / 60} minutes)");

// Use credentials to connect to PostgreSQL
var connectionString = $"Host=prod-db.internal;Username={username};Password={password};Database=appdb";
using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();

// Execute queries with dynamic credentials
using var cmd = new NpgsqlCommand("SELECT id, name FROM users WHERE active = true", connection);
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
    Console.WriteLine($"User: {reader.GetInt32(0)} - {reader.GetString(1)}");
}

// The credentials will be automatically revoked when the lease expires.
// If the application needs credentials for longer, it can renew the lease:
await vaultClient.V1.Secrets.Database.RenewCredentialsAsync(
    role: "app-production-role",
    mountPoint: "database",
    leaseId: credentials.Data.LeaseId
);

Console.WriteLine($"Lease renewed for another {ttl} seconds.");

The v-app-prod-aB3xK9mQ username format is significant—it encodes the Vault mount point (v-), the role name (app-prod-), and a random suffix. This makes it immediately clear in database logs which Vault role generated which credential, simplifying auditing and troubleshooting.

Dynamic secrets also introduce important operational considerations. First, the connection configuration (the admin credentials Vault uses to create dynamic users) must be highly secure and have the necessary database privileges. Second, the role definitions must follow the principle of least privilege—a role for a read-only reporting service should only have SELECT permissions. Third, TTL configuration must balance security (shorter is better) with practicality (too short creates excessive load). Fourth, database performance must be considered—creating and dropping thousands of database users per hour can impact database performance, and some databases have limits on the number of concurrent users.

A production-ready pattern for dynamic secrets includes health checking, retry logic, and credential caching. Vault agents can handle credential renewal automatically, caching credentials locally and renewing leases before they expire. This means applications can read credentials from a local file or environment variable, and the Vault agent handles all Vault communication transparently.

AspectStatic SecretsDynamic Secrets
Credential LifecycleLong-lived (months/years)Short-lived (minutes/hours)
UniquenessShared across consumersUnique per request/consumer
RevocationManual, error-proneAutomatic on TTL expiry
Blast RadiusFull access until rotationLimited to TTL window
Audit TrailCannot distinguish consumersUnique user per consumer
Operational OverheadLow initial, high ongoingHigh initial, low ongoing

6. Encryption as a Service

Encryption as a Service (EaaS) through Vault's Transit engine represents a fundamentally different approach to cryptographic management. Instead of applications managing their own encryption keys—which requires secure key storage, rotation policies, and compliance with key management standards—applications delegate all cryptographic operations to Vault. The encryption keys never leave Vault's security boundary; applications simply send plaintext to Vault and receive ciphertext, or vice versa. This separation of concerns is critical for organizations that need to implement encryption without adding cryptographic complexity to every application.

The Transit engine supports a comprehensive set of cryptographic operations: Encrypt (plaintext to ciphertext), Decrypt (ciphertext to plaintext), Sign (create digital signatures), Verify (verify signatures), HMAC (create hash-based message authentication codes), and Hash (create cryptographic hashes). Each operation is performed within the context of a named encryption key, and each key has its own type, version, and configuration.

Key management in the Transit engine is sophisticated. Keys can be symmetric (AES-256-GCM, ChaCha20-Poly1305) or asymmetric (RSA-2048, RSA-4096, ECDSA P-256, ECDSA P-384, Ed25519). Keys support automatic rotation—Vault can maintain multiple key versions simultaneously, encrypting with the latest version while still being able to decrypt data encrypted with any previous version. This allows zero-downtime key rotation: encrypt all new data with the new key, and gradually re-encrypt old data as it is accessed.

One of the most powerful features is convergent encryption. With standard encryption, encrypting the same plaintext twice produces different ciphertext (due to random initialization vectors). This is a security feature but makes it impossible to check for duplicate encrypted values. Convergent encryption uses a hash of the plaintext as the initialization vector, ensuring that the same plaintext always produces the same ciphertext. This enables encrypted search, deduplication of encrypted data, and tokenization patterns where the same input must consistently map to the same encrypted output.

Key wrapping is another critical feature that enables envelope encryption. In envelope encryption, data is encrypted with a data encryption key (DEK), and the DEK is encrypted with a key encryption key (KEK). The Transit engine can act as the KEK, wrapping DEKs that are managed by the application. This pattern is used extensively in cloud storage encryption, database field-level encryption, and secure data exchange between services.

Here is a C# example implementing encryption as a service with key rotation and convergent encryption:

C#
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
using VaultSharp.V1.SecretsEngines.Transit;
using VaultSharp.V1.SecretsEngines.Transit.Models;

// Initialize Vault client
var authMethod = new TokenAuthMethodInfo(Environment.GetEnvironmentVariable("VAULT_TOKEN"));
var settings = new VaultClientSettings("https://vault.internal:8200", authMethod);
var vaultClient = new VaultClient(settings);

// Create a new encryption key in the Transit engine
await vaultClient.V1.Secrets.Transit.CreateEncryptionKeyAsync(
    keyName: "payment-card-encryption",
    mountPoint: "transit",
    request: new CreateEncryptionKeyRequest
    {
        KeyType =KeyType.Aes256Gcm96,
        Exportable = false,
        AllowPlaintextBackup = false,
        AutoRotate = true,
        RotationPeriod = 86400  // Rotate every 24 hours
    }
);

Console.WriteLine("Created encryption key 'payment-card-encryption' with auto-rotation.");

// Encrypt sensitive data (e.g., credit card number)
string sensitiveData = "4111-1111-1111-1111";
var encryptResult = await vaultClient.V1.Secrets.Transit.EncryptAsync(
    keyName: "payment-card-encryption",
    mountPoint: "transit",
    request: new TransitEncryptionRequest
    {
        PlainText = Convert.ToBase64String(Encoding.UTF8.GetBytes(sensitiveData))
    }
);

string ciphertext = encryptResult.Data.Ciphertext;
Console.WriteLine($"Encrypted: {ciphertext}");
// Output: vault:v1:eyJhbGciOiJBLUdDTTI1NiIs...

// Store ciphertext in database (safe to store anywhere)
await StoreInDatabase("payments", "card_number_encrypted", ciphertext);

// Decrypt when needed (e.g., during payment processing)
var decryptResult = await vaultClient.V1.Secrets.Transit.DecryptAsync(
    keyName: "payment-card-encryption",
    mountPoint: "transit",
    request: new TransitDecryptionRequest
    {
        Ciphertext = ciphertext
    }
);

string decryptedData = Encoding.UTF8.GetString(
    Convert.FromBase64String(decryptResult.Data.Plaintext)
);
Console.WriteLine($"Decrypted: {decryptedData}");

// Sign data for integrity verification
var signResult = await vaultClient.V1.Secrets.Transit.SignAsync(
    keyName: "payment-card-encryption",
    mountPoint: "transit",
    request: new TransitSignRequest
    {
        Input = Convert.ToBase64String(Encoding.UTF8.GetBytes(ciphertext)),
        HashAlgorithm = HashAlgorithm.Sha256,
        SignatureAlgorithm = SignatureAlgorithm.HmacSha256
    }
);

string signature = signResult.Data.Signature;
Console.WriteLine($"HMAC Signature: {signature}");

// Verify the signature
var verifyResult = await vaultClient.V1.Secrets.Transit.VerifySignedDataAsync(
    keyName: "payment-card-encryption",
    mountPoint: "transit",
    request: new TransitVerifyRequest
    {
        Input = Convert.ToBase64String(Encoding.UTF8.GetBytes(ciphertext)),
        Hmac = signature
    }
);

Console.WriteLine($"Signature valid: {verifyResult.Data.Valid}");

The Transit engine also supports tokenization, which replaces sensitive data with a token that has no cryptographic relationship to the original data. Unlike encryption, which produces deterministic output for the same input (with convergent mode) or non-deterministic output (without), tokenization produces a completely random token that maps to the original data through a Vault-internal lookup table. This is essential for compliance scenarios where encrypted PII is still considered sensitive under regulations like PCI DSS.

Performance considerations for the Transit engine include Vault's in-memory caching (encrypted values can be cached locally by Vault for faster decryption), batch operations (Vault supports encrypting/decrypting multiple values in a single request), and key version management (keeping too many key versions increases memory usage). For high-throughput scenarios, Vault's performance stand-by nodes can handle Transit operations, providing linear read scaling.

OperationInputOutputUse Case
EncryptPlaintext + KeyCiphertextData at rest encryption
DecryptCiphertext + KeyPlaintextData retrieval
SignData + Private KeyDigital SignatureCode signing, JWT signing
VerifyData + Signature + Public KeyValid/InvalidSignature verification
HMACData + KeyHMACIntegrity verification
HashDataHashConsistent hashing, lookups
TokenizePlaintext + KeyTokenPCI DSS compliance
DetokenizeToken + KeyPlaintextData recovery

7. Policies

Vault policies are the authorization layer that determines what operations an authenticated identity can perform on which paths. Policies are written in HashiCorp Configuration Language (HCL) and are attached to tokens through auth method roles or direct assignment. The policy engine is the enforcement point for the principle of least privilege—every request to Vault is evaluated against the caller's policies, and if no policy grants the requested permission, the request is denied. There is no implicit allow; Vault follows a default-deny model.

A policy defines a set of path capabilities. Each path rule specifies the path pattern, the capabilities allowed (create, read, update, delete, list, sudo), and optionally, required parameters, maximum TTLs, and other constraints. Path patterns support wildcards, allowing policies to grant access to a hierarchy of paths. For example, the path database/creds/app-* would match database/creds/app-production, database/creds/app-staging, and any other role starting with app-.

Capabilities in Vault policies have specific meanings that are important to understand: read allows reading data at a path (GET requests), create allows creating new data at a path (POST requests when no data exists), update allows updating existing data at a path (POST requests when data exists), delete allows deleting data at a path (DELETE requests), list allows listing keys at a path (LIST requests), and sudo allows access to paths that are restricted even for root users (like seal/unseal operations).

Policies support parameter constraints that add another layer of security. You can restrict the maximum TTL that a token can request, limit the allowed parameter values, or require specific fields to be present. This is particularly useful for preventing privilege escalation—a policy can grant database credential access but limit the maximum TTL to 1 hour, preventing a user from requesting credentials with a 24-hour lifetime.

Vault also supports policy templates that use the identity of the caller to dynamically generate policy paths. This is extremely powerful for multi-tenant environments. For example, a policy template can use the {{identity.entity.aliases.auth_kubernetes_0.metadata.service_account_namespace}} template variable to generate path rules based on the Kubernetes namespace of the requesting pod. This means a single policy definition can serve thousands of different tenants, each getting access only to their own secrets.

Here is a C# example demonstrating how to programmatically create and manage Vault policies using the VaultSharp client:

C#
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
using VaultSharp.V1.SystemBackend;

var authMethod = new TokenAuthMethodInfo("s.root-token");
var settings = new VaultClientSettings("https://vault.internal:8200", authMethod);
var vaultClient = new VaultClient(settings);

// Define a policy for a microservice that needs database access and KV secrets
string policyHcl = @"
# Allow reading database credentials for the service's own roles
path ""database/creds/my-service-*"" {
  capabilities = [""read""]
  allowed_parameters = {
    ""ttl"" = [3600]    # Max 1 hour TTL
    ""max_ttl"" = [3600]
  }
}

# Allow reading application configuration from KV store
path ""secret/data/my-service/config"" {
  capabilities = [""read""]
}

# Allow reading my-service specific secrets
path ""secret/data/my-service/*"" {
  capabilities = [""read"", ""list""]
}

# Allow encrypting data via the Transit engine
path ""transit/encrypt/my-service-key"" {
  capabilities = [""update""]
}

# Allow decrypting data via the Transit engine
path ""transit/decrypt/my-service-key"" {
  capabilities = [""update""]
}

# Deny listing all secrets engine mounts (security boundary)
path ""sys/mounts"" {
  capabilities = [""deny""]
}

# Allow renewing own token
path ""auth/token/renew-self"" {
  capabilities = [""update""]
}
""";

// Write the policy to Vault
await vaultClient.V1.SystemBackend.CreatePolicyAsync(
    policyName: "my-service-policy",
    policy: new Policy
    {
        Name = "my-service-policy",
        PolicyContents = policyHcl
    }
);

Console.WriteLine("Policy 'my-service-policy' created successfully.");

// Create a token with the policy attached
var tokenRequest = await vaultClient.V1.SystemBackend.CreateTokenAsync(
    new TokenCreateRequest
    {
        Policies = new[] { "my-service-policy" },
        TTL = "1h",
        Renewable = true,
        NumUses = 0  # Unlimited uses within TTL
    }
);

string serviceToken = tokenRequest.Auth.ClientToken;
Console.WriteLine($"Service token created: {serviceToken.Substring(0, 20)}...");

// Verify the token's capabilities
var capabilities = await vaultClient.V1.SystemBackend.GetCapabilitiesAsync(
    token: serviceToken,
    path: "database/creds/my-service-production"
);

Console.WriteLine($"Capabilities for 'database/creds/my-service-production':");
Console.WriteLine($"  {string.Join(", ", capabilities.Data.Capabilities)}");

// Create a policy with identity-based templates
string templatePolicy = @"
# Template policy using identity metadata
path ""secret/data/{{identity.entity.aliases.auth_kubernetes_0.metadata.service_account_namespace}}/*"" {
  capabilities = [""read"", ""list""]
}

path ""database/creds/{{identity.entity.aliases.auth_kubernetes_0.metadata.service_account_namespace}}-*"" {
  capabilities = [""read""]
}

path ""transit/encrypt/{{identity.entity.aliases.auth_kubernetes_0.metadata.service_account_namespace}}-key"" {
  capabilities = [""update""]
}
""";

await vaultClient.V1.SystemBackend.CreatePolicyAsync(
    policyName: "namespace-based-template-policy",
    policy: new Policy
    {
        Name = "namespace-based-template-policy",
        PolicyContents = templatePolicy
    }
);

Console.WriteLine("Template policy created for dynamic namespace-based access.");

Policy design follows several important best practices. First, always start with deny-all and add only the minimum necessary permissions. Second, use descriptive policy names that indicate the service or role they serve. Third, separate human and machine policies—human access policies should have additional constraints (like requiring MFA or limiting to business hours). Fourth, test policies thoroughly before deploying to production using vault token capabilities and vault policy read. Fifth, version control your policies—treat policy definitions as code and review changes through pull requests.

One common mistake is granting sudo capability too broadly. The sudo capability bypasses Vault's root protection mechanisms and should only be granted to operations policies used by infrastructure administrators. Similarly, granting create and update together should be done carefully—create allows creating new entries, while update allows modifying existing entries. In some contexts, granting only update without create prevents accidental creation of new secrets.

CapabilityHTTP MethodDescriptionRisk Level
readGETRead data at pathLow
createPOST (new)Create new dataMedium
updatePOST (existing)Update existing dataMedium
deleteDELETEDelete dataHigh
listLISTList keys at pathLow-Medium
sudoAnyAccess restricted pathsCritical

8. Audit Backend

The audit backend in Vault is the comprehensive logging system that records every authenticated request and response flowing through the Vault server. Audit logging is not optional for production deployments—it is a fundamental security requirement that provides visibility into who accessed what secrets, when, from where, and with what result. Vault's audit backend is designed with a critical security property: sensitive data in audit logs is automatically hashed (using HMAC) to prevent log-based credential theft while still providing enough information for security analysis and compliance reporting.

When a request arrives at Vault, the audit backend logs the complete request before it is processed, including the request path, HTTP method, request body (with sensitive fields hashed), the remote address of the client, the token accessor, and a monotonically increasing sequence number. After the request is processed, the audit backend logs the complete response, including the response body (with sensitive fields hashed), the HTTP status code, and a wrapping accessor (if applicable). This before-and-after logging provides a complete audit trail that can be used to reconstruct the exact sequence of events.

The hashing mechanism in Vault audit logs is sophisticated. Rather than simply redacting sensitive fields, Vault uses HMAC with a per-audit-device key to hash sensitive values. This means the values are not readable from the logs, but they can be correlated—if the same secret is accessed multiple times, the hashed values will be identical, allowing security teams to track access patterns. The HMAC key is stored in the audit device configuration and should be protected with the same rigor as Vault's unseal keys.

Vault supports two primary audit backends: file (writing JSON logs to a file on the local filesystem) and syslog (sending logs to the system syslog daemon). For production deployments, syslog is generally preferred because it integrates with centralized logging infrastructure (ELK, Splunk, Datadog, Fluentd). However, syslog-based logging may require additional configuration for reliability—syslog can drop messages under high load, and the log-raw and hmac settings need careful consideration.

Audit log entries contain rich metadata that enables detailed security analysis. Each entry includes: the type (request or response), the auth block (token accessor, policies, metadata, client token), the request block (id, operation, path, data, remote_address), the response block (data, wrap_info, warnings, auth), and a time timestamp. The sequence number in each entry allows detection of log gaps—if a sequence number is missing, it indicates potential log tampering or loss.

Here is a C# example of reading and analyzing Vault audit logs for security monitoring:

C#
using System.Text.Json;
using System.Security.Cryptography;

public class VaultAuditLogEntry
{
    public string Type { get; set; }
    public string Time { get; set; }
    public string AuditID { get; set; }
    public AuthInfo Auth { get; set; }
    public RequestInfo Request { get; set; }
    public ResponseInfo Response { get; set; }
}

public class AuthInfo
{
    public string ClientTokenAccessor { get; set; }
    public string[] Policies { get; set; }
    public Dictionary Metadata { get; set; }
    public string ClientToken { get; set; }
}

public class RequestInfo
{
    public string ID { get; set; }
    public string Operation { get; set; }
    public string Path { get; set; }
    public string RemoteAddress { get; set; }
    public Dictionary Data { get; set; }
}

public class ResponseInfo
{
    public Dictionary Data { get; set; }
    public int StatusCode { get; set; }
}

public class VaultAuditAnalyzer
{
    private readonly string _hmacKey;

    public VaultAuditAnalyzer(string hmacKey)
    {
        _hmacKey = hmacKey;
    }

    // Parse audit log entries from a file
    public async Task> ParseAuditLogAsync(string logFilePath)
    {
        var entries = new List();
        using var reader = new StreamReader(logFilePath);
        string line;
        while ((line = await reader.ReadLineAsync()) != null)
        {
            if (string.IsNullOrWhiteSpace(line)) continue;
            try
            {
                var entry = JsonSerializer.Deserialize(line);
                if (entry != null) entries.Add(entry);
            }
            catch (JsonException)
            {
                Console.WriteLine($"Warning: Could not parse audit log line: {line.Substring(0, Math.Min(100, line.Length))}");
            }
        }
        return entries;
    }

    // Detect suspicious access patterns
    public Dictionary> AnalyzeSuspiciousPatterns(
        List entries, int timeWindowMinutes = 60)
    {
        var findings = new Dictionary>();

        // Detect excessive secret access from single token
        var tokenAccessCounts = entries
            .Where(e => e.Type == "request" && e.Request?.Operation == "read")
            .GroupBy(e => e.Auth?.ClientTokenAccessor)
            .Where(g => g.Count() > 100)
            .ToDictionary(g => g.Key, g => g.Select(e => e.Request.Path).ToList());

        if (tokenAccessCounts.Any())
        {
            findings["excessive_access"] = tokenAccessCounts
                .SelectMany(kv => kv.Value.Select(p => $"Token {kv.Key} accessed {p} excessively"))
                .ToList();
        }

        // Detect access outside business hours
        var afterHoursAccess = entries
            .Where(e => e.Type == "request")
            .Where(e =>
            {
                if (DateTime.TryParse(e.Time, out var time))
                {
                    return time.Hour < 6 || time.Hour > 22;
                }
                return false;
            })
            .Select(e => $"After-hours access: {e.Auth?.ClientTokenAccessor} -> {e.Request?.Path}")
            .ToList();

        if (afterHoursAccess.Any())
        {
            findings["after_hours_access"] = afterHoursAccess;
        }

        // Detect failed authentication attempts
        var failedAuths = entries
            .Where(e => e.Request?.Path?.StartsWith("auth/") == true && e.Response?.StatusCode == 403)
            .GroupBy(e => e.Request?.RemoteAddress)
            .Where(g => g.Count() > 5)
            .ToDictionary(g => g.Key, g => g.Count().ToString());

        if (failedAuths.Any())
        {
            findings["brute_force_attempt"] = failedAuths
                .Select(kv => $"IP {kv.Key} had {kv.Value} failed auth attempts")
                .ToList();
        }

        // Detect access to sensitive paths
        var sensitivePaths = new[] { "sys/policies", "sys/auth", "sys/mounts", "sys/seal" };
        var sensitiveAccess = entries
            .Where(e => e.Type == "request" && sensitivePaths.Any(sp => e.Request?.Path?.StartsWith(sp) == true))
            .Select(e => $"Sensitive path access: {e.Auth?.ClientTokenAccessor} -> {e.Request?.Path}")
            .ToList();

        if (sensitiveAccess.Any())
        {
            findings["sensitive_path_access"] = sensitiveAccess;
        }

        return findings;
    }

    // Generate compliance report
    public string GenerateComplianceReport(List entries, DateTime from, DateTime to)
    {
        var scopedEntries = entries
            .Where(e => DateTime.TryParse(e.Time, out var t) && t >= from && t <= to)
            .ToList();

        var report = new
        {
            Period = $"{from:yyyy-MM-dd} to {to:yyyy-MM-dd}",
            TotalRequests = scopedEntries.Count(e => e.Type == "request"),
            TotalResponses = scopedEntries.Count(e => e.Type == "response"),
            UniqueTokens = scopedEntries.Select(e => e.Auth?.ClientTokenAccessor).Distinct().Count(),
            TopAccessedPaths = scopedEntries
                .Where(e => e.Type == "request")
                .GroupBy(e => e.Request?.Path)
                .OrderByDescending(g => g.Count())
                .Take(10)
                .Select(g => new { Path = g.Key, Count = g.Count() })
                .ToList(),
            FailedRequests = scopedEntries.Count(e => e.Type == "response" && e.Response?.StatusCode >= 400),
            SensitivePathAccesses = scopedEntries.Count(e =>
                e.Type == "request" &&
                (e.Request?.Path?.StartsWith("sys/") == true ||
                 e.Request?.Path?.Contains("/creds/") == true))
        };

        return JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true });
    }
}

For production audit log management, several operational considerations are critical. First, log rotation and retention must comply with organizational and regulatory requirements—financial services may require 7-year retention while healthcare requires specific HIPAA-compliant storage. Second, log integrity must be ensured—append-only storage or cryptographic verification of log chains prevents tampering. Third, log performance must be monitored—audit logging adds latency to every request, and under high load, slow audit backends can become bottlenecks. Fourth, log alerting should be configured for suspicious patterns—unusual access volumes, after-hours access, access to sensitive paths, or failed authentication attempts.

A recommended production architecture for Vault audit logs is: Vault Server → Syslog (local buffer) → Fluentd/Filebeat → Kafka (reliable delivery) → Elasticsearch/OpenSearch (storage and indexing) → Kibana/Grafana (visualization) → Alerting (PagerDuty/Slack). This pipeline provides reliable delivery, scalable storage, fast search, and real-time alerting. The HMAC key should be stored separately from the logs and accessible only to authorized security analysts who need to correlate hashed values.

Log FieldDescriptionExample
typeRequest or Response"request", "response"
timeTimestamp (RFC3339)"2026-07-15T10:30:00Z"
auth.client_token_accessorToken accessor (hashed)"hmac-sha256:abc123..."
auth.policiesPolicies on the token["app-production", "db-readonly"]
request.idUnique request identifier"d1e2f3a4-b5c6-..."
request.operationRead, update, create, delete"read"
request.pathAPI path"database/creds/my-role"
request.remote_addressClient IP address"10.0.1.50"
response.dataResponse data (sensitive fields hashed){"username": "hmac...", "password": "hmac..."}
response.wrap_infoResponse wrapping info{"accessor": "hmac...", "ttl": 300}

9. High Availability

High availability (HA) in Vault ensures that the secrets management platform remains operational despite individual node failures, network partitions, or maintenance events. Vault's HA architecture is built around the concept of a leader node that handles all write operations and one or more standby nodes that serve read requests and can quickly take over leadership if the current leader fails. This active-standby pattern ensures that there is always exactly one node processing writes, preventing split-brain scenarios that could corrupt the storage backend.

The recommended storage backend for HA deployments is Vault Integrated Storage (Raft). Raft is a consensus algorithm that provides strong consistency guarantees across a cluster of Vault nodes. Unlike external storage backends (Consul, DynamoDB, PostgreSQL), Raft is built directly into the Vault binary, eliminating the operational complexity of managing a separate storage cluster. In a Raft cluster, all nodes participate in the consensus protocol—one node is the leader, and all others are followers. The leader handles all write operations and replicates them to followers. When the leader fails, the followers hold a election and one of them becomes the new leader within seconds.

A production Vault cluster typically consists of 5 nodes (for quorum tolerance of 2 failures) or 3 nodes (for single failure tolerance). The nodes communicate over a dedicated network interface (the cluster_addr) separate from the client-facing API address. Each node has its own seal/unseal state, and all nodes must be unsealed for the cluster to be fully operational. In practice, with auto-unseal configured (using cloud KMS), nodes automatically unseal on startup, making recovery from failures nearly instantaneous.

Disaster recovery (DR) in Vault involves maintaining a separate cluster that can take over operations if the primary cluster becomes entirely unavailable. Vault supports two DR strategies: performance standby replication (for read scaling within a data center) and disaster recovery replication (for cross-data-center resilience). In DR replication, the primary cluster continuously replicates its state to the secondary cluster. If the primary becomes unavailable, the secondary can be promoted to primary, and operations continue with minimal disruption.

graph TB subgraph Primary_DC[Primary Data Center] LB1[Load Balancer] V1[Vault Node 1 - Leader] V2[Vault Node 2 - Standby] V3[Vault Node 3 - Standby] LB1 --> V1 LB1 --> V2 LB1 --> V3 V1 <-->|Raft Consensus| V2 V2 <-->|Raft Consensus| V3 V1 <-->|Raft Consensus| V3 end subgraph DR_DC[DR Data Center] LB2[Load Balancer] V4[Vault Node 4 - DR Secondary] V5[Vault Node 5 - DR Secondary] V6[Vault Node 6 - DR Secondary] LB2 --> V4 LB2 --> V5 LB2 --> V6 V4 <-->|Raft Consensus| V5 V5 <-->|Raft Consensus| V6 V4 <-->|Raft Consensus| V6 end V1 -->|Replication Stream| V4 V2 -->|Replication Stream| V5 V3 -->|Replication Stream| V6 CloudKMS[Cloud KMS - Auto Unseal] CloudKMS --> V1 CloudKMS --> V2 CloudKMS --> V3 CloudKMS --> V4 CloudKMS --> V5 CloudKMS --> V6

The Performance Standby pattern is used within a single data center to scale read operations. Performance standby nodes can process read requests locally without forwarding to the leader, providing linear read scaling. This is particularly valuable for workloads with high read-to-write ratios—secrets management typically involves many more reads (applications fetching credentials) than writes (credential rotation, policy updates). Performance standbys can serve the Transit engine's encrypt/decrypt operations entirely locally, providing sub-millisecond latency for encryption operations.

Here is a C# example of a Vault client with HA awareness and automatic failover:

C#
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
using Polly;
using Polly.CircuitBreaker;

public class VaultHAClient
{
    private readonly List _vaultAddresses;
    private readonly string _vaultToken;
    private int _currentAddressIndex;
    private readonly IAsyncPolicy _retryPolicy;
    private readonly IAsyncPolicy _circuitBreakerPolicy;

    public VaultHAClient(List vaultAddresses, string vaultToken)
    {
        _vaultAddresses = vaultAddresses;
        _vaultToken = vaultToken;
        _currentAddressIndex = 0;

        // Circuit breaker: open after 3 consecutive failures, half-open after 30s
        _circuitBreakerPolicy = Policy
            .Handle()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 3,
                durationOfBreak: TimeSpan.FromSeconds(30),
                onBreak: (ex, duration) =>
                    Console.WriteLine($"Circuit opened for {duration.TotalSeconds}s due to: {ex.Message}"),
                onReset: () =>
                    Console.WriteLine("Circuit closed - Vault node healthy")
            );

        // Retry policy with exponential backoff
        _retryPolicy = Policy
            .Handle()
            .Or()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
                onRetry: (result, delay, attempt, context) =>
                    Console.WriteLine($"Retry {attempt} after {delay.TotalSeconds}s")
            );
    }

    // Create a Vault client with automatic failover
    private IVaultClient CreateClient(string address)
    {
        var authMethod = new TokenAuthMethodInfo(_vaultToken);
        var settings = new VaultClientSettings(address, authMethod)
        {
            HttpTimeout = TimeSpan.FromSeconds(10)
        };
        return new VaultClient(settings);
    }

    // Health check with automatic failover
    public async Task GetHealthWithFailoverAsync()
    {
        for (int i = 0; i < _vaultAddresses.Count; i++)
        {
            int index = (_currentAddressIndex + i) % _vaultAddresses.Count;
            string address = _vaultAddresses[index];

            try
            {
                var client = CreateClient(address);
                var health = await _retryPolicy.ExecuteAsync(async () =>
                {
                    // Attempt to read a known path to verify health
                    try
                    {
                        var secret = await client.V1.Secrets.KV.V2.ReadSecretAsync(
                            path: "health-check",
                            mountPoint: "secret"
                        );
                        return new VaultHealth { IsHealthy = true, ActiveNode = address };
                    }
                    catch (Exception)
                    {
                        return new VaultHealth { IsHealthy = false, ActiveNode = address };
                    }
                });

                if (health.IsHealthy)
                {
                    _currentAddressIndex = index;
                    Console.WriteLine($"Active Vault node: {address}");
                    return health;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Node {address} unavailable: {ex.Message}");
            }
        }

        throw new Exception("All Vault nodes are unavailable");
    }

    // Read secrets with automatic failover to next node
    public async Task ReadSecretWithFailoverAsync(string path, string mountPoint = "secret")
    {
        var exceptions = new List();

        for (int i = 0; i < _vaultAddresses.Count; i++)
        {
            int index = (_currentAddressIndex + i) % _vaultAddresses.Count;
            string address = _vaultAddresses[index];

            try
            {
                var client = CreateClient(address);
                var secret = await client.V1.Secrets.KV.V2.ReadSecretAsync(
                    path: path,
                    mountPoint: mountPoint
                );

                // Update current index to prefer this node going forward
                _currentAddressIndex = index;
                return secret.Data.Data;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to read from {address}: {ex.Message}");
                exceptions.Add(ex);
            }
        }

        throw new AggregateException(
            "Failed to read secret from any Vault node", exceptions);
    }

    // Check cluster health and leader status
    public async Task GetClusterHealthAsync()
    {
        var report = new ClusterHealthReport
        {
            CheckedAt = DateTime.UtcNow,
            Nodes = new List()
        };

        foreach (var address in _vaultAddresses)
        {
            try
            {
                var client = CreateClient(address);
                var health = await client.V1.SystemBackend.GetHealthAsync();

                report.Nodes.Add(new NodeHealth
                {
                    Address = address,
                    IsLeader = health.IsLeader,
                    IsPerfStandby = health.IsPerformanceStandby,
                    ClusterID = health.ClusterID,
                    ClusterName = health.ClusterName,
                    Version = health.Version,
                    IsHealthy = true
                });
            }
            catch (Exception ex)
            {
                report.Nodes.Add(new NodeHealth
                {
                    Address = address,
                    IsHealthy = false,
                    Error = ex.Message
                });
            }
        }

        report.IsClusterHealthy = report.Nodes.All(n => n.IsHealthy);
        report.LeaderNode = report.Nodes.FirstOrDefault(n => n.IsLeader)?.Address;
        return report;
    }
}

public class VaultHealth
{
    public bool IsHealthy { get; set; }
    public string ActiveNode { get; set; }
}

public class ClusterHealthReport
{
    public DateTime CheckedAt { get; set; }
    public bool IsClusterHealthy { get; set; }
    public string LeaderNode { get; set; }
    public List Nodes { get; set; }
}

public class NodeHealth
{
    public string Address { get; set; }
    public bool IsLeader { get; set; }
    public bool IsPerfStandby { get; set; }
    public string ClusterID { get; set; }
    public string ClusterName { get; set; }
    public string Version { get; set; }
    public bool IsHealthy { get; set; }
    public string Error { get; set; }
}

Key operational considerations for HA include: load balancer configuration (health checks must verify the node is unsealed and initialized), session affinity (not strictly required but can reduce latency for token-based operations), network latency between nodes (Raft consensus requires low-latency communication; nodes should be in the same availability zone or data center for optimal performance), and automated recovery procedures (scripts or operators that handle node replacement, seal/unseal, and cluster rebalancing).

HA StrategyUse CaseRPORTOComplexity
Raft Cluster (3 nodes)Single DC, basic HA0 (sync)<30sLow
Raft Cluster (5 nodes)Single DC, fault-tolerant0 (sync)<30sMedium
Performance StandbyRead scaling within DC0 (sync)<30sMedium
DR ReplicationCross-DC disaster recoverySeconds<5minHigh
Multi-DC with Auto-FailoverGlobal deploymentSeconds<5minVery High

10. Vault Agent

Vault Agent is a client-side daemon that simplifies the interaction between applications and Vault. It handles authentication, secret retrieval, lease renewal, and template rendering, allowing applications to consume secrets without directly interacting with Vault's API. This is particularly valuable in environments where modifying application code to integrate with Vault is impractical or undesirable—Vault Agent can inject secrets into files, environment variables, or render templates that applications already consume.

The core capabilities of Vault Agent include auto-auth (automatically authenticating to Vault using a configured auth method), secret caching (caching retrieved secrets locally to reduce Vault load and improve availability during Vault outages), lease management (automatically renewing leases before they expire and re-fetching secrets when leases are revoked), and template rendering (rendering secrets into arbitrary file formats using Go templates).

Auto-Auth is the mechanism by which Vault Agent authenticates to Vault and maintains a valid token. It supports the same auth methods as the Vault server (Kubernetes, AWS, AppRole, etc.). The auto-auth method handles the entire authentication lifecycle—initial authentication, token renewal, and re-authentication when the token expires. The token is stored locally in a encrypted file and is never exposed to the application.

Sidecar Injection in Kubernetes is the most common deployment pattern for Vault Agent. When a pod is annotated with Vault-specific annotations, the Vault Agent Injector mutating admission webhook automatically injects an init container and a sidecar container into the pod. The init container authenticates to Vault and retrieves secrets before the application container starts. The sidecar container continuously monitors for secret changes and updates the rendered templates. This pattern provides transparent secret injection with zero application code changes.

Here is a C# example showing how to configure Vault Agent templates for application configuration:

C#
// This is a configuration generator for Vault Agent template rendering.
// The actual template files are used by Vault Agent running as a sidecar.

public class VaultAgentConfigGenerator
{
    // Generate the Vault Agent configuration file
    public static string GenerateAgentConfig(string vaultAddress, string authPath)
    {
        return $@"
# Vault Agent Configuration
vault {{
  address = ""{vaultAddress}""
}}

auto_auth {{
  method ""kubernetes"" {{
    config {{
      role = ""my-application""
      token_path = ""/var/run/secrets/kubernetes.io/serviceaccount/token""
    }}
  }}

  sink ""file"" {{
    config {{
      path = ""/vault/.vault-token""
    }}
  }}
}}

cache {{
  use_auto_auth_token = true
}}

# Listener for application to retrieve secrets
listener ""tcp"" {{
  address = ""127.0.0.1:8100""
  tls_disable = true
}}

# Template for database credentials
template {{
  source      = ""/vault/templates/db-creds.ctmpl""
  destination = ""/vault/secrets/database.json""
  perms       = 0640
  command     = ""/app/scripts/reload-db-connection.sh""
}}

# Template for API keys
template {{
  source      = ""/vault/templates/api-keys.ctmpl""
  destination = ""/vault/secrets/api-keys.json""
  perms       = 0640
}}

# Template for application config with secrets embedded
template {{
  source      = ""/vault/templates/app-config.ctmpl""
  destination = ""/etc/app/config.yaml""
  perms       = 0640
  command     = ""/app/scripts/restart-graceful.sh""
}}
";
    }

    // Generate the database credentials template
    public static string GenerateDBCredentialsTemplate()
    {
        return @"
{{- with secret ""database/creds/my-application-role"" -}}
{
  ""database"": {
    ""host"": ""prod-db.internal"",
    ""port"": 5432,
    ""username"": ""{{ .Data.username }}"",
    ""password"": ""{{ .Data.password }}"",
    ""database"": ""appdb"",
    ""sslmode"": ""require"",
    ""connection_timeout"": 30,
    ""max_open_connections"": 25
  },
  ""lease"": {
    ""id"": ""{{ .LeaseID }}"",
    ""duration"": {{ .LeaseDuration }},
    ""renewable"": {{ .Renewable }}
  }
}
{{- end -}}
";
    }

    // Generate the API keys template
    public static string GenerateAPIKeysTemplate()
    {
        return @"
{{- with secret ""secret/data/my-application/api-keys"" -}}
{
  ""stripe"": {
    ""api_key"": ""{{ .Data.data.stripe_key }}"",
    ""webhook_secret"": ""{{ .Data.data.stripe_webhook }}""
  },
  ""sendgrid"": {
    ""api_key"": ""{{ .Data.data.sendgrid_key }}"",
    ""from_email"": ""{{ .Data.data.sendgrid_from }}""
  },
  ""datadog"": {
    ""api_key"": ""{{ .Data.data.datadog_key }}"",
    ""app_key"": ""{{ .Data.data.datadog_app_key }}""
  }
}
{{- end -}}
";
    }

    // Generate the application config template (mixing static and dynamic config)
    public static string GenerateAppConfigTemplate()
    {
        return @"
# Application Configuration
# Auto-generated by Vault Agent - DO NOT EDIT MANUALLY

server:
  host: 0.0.0.0
  port: 8080
  environment: production

database:
{{- with secret ""database/creds/my-application-role"" }}
  host: prod-db.internal
  port: 5432
  username: {{ .Data.username }}
  password: {{ .Data.password }}
  pool_size: 25
  ssl_mode: require
{{- end }}

redis:
{{- with secret ""secret/data/my-application/redis"" }}
  host: {{ .Data.data.host }}
  port: {{ .Data.data.port }}
  password: {{ .Data.data.password }}
  db: 0
{{- end }}

logging:
  level: info
  format: json
  output: stdout

metrics:
  enabled: true
  port: 9090
  path: /metrics
";
    }

    // Generate Kubernetes deployment manifest with Vault Agent annotations
    public static string GenerateK8sDeployment()
    {
        return @"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-application
  namespace: production
  labels:
    app: my-application
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-application
  template:
    metadata:
      labels:
        app: my-application
      annotations:
        # Vault Agent Injector annotations
        vault.hashicorp.com/agent-inject: ""true""
        vault.hashicorp.com/role: ""my-application""
        vault.hashicorp.com/agent-inject-status: ""update""
        
        # Database credentials template
        vault.hashicorp.com/agent-inject-secret-database: ""database/creds/my-application-role""
        vault.hashicorp.com/agent-inject-template-database: ""/vault/templates/db-creds.ctmpl""
        
        # API keys template
        vault.hashicorp.com/agent-inject-secret-apikeys: ""secret/data/my-application/api-keys""
        vault.hashicorp.com/agent-inject-template-apikeys: ""/vault/templates/api-keys.ctmpl""
        
        # Agent resource limits
        vault.hashicorp.com/agent-requests-cpu: ""50m""
        vault.hashicorp.com/agent-limits-cpu: ""100m""
        vault.hashicorp.com/agent-requests-mem: ""64Mi""
        vault.hashicorp.com/agent-limits-mem: ""128Mi""
    spec:
      serviceAccountName: my-application
      containers:
        - name: my-application
          image: myregistry/my-application:v2.1.0
          ports:
            - containerPort: 8080
          env:
            - name: DB_CONFIG_PATH
              value: ""/vault/secrets/database.json""
            - name: API_KEYS_PATH
              value: ""/vault/secrets/api-keys.json""
          volumeMounts:
            - name: vault-secrets
              mountPath: /vault/secrets
              readOnly: true
      volumes:
        - name: vault-secrets
          emptyDir:
            medium: Memory
            sizeLimit: 10Mi
";
    }
}

Vault Agent's template engine uses Go's text/template syntax and provides several custom functions for interacting with Vault. The secret function retrieves a secret from a specific path, the with block provides scoped access to the secret data, and the secret function also supports lease information. Templates can include conditional logic, loops, and string manipulation, making them suitable for generating complex configuration files.

The performance characteristics of Vault Agent are important for production planning. The agent caches secrets locally, so once a secret is fetched, subsequent reads are served from cache without contacting Vault. The agent also handles lease renewal in the background, ensuring secrets are always fresh without application involvement. When a secret's lease expires and cannot be renewed, the agent re-fetches the secret from Vault and updates the rendered template. The command configuration in templates allows specifying a script to execute when a secret changes, enabling applications to reload configuration without restarting.

FeatureDescriptionBenefit
Auto-AuthAutomatic authentication and token managementNo token handling in application code
Secret CachingLocal caching of retrieved secretsReduced Vault load, offline availability
Template RenderingGo templates for secret injectionZero application code changes
Lease RenewalAutomatic lease managementAlways-fresh credentials
Sidecar InjectionKubernetes admission webhookTransparent deployment integration
Process SupervisionCan manage application processSecret-aware process lifecycle

11. Namespaces

Vault namespaces provide multi-tenancy isolation within a single Vault deployment. Each namespace is a completely isolated environment with its own auth methods, secrets engines, policies, and tokens. A secret in namespace A is completely invisible and inaccessible from namespace B, even if the path names are identical. This architectural feature is essential for organizations that need to share a single Vault infrastructure across multiple teams, business units, or environments while maintaining strict isolation.

Namespaces are a Vault Enterprise feature and are fundamental to operating Vault at enterprise scale. Without namespaces, an organization would need separate Vault clusters for each tenant, which dramatically increases operational complexity and cost. With namespaces, a single 5-node cluster can serve dozens of isolated tenants, each with their own policies, auth methods, and secrets engines. The root namespace is the top-level namespace where the Vault administrator operates, and child namespaces are created and managed by namespace administrators.

The isolation properties of namespaces are comprehensive. Auth method isolation means each namespace can have its own Kubernetes auth, LDAP auth, or OIDC auth configuration, pointing to different identity providers or using different role mappings. Secrets engine isolation means each namespace can mount its own database engines, KV stores, and transit engines at the same paths without interference. Policy isolation means policies in one namespace cannot grant access to paths in another namespace. Token isolation means tokens created in one namespace cannot be used in another namespace.

Namespaces also support hierarchical relationships. A parent namespace can create child namespaces and delegate administrative control. This enables organizational structures like: Root → Engineering → Platform → Production, where each level can manage its own sub-namespaces. The parent namespace administrator can set quotas and restrictions on child namespaces, preventing any single tenant from consuming excessive Vault resources.

Resource quotas at the namespace level provide additional control. Administrators can set secret count quotas (limiting the number of secrets stored in a namespace), lease count quotas (limiting the number of active leases), and rate limit quotas (limiting API request rates). These quotas prevent noisy-neighbor problems and ensure fair resource distribution across tenants.

When designing a namespace hierarchy, consider the following patterns. Environment-based isolation creates separate namespaces for production, staging, and development, with stricter policies in production. Team-based isolation creates namespaces per team or business unit, each managing their own secrets. Project-based isolation creates namespaces per project or product, useful in organizations where projects have different compliance requirements. Most enterprise deployments combine these patterns—for example, a top-level namespace per business unit with child namespaces per environment.

Isolation DimensionRoot NamespaceNamespace ANamespace B
Auth MethodsAll methodsOwn methods onlyOwn methods only
Secrets EnginesAll enginesOwn engines onlyOwn engines only
PoliciesGlobal policiesNamespace policies onlyNamespace policies only
TokensAll tokensNamespace tokens onlyNamespace tokens only
KV Secrets at "secret/"Root secretsA's secrets (isolated)B's secrets (isolated)
Audit LogsAll operationsA's operations onlyB's operations only

12. Vault Secrets Operator for Kubernetes

The Vault Secrets Operator (VSO) is a Kubernetes-native operator that synchronizes secrets from Vault into Kubernetes Secrets. Unlike Vault Agent, which runs as a sidecar container in each pod, the VSO operates at the cluster level, watching custom resources (CRDs) and maintaining corresponding Kubernetes Secrets. This approach centralizes secret management at the Kubernetes level, making secrets available as native Kubernetes resources that can be mounted as volumes or injected as environment variables in any pod.

The VSO introduces two primary custom resources: VaultStaticSecret and VaultDynamicSecret. A VaultStaticSecret maps a specific path in Vault's KV store to a Kubernetes Secret, with configurable sync intervals. A VaultDynamicSecret maps a Vault dynamic secret role (like a database credential role) to a Kubernetes Secret, with automatic lease management and rotation. The operator handles all Vault authentication, secret fetching, and Kubernetes Secret creation/updates.

The VSO uses Kubernetes auth to authenticate with Vault, which means it operates using a Kubernetes service account. The operator authenticates once and maintains a valid Vault token, handling renewal automatically. For multi-namespace Kubernetes deployments, the VSO can operate across namespaces, creating Kubernetes Secrets in the same namespace as the CRD that defines them.

One significant advantage of the VSO over Vault Agent is that it does not require sidecar containers in each pod. The operator runs once per cluster and manages secrets for all namespaces. This reduces resource consumption (no sidecar containers), simplifies pod specifications (no Vault annotations needed), and provides a single point of configuration for secret management across the entire cluster. However, this also means secrets are stored as Kubernetes Secrets, which are encrypted at rest by Kubernetes (if configured) but are accessible to anyone with Kubernetes Secrets access in that namespace.

The VSO also supports VaultPKISecret for issuing and managing X.509 certificates from Vault's PKI engine, storing the resulting certificates as Kubernetes TLS Secrets. This is valuable for service mesh environments where workloads need TLS certificates managed by Vault but consumed as standard Kubernetes TLS Secrets.

FeatureVault AgentVault Secrets Operator
Deployment ModelSidecar per podCluster-level operator
Resource UsagePer-pod overheadSingle deployment overhead
Secret AccessLocal files/env varsKubernetes Secrets
Template SupportGo templatesKubernetes Secret format
Lease ManagementPer-sidecarCentralized
ComplexityAnnotations per podCRD per secret
Best ForPer-pod secret customizationCluster-wide secret management

13. PKI Engine

The PKI secrets engine in Vault transforms Vault into a fully functional Certificate Authority (CA) capable of issuing, signing, and revoking X.509 certificates. In modern infrastructure, particularly with the adoption of service mesh technologies like Istio, Linkerd, and Consul Connect, the ability to dynamically issue short-lived TLS certificates is fundamental to implementing mutual TLS (mTLS) across services. Vault's PKI engine eliminates the need for long-lived certificates—which are a persistent security risk—and provides a complete certificate lifecycle management solution.

Vault's PKI engine can operate in two modes: as a root CA (the ultimate trust anchor) or as an intermediate CA (delegated to issue certificates under the root CA's authority). The recommended production pattern is a two-tier PKI hierarchy: a root CA that is offline or heavily restricted, and one or more intermediate CAs that handle day-to-day certificate issuance. This pattern limits the exposure of the root CA's private key while still providing the flexibility needed for operational certificate management.

The PKI engine's configuration involves several key components. The root certificate is generated when the PKI engine is first configured and is stored in Vault's encrypted storage. Intermediate certificates are signed by the root CA (either through Vault's built-in signing or through an external CSR process). Certificate roles define the constraints for issued certificates—allowed domains, TTL, key types, key sizes, allowed URI SANs, and other X.509 extensions. Certificate templates use Go templates to customize the certificate's content, including custom OIDs, extended key usage, and name constraints.

A C# example demonstrating Vault PKI certificate issuance and management:

C#
using VaultSharp;
using VaultSharp.V1.AuthMethods.Token;
using VaultSharp.V1.SecretsEngines.PKI;
using System.Security.Cryptography.X509Certificates;

// Initialize Vault client
var authMethod = new TokenAuthMethodInfo(Environment.GetEnvironmentVariable("VAULT_TOKEN"));
var settings = new VaultClientSettings("https://vault.internal:8200", authMethod);
var vaultClient = new VaultClient(settings);

// Configure the PKI engine
await vaultClient.V1.Secrets.PKI.ConfigureAsync(
    mountPoint: "pki",
    request: new PKIConfigureRequest
    {
        IssuingCertificates = new[] { "https://vault.internal:8200/v1/pki/ca" },
        CrlDistributionPoints = new[] { "https://vault.internal:8200/v1/pki/crl" }
    }
);

// Create a role for internal service certificates
await vaultClient.V1.Secrets.PKI.CreateOrUpdateRoleAsync(
    mountPoint: "pki",
    roleName: "internal-service",
    request: new PKICreateOrUpdateRoleRequest
    {
        AllowedDomains = new[] { "internal.service.consul", "internal.service.prod" },
        AllowSubdomains = true,
        AllowBareDomains = false,
        AllowGlobDomains = true,
        MaxTTL = "72h",
        DefaultTTL = "24h",
        KeyType = "ec",
        KeyBits = 256,
        RequireCn = true,
        EnforceHostnames = true,
        ServerFlag = true,
        ClientFlag = true,
        CodeSigningFlag = false,
        EmailProtectionFlag = false,
        KeyUsage = new[] { "DigitalSignature", "KeyEncipherment", "KeyAgreement" },
        ExtKeyUsage = new[] { "ServerAuth", "ClientAuth" }
    }
);

Console.WriteLine("PKI role 'internal-service' created.");

// Issue a certificate for a microservice
var certResponse = await vaultClient.V1.Secrets.PKI.GenerateCertificateAsync(
    mountPoint: "pki",
    roleName: "internal-service",
    request: new PKIGenerateCertificateRequest
    {
        CommonName = "payment-service.internal.service.prod",
        AltNames = "payment-service.internal.service.prod, payment.internal.service.prod",
        TTL = "24h",
        Format = "pem_bundle"
    }
);

// Parse the issued certificate
string certificatePem = certResponse.Data.Certificate;
string privateKeyPem = certResponse.Data.PrivateKey;
string serialNumber = certResponse.Data.SerialNumber;
int leaseDuration = certResponse.Data.LeaseDuration;

var cert = new X509Certificate2(
    Encoding.UTF8.GetBytes(certificatePem + privateKeyPem),
    (string)null,
    X509KeyStorageFlags.Exportable
);

Console.WriteLine($"Certificate issued:");
Console.WriteLine($"  Subject: {cert.Subject}");
Console.WriteLine($"  Issuer: {cert.Issuer}");
Console.WriteLine($"  Serial: {serialNumber}");
Console.WriteLine($"  Valid From: {cert.NotBefore:u}");
Console.WriteLine($"  Valid To: {cert.NotAfter:u}");
Console.WriteLine($"  Thumbprint: {cert.Thumbprint}");
Console.WriteLine($"  Key Algorithm: {cert.PublicKey.Oid.FriendlyName}");
Console.WriteLine($"  Key Size: {cert.PublicKey.Key.KeySize} bits");
Console.WriteLine($"  Lease Duration: {leaseDuration} seconds ({leaseDuration / 3600} hours)");

// List all certificates issued by a role
var certs = await vaultClient.V1.Secrets.PKI.ListCertificatesAsync(
    mountPoint: "pki",
    roleName: "internal-service"
);

Console.WriteLine($"\nTotal issued certificates for role 'internal-service': {certs.Data.Keys.Count}");

// Revoke a certificate that is no longer needed
await vaultClient.V1.Secrets.PKI.RevokeCertificateAsync(
    mountPoint: "pki",
    serialNumber: serialNumber
);

Console.WriteLine($"Certificate {serialNumber} revoked.");

// Check CRL status
var crl = await vaultClient.V1.Secrets.PKI.GetCRLAsync(mountPoint: "pki");
Console.WriteLine($"\nCRL last updated: {crl.Data.Headers}");
Console.WriteLine($"CRL entries: {crl.Data.RevokedCertificates?.Count ?? 0}");

The PKI engine's integration with Kubernetes is particularly powerful. When used with the Vault Secrets Operator's VaultPKISecret CRD, certificates can be automatically issued and stored as Kubernetes TLS Secrets, which are then available for Ingress controllers, service mesh sidecars, and application pods. The certificate can be configured to auto-renew before expiry, ensuring zero-downtime certificate rotation.

Security considerations for the PKI engine include: protecting the root CA's private key (ideally by keeping it offline or in a separate, more restricted Vault namespace), using ECDSA keys over RSA for better performance and security, limiting certificate TTLs to minimize the impact of compromised certificates, implementing CRL and OCSP for certificate revocation, and monitoring certificate issuance patterns for anomalies.

PKI ConfigurationValueRationale
Root CA Key TypeEC P-384Strongest ECC curve for root CA
Intermediate CA Key TypeEC P-256Balanced performance and security
Leaf Certificate Key TypeEC P-256Fast, secure, widely supported
Root CA TTL10 yearsLong-lived, rarely used directly
Intermediate CA TTL3-5 yearsReplaced periodically
Leaf Certificate TTL24-72 hoursShort-lived, auto-rotated
Key Size (RSA fallback)4096 bitsMinimum for production RSA

14. Security Hardening

Security hardening of a Vault deployment encompasses every layer of the system—from the physical or virtual infrastructure hosting Vault, through the cryptographic barrier that protects stored data, to the network policies that control access. Vault's security model is built on the principle that secrets should be protected even if the underlying infrastructure is compromised. The security barrier ensures that all data in the storage backend is encrypted, and the seal/unseal mechanism ensures that Vault cannot be accessed until a quorum of unseal key holders authorizes operation.

The seal/unseal mechanism is Vault's most fundamental security feature. When Vault is sealed, the master key used by the barrier is not in memory—all data in the storage backend is encrypted and inaccessible. To unseal Vault, a quorum of unseal key holders must provide their key shares (generated during initialization using Shamir's Secret Sharing). For a standard 5-key, 3-threshold configuration, any 3 of the 5 key holders must provide their shares. These shares are combined to reconstruct the master key, which is then loaded into memory and the barrier is opened.

Auto-unseal delegates the unseal operation to a trusted cloud KMS (Key Management Service) such as AWS KMS, Azure Key Vault, Google Cloud KMS, or HashiCorp Cloud Platform (HCP). When Vault starts, it retrieves the master key from the KMS using a pre-configured authentication mechanism (IAM role, managed identity, service account). This eliminates the operational burden of manual unseal ceremonies while maintaining security—the KMS is the trust anchor, and access to the KMS is controlled by cloud IAM policies.

Network security for Vault should follow zero-trust principles. All communication with Vault should be over TLS with mutual TLS (mTLS) where possible. The Vault API port (8200) should be accessible only to authorized clients, and the cluster communication port (8201) should be restricted to Vault cluster nodes. Network policies should enforce that only specific service accounts or IP ranges can communicate with Vault. In Kubernetes, NetworkPolicies can restrict Vault access to specific namespaces and pods.

Sealing with Azure Key Vault for auto-unseal is a common enterprise pattern:

C#
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using Azure.Security.KeyVault.Secrets;

public class VaultAutoUnsealManager
{
    private readonly KeyClient _keyClient;
    private readonly string _keyVaultUri;

    public VaultAutoUnsealManager(string keyVaultUri)
    {
        _keyVaultUri = keyVaultUri;
        var credential = new DefaultAzureCredential();
        _keyClient = new KeyClient(new Uri(keyVaultUri), credential);
    }

    // Configure Azure Key Vault for Vault auto-unseal
    public async Task ConfigureAutoUnsealAsync()
    {
        var credential = new DefaultAzureCredential();

        // Create a key in Azure Key Vault for Vault's master key
        var keyName = "vault-unseal-key";
        var key = await _keyClient.CreateRsaKeyAsync(new CreateRsaKeyOptions(keyName)
        {
            KeySize = 2048,
            KeyOperations =
            {
                KeyOperation.WrapKey,
                KeyOperation.UnwrapKey
            },
            ExpiresOn = DateTimeOffset.UtcNow.AddYears(2)
        });

        Console.WriteLine($"Created unseal key: {key.Value.Name}");
        Console.WriteLine($"Key ID: {key.Value.Id}");

        // Verify key permissions
        var keyVaultClient = new KeyClient(new Uri(_keyVaultUri), credential);
        var retrievedKey = await keyVaultClient.GetKeyAsync(keyName);
        Console.WriteLine($"Key algorithm: {retrievedKey.Value.KeyType}");
        Console.WriteLine($"Key operations: {string.Join(", ", retrievedKey.Value.KeyOperations)}");

        var config = new AutoUnsealConfig
        {
            KeyVaultUri = _keyVaultUri,
            KeyName = keyName,
            KeyType = "azurekeyvault",
            // Vault server configuration (to be placed in vault.hcl)
            VaultConfig = $@"
seal ""azurekeyvault"" {{
  tenant_id     = ""{Environment.GetEnvironmentVariable("AZURE_TENANT_ID")}""
  client_id     = ""{Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")}""
  client_secret = ""{Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET")}""
  vault_name    = ""{ExtractVaultName(_keyVaultUri)}""
  key_name      = ""{keyName}""
}}
"
        };

        return config;
    }

    // Monitor key health and rotation status
    public async Task CheckKeyHealthAsync()
    {
        var keyName = "vault-unseal-key";
        var key = await _keyClient.GetKeyAsync(keyName);

        var health = new UnsealKeyHealth
        {
            KeyName = key.Value.Name,
            KeyId = key.Value.Id.ToString(),
            CreatedOn = key.Value.CreatedOn,
            UpdatedOn = key.Value.UpdatedOn,
            ExpiresOn = key.Value.ExpiresOn,
            IsExpired = key.Value.ExpiresOn < DateTimeOffset.UtcNow,
            DaysUntilExpiry = key.Value.ExpiresOn.HasValue
                ? (key.Value.ExpiresOn.Value - DateTimeOffset.UtcNow).Days
                : (int?)null,
            KeyType = key.Value.KeyType.ToString(),
            KeySize = key.Value.KeySize
        };

        // Alert if key is expiring soon
        if (health.DaysUntilExpiry.HasValue && health.DaysUntilExpiry.Value < 90)
        {
            Console.WriteLine($"WARNING: Unseal key expires in {health.DaysUntilExpiry.Value} days!");
            Console.WriteLine("Please rotate the key before expiry to prevent Vault seal.");
        }

        return health;
    }

    // Rotate the unseal key (requires re-sealing Vault)
    public async Task RotateUnsealKeyAsync()
    {
        Console.WriteLine("Starting unseal key rotation...");
        Console.WriteLine("WARNING: This operation will seal Vault. Ensure HA is configured.");

        // Create new key
        var newKeyName = $"vault-unseal-key-{DateTime.UtcNow:yyyyMMdd}";
        var newKey = await _keyClient.CreateRsaKeyAsync(new CreateRsaKeyOptions(newKeyName)
        {
            KeySize = 2048,
            KeyOperations = { KeyOperation.WrapKey, KeyOperation.UnwrapKey },
            ExpiresOn = DateTimeOffset.UtcNow.AddYears(2)
        });

        Console.WriteLine($"New key created: {newKey.Value.Name}");
        Console.WriteLine("To complete rotation:");
        Console.WriteLine("1. Update vault.hcl with the new key_name");
        Console.WriteLine("2. Seal Vault: vault operator seal");
        Console.WriteLine("3. Update all cluster nodes with new configuration");
        Console.WriteLine("4. Unseal all nodes with: vault operator unseal");

        return new KeyRotationResult
        {
            NewKeyName = newKeyName,
            NewKeyId = newKey.Value.Id.ToString(),
            RequiresReSeal = true,
            NextSteps = new[]
            {
                "Update vault.hcl on all nodes",
                "Seal the Vault cluster",
                "Update configuration",
                "Unseal all nodes"
            }
        };
    }

    private string ExtractVaultName(string keyVaultUri)
    {
        return new Uri(keyVaultUri).Host.Split('.')[0];
    }
}

public class AutoUnsealConfig
{
    public string KeyVaultUri { get; set; }
    public string KeyName { get; set; }
    public string KeyType { get; set; }
    public string VaultConfig { get; set; }
}

public class UnsealKeyHealth
{
    public string KeyName { get; set; }
    public string KeyId { get; set; }
    public DateTimeOffset? CreatedOn { get; set; }
    public DateTimeOffset? UpdatedOn { get; set; }
    public DateTimeOffset? ExpiresOn { get; set; }
    public bool IsExpired { get; set; }
    public int? DaysUntilExpiry { get; set; }
    public string KeyType { get; set; }
    public int? KeySize { get; set; }
}

public class KeyRotationResult
{
    public string NewKeyName { get; set; }
    public string NewKeyId { get; set; }
    public bool RequiresReSeal { get; set; }
    public string[] NextSteps { get; set; }
}

Additional security hardening measures include response wrapping (where Vault wraps secrets in a one-time-use token with a short TTL, preventing secret exposure in transit), control groups (requiring multiple approvals before sensitive operations are performed), sentinel policies (enterprise-grade policy-as-code with advanced conditional logic), mount tuning (setting appropriate TTLs and max-TTLs on all mounts), and audit log monitoring (detecting anomalous access patterns in real time).

Infrastructure-level hardening includes running Vault on dedicated nodes (no other services), using encrypted storage for the Raft data directory, configuring firewall rules to restrict network access, enabling SELinux or AppArmor profiles, using read-only filesystems where possible, running as a non-root user, and regularly patching the Vault binary and operating system. The Vault binary should always be verified against HashiCorp's published checksums before deployment.

Hardening LayerMeasureImpact
CryptographicAES-256-GCM barrier encryptionData encrypted at rest
Access ControlShamir seal/unseal (3-of-5)Quorum required to operate
Auto-UnsealCloud KMS integrationAutomated recovery
NetworkTLS + mTLSEncrypted transit
NetworkFirewall + NetworkPoliciesAccess restriction
OperationalResponse wrappingSecret transit protection
AuthorizationLeast-privilege policiesMinimal access surface
MonitoringAudit log alertingAnomaly detection

15. Monitoring and Operations

Operating Vault in production requires comprehensive monitoring, alerting, and operational runbooks. Vault is a critical infrastructure component—when it becomes unavailable, applications cannot retrieve secrets, certificates cannot be renewed, and dynamic credentials cannot be generated. A well-monitored Vault deployment includes metrics collection, health checking, log aggregation, alerting, and automated recovery procedures.

Vault exposes a rich set of metrics through its /v1/sys/metrics endpoint (or the /metrics Prometheus endpoint when configured). Key metrics include vault_core_active (1 if this node is the active leader), vault_core_unsealed (1 if unsealed), vault_runtime_sys_bytes (system memory usage), vault_runtime_alloc_bytes (heap allocation), vault_token_count_by_auth_type (tokens created by auth method), vault_secret_lease_count_by_engine_total (active leases by engine), and vault_request_count (total API requests). These metrics provide visibility into Vault's health, performance, and usage patterns.

Health checks for Vault should verify multiple dimensions: the node is initialized and unsealed, the node can reach the storage backend, the node can communicate with other cluster members, and the node can process requests (not in a degraded state). A simple HTTP health check is insufficient—Vault's /v1/sys/health endpoint returns different status codes for different states: 200 (initialized, unsealed, and active), 429 (standby), 472 (DR secondary), 473 (performance standby), 501 (not initialized), and 503 (sealed). Load balancers should be configured to route traffic only to nodes returning 200.

Log aggregation for Vault audit logs is critical for security monitoring. The recommended pipeline is: Vault audit log → Fluentd/Filebeat (collection) → Kafka (buffering and reliable delivery) → Elasticsearch/OpenSearch (storage and indexing) → Kibana/Grafana (visualization and search) → Alerting rules (security and operational alerts). Key alerting rules include: excessive failed authentication attempts (potential brute force), access to sensitive paths (sys/policies, sys/seals), unusual secret access volumes, after-hours access patterns, and certificate issuance anomalies.

Operational runbooks should cover common scenarios: node failure and replacement, seal/unseal procedures, certificate rotation, storage backend migration, performance tuning, capacity planning, disaster recovery failover, and version upgrades. Each runbook should include pre-conditions, step-by-step procedures, verification steps, and rollback procedures.

graph TB subgraph Vault_Cluster[Vault Cluster] V1[Vault Node 1 - Leader] V2[Vault Node 2 - Standby] V3[Vault Node 3 - Standby] end V1 -->|/metrics| Prometheus[Prometheus] V2 -->|/metrics| Prometheus V3 -->|/metrics| Prometheus V1 -->|Audit Logs| Fluentd[Fluentd/Fluent Bit] V2 -->|Audit Logs| Fluentd V3 -->|Audit Logs| Fluentd Fluentd --> Kafka[Kafka Buffer] Kafka --> Elasticsearch[Elasticsearch] Elasticsearch --> Kibana[Kibana Dashboard] Prometheus --> Grafana[Grafana Dashboards] Grafana --> AlertManager[Alert Manager] AlertManager --> PagerDuty[PagerDuty] AlertManager --> Slack[Slack Alerts] Prometheus --> VaultExporter[Vault Exporter] VaultExporter -->|Custom Metrics| Prometheus

Capacity planning for Vault involves monitoring several dimensions: request throughput (requests per second), lease count (active dynamic secrets), storage usage (especially for KV stores and Raft data), memory usage (Vault caches data in memory), and connection count (concurrent API connections). As a general guideline, a single Vault node can handle 500-1500 requests per second depending on the workload (Transit operations are faster than database credential generation), and a 5-node Raft cluster can handle 2000-5000 requests per second with performance standbys.

Version upgrade procedures for Vault should follow a rolling upgrade pattern: upgrade standby nodes first, then perform a leadership transfer to an upgraded node, then upgrade the remaining nodes. This ensures zero-downtime upgrades. Before upgrading, always review the changelog for breaking changes, test the upgrade in a staging environment, and take a backup of the Raft snapshot.

MetricPrometheus NameAlert ThresholdDescription
Active Nodevault_core_active!= 1 on any nodeCluster has no active leader
Sealed Nodesvault_core_unsealedAny node sealedNode is sealed and unusable
Request Ratevault_request_count> 80% of capacityHigh request throughput
Active Leasesvault_secret_lease_count> 100000Too many active leases
Memory Usagevault_runtime_alloc_bytes> 80% of allocatedHigh memory consumption
Go Routinesvault_runtime_go_routines> 10000Potential goroutine leak
Auth Failuresvault_token_create_errors> 10/minPossible brute force attack
Post-Replication Lagvault_core_post_replication> 5sReplication falling behind

16. Comparison with AWS Secrets Manager, Azure Key Vault, SOPS

Choosing a secrets management solution depends on organizational requirements, existing infrastructure, compliance needs, and operational capabilities. Vault, AWS Secrets Manager, Azure Key Vault, and SOPS each serve the secrets management problem differently, with distinct trade-offs in functionality, portability, operational complexity, and cost.

HashiCorp Vault is a platform-agnostic, self-hosted (or HCP-managed) secrets management solution that provides the most comprehensive feature set: dynamic secrets, encryption as a service, PKI, SSH signing, and fine-grained policy control. Its primary advantage is vendor independence—Vault works identically across AWS, Azure, GCP, and on-premises infrastructure. This makes it ideal for multi-cloud or hybrid-cloud environments. The trade-off is operational complexity: running a Vault cluster requires dedicated infrastructure, monitoring, and operational expertise.

AWS Secrets Manager is a fully managed service that stores and rotates secrets with automatic rotation through Lambda functions. Its primary advantage is tight integration with the AWS ecosystem—IAM policies control access, Lambda handles rotation, and CloudTrail logs all access. However, it only supports static secrets (no dynamic generation), has limited encryption capabilities (no encryption-as-a-service), and is AWS-only. It is an excellent choice for AWS-native applications that do not need dynamic secrets or cross-cloud portability.

Azure Key Vault is Azure's managed HSM and secrets management service. It provides hardware security module (HSM) backing for keys, certificate management, and secret storage. Like AWS Secrets Manager, it integrates deeply with its cloud ecosystem (Azure RBAC, Managed Identities, Azure Policy). It supports HSM-backed keys for compliance requirements that mandate FIPS 140-2 Level 2 or Level 3 validation. However, it lacks dynamic secrets, encryption-as-a-service, and is limited to Azure.

SOPS (Secrets OPerationS) is a tool by Mozilla for managing encrypted secrets files. Unlike Vault, SOPS does not run a server—it encrypts secrets in files (YAML, JSON, ENV) using a key hierarchy (AWS KMS, GCP KMS, Azure Key Vault, PGP). SOPS is file-based, meaning secrets are stored in version control (encrypted) and decrypted at deployment time. Its primary advantage is simplicity and GitOps compatibility. However, it lacks dynamic secrets, audit logging, access control, and encryption-as-a-service. SOPS is best for small teams or projects that need basic secret management without the overhead of running a server.

The choice between these solutions is often not either/or. Many organizations use Vault as their primary secrets management platform while leveraging cloud-native services for specific use cases—for example, using Vault for application secrets and dynamic credentials while using AWS Secrets Manager for Lambda function environment variables. The key is understanding the trade-offs and designing a coherent strategy that meets all requirements.

FeatureHashiCorp VaultAWS Secrets ManagerAzure Key VaultSOPS
DeploymentSelf-hosted / HCPFully managedFully managedCLI tool
Dynamic SecretsYes (DB, AWS, PKI, SSH)NoNoNo
Encryption as a ServiceYes (Transit engine)Limited (KMS only)Yes (Key Vault keys)No
PKI / Certificate IssuanceYes (Full CA)AWS Certificate ManagerYes (Key Vault certs)No
Multi-CloudYesAWS onlyAzure onlyCloud KMS support
Access ControlGranular policiesIAM policiesAzure RBACFile permissions
Audit LoggingBuilt-in, comprehensiveCloudTrailAzure MonitorNo
High AvailabilityRaft cluster / DRManaged HAManaged HAN/A (file-based)
Operational ComplexityHighLowLowLow
Cost ModelInfrastructure + licensePer-secret + per-API callPer-key + per-operationFree (open source)

17. Interview Q&A

Q1: How does Vault ensure that secrets are never exposed in plaintext in the storage backend?

Vault uses a security barrier with AES-256-GCM encryption. All data is encrypted before being written to the storage backend using a master key that is derived from the unseal keys through Shamir's Secret Sharing. The master key exists only in Vault's memory while Vault is unsealed. When Vault is sealed, the master key is not in memory, and all data in storage is encrypted and inaccessible. Even if an attacker gains root access to the storage system, the data is cryptographically protected and useless without the unseal keys.

Q2: Explain the difference between dynamic secrets and static secrets. When would you use each?

Static secrets are fixed values (API keys, passwords) stored in Vault's KV engine. They persist until manually changed and are shared across all consumers. Dynamic secrets are generated on demand by secrets engines (Database, AWS, PKI) and are unique to each request with a configurable TTL. Use static secrets for values that cannot be dynamically generated (third-party API keys, configuration values). Use dynamic secrets for infrastructure credentials (database users, cloud IAM, SSH certificates) where short-lived, unique credentials provide better security through limited blast radius and automatic revocation.

Q3: How would you design Vault for a multi-cloud Kubernetes deployment spanning AWS EKS and Azure AKS?

Deploy a single Vault cluster (either on-premises or HCP Vault) with Raft storage spanning both cloud regions. Configure Kubernetes auth for both EKS and AKS clusters using their respective Kubernetes API endpoints. Use namespaces to isolate secrets per cloud/per team. Configure cloud-specific auth methods (AWS IAM auth for EKS pods, Azure auth for AKS pods). Use the Transit engine for cross-cloud encryption (both clouds encrypt/decrypt through the same Vault cluster). Deploy Vault agents as sidecars in both clusters for transparent secret injection. Network connectivity between the Vault cluster and both Kubernetes clusters must be reliable and low-latency.

Q4: What happens when a Vault node fails in a Raft cluster? How does the system recover?

When a Vault node fails, the Raft consensus protocol detects the failure through missed heartbeats (typically within 5-10 seconds). If the failed node was the leader, the remaining nodes hold an election and one of them becomes the new leader within seconds. Clients are automatically redirected to the new leader through the load balancer. If the failed node was a standby, there is no impact on write operations—the leader continues processing requests. The failed node can be replaced by starting a new Vault instance with the same cluster configuration and joining the existing cluster. Raft handles data replication to bring the new node up to date automatically.

Q5: How does Vault's response wrapping protect secrets in transit?

Response wrapping creates a one-time-use token that wraps the actual secret. Instead of returning the secret directly in the API response, Vault returns a wrapping token with a short TTL (e.g., 60 seconds). The client must then call /v1/sys/wrapping/unwrap with this token to retrieve the actual secret. The wrapping token can only be used once—if someone intercepts the token and uses it before the intended recipient, the legitimate recipient's unwrap call will fail. This protects against log exposure (the actual secret is never in the HTTP response logs), proxy logging, and intermediary access. Response wrapping is essential for audit compliance in environments where HTTP traffic is logged.

Q6: Explain how Vault's Transit engine supports envelope encryption and why it's important.

In envelope encryption, data is encrypted with a per-record Data Encryption Key (DEK), and the DEK itself is encrypted (wrapped) with a Key Encryption Key (KEK) stored in Vault's Transit engine. The encrypted DEK is stored alongside the encrypted data. To decrypt, the application retrieves the encrypted DEK, sends it to Vault for unwrapping, and uses the plaintext DEK to decrypt the data. This pattern is important because: (1) the KEK never leaves Vault's security boundary, (2) each record has a unique DEK limiting blast radius, (3) key rotation only requires re-wrapping DEKs with the new KEK, not re-encrypting all data, and (4) the KEK can be managed with strict access controls separate from the data access controls.

Q7: How would you handle Vault disaster recovery across two data centers?

Deploy a primary Vault cluster in DC1 (5-node Raft cluster) and a DR secondary cluster in DC2 (5-node Raft cluster). Enable disaster recovery replication: the primary continuously replicates its state to the secondary. The DR secondary is read-only and cannot be written to directly. In a disaster, the DR secondary is promoted to primary (using vault operator promote), and clients are redirected to the new primary. The RPO (Recovery Point Objective) depends on replication lag (typically seconds), and the RTO (Recovery Time Objective) is the time to promote the secondary and update DNS/load balancer (typically under 5 minutes). For zero RPO, consider a stretched cluster with nodes in both DCs, though this requires very low inter-DC latency.

Q8: What are the security implications of Vault Agent caching, and how do you mitigate risks?

Vault Agent caches secrets locally in memory and/or on disk to reduce Vault load and provide availability during Vault outages. The security implications are: (1) cached secrets are in plaintext in the agent's memory, accessible to anyone with process-level access to the host, (2) if the agent's token file is compromised, cached secrets could be decrypted. Mitigations include: running the agent as a sidecar (namespace isolation), using encrypted file permissions (0600 on token and cache files), setting short cache TTLs, using Kubernetes security contexts to limit host access, running agents on nodes with encrypted disks, and using response wrapping so the agent never stores the actual token in plaintext. For high-security workloads, disable caching entirely and accept the performance impact.

Q9: How do you design Vault policies for a microservices architecture with 200 services?

Use a template-based policy approach. Create a base policy template that defines the common permissions pattern (read own database credentials, read own KV config, encrypt/decrypt with own transit key). Use Vault's identity groups and policy templating to automatically generate per-service policies. In Kubernetes, map each service's Kubernetes service account to a Vault role with a bound policy. Use path-based conventions strictly: database/creds/{service-name}-*, secret/data/{service-name}/*, transit/encrypt/{service-name}-key. Automate policy creation through a CI/CD pipeline that generates HCL policies from a service catalog. Implement policy testing in CI using vault policy fmt and simulated capability checks. Use namespaces if services belong to different teams with different trust levels.

Q10: Explain how Vault's lease mechanism works and why it matters for security.

Every secret generated by Vault (dynamic credentials, wrapped tokens, certificates) is associated with a lease. The lease has a TTL (time-to-live) and a lease ID. When the TTL expires, Vault automatically revokes the secret—dropping database users, revoking AWS credentials, or marking certificates as revoked. Leases can be renewed (extending the TTL) up to the engine's maximum TTL. This mechanism matters because it ensures no secret lives longer than intended. Even if an attacker steals a credential, the credential will automatically expire and be revoked. Combined with unique-per-consumer credentials, this provides defense in depth: limited blast radius (unique credentials) and limited time window (automatic expiry). The lease mechanism also simplifies credential rotation—applications receive new credentials automatically when the old lease expires and a new one is created.

Ayodhyya - System Design Blog Series | HashiCorp Vault Secrets Management - Senior+ Guide

© 2026 Ayodhyya. All rights reserved.