<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Hashicorp Tools Chat]]></title><description><![CDATA[Hashicorp Tools Chat]]></description><link>https://myros.net</link><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 01:45:30 GMT</lastBuildDate><atom:link href="https://myros.net/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How Vault Enterprise Actually Counts Clients]]></title><description><![CDATA[Summary for decision-makers

Vault Enterprise bills by client identity, not by token count
Entity-backed authentication (LDAP, OIDC, userpass, AppRole with aliases) produces stable, predictable billing
Non-entity tokens (direct token creation, orphan...]]></description><link>https://myros.net/how-vault-enterprise-actually-counts-clients</link><guid isPermaLink="true">https://myros.net/how-vault-enterprise-actually-counts-clients</guid><category><![CDATA[vault-billing]]></category><category><![CDATA[Vault]]></category><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[hashicorp]]></category><category><![CDATA[Devops]]></category><category><![CDATA[secrets management]]></category><dc:creator><![CDATA[Miroslav Milak]]></dc:creator><pubDate>Wed, 14 Jan 2026 20:49:58 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-summary-for-decision-makers">Summary for decision-makers</h2>
<ul>
<li>Vault Enterprise bills by <strong>client identity</strong>, not by token count</li>
<li><strong>Entity-backed authentication</strong> (LDAP, OIDC, userpass, AppRole with aliases) produces stable, predictable billing</li>
<li><strong>Non-entity tokens</strong> (direct token creation, orphan tokens, token auth) are billed by unique policy set and can grow unexpectedly</li>
<li>Migrating to entity-backed auth is the only reliable way to control client counts</li>
</ul>
<h3 id="heading-current-best-practices-to-control-client-counts">Current best practices to control client counts</h3>
<ol>
<li>Prefer entity-aware auth methods whenever possible</li>
<li>Use entity-aware auth for CI/CD and bootstrap (AppRole, Kubernetes auth, cloud IAM, JWT/OIDC)</li>
<li>For lifecycle independence, re-authenticate via entity-aware method instead of using orphan tokens</li>
<li>Use batch tokens (non-orphan) for short-lived credentials</li>
<li>Audit regularly with <code>sys/internal/counters/activity</code> and audit logs</li>
<li>Monitor for policy name changes and refactors (they create new client identities)</li>
</ol>
<hr />
<p>If you run Vault Enterprise in production, you will eventually encounter a moment where the client count doesn't match your expectations.</p>
<p>Nothing changed in your workloads.
Nothing new was deployed.
And yet the number moved—sometimes sharply.</p>
<p>This post exists to explain why.</p>
<p>Not based on assumptions.
Not based on sales slides.
But based on controlled, repeatable testing against Vault Enterprise.</p>
<p>The short version is this:</p>
<p><strong>Vault Enterprise does not count tokens.
It counts clients.
And client identity is derived in two fundamentally different ways, depending on how a token was created.</strong></p>
<p>Understanding that distinction explains most billing surprises—and helps you avoid creating them.</p>
<hr />
<h2 id="heading-scope-and-methodology">Scope and methodology</h2>
<p>All findings below are based on:</p>
<ul>
<li>Vault Enterprise 1.21.1</li>
<li>Raft storage</li>
<li>Tokens were actually exercised against Vault (unused tokens do not count as clients)</li>
<li>Client counts observed via <code>sys/internal/counters/activity</code></li>
</ul>
<p>Where Vault behavior is documented, I say so.
Where behavior is empirical, I am explicit about that.</p>
<hr />
<h2 id="heading-the-two-client-identity-models">The two client identity models</h2>
<p>Vault Enterprise uses two distinct client identity models.</p>
<h3 id="heading-1-entity-backed-clients-stable-predictable">1. Entity-backed clients (stable, predictable)</h3>
<p>When authentication happens via an entity-aware auth method, Vault assigns an <code>entity_id</code>.</p>
<p>Examples:</p>
<ul>
<li>userpass</li>
<li>LDAP</li>
<li>OIDC / JWT</li>
<li>AppRole</li>
</ul>
<p>In this model:</p>
<ul>
<li>All tokens associated with the same <code>entity_id</code> count as one client</li>
<li>Token policies do not affect client count</li>
<li>Child tokens inherit the entity automatically</li>
</ul>
<p>This behavior is documented and stable.</p>
<blockquote>
<p><strong>Note:</strong> This deduplication can be dramatic. A single entity-backed user with <code>sudo</code> capability on <code>auth/token/create</code> can mint tokens with any policy—not just subsets of their own. This bypasses the normal "child policies must be subset of parent" rule (see <a target="_blank" href="https://developer.hashicorp.com/vault/api-docs/auth/token#create-token">token create documentation</a>). The result: one user creating hundreds of unique policy set combinations, all collapsing to one client. This is efficient for billing, but means client count no longer reflects actual token or policy diversity.</p>
</blockquote>
<p>Effectively, client identity behaves as:</p>
<pre><code>client ~ namespace + entity_id
</code></pre><h3 id="heading-2-non-entity-clients-policy-based-unstable">2. Non-entity clients (policy-based, unstable)</h3>
<p>When a token is created without entity context, Vault assigns no <code>entity_id</code>.</p>
<p>In this case, client identity is derived from the policy name combination attached to the token.</p>
<p>Empirically, client identity behaves as if derived from:</p>
<pre><code>client ~ namespace + sorted(unique_policy_names)
</code></pre><p>This behavior is not fully documented, but it is consistent, repeatable, and observable across versions. And it is the source of most billing surprises.</p>
<blockquote>
<p><strong>Important:</strong> Since Vault 1.9, non-entity tokens with identical namespace + policy sets are deduplicated into a single client (via a constructed entity). Pre-1.9 behavior was much worse—often counting one client per token. The combinatorial explosion described below still applies, but only unique policy sets count.</p>
</blockquote>
<hr />
<h2 id="heading-the-policy-set-explosion">The policy-set explosion</h2>
<p>For non-entity tokens, each unique set of attached policy names creates a separate billed client. Order is irrelevant—<code>[p1, p2]</code> and <code>[p2, p1]</code> are the same client.</p>
<p>Example:</p>
<pre><code>[p1]        -&gt; client A
[p2]        -&gt; client B
[p1, p2]    -&gt; client C
[p1, p3]    -&gt; client D
</code></pre><p>Four unique policy sets.
Four billable clients.</p>
<p>With N policies, the worst case is:</p>
<pre><code><span class="hljs-number">2</span>^N - <span class="hljs-number">1</span> clients
</code></pre><p>This is combinatorial growth, not theoretical—and it appears quickly in real systems.</p>
<p>In a real environment with 10 policies, modest variation in policy sets can already produce hundreds of clients where entity-backed authentication would produce one.</p>
<hr />
<h2 id="heading-how-non-entity-tokens-are-created">How non-entity tokens are created</h2>
<p>Non-entity tokens are not edge cases.
They are easy to create—often accidentally.</p>
<h3 id="heading-confirmed-creation-paths">Confirmed creation paths</h3>
<p><strong>Direct token creation</strong></p>
<pre><code class="lang-bash">vault token create
</code></pre>
<p>Common during bootstrap, automation, or CI/CD.</p>
<p><strong>Token chains from a non-entity parent</strong></p>
<p>If the parent token has no entity, children inherit none.
The entire chain remains non-entity forever.</p>
<p><strong>Token auth method</strong></p>
<pre><code>auth/token/create
</code></pre><p>Tokens created via the token auth method have no entity by design.</p>
<p><strong>Orphan tokens</strong></p>
<pre><code class="lang-bash">vault token create -orphan
</code></pre>
<p>Even when created by an entity-backed user, orphan tokens explicitly drop entity context.</p>
<p>Once a token is non-entity, policy-set-based identity rules apply and client counting becomes unstable.</p>
<p>These paths aren't rare edge cases—they're everyday automation patterns that sneak in during bootstrap, scaling, or CI/CD integration.</p>
<hr />
<h2 id="heading-why-orphan-tokens-exist">Why orphan tokens exist</h2>
<p>Orphan tokens solve a real operational problem: <strong>lifecycle independence</strong>.</p>
<p>They are commonly used for:</p>
<ul>
<li>Long-running batch jobs</li>
<li>CI/CD pipelines</li>
<li>Service bootstrap tokens</li>
<li>Cross-team delegation</li>
<li>Break-glass scenarios</li>
</ul>
<p>In all cases, the requirement is the same:</p>
<blockquote>
<p>"This token must survive the revocation or expiration of the token that created it."</p>
</blockquote>
<p>That is exactly what orphan tokens do. But there is a cost.</p>
<hr />
<h2 id="heading-the-critical-orphan-token-rule">The critical orphan token rule</h2>
<p><strong>Orphan tokens never inherit entity_id.</strong>
Not sometimes. Not conditionally. Never.</p>
<p>This was tested via:</p>
<ul>
<li>Orphan creation from root</li>
<li>Orphan creation from entity-backed users with sudo</li>
<li>Orphan token child chains</li>
</ul>
<p>In all cases:</p>
<ul>
<li><code>entity_id = n/a</code></li>
<li>Token is treated as non-entity</li>
<li>Policy-based client identity applies</li>
</ul>
<h3 id="heading-child-vs-orphan-tokens">Child vs orphan tokens</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Property</td><td>Child token</td><td>Orphan token</td></tr>
</thead>
<tbody>
<tr>
<td>Inherits entity_id</td><td>Yes (if parent has one)</td><td>No</td></tr>
<tr>
<td>Lifecycle</td><td>Revoked with parent</td><td>Independent</td></tr>
<tr>
<td>Client identity</td><td>Entity-based</td><td>Policy-set-based</td></tr>
<tr>
<td>Client explosion risk</td><td>Only if parent is non-entity</td><td>Always</td></tr>
</tbody>
</table>
</div><p>There is no native Vault mechanism that provides both:</p>
<ul>
<li>lifecycle independence <strong>and</strong></li>
<li>entity-anchored client identity</li>
</ul>
<p>If you need both, you must re-authenticate via an entity-aware auth method.</p>
<hr />
<h2 id="heading-children-of-orphan-tokens">Children of orphan tokens</h2>
<p>Orphan tokens do not inherit entity context.
Neither do their children.</p>
<pre><code>Entity user (entity_id: abc<span class="hljs-number">-123</span>)
    |
    +-- [creates orphan] --&gt; orphan token (entity_id: n/a)
                                  |
                                  +-- child A (entity_id: n/a)
                                  |       |
                                  |       +-- grandchild (entity_id: n/a)
                                  |
                                  +-- child B (entity_id: n/a)
</code></pre><p>Once a token becomes non-entity, the entire chain remains non-entity forever. There is no mechanism to recover entity context downstream—the only option is to discard the chain and re-authenticate.</p>
<hr />
<h2 id="heading-batch-tokens-the-safe-alternative">Batch tokens: the safe alternative</h2>
<p>Batch tokens behave differently—and this matters.</p>
<p>Batch tokens inherit entity_id like service tokens, but can't be renewed or create children—making them safer for short-lived automation where you don't want token sprawl.</p>
<pre><code class="lang-bash">vault token create -<span class="hljs-built_in">type</span>=batch -policy=p1
</code></pre>
<p>Result:</p>
<ul>
<li>entity_id inherited</li>
<li>No new client created</li>
</ul>
<p>However:</p>
<pre><code class="lang-bash">vault token create -<span class="hljs-built_in">type</span>=batch -orphan -policy=p1
</code></pre>
<p>Result:</p>
<ul>
<li>entity_id lost</li>
<li>New non-entity client (based on policy set)</li>
</ul>
<h3 id="heading-batch-vs-service-tokens">Batch vs service tokens</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Token Type</td><td>entity_id</td><td>New Client?</td><td>Renewable</td><td>Children</td></tr>
</thead>
<tbody>
<tr>
<td>Service (child)</td><td>Inherited</td><td>No</td><td>Yes</td><td>Yes</td></tr>
<tr>
<td>Service (orphan)</td><td>Lost</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
<tr>
<td>Batch (normal)</td><td>Inherited</td><td>No</td><td>No</td><td>No</td></tr>
<tr>
<td>Batch (orphan)</td><td>Lost</td><td>Yes</td><td>No</td><td>No</td></tr>
</tbody>
</table>
</div><p>Batch tokens are appropriate when you need:</p>
<ul>
<li>Short-lived credentials</li>
<li>No renewal</li>
<li>No child tokens</li>
<li>Entity-anchored billing</li>
</ul>
<p>From a billing perspective, batch tokens behave like entity-anchored child tokens—unless created with the <code>-orphan</code> flag.</p>
<hr />
<h2 id="heading-remediation-options">Remediation options</h2>
<p>If you're currently using non-entity tokens and want to reduce client count:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Current State</td><td>Recommended Migration</td><td>Client Impact</td></tr>
</thead>
<tbody>
<tr>
<td>Root token -&gt; child tokens</td><td>Entity-aware auth method</td><td>Reduces to 1 client per identity</td></tr>
<tr>
<td>Orphan tokens for CI/CD</td><td>Entity-aware auth with short TTL</td><td>Entity-anchored, no explosion</td></tr>
<tr>
<td>Orphan tokens for long jobs</td><td>Re-auth via entity-aware method</td><td>Fresh entity-backed token</td></tr>
<tr>
<td>Token auth method</td><td>Migrate to entity-aware auth</td><td>Stable identity</td></tr>
<tr>
<td>Non-entity token chains</td><td>Re-auth at chain root</td><td>Entire chain becomes entity-backed</td></tr>
<tr>
<td>Short-lived automation</td><td>Batch tokens (non-orphan)</td><td>Inherits parent entity</td></tr>
</tbody>
</table>
</div><p>The common theme: <strong>re-authenticate via an entity-aware method</strong> rather than creating tokens from tokens.</p>
<hr />
<h2 id="heading-what-does-not-affect-client-identity">What does not affect client identity</h2>
<p>Explicitly tested and not used in client counting:</p>
<ul>
<li>Token accessor</li>
<li>Token creation time</li>
<li>Parent token</li>
<li>Periodic vs non-periodic</li>
<li>Policy content</li>
</ul>
<p><strong>Policy name matters.</strong>
Renaming a policy creates a new client.
Changing what it allows does not.</p>
<hr />
<h2 id="heading-namespaces-matter">Namespaces matter</h2>
<p>Client identity is namespace-scoped, which means the same policy set used in different namespaces counts as different clients. This is expected behavior, but it's easy to overlook in multi-namespace deployments where teams might unknowingly duplicate policy structures across namespaces.</p>
<hr />
<h2 id="heading-auditing-your-current-exposure">Auditing your current exposure</h2>
<pre><code class="lang-bash">vault <span class="hljs-built_in">read</span> sys/internal/counters/activity/monthly
</code></pre>
<h3 id="heading-warning-signs">Warning signs</h3>
<p>Look for these patterns in your activity data:</p>
<ul>
<li>Non-entity clients significantly outnumber entity clients</li>
<li>Client count grows after policy refactors or renames</li>
<li>Many clients with similar or overlapping policy sets</li>
<li>Client spikes without corresponding workload changes</li>
</ul>
<p>If you see these patterns, you likely have policy-set-based client identity at work—and it may be worth investigating migration options.</p>
<hr />
<h2 id="heading-documentation-and-stability">Documentation and stability</h2>
<p>HashiCorp explicitly warns that token behavior may change across versions (see the <a target="_blank" href="https://developer.hashicorp.com/vault/docs/concepts/tokens">token documentation</a>). That alone is reason to be cautious about relying on undocumented heuristics for billing-sensitive behavior. The patterns described in this post have been stable since Vault 1.9, but always verify after major upgrades.</p>
<hr />
<h2 id="heading-why-this-matters">Why this matters</h2>
<p>This behavior enables both:</p>
<p><strong>Over-counting</strong>
Policy set sprawl leads to combinatorial client growth, which leads to billing surprises.</p>
<p><strong>Under-counting</strong>
Many workloads anchored under a single entity can collapse into fewer clients than expected—which might seem like a win until you realize your billing doesn't reflect actual usage patterns.</p>
<h3 id="heading-real-world-example-dozens-of-microservices-as-1-client">Real-world example: dozens of microservices as 1 client</h3>
<p>A client with a lot of microservices <em>each using distinct policy sets</em> but Vault bills them as a single client.</p>
<p><strong>How it works:</strong></p>
<ol>
<li>Entity-backed auth (userpass or AppRole) → gets <code>entity_id</code></li>
<li><code>sudo</code> on <code>auth/token/create</code> → bypasses "child must be subset of parent" rule</li>
<li>Creates tokens with any arbitrary policy combination</li>
<li>All tokens inherit same <code>entity_id</code> → <strong>1 client</strong></li>
</ol>
<p>This is not a bug. It's how entity deduplication works when combined with <code>sudo</code> capability on token creation (see <a target="_blank" href="https://developer.hashicorp.com/vault/api-docs/auth/token#create-token">token create documentation</a>).</p>
<p><strong>Trade-off:</strong> Billing is efficient, but client count no longer reflects workload diversity. To maintain visibility, consider using audit logs with client ID correlation, or split into multiple entities where usage diversity matters for reporting.</p>
<hr />
<p>This behavior isn't misuse or misconfiguration. It's simply how Vault works today. The mechanics are consistent and predictable once you understand them, but most teams learn the hard way—staring at an unexpected invoice wondering what changed.</p>
<p>This post exists so you don't have to go through that.</p>
<hr />
<p><em>Tested on Vault Enterprise 1.21.1 (Jan 2026). Behavior stable since 1.9, but always verify after upgrades as token mechanics may evolve.</em></p>
]]></content:encoded></item><item><title><![CDATA[HashiCorp Vault with Docker Compose: Advanced Clustering and Auto-Unseal (Part 4)]]></title><description><![CDATA[HashiCorp Vault with Docker Compose: Advanced Auto-Unseal with Transit (Part 4)
Subtitle: Transition from manual unsealing to auto-unseal using a dedicated transit Vault
In Part 3, you built a three-node Vault cluster using Raft storage and retry_joi...]]></description><link>https://myros.net/hashicorp-vault-docker-compose-part4</link><guid isPermaLink="true">https://myros.net/hashicorp-vault-docker-compose-part4</guid><category><![CDATA[hashicorp]]></category><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[secrets management]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Security]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[clustering]]></category><category><![CDATA[high availability]]></category><category><![CDATA[Auto Unseal]]></category><dc:creator><![CDATA[Miroslav Milak]]></dc:creator><pubDate>Thu, 15 May 2025 14:29:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747320075094/667fdd52-e443-4aa6-9f17-c90cf60302c2.avif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-hashicorp-vault-with-docker-compose-advanced-auto-unseal-with-transit-part-4">HashiCorp Vault with Docker Compose: Advanced Auto-Unseal with Transit (Part 4)</h1>
<p><strong>Subtitle</strong>: Transition from manual unsealing to auto-unseal using a dedicated transit Vault</p>
<p>In <a target="_blank" href="hashicorp-vault-docker-compose-part3">Part 3</a>, you built a three-node Vault cluster using Raft storage and <code>retry_join</code> for automated clustering, with separate storage volumes for high availability (HA). However, each node required manual unsealing, which is cumbersome. In this final, comprehensive part of our series, we’ll enhance the cluster by implementing <strong>auto-unseal</strong> using the <strong>transit secrets engine</strong> hosted on a dedicated Vault container, eliminating manual key entry. For simplicity, we use transit, but <strong>production environments should use a Key Management Service (KMS)</strong> (e.g., AWS KMS, Azure Key Vault) or <strong>Hardware Security Module (HSM)</strong> for enhanced security. This intermediate-to-advanced guide builds on Parts 1-3, delivering a production-like setup.</p>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, ensure you have:</p>
<ul>
<li><p><strong>Docker</strong> (version 20.10 or later). Install from <a target="_blank" href="https://docs.docker.com/get-docker/">Docker’s guide</a>.</p>
</li>
<li><p><strong>Docker Compose</strong> (version 2.0 or later). See <a target="_blank" href="https://docs.docker.com/compose/install/">Docker Compose installation</a>.</p>
</li>
<li><p><strong>Vault CLI</strong>. Download from <a target="_blank" href="https://releases.hashicorp.com/vault/">releases.hashicorp.com/vault</a> or use a package manager (e.g., <code>brew install vault</code> on macOS).</p>
</li>
<li><p><strong>jq</strong> for parsing JSON (optional, for token extraction). Install via <code>brew install jq</code> (macOS) or equivalent.</p>
</li>
<li><p>A text editor (e.g., VS Code).</p>
</li>
<li><p>Familiarity with Parts 1-3 (development mode, persistent storage, Raft clustering with <code>retry_join</code>).</p>
</li>
<li><p>Commands are tested on Linux/macOS; Windows users may need to use <code>set</code> instead of <code>export</code>.</p>
</li>
</ul>
<hr />
<h2 id="heading-reviewing-the-cluster-with-manual-unseal">Reviewing the Cluster with Manual Unseal</h2>
<p>In Part 3, you set up a three-node cluster (<code>vault1</code>, <code>vault2</code>, <code>vault3</code>) using Raft storage for HA, with <code>retry_join</code> for automated node discovery and separate volumes (<code>vault1-data</code>, <code>vault2-data</code>, <code>vault3-data</code>) to support Raft’s independent storage needs. As explained in Part 3, this transitioned from Part 2’s shared file storage to enable clustering. Each node requires manual unsealing with a key, which ensures security but is time-consuming. Let’s verify the setup before transitioning to auto-unseal.</p>
<h3 id="heading-diagram">Diagram</h3>
<pre><code class="lang-mermaid">graph TD
    A[Client: Browser/CLI] --&gt; B[Docker Compose]
    B --&gt; C[Vault Node 1: Port 8200&lt;br&gt;Manual Unseal]
    B --&gt; D[Vault Node 2: Port 8201&lt;br&gt;Manual Unseal]
    B --&gt; E[Vault Node 3: Port 8202&lt;br&gt;Manual Unseal]
    C --&gt; F[Vault1 Data]
    D --&gt; G[Vault2 Data]
    E --&gt; H[Vault3 Data]
</code></pre>
<h3 id="heading-step-1-verify-the-existing-cluster">Step 1: Verify the Existing Cluster</h3>
<p>Assuming you have the Part 3 setup, verify the cluster:</p>
<ol>
<li><p>Navigate to <code>vault-cluster</code> and check containers:</p>
<pre><code class="lang-bash"> docker ps
</code></pre>
<p> You should see <code>vault1</code>, <code>vault2</code>, and <code>vault3</code>.</p>
</li>
<li><p>Check cluster status on <code>vault1</code>:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 <span class="hljs-built_in">export</span> VAULT_TOKEN=&lt;root-token-from-part3&gt;
 vault operator raft list-peers
</code></pre>
<p> <strong>Sample Response</strong>:</p>
<pre><code class="lang-bash"> Node      Address          State       Voter
 ----      -------          -----       -----
 vault1    vault1:8201      leader      <span class="hljs-literal">true</span>
 vault2    vault2:8201      follower    <span class="hljs-literal">true</span>
 vault3    vault3:8201      follower    <span class="hljs-literal">true</span>
</code></pre>
</li>
<li><p>Test manual unseal by restarting:</p>
<pre><code class="lang-bash"> docker-compose down
 docker-compose up -d
</code></pre>
</li>
<li><p>Unseal each node (using keys from Part 3):</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 vault operator unseal &lt;vault1-unseal-key&gt;
 <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8201
 vault operator unseal &lt;vault2-unseal-key&gt;
 <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8202
 vault operator unseal &lt;vault3-unseal-key&gt;
</code></pre>
</li>
<li><p>Verify a secret:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 vault kv get secret/my-app
</code></pre>
</li>
</ol>
<p>This manual process underscores the need for automation.</p>
<hr />
<h2 id="heading-transit-based-auto-unseal-with-dedicated-vault">Transit-Based Auto-Unseal with Dedicated Vault</h2>
<p>Manual unsealing is tedious, time-consuming, and prone to errors, especially in a production environment where downtime matters. Auto-unseal offers a better way by automating the process, making Vault startup faster and more reliable. Now, we’ll introduce a completely separate vault-transit container to host the transit secrets engine, enabling auto-unseal for vault1, vault2, and vault3. This standalone vault-transit instance ensures isolation from the cluster. We’ll set up vault-transit first, then reconfigure the cluster to use auto-unseal, eliminating manual key entry.</p>
<h3 id="heading-diagram-1">Diagram</h3>
<pre><code class="lang-mermaid">graph TD
    A[Client: Browser/CLI] --&gt; B[Docker Compose]
    B --&gt; C[Vault Node 1: Port 8200&lt;br&gt;Transit Auto-Unseal]
    B --&gt; D[Vault Node 2: Port 8201&lt;br&gt;Transit Auto-Unseal]
    B --&gt; E[Vault Node 3: Port 8202&lt;br&gt;Transit Auto-Unseal]
    B --&gt; F[Vault-Transit: Port 8203&lt;br&gt;Transit Engine]
    C --&gt; G[Vault1 Data]
    D --&gt; H[Vault2 Data]
    E --&gt; I[Vault3 Data]
    F --&gt; J[Vault-Transit Data]
    C --&gt; F[Transit Auto-Unseal]
    D --&gt; F[Transit Auto-Unseal]
    E --&gt; F[Transit Auto-Unseal]
</code></pre>
<h3 id="heading-step-2-update-docker-compose">Step 2: Update Docker Compose</h3>
<p>Update <code>docker-compose.yml</code> to add <code>vault-transit</code>, keeping separate volumes:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">vault1:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault1</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8200:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault1-data:/vault/file</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config/vault1:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-net</span>

  <span class="hljs-attr">vault2:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault2</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8201:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault2-data:/vault/file</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config/vault2:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-net</span>

  <span class="hljs-attr">vault3:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault3</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8202:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault3-data:/vault/file</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config/vault3:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-net</span>

  <span class="hljs-attr">vault-transit:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault-transit</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8203:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-transit-data:/vault/file</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config/vault-transit:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-net</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">vault1-data:</span>
  <span class="hljs-attr">vault2-data:</span>
  <span class="hljs-attr">vault3-data:</span>
  <span class="hljs-attr">vault-transit-data:</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">vault-net:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
</code></pre>
<p><strong>Key Notes</strong>:</p>
<ul>
<li><p>Retains Part 3’s separate volumes (<code>vault1-data</code>, <code>vault2-data</code>, <code>vault3-data</code>) for Raft.</p>
</li>
<li><p>Adds <code>vault-transit-data</code> for <code>vault-transit</code>.</p>
</li>
<li><p><code>vault-net</code> ensures communication.</p>
</li>
</ul>
<h3 id="heading-step-3-configure-transit-vault">Step 3: Configure Transit Vault</h3>
<ol>
<li><p>Create a <code>vault-transit</code> subdirectory in <code>vault-config</code> and add <code>vault.hcl</code>:</p>
<p> <strong>vault-config/vault-transit/vault.hcl</strong>:</p>
<pre><code class="lang-bash"> ui = <span class="hljs-literal">true</span>
 api_addr = <span class="hljs-string">"http://vault-transit:8200"</span>

 storage <span class="hljs-string">"file"</span> {
   path = <span class="hljs-string">"/vault/file"</span>
 }

 listener <span class="hljs-string">"tcp"</span> {
   address = <span class="hljs-string">"0.0.0.0:8200"</span>
   tls_disable = 1
 }
</code></pre>
</li>
<li><p>Start containers:</p>
<pre><code class="lang-bash"> docker-compose up -d
</code></pre>
</li>
<li><p>Initialize <code>vault-transit</code>:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8203
 vault operator init -key-shares=1 -key-threshold=1
</code></pre>
<p> <strong>Sample Response</strong>:</p>
<pre><code class="lang-bash"> Unseal Key 1: &lt;transit-unseal-key&gt;
 Initial Root Token: s.&lt;transit-root-token&gt;

 Vault initialized with 1 key shares and a key threshold of 1.
 Please securely distribute the key shares printed above.
</code></pre>
</li>
<li><p>Unseal <code>vault-transit</code>:</p>
<pre><code class="lang-bash"> vault operator unseal &lt;transit-unseal-key&gt;
</code></pre>
</li>
<li><p>Log in:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_TOKEN=&lt;transit-root-token&gt;
 vault login
</code></pre>
</li>
<li><p>Enable transit engine:</p>
<pre><code class="lang-bash"> vault secrets <span class="hljs-built_in">enable</span> -path=transit transit
</code></pre>
<p> <strong>Sample Response</strong>:</p>
<pre><code class="lang-bash"> Success! Enabled the transit secrets engine at: transit/
</code></pre>
</li>
<li><p>Create an unseal key:</p>
<pre><code class="lang-bash"> vault write -f transit/keys/unseal_key
</code></pre>
<p> <strong>Sample Response</strong>:</p>
<pre><code class="lang-bash"> Success! Data written to: transit/keys/unseal_key
</code></pre>
</li>
<li><p>Create a policy:</p>
<pre><code class="lang-bash"> vault policy write transit-unseal - &lt;&lt;EOF
 path <span class="hljs-string">"transit/encrypt/unseal_key"</span> {
   capabilities = [<span class="hljs-string">"update"</span>]
 }
 path <span class="hljs-string">"transit/decrypt/unseal_key"</span> {
   capabilities = [<span class="hljs-string">"update"</span>]
 }
 EOF
</code></pre>
</li>
<li><p>Create a transit token:</p>
<pre><code class="lang-bash"> vault token create -policy=transit-unseal -format=json | jq -r .auth.client_token &gt; transit-token.txt
</code></pre>
<p> <strong>Sample Response</strong>:</p>
<pre><code class="lang-bash"> {
   <span class="hljs-string">"auth"</span>: {
     <span class="hljs-string">"client_token"</span>: <span class="hljs-string">"&lt;transit-token&gt;"</span>,
     <span class="hljs-string">"policies"</span>: [<span class="hljs-string">"default"</span>, <span class="hljs-string">"transit-unseal"</span>],
     <span class="hljs-string">"lease_duration"</span>: 2764800,
     ...
   }
 }
</code></pre>
</li>
</ol>
<h3 id="heading-step-4-update-cluster-for-auto-unseal">Step 4: Update Cluster for Auto-Unseal</h3>
<ol>
<li><p>Update <code>vault1/vault.hcl</code>, <code>vault2/vault.hcl</code>, and <code>vault3/vault.hcl</code> to add the <code>seal</code> stanza with the transit token from <code>transit-token.txt</code>:</p>
<p> <strong>vault-config/vault1/vault.hcl</strong>:</p>
<pre><code class="lang-bash"> ui = <span class="hljs-literal">true</span>
 api_addr = <span class="hljs-string">"http://vault1:8200"</span>
 cluster_addr = <span class="hljs-string">"http://vault1:8201"</span>

 storage <span class="hljs-string">"raft"</span> {
   path = <span class="hljs-string">"/vault/file"</span>
   node_id = <span class="hljs-string">"vault1"</span>
   retry_join {
     leader_api_addr = <span class="hljs-string">"http://vault2:8200"</span>
     leader_api_addr = <span class="hljs-string">"http://vault3:8200"</span>
   }
 }

 listener <span class="hljs-string">"tcp"</span> {
   address = <span class="hljs-string">"0.0.0.0:8200"</span>
   tls_disable = 1
 }

 seal <span class="hljs-string">"transit"</span> {
   address = <span class="hljs-string">"http://vault-transit:8200"</span>
   token = <span class="hljs-string">"&lt;transit-token&gt;"</span>
   mount_path = <span class="hljs-string">"transit/"</span>
 }
</code></pre>
<p> <strong>vault-config/vault2/vault.hcl</strong>:</p>
<pre><code class="lang-bash"> ui = <span class="hljs-literal">true</span>
 api_addr = <span class="hljs-string">"http://vault2:8200"</span>
 cluster_addr = <span class="hljs-string">"http://vault2:8201"</span>

 storage <span class="hljs-string">"raft"</span> {
   path = <span class="hljs-string">"/vault/file"</span>
   node_id = <span class="hljs-string">"vault2"</span>
   retry_join {
     leader_api_addr = <span class="hljs-string">"http://vault1:8200"</span>
     leader_api_addr = <span class="hljs-string">"http://vault3:8200"</span>
   }
 }

 listener <span class="hljs-string">"tcp"</span> {
   address = <span class="hljs-string">"0.0.0.0:8200"</span>
   tls_disable = 1
 }

 seal <span class="hljs-string">"transit"</span> {
   address = <span class="hljs-string">"http://vault-transit:8200"</span>
   token = <span class="hljs-string">"&lt;transit-token&gt;"</span>
   mount_path = <span class="hljs-string">"transit/"</span>
 }
</code></pre>
<p> <strong>vault-config/vault3/vault.hcl</strong>:</p>
<pre><code class="lang-bash"> ui = <span class="hljs-literal">true</span>
 api_addr = <span class="hljs-string">"http://vault3:8200"</span>
 cluster_addr = <span class="hljs-string">"http://vault3:8201"</span>

 storage <span class="hljs-string">"raft"</span> {
   path = <span class="hljs-string">"/vault/file"</span>
   node_id = <span class="hljs-string">"vault3"</span>
   retry_join {
     leader_api_addr = <span class="hljs-string">"http://vault1:8200"</span>
     leader_api_addr = <span class="hljs-string">"http://vault2:8200"</span>
   }
 }

 listener <span class="hljs-string">"tcp"</span> {
   address = <span class="hljs-string">"0.0.0.0:8200"</span>
   tls_disable = 1
 }

 seal <span class="hljs-string">"transit"</span> {
   address = <span class="hljs-string">"http://vault-transit:8200"</span>
   token = <span class="hljs-string">"&lt;transit-token&gt;"</span>
   mount_path = <span class="hljs-string">"transit/"</span>
 }
</code></pre>
</li>
<li><p>Restart cluster nodes:</p>
<pre><code class="lang-bash"> docker-compose restart vault1 vault2 vault3
</code></pre>
</li>
<li><p>Initialize cluster nodes:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 vault operator init -key-shares=1 -key-threshold=1
</code></pre>
<p> <strong>Sample Response</strong>:</p>
<pre><code class="lang-bash"> Success! Vault is initialized
 Initial Root Token: &lt;cluster-root-token&gt;

 Vault initialized with 1 key shares and a key threshold of 1.
 Please securely distribute the key shares printed above.
</code></pre>
<p> <em>If you’re continuing from Part 3, you might wonder why reinitialization is needed. Switching from manual unsealing to transit-based auto-unseal requires a new keyring compatible with the transit secrets engine, as the existing Raft data is tied to manual unseal keys. Reinitializing applies the new seal configuration while preserving your data in the Raft storage volumes (vault1-data, vault2-data, vault3-data). Initializing one node (e.g., vault1) is sufficient, as Raft replicates the state to vault2 and vault3 via retry_join. All nodes auto-unseal automatically using vault-transit, so no unseal keys or additional initialization is needed.</em></p>
</li>
</ol>
<h3 id="heading-step-5-test-the-cluster">Step 5: Test the Cluster</h3>
<ol>
<li><p>Log in to <code>vault1</code>:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 <span class="hljs-built_in">export</span> VAULT_TOKEN=&lt;cluster-root-token&gt;
 vault login
</code></pre>
</li>
<li><p>Store a secret:</p>
<pre><code class="lang-bash"> vault kv put secret/my-app db_password=supersecret
</code></pre>
</li>
<li><p>Retrieve from <code>vault2</code>:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8201
 vault kv get secret/my-app
</code></pre>
</li>
<li><p>Check cluster status:</p>
<pre><code class="lang-bash"> vault operator raft list-peers
</code></pre>
<p> <strong>Sample Response</strong>:</p>
<pre><code class="lang-bash"> Node      Address          State       Voter
 ----      -------          -----       -----
 vault1    vault1:8201      leader      <span class="hljs-literal">true</span>
 vault2    vault2:8201      follower    <span class="hljs-literal">true</span>
 vault3    vault3:8201      follower    <span class="hljs-literal">true</span>
</code></pre>
</li>
</ol>
<h3 id="heading-step-6-access-vault">Step 6: Access Vault</h3>
<ul>
<li><p><strong>Web UI</strong>: Open <code>http://localhost:8200</code>, <code>8201</code>, or <code>8202</code> for the cluster; <code>8203</code> for <code>vault-transit</code>. Use the cluster or transit root token, respectively.</p>
</li>
<li><p><strong>CLI</strong>: Use any cluster node’s address (e.g., <code>VAULT_ADDR=http://localhost:8200</code>).</p>
</li>
<li><p>Secrets persist via separate Raft storage volumes.</p>
</li>
</ul>
<h3 id="heading-step-7-stop-the-cluster">Step 7: Stop the Cluster</h3>
<pre><code class="lang-bash">docker-compose down
</code></pre>
<p>On restart, unseal <code>vault-transit</code> manually; cluster nodes auto-unseal.</p>
<hr />
<h2 id="heading-best-practices">Best Practices</h2>
<ul>
<li><p><strong>Use KMS or HSM in Production</strong>: Transit is for learning. Use a KMS (e.g., AWS KMS, Google Cloud KMS) or HSM for auto-unseal. See <a target="_blank" href="https://www.vaultproject.io/docs/configuration/seal">Vault Auto-Unseal Docs</a>.</p>
</li>
<li><p><strong>Secure Tokens</strong>: Store transit and root tokens in a password manager or HSM. Rotate transit tokens:</p>
<pre><code class="lang-bash">  vault token revoke &lt;transit-token&gt;
  vault token create -policy=transit-unseal
</code></pre>
</li>
<li><p><strong>Multiple Keys in Production</strong>: Use multiple unseal keys (e.g., three out of five) for <code>vault-transit</code>.</p>
</li>
<li><p><strong>Enable TLS</strong>: Set <code>tls_disable = 0</code> and use certificates.</p>
</li>
<li><p><strong>Monitor Raft and Retry Join</strong>: Ensure Raft storage is backed up and <code>retry_join</code> targets are stable.</p>
</li>
<li><p><strong>Backup Storage</strong>: Back up <code>vault1-data</code>, <code>vault2-data</code>, <code>vault3-data</code>, and <code>vault-transit-data</code> volumes.</p>
</li>
<li><p><strong>Isolate Transit Vault</strong>: The separate <code>vault-transit</code> instance enhances security.</p>
</li>
</ul>
<hr />
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<ul>
<li><p><strong>Retry Join Fails</strong>: Verify target nodes (<code>curl http://vault1:8200/v1/sys/health</code>). Check <code>vault-net</code> (<code>docker network inspect vault-net</code>).</p>
</li>
<li><p><strong>Auto-Unseal Fails</strong>: Ensure <code>vault-transit</code> is unsealed and the transit token is valid. Check logs (<code>docker logs vault1</code>).</p>
</li>
<li><p><strong>Raft Issues</strong>: Verify separate volumes (<code>docker inspect vault1</code>). Check logs (<code>docker logs vault1</code>).</p>
</li>
<li><p><strong>Token Errors</strong>: Confirm the transit token matches <code>transit-token.txt</code> and has the <code>transit-unseal</code> policy.</p>
</li>
</ul>
<hr />
<h2 id="heading-whats-next">What’s Next?</h2>
<p>You’ve transitioned your Vault cluster to transit-based auto-unseal! To extend your learning:</p>
<ul>
<li><p>Adapt MySQL dynamic secrets from <a target="_blank" href="hashicorp-vault-docker-compose-part2">Part 2</a>.</p>
</li>
<li><p>Test leader failover by stopping <code>vault1</code> and checking <code>vault operator raft list-peers</code>.</p>
</li>
<li><p>Explore KMS-based auto-unseal using <a target="_blank" href="https://www.vaultproject.io/docs/configuration/seal">Vault Auto-Unseal Docs</a>.</p>
</li>
</ul>
<p>Share your progress in the comments or join the <a target="_blank" href="https://discuss.hashicorp.com">HashiCorp Community Forum</a>!</p>
<hr />
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><strong>HashiCorp Vault Documentation</strong>: <a target="_blank" href="https://www.vaultproject.io">vaultproject.io</a></p>
</li>
<li><p><strong>Vault Auto-Unseal Docs</strong>: <a target="_blank" href="https://www.vaultproject.io/docs/configuration/seal">vaultproject.io/docs/configuration/seal</a></p>
</li>
<li><p><strong>Vault HA Documentation</strong>: <a target="_blank" href="https://www.vaultproject.io/docs/concepts/ha">vaultproject.io/docs/concepts/ha</a></p>
</li>
<li><p><strong>Docker Compose Documentation</strong>: <a target="_blank" href="https://docs.docker.com/compose">docs.docker.com/compose</a></p>
</li>
</ul>
<p><em>Note</em>: For questions or setup help, comment below or check the Vault documentation.</p>
]]></content:encoded></item><item><title><![CDATA[HashiCorp Vault with Docker Compose: Three-Node Cluster Setup (Part 3)]]></title><description><![CDATA[In Part 1, you set up a simple Vault instance in development mode, and in Part 2, you added persistent storage, manual unsealing, and MySQL dynamic secrets. While functional, a single node lacks high availability (HA). Now, in Part 3 of our series, y...]]></description><link>https://myros.net/hashicorp-vault-docker-compose-part3</link><guid isPermaLink="true">https://myros.net/hashicorp-vault-docker-compose-part3</guid><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[secrets management]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Security]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[clustering]]></category><category><![CDATA[high availability]]></category><dc:creator><![CDATA[Miroslav Milak]]></dc:creator><pubDate>Mon, 12 May 2025 10:42:26 GMT</pubDate><content:encoded><![CDATA[<p>In <a target="_blank" href="hashicorp-vault-docker-compose-part1">Part 1</a>, you set up a simple Vault instance in development mode, and in <a target="_blank" href="hashicorp-vault-docker-compose-part2">Part 2</a>, you added persistent storage, manual unsealing, and MySQL dynamic secrets. While functional, a single node lacks high availability (HA). Now, in Part 3 of our series, you’ll create a <strong>three-node Vault cluster</strong> using Docker Compose to achieve <strong>high availability</strong> (HA). A cluster ensures Vault remains accessible if a node fails, making it more production-ready. This guide walks you through setting up three Vault nodes with individual configuration files and manual joining, keeping it beginner-to-intermediate friendly.</p>
<p><em>Cover Image</em>: A digital padlock on a circuit board, symbolizing secure secrets management. (Source: <a target="_blank" href="https://images.unsplash.com/photo-1550751827-4bd374c3f58b?ixlib=rb-4.0.3&amp;auto=format&amp;fit=crop&amp;w=1350&amp;q=80">Unsplash</a>)</p>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, ensure you have:</p>
<ul>
<li><p><strong>Docker</strong> (version 20.10 or later). Install from <a target="_blank" href="https://docs.docker.com/get-docker/">Docker’s guide</a>.</p>
</li>
<li><p><strong>Docker Compose</strong> (version 2.0 or later). See <a target="_blank" href="https://docs.docker.com/compose/install/">Docker Compose installation</a>.</p>
</li>
<li><p><strong>Vault CLI</strong> for CLI access. Download from <a target="_blank" href="https://releases.hashicorp.com/vault/">releases.hashicorp.com/vault</a> or use a package manager (e.g., <code>brew install vault</code> on macOS).</p>
</li>
<li><p>A text editor (e.g., VS Code).</p>
</li>
<li><p>Familiarity with Parts 1 and 2 (development mode, persistent storage, unsealing).</p>
</li>
<li><p>Commands are tested on Linux/macOS; Windows users may need to use <code>set</code> instead of <code>export</code> for environment variables.</p>
</li>
</ul>
<hr />
<h2 id="heading-setting-up-a-three-node-vault-cluster">Setting Up a Three-Node Vault Cluster</h2>
<p>A Vault cluster consists of multiple nodes sharing the same storage backend, with one node acting as the <strong>leader</strong> and others as <strong>followers</strong>. If the leader fails, a follower takes over. Below is a diagram of the setup:</p>
<pre><code class="lang-mermaid">graph TD
    A[Client: Browser/CLI] --&gt; B[Docker Compose]
    B --&gt; C[Vault Node 1: Port 8200]
    B --&gt; D[Vault Node 2: Port 8201]
    B --&gt; E[Vault Node 3: Port 8202]
    C --&gt; F[Shared Storage]
    D --&gt; F
    E --&gt; F
</code></pre>
<h3 id="heading-step-1-create-the-docker-compose-file">Step 1: Create the Docker Compose File</h3>
<p>Create a file named <code>docker-compose.yml</code> in a new directory (e.g., <code>vault-cluster</code>):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">vault1:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault1</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8200:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault1-data:/vault/file</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config/vault1:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-net</span>

  <span class="hljs-attr">vault2:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault2</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8201:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault2-data:/vault/file</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config/vault2:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-net</span>

  <span class="hljs-attr">vault3:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vault3</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8202:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault3-data:/vault/file</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config/vault3:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-net</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">vault1-data:</span>
  <span class="hljs-attr">vault2-data:</span>
  <span class="hljs-attr">vault3-data:</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">vault-net:</span>
    <span class="hljs-attr">driver:</span> <span class="hljs-string">bridge</span>
</code></pre>
<p><strong>Key Notes</strong>:</p>
<ul>
<li><p>Separate volumes (vault1-data, vault2-data, vault3-data) ensure Raft storage is independent per node.</p>
</li>
<li><p>Each node maps a unique config directory (<code>vault-config/vault1</code>, etc.) and exposes a different host port (<code>8200</code>, <code>8201</code>, <code>8202</code>).</p>
</li>
<li><p><code>vault-net</code>: A custom network ensures nodes can communicate.</p>
</li>
<li><p>Inside containers, Vault listens on port <code>8200</code>, mapped to unique host ports.</p>
</li>
</ul>
<h3 id="heading-step-2-create-vault-configuration-files">Step 2: Create Vault Configuration Files</h3>
<p>To simplify cluster setup, we could manually join nodes using <code>vault operator raft join</code>, but this approach is tedious and error-prone, requiring careful coordination for each node. Instead, we’ll use retry_join to automate node discovery, making the process faster, more reliable, and less boring. This configuration allows each node to automatically find and join the cluster by contacting other nodes, streamlining setup and improving resilience.</p>
<h3 id="heading-moving-to-raft-storage">Moving to Raft storage</h3>
<p>In Part 2, we used file storage with a shared volume across containers for simplicity, enabling a single-node Vault to persist data easily. In Part 3, we switch to Raft storage, which is integrated into Vault and designed for high availability clustering. Raft ensures each node maintains its own consistent state and supports leader election and replication, making it ideal for a resilient, multi-node cluster. Additionally, unlike Part 2’s shared volume, Part 3 uses separate storage volumes for each node (vault1-data, vault2-data, vault3-data), eliminating shared storage dependencies and aligning with Raft’s requirement for independent node data.</p>
<p>Create a vault-config directory with subdirectories: <code>vault1</code>, <code>vault2</code>, <code>vault3</code>. Add <code>vault.hcl</code> files following the specified pattern:</p>
<p><strong>vault-config/vault1/vault.hcl</strong>:</p>
<pre><code class="lang-plaintext">ui = true
api_addr = "http://vault1:8200"
cluster_addr = "http://vault1:8201"

storage "raft" {
  path = "/vault/file"
  node_id = "vault1"

  retry_join {
    leader_api_addr = "http://vault2:8200"
    leader_api_addr = "http://vault3:8200"
  }
}

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = 1
}
</code></pre>
<p><em>The retry_join configuration automates cluster formation by enabling each Vault node to attempt joining other nodes’ API addresses (e.g.,</em> <code>vault1</code> <em>tries</em> <code>vault2</code> <em>and</em> <code>vault3</code><em>) until successful. This resilient mechanism retries on failure and excludes the current node to avoid self-joining, ensuring robust clustering. By specifying multiple</em> <code>leader_api_addr</code> <em>entries,</em> <code>retry_join</code> <em>simplifies setup and enhances reliability compared to manual joining.</em></p>
<p><strong>vault-config/vault2/vault.hcl</strong>:</p>
<pre><code class="lang-plaintext">ui = true
api_addr = "http://vault2:8200"
cluster_addr = "http://vault2:8201"

storage "raft" {
  path = "/vault/file"
  node_id = "vault2"

  retry_join {
    leader_api_addr = "http://vault1:8200"
    leader_api_addr = "http://vault3:8200"
  }
}

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = 1
}
</code></pre>
<p><strong>vault-config/vault3/vault.hcl</strong>:</p>
<pre><code class="lang-plaintext">ui = true
api_addr = "http://vault3:8200"
cluster_addr = "http://vault3:8201"

storage "raft" {
  path = "/vault/file"
  node_id = "vault3"

  retry_join {
    leader_api_addr = "http://vault1:8200"
    leader_api_addr = "http://vault2:8200"
  }
}

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = 1
}
</code></pre>
<p><strong>Key Notes</strong>:</p>
<ul>
<li><p><code>storage "raft"</code>: Uses Raft for HA, with a unique node_id per node.</p>
</li>
<li><p><code>retry_join</code>: Excludes the current node (e.g., vault1 joins vault2 and vault3), improving resilience.</p>
</li>
<li><p><code>tls_disable = 1</code>: Disabled for simplicity; production requires TLS.</p>
</li>
<li><p><strong>Production Warning</strong>: Use multiple unseal keys and TLS in production.</p>
</li>
<li><p><code>api_addr</code>: Specifies the node’s API address for client communication.</p>
</li>
<li><p><code>cluster_addr</code>: Defines the address for cluster communication (port <code>8201</code> to avoid conflicts with <code>8200</code>).</p>
</li>
</ul>
<h3 id="heading-step-3-start-and-initialize-the-cluster">Step 3: Start and Initialize the Cluster</h3>
<ol>
<li><p>Navigate to <code>vault-cluster</code> and start the containers:</p>
<pre><code class="lang-bash"> docker-compose up -d
</code></pre>
</li>
<li><p>Verify the containers are running:</p>
<pre><code class="lang-bash"> docker ps
</code></pre>
<p> You should see <code>vault1</code>, <code>vault2</code>, and <code>vault3</code>.</p>
</li>
<li><p>Initialize the leader node (<code>vault1</code>):</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 vault operator init -key-shares=1 -key-threshold=1

 Unseal Key 1: &lt;unseal-key&gt;
 Initial Root Token: &lt;root-token&gt;

 Vault initialized with 1 key shares and a key threshold of 1.
 Please securely distribute the key shares printed above.
</code></pre>
</li>
<li><p>This outputs <strong>one unseal key</strong> and a <strong>root token</strong>. Save the <strong>unseal key</strong> and <strong>root token</strong> securely (e.g., in a password manager). As in Part 2, we’re using a single key for simplicity, but production clusters typically use multiple keys (e.g., three out of five) for security. <strong>Do not use a single key in production</strong>.</p>
</li>
<li><p>Unseal <code>vault1</code>:</p>
<pre><code class="lang-bash"> vault operator unseal &lt;unseal-key&gt;
</code></pre>
</li>
<li><p>Log in with the root token:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_TOKEN=&lt;root-token&gt;
 vault login
</code></pre>
</li>
<li><p>Join <code>vault2</code> to the cluster:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8201
 vault operator init -key-shares=1 -key-threshold=1
 vault operator unseal &lt;vault2-unseal-key&gt;
</code></pre>
</li>
<li><p>Join <code>vault3</code> to the cluster:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8202
 vault operator init -key-shares=1 -key-threshold=1
 vault operator unseal &lt;vault3-unseal-key&gt;
</code></pre>
</li>
</ol>
<h3 id="heading-step-4-test-the-cluster">Step 4: Test the Cluster</h3>
<ol>
<li><p>Store a secret on <code>vault1</code>:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 vault kv put secret/my-app db_password=supersecret
</code></pre>
</li>
<li><p>Retrieve it from <code>vault2</code>:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8201
 vault kv get secret/my-app
</code></pre>
</li>
<li><p>Check cluster status:</p>
<pre><code class="lang-bash"> vault operator raft list-peers

 Node      Address          State       Voter
 ----      -------          -----       -----
 vault1    vault1:8201      leader      <span class="hljs-literal">true</span>
 vault2    vault2:8201      follower    <span class="hljs-literal">true</span>
 vault3    vault3:8201      follower    <span class="hljs-literal">true</span>
</code></pre>
<p> This shows all three nodes, with <code>vault1</code> as the leader.</p>
</li>
</ol>
<h3 id="heading-step-5-access-vault">Step 5: Access Vault</h3>
<ul>
<li><p><strong>Web UI</strong>: Open <code>http://localhost:8200</code>, <code>http://localhost:8201</code>, or <code>http://localhost:8202</code>. Log in with the root token. Navigate to “Secrets” to view or create secrets.</p>
</li>
<li><p><strong>CLI</strong>: Use any node’s address to interact with the cluster (e.g., <code>VAULT_ADDR=http://localhost:8200</code>).</p>
</li>
<li><p>Secrets persist via separate Raft storage volumes.</p>
</li>
</ul>
<h3 id="heading-step-6-stop-the-cluster">Step 6: Stop the Cluster</h3>
<p>To stop the containers:</p>
<pre><code class="lang-bash">docker-compose down
</code></pre>
<p>On restart, unseal each node using the same unseal key.</p>
<hr />
<h2 id="heading-best-practices">Best Practices</h2>
<ul>
<li><p><strong>Use Multiple Keys in Production</strong>: Configure multiple unseal keys (e.g., three out of five) for security. Avoid a single key.</p>
</li>
<li><p><strong>Monitor Retry Join</strong>: Ensure retry_join targets are stable. Use cloud metadata or multiple addresses in production.</p>
</li>
<li><p><strong>Backup Raft Storage</strong>: Regularly back up vault1-data, vault2-data, and vault3-data volumes.</p>
</li>
<li><p><strong>Secure Unseal Key and Root Token</strong>: Store the unseal key and root token in a secure location (e.g., password manager or HSM). In production, use multiple keys (e.g., three out of five) per node.</p>
</li>
<li><p><strong>Ensure Network Reliability</strong>: Nodes must communicate via <code>cluster_addr</code>. Use a stable network (e.g., <code>vault-net</code>) and monitor connectivity.</p>
</li>
<li><p><strong>Monitor Leader Election</strong>: Check <code>vault operator raft list-peers</code> to ensure a leader is active. If the leader fails, a follower takes over automatically.</p>
</li>
<li><p><strong>Enable TLS</strong>: In production, set <code>tls_disable = 0</code> in <code>vault.hcl</code> and use certificates for secure communication.</p>
</li>
<li><p><strong>Backup Storage</strong>: Regularly back up the <code>vault-data</code> volume to prevent data loss.</p>
</li>
</ul>
<hr />
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<ul>
<li><p><strong>Node Fails to Join</strong>: Verify <code>vault1</code> is unsealed and accessible (<code>curl http://vault1:8200/v1/sys/health</code>). Check Docker network (<code>vault-net</code>) with <code>docker network inspect vault-net</code>.</p>
</li>
<li><p><strong>Raft Issues</strong>: Ensure separate volumes are mounted (docker inspect vault1). Check logs (docker logs vault1).</p>
</li>
<li><p><strong>Unseal Fails</strong>: Ensure the unseal key is correct. Re-run <code>vault operator init -key-shares=1 -key-threshold=1</code> on <code>vault1</code> if lost (this resets the cluster).</p>
</li>
<li><p><strong>Cluster Not Responding</strong>: Check logs (<code>docker logs vault1</code>) for errors. Ensure ports <code>8200-8202</code> are free (<code>lsof -i :8200-8202</code>).</p>
</li>
</ul>
<hr />
<h2 id="heading-whats-next">What’s Next?</h2>
<p>You’ve built a high-availability Vault cluster with Raft and retry_join! In <strong>Part 4</strong>, we’ll eliminate manual unsealing by implementing transit-based auto-unseal with a dedicated Vault instance. Try:</p>
<ul>
<li><p>Stop <code>vault1</code> and verify a new leader is elected (<code>vault operator raft list-peers</code>).</p>
</li>
<li><p>Adapting MySQL secrets from Part 2.</p>
</li>
<li><p>Store a secret on one node and retrieve it from another.</p>
</li>
</ul>
<p>For dynamic secrets (e.g., MySQL), revisit <a target="_blank" href="hashicorp-vault-docker-compose-part2">Part 2</a> and adapt the configuration for this cluster. Share your progress in the comments or join the <a target="_blank" href="https://discuss.hashicorp.com">HashiCorp Community Forum</a>!</p>
<hr />
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><strong>HashiCorp Vault Documentation</strong>: <a target="_blank" href="https://www.vaultproject.io">vaultproject.io</a></p>
</li>
<li><p><strong>Vault HA Documentation</strong>: <a target="_blank" href="https://www.vaultproject.io/docs/concepts/ha">vaultproject.io/docs/concepts/ha</a></p>
</li>
<li><p><strong>Raft Storage Docs</strong>: vaultproject.io/docs/configuration/storage/raft</p>
</li>
<li><p><strong>Docker Compose Documentation</strong>: <a target="_blank" href="https://docs.docker.com/compose">docs.docker.com/compose</a></p>
</li>
</ul>
<p><em>Note</em>: For questions or setup help, comment below or check the Vault documentation.</p>
]]></content:encoded></item><item><title><![CDATA[HashiCorp Vault with Docker Compose: Persistent Storage and Dynamic Secrets (Part 2)]]></title><description><![CDATA[In Part 1, you set up a simple HashiCorp Vault instance using Docker Compose in development mode - perfect for local testing and quick prototyping. It stores everything in memory, which is ideal for developers who want to explore Vault features fast ...]]></description><link>https://myros.net/hashicorp-vault-docker-compose-part2</link><guid isPermaLink="true">https://myros.net/hashicorp-vault-docker-compose-part2</guid><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[secrets management]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Security]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[beginner]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Vault]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Miroslav Milak]]></dc:creator><pubDate>Fri, 09 May 2025 12:38:03 GMT</pubDate><content:encoded><![CDATA[<p>In <a target="_blank" href="hashicorp-vault-docker-compose-part1">Part 1</a>, you set up a simple HashiCorp Vault instance using Docker Compose in <strong>development mode</strong> - perfect for local testing and quick prototyping. It stores everything in memory, which is ideal for developers who want to explore Vault features fast without setup overhead.</p>
<blockquote>
<p>⚠️ In dev mode, <strong>all secrets are lost when the container stops</strong>.</p>
</blockquote>
<p>In this second part of the series, we’ll take a small step toward a more realistic setup:</p>
<ul>
<li><p>Enable <strong>persistent storage</strong> so secrets survive restarts,</p>
</li>
<li><p>Use <strong>manual unsealing</strong> (a core Vault security feature),</p>
</li>
<li><p>Connect Vault to a <strong>MySQL database</strong> to issue <strong>dynamic secrets</strong> - temporary credentials that expire automatically.</p>
</li>
</ul>
<p>This guide is still beginner-friendly and assumes no prior production Vault experience. You’ll build confidence by running everything locally with Docker, just like in Part 1, but with added layers of realism.</p>
<hr />
<h2 id="heading-why-dynamic-secrets">🧠 <em>Why dynamic secrets?</em></h2>
<p>Unlike static secrets, dynamic secrets are generated on demand. When an app or user needs access to a database, Vault can create a <strong>temporary username and password</strong> with limited permissions that <strong>self-destruct after an hour or so</strong>. This removes the need to manage and rotate credentials manually.</p>
<hr />
<h3 id="heading-quick-recap-of-part-1">🔁 Quick Recap of Part 1</h3>
<p>Previously, we:</p>
<ul>
<li><p>Ran Vault in development mode using Docker Compose.</p>
</li>
<li><p>Stored secrets in memory (they were lost after each restart).</p>
</li>
<li><p>Explored the UI and created static secrets manually.</p>
</li>
</ul>
<p>Now, we're building a <strong>persistent, more production-like setup</strong> with real secret automation</p>
<hr />
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, ensure you have:</p>
<ul>
<li><p><strong>Docker</strong> (version 20.10 or later). Install from <a target="_blank" href="https://docs.docker.com/get-docker/">Docker’s guide</a>.</p>
</li>
<li><p><strong>Docker Compose</strong> (version 2.0 or later). See <a target="_blank" href="https://docs.docker.com/compose/install/">Docker Compose installation</a>.</p>
</li>
<li><p><strong>Vault CLI</strong> for CLI access. Download from <a target="_blank" href="https://releases.hashicorp.com/vault/">releases.hashicorp.com/vault</a> or use a package manager (e.g., <code>brew install vault</code> on macOS).</p>
</li>
<li><p>A text editor (e.g., VS Code).</p>
</li>
<li><p>Familiarity with Part 1’s setup.</p>
</li>
</ul>
<blockquote>
<p>🪟 Windows users: Use <code>set</code> instead of <code>export</code> for environment variables.</p>
</blockquote>
<hr />
<h2 id="heading-setting-up-vault-with-persistent-storage">Setting Up Vault with Persistent Storage</h2>
<h3 id="heading-step-1-create-the-docker-compose-file">Step 1: Create the Docker Compose File</h3>
<p>Create a file named <code>docker-compose.yml</code> in a new directory (e.g., <code>vault-docker</code>):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">vault:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">hashicorp/vault:latest</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8200:8200"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">VAULT_ADDR=http://0.0.0.0:8200</span>
    <span class="hljs-attr">cap_add:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">IPC_LOCK</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">vault-data:/vault/data</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./vault-config:/vault/config</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">vault</span> <span class="hljs-string">server</span> <span class="hljs-string">-config=/vault/config/vault.hcl</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">mysql</span>
  <span class="hljs-attr">mysql:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">mysql:8</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">mysql</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">MYSQL_ROOT_PASSWORD:</span> <span class="hljs-string">rootpass</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"3306:3306"</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">vault-data:</span>
</code></pre>
<p><strong>Key Notes</strong>:</p>
<ul>
<li><p><code>vault-data</code>: A Docker volume for persistent storage, keeping secrets between restarts.</p>
</li>
<li><p><code>vault-config</code>: Maps a local directory for the Vault configuration file.</p>
</li>
<li><p><code>mysql</code>: A MySQL container for dynamic secrets, with a root password (<code>rootpass</code>).</p>
</li>
<li><p><code>depends_on</code>: Ensures MySQL starts before Vault for dynamic secrets setup.</p>
</li>
</ul>
<h3 id="heading-step-2-create-the-vault-configuration">Step 2: Create the Vault Configuration</h3>
<p>Unlike dev mode, we now provide an actual configuration file (vault.hcl) that Vault reads when starting in server mode.</p>
<p>Create a <code>vault-config</code> directory in <code>vault-docker</code> and add a file named <code>vault.hcl</code>.</p>
<pre><code class="lang-plaintext">storage "file" {
  path = "/vault/data"
}

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = 1
}

ui = true
</code></pre>
<p><strong>Key Notes</strong>:</p>
<ul>
<li><p><code>storage "file"</code>: Stores secrets in <code>/vault/data</code>, mapped to the <code>vault-data</code> volume.</p>
</li>
<li><p><code>tls_disable = 1</code>: Disables TLS for simplicity; in production, enable TLS with certificates.</p>
</li>
<li><p><code>ui = true</code>: Enables the Vault web UI.</p>
</li>
</ul>
<h3 id="heading-step-3-start-and-initialize-vault">Step 3: Start and Initialize Vault</h3>
<ol>
<li><p>Navigate to <code>vault-docker</code> folder and start the containers:</p>
<pre><code class="lang-bash"> docker-compose up -d
</code></pre>
</li>
<li><p>Verify the containers are running:</p>
<pre><code class="lang-bash"> docker ps
</code></pre>
<p> You should see <code>vault</code> and <code>mysql</code> containers.</p>
</li>
<li><p>Initialize Vault (run once):</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
 vault operator init -key-shares=1 -key-threshold=1
</code></pre>
<p> This outputs <strong>one unseal key</strong> and a <strong>root token</strong>. Save them securely (e.g., in a password manager). Normally, Vault generates multiple keys (e.g., five, with a threshold of three) for added security, but we’re using a single key for simplicity. <strong>Do not use a single key in production</strong>, as it reduces security.</p>
</li>
<li><p>Unseal Vault:</p>
<pre><code class="lang-bash"> vault operator unseal &lt;unseal-key&gt;
</code></pre>
<p> Use the single unseal key from the <code>init</code> output. Vault is now unsealed and ready.</p>
</li>
<li><p>Log in with the root token:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">export</span> VAULT_TOKEN=&lt;root-token&gt;
 vault login
</code></pre>
</li>
</ol>
<h3 id="heading-step-4-set-up-mysql-dynamic-secrets">Step 4: Set Up MySQL Dynamic Secrets</h3>
<p>Vault can generate temporary MySQL credentials that expire after a set time (e.g., 1 hour). Let’s configure this:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Enable the database secrets engine</span>
vault secrets <span class="hljs-built_in">enable</span> database

<span class="hljs-comment"># Configure MySQL connection so Vault can communicate with MySQL</span>
vault write database/config/my-mysql \
    plugin_name=mysql-database-plugin \
    connection_url=<span class="hljs-string">"root:rootpass@tcp(mysql:3306)/"</span> \
    allowed_roles=<span class="hljs-string">"my-role"</span>

<span class="hljs-comment"># Create a role that controls how dynamic users are created</span>
vault write database/roles/my-role \
    db_name=my-mysql \
    creation_statements=<span class="hljs-string">"CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT ON *.* TO '{{name}}'@'%';"</span> \
    default_ttl=<span class="hljs-string">"1h"</span> \
    max_ttl=<span class="hljs-string">"24h"</span>

<span class="hljs-comment"># Test: generate credentials on the fly!</span>
vault <span class="hljs-built_in">read</span> database/creds/my-role
</code></pre>
<p><strong>Key Notes</strong>:</p>
<ul>
<li><p>This creates temporary MySQL credentials that expire after 1 hour (<code>default_ttl="1h"</code>).</p>
</li>
<li><p>The credentials grant <code>SELECT</code> permissions on all databases.</p>
</li>
<li><p>Run <code>vault read database/creds/my-role</code> again to generate new credentials.</p>
</li>
</ul>
<h3 id="heading-step-5-access-vault">Step 5: Access Vault</h3>
<ul>
<li><p><strong>Web UI</strong>: Open <code>http://localhost:8200</code> (or <code>http://127.0.0.1:8200</code> if localhost is blocked). Log in with the root token. Navigate to “Secrets” to view or create secrets.</p>
</li>
<li><p><strong>CLI</strong>: Check Vault status or store a secret:</p>
<pre><code class="lang-bash">  vault status
  vault kv put secret/my-app db_password=supersecret
  vault kv get secret/my-app
</code></pre>
</li>
<li><p>Secrets persist across container restarts due to the <code>vault-data</code> volume.</p>
</li>
</ul>
<h3 id="heading-step-6-stop-vault">Step 6: Stop Vault</h3>
<p>To stop the containers:</p>
<pre><code class="lang-bash">docker-compose down
</code></pre>
<p>On restart, you’ll need to unseal Vault again using the single unseal key.</p>
<hr />
<h2 id="heading-best-practices">Best Practices</h2>
<ul>
<li><p><strong>Secure Unseal Key and Root Token</strong>: Store the unseal key and root token in a secure location (e.g., password manager or HSM). Never store them in plain text or version control. In production, use multiple keys (e.g., three out of five) for better security.</p>
</li>
<li><p><strong>Use Multiple Unseal Keys</strong>: In production, use e.g., 3-of-5 shares.</p>
</li>
<li><p><strong>Limit Root Token Use</strong>: Use the root token only for setup. Create non-root users (see Part 1 experiments) for regular access.</p>
</li>
<li><p><strong>Backup Storage</strong>: The <code>vault-data</code> volume persists data, but back up the storage directory regularly in production.</p>
</li>
<li><p><strong>Enable TLS</strong>: In production, set <code>tls_disable = 0</code> in <code>vault.hcl</code> and provide TLS certificates.</p>
</li>
<li><p><strong>Dynamic Secrets Are Short-Lived</strong>: Vault-created DB credentials only live for the TTL you set. This reduces risk if credentials leak or are forgotten.</p>
</li>
</ul>
<hr />
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<ul>
<li><p><strong>Vault Not Accessible</strong>: Ensure containers are running (<code>docker ps</code>) and port <code>8200</code> is free (<code>lsof -i :8200</code>). Check firewall settings.</p>
</li>
<li><p><strong>Unseal Fails</strong>: Verify the unseal key is correct. If lost, re-run <code>vault operator init -key-shares=1 -key-threshold=1</code> (this resets Vault).</p>
</li>
<li><p><strong>MySQL Connection Error</strong>: Confirm MySQL is running (<code>docker logs mysql</code>) and the <code>connection_url</code> matches the MySQL container’s credentials.</p>
</li>
</ul>
<hr />
<h2 id="heading-whats-next">What’s Next?</h2>
<p>You’ve set up Vault with persistent storage and dynamic MySQL credentials! This setup is closer to production but still runs on a single node. In <strong>Part 3</strong>, we’ll create a <strong>three-node Vault cluster</strong> for high availability. Try these experiments:</p>
<ul>
<li><p>Store a secret in the web UI and retrieve it via CLI.</p>
</li>
<li><p>Generate new MySQL credentials and test them with a MySQL client.</p>
</li>
<li><p>Explore token time-to-live (TTL) behavior for dynamic secrets.</p>
</li>
</ul>
<p>Share your progress in the comments or join the <a target="_blank" href="https://discuss.hashicorp.com">HashiCorp Community Forum</a>!</p>
<hr />
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><strong>HashiCorp Vault Documentation</strong>: <a target="_blank" href="https://www.vaultproject.io">vaultproject.io</a></p>
</li>
<li><p><strong>Docker Compose Documentation</strong>: <a target="_blank" href="https://docs.docker.com/compose">docs.docker.com/compose</a></p>
</li>
<li><p><strong>HashiCorp Learn: Database Secrets</strong>: <a target="_blank" href="https://learn.hashicorp.com/tutorials/vault/database-secrets">learn.hashicorp.com/tutorials/vault/database-secrets</a></p>
</li>
</ul>
<p><em>Note</em>: For questions or setup help, reach out, comment below or check the Vault documentation.</p>
]]></content:encoded></item><item><title><![CDATA[Getting Started with HashiCorp Vault and Docker Compose (Part 1)]]></title><description><![CDATA[HashiCorp Vault is a tool for securely managing secrets, like API keys, passwords, or database credentials. For example, if your app needs a database password, Vault can store it encrypted and control who can access it. This Set up a simple Vault ins...]]></description><link>https://myros.net/hashicorp-vault-docker-compose-part1</link><guid isPermaLink="true">https://myros.net/hashicorp-vault-docker-compose-part1</guid><category><![CDATA[hashicorp]]></category><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[Devops]]></category><category><![CDATA[secrets management]]></category><category><![CDATA[Security]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Begginers]]></category><dc:creator><![CDATA[Miroslav Milak]]></dc:creator><pubDate>Wed, 07 May 2025 13:17:05 GMT</pubDate><content:encoded><![CDATA[<p>HashiCorp Vault is a tool for securely managing <strong>secrets</strong>, like API keys, passwords, or database credentials. For example, if your app needs a database password, Vault can store it encrypted and control who can access it. This Set up a simple Vault instance to manage secrets in development modeguide is the first in a series to help beginners explore Vault using Docker Compose, a tool that simplifies running containerized applications. In Part 1, you’ll set up a minimal Vault instance in <strong>development mode</strong> to store and retrieve secrets. Future parts will cover persistent storage, clustering, and advanced features like auto-unseal.</p>
<h2 id="heading-what-is-hashicorp-vault"><strong>What is HashiCorp Vault?</strong></h2>
<p>Vault securely stores and manages sensitive data, called <strong>secrets</strong>, and controls access to them. Key features include:</p>
<ul>
<li><p><strong>Secrets storage</strong>: Safely store API keys, passwords, or certificates.</p>
</li>
<li><p><strong>Access control</strong>: Define who or what can access secrets using policies.</p>
</li>
<li><p><strong>Dynamic secrets</strong>: Generate temporary credentials (e.g., for databases), which we’ll explore in later parts.</p>
</li>
</ul>
<p>Running Vault with Docker Compose is perfect for beginners, as it simplifies setup and lets you focus on learning Vault’s basics.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before starting, ensure you have:</p>
<ul>
<li><p><strong>Docker</strong> (version 20.10 or later). Install from <a target="_blank" href="https://docs.docker.com/get-docker/">Docker’s guide</a>.</p>
</li>
<li><p><strong>Docker Compose</strong> (version 2.0 or later). See <a target="_blank" href="https://docs.docker.com/compose/install/">Docker Compose installation</a>.</p>
</li>
<li><p><strong>Vault CLI</strong> (optional, for CLI access). Download from <a target="_blank" href="https://releases.hashicorp.com/vault/">releases.hashicorp.com/vault</a> or use a package manager (e.g., <code>brew install vault</code> on macOS).</p>
</li>
<li><p>A text editor (e.g., VS Code).</p>
</li>
<li><p>Commands are tested on Linux/macOS; Windows users may need to use <code>set</code> instead of <code>export</code> for environment variables (see below).</p>
</li>
</ul>
<h2 id="heading-important-dev-mode-warning"><strong>⚠️ Important: Dev Mode Warning</strong></h2>
<blockquote>
<p><strong><em>Warning: Never use Vault dev mode for production workloads. All secrets are lost on restart, and security is minimal. Dev mode is only for local testing and learning.</em></strong></p>
</blockquote>
<h2 id="heading-setting-up-vault-with-docker-compose"><strong>Setting Up Vault with Docker Compose</strong></h2>
<p>Let’s create a simple Vault setup using Docker Compose. This configuration runs Vault in <strong>development mode</strong>, which uses in-memory storage and auto-unseals (no manual key entry). It’s ideal for testing but <strong>not suitable for production</strong>, as secrets are lost on restart.</p>
<h2 id="heading-step-1-create-the-docker-compose-file"><strong>Step 1: Create the Docker Compose File</strong></h2>
<p>Create a file named <code>docker-compose.yml</code> in a new directory (e.g., <code>vault-docker</code>):</p>
<pre><code class="lang-python">services:
  vault:
    image: hashicorp/vault:latest
    container_name: vault
    ports:
      - <span class="hljs-string">"8200:8200"</span>
    environment:
      - VAULT_DEV_ROOT_TOKEN_ID=myroot
      - VAULT_DEV_LISTEN_ADDRESS=<span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>:<span class="hljs-number">8200</span>
    cap_add:
      - IPC_LOCK
    command: vault server -dev
</code></pre>
<p><strong>Key Notes</strong>:</p>
<ul>
<li><p><code>VAULT_DEV_ROOT_TOKEN_ID=myroot</code>: Sets a root token for testing. Change it for security.</p>
</li>
<li><p><code>cap_add: [IPC_LOCK]</code>: Prevents Vault from swapping sensitive data to disk.</p>
</li>
<li><p>Port <code>8200</code> is exposed for Vault’s web UI and API.</p>
</li>
</ul>
<h2 id="heading-step-2-start-vault"><strong>Step 2: Start Vault</strong></h2>
<ol>
<li>Open a terminal, navigate to the <code>vault-docker</code> directory, and run:</li>
</ol>
<pre><code class="lang-python">docker compose up -d
</code></pre>
<p>The <code>-d</code> flag runs the container in the background.</p>
<ol start="2">
<li>Verify the container is running:</li>
</ol>
<pre><code class="lang-python">docker ps
</code></pre>
<p>You should see a <code>vault</code> container on port <code>8200</code>.</p>
<h2 id="heading-step-3-access-vault"><strong>Step 3: Access Vault</strong></h2>
<ul>
<li><p><strong>Web UI</strong>:<br />  Open <a target="_blank" href="http://localhost:8200/ui">http://localhost:8200/ui</a> (or <a target="_blank" href="http://127.0.0.1:8200/ui">http://127.0.0.1:8200/ui</a> if localhost is blocked) in your browser.<a target="_blank" href="http://127.0.0.1:8200/ui">  
  </a>Log in with the token <code>myroot</code>. You can create, view, and manage secrets visually.</p>
</li>
<li><p><strong>CLI</strong>:<br />  You can authenticate to Vault using one of two methods:</p>
<p>  <strong>Option 1: Use</strong> <code>vault login</code> <strong>(recommended for CLI use)</strong></p>
<pre><code class="lang-bash">  vault login myroot
</code></pre>
<p>  This command authenticates you and securely stores your token in a local helper file (usually <code>~/.vault-token</code>). You won’t need to provide the token again for future CLI commands.</p>
<p>  <strong>Option 2: Set the</strong> <code>VAULT_TOKEN</code> <strong>environment variable (useful for scripts or temporary sessions)</strong></p>
<p>  On <strong>Linux/macOS</strong>:</p>
<pre><code class="lang-bash">  <span class="hljs-built_in">export</span> VAULT_ADDR=http://localhost:8200
  <span class="hljs-built_in">export</span> VAULT_TOKEN=myroot
  vault status
</code></pre>
<p>  On <strong>Windows (Command Prompt)</strong>:</p>
<pre><code class="lang-bash">  <span class="hljs-built_in">set</span> VAULT_ADDR=http://localhost:8200
  <span class="hljs-built_in">set</span> VAULT_TOKEN=myroot
  vault status
</code></pre>
<p>  This method makes the token available only for your curre<a target="_blank" href="http://localhost:8200/ui">nt shell session.</a></p>
<p>  <strong>What’s the difference?</strong></p>
<ul>
<li><p><code>vault login</code> is best for daily CLI use: it stores your token securely and you don’t need to set it again.</p>
</li>
<li><p>Setting <code>VAULT_TOKEN</code> is great for scripts or temporary sessions, but less secure for long-term use.</p>
</li>
</ul>
</li>
<li><p><strong>Store a secret:</strong></p>
<pre><code class="lang-bash">  vault kv put secret/my-secret password=supersecret
</code></pre>
</li>
<li><p><strong>Retrieve it:</strong></p>
<pre><code class="lang-bash">  vault kv get secret/my-secret
</code></pre>
</li>
</ul>
<h2 id="heading-step-4-stop-vault"><strong>Step 4: Stop Vault</strong></h2>
<p>To stop the container:</p>
<pre><code class="lang-bash">docker compose down
</code></pre>
<p>To remove all containers, networks, and volumes for a clean slate:</p>
<pre><code class="lang-bash">docker compose down --volumes
</code></pre>
<blockquote>
<p><strong><em>Note: In development mode, secrets are lost when the container stops.</em></strong></p>
</blockquote>
<h2 id="heading-troubleshooting"><strong>Troubleshooting</strong></h2>
<ul>
<li><p><strong>Vault Not Accessible</strong>: Ensure the container is running (<code>docker ps</code>) and port <code>8200</code> is free (<code>lsof -i :8200</code>). Check your firewall settings.</p>
</li>
<li><p><strong>Login Fails</strong>: Verify the token is <code>myroot</code> and <code>VAULT_ADDR</code> is <code>http://localhost:8200</code>.</p>
</li>
<li><p><strong>Browser Blocks Localhost</strong>: Use <code>http://127.0.0.1:8200/ui</code>.</p>
</li>
</ul>
<h2 id="heading-whats-next"><strong>What’s Next?</strong></h2>
<p>You’ve set up a basic Vault instance and stored a secret! This development mode is great for testing, but it’s not secure for production. In <strong>Part 2</strong>, we’ll add persistent storage, manual unsealing, and a MySQL database for dynamic secrets. Future parts will cover clustering, auto-unseal, and load balancing.</p>
<p>Try these quick experiments:</p>
<ul>
<li><p>Store another secret in the web UI and retrieve it via CLI.</p>
</li>
<li><p>Explore the Vault UI’s “Policies” tab to see how access control works.</p>
</li>
</ul>
<p>Share your progress in the comments or join the <a target="_blank" href="https://discuss.hashicorp.com/">HashiCorp Community Forum</a>!</p>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<ul>
<li><p><strong>HashiCorp Vault Documentation</strong>: <a target="_blank" href="https://www.vaultproject.io/">vaultproject.io</a></p>
</li>
<li><p><strong>Docker Compose Documentation</strong>: <a target="_blank" href="https://docs.docker.com/compose">docs.docker.com/compose</a></p>
</li>
<li><p><strong>HashiCorp Learn</strong>: Free Vault tutorials (<a target="_blank" href="https://learn.hashicorp.com/">learn.hashicorp.com</a>)</p>
</li>
</ul>
<p><em>Note</em>: For questions or setup help, reach out, comment below or check the Vault documentation.</p>
]]></content:encoded></item><item><title><![CDATA[GitLab Pipeline JWT Authentication with HashiCorp Vault: A Comprehensive Guide]]></title><description><![CDATA[This guide explores how to integrate GitLab CI/CD pipelines with HashiCorp Vault using JWT (JSON Web Token) authentication to securely fetch secrets. We’ll cover three approaches—manual CI_JOB_JWT (older GitLab versions), manual id_tokens without sec...]]></description><link>https://myros.net/gitlab-pipeline-jwt-authentication-with-vault</link><guid isPermaLink="true">https://myros.net/gitlab-pipeline-jwt-authentication-with-vault</guid><category><![CDATA[hashicorp]]></category><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[GitLab]]></category><category><![CDATA[GitLab-CI]]></category><category><![CDATA[gitlab-cicd]]></category><dc:creator><![CDATA[Miroslav Milak]]></dc:creator><pubDate>Fri, 28 Mar 2025 09:20:32 GMT</pubDate><content:encoded><![CDATA[<p>This guide explores how to integrate GitLab CI/CD pipelines with HashiCorp Vault using JWT (JSON Web Token) authentication to securely fetch secrets. We’ll cover three approaches—manual <code>CI_JOB_JWT</code> (older GitLab versions), manual <code>id_tokens</code> without secrets, and the modern <code>secrets</code> method—along with Vault setup, best practices, and testing tips. Whether you’re on GitLab 13.4 or the latest version, this guide has you covered.</p>
<h3 id="heading-why-use-jwt-for-vault-authentication"><strong>Why Use JWT for Vault Authentication?</strong></h3>
<p>Using JWT for authentication allows GitLab runners to securely authenticate to Vault without embedding static credentials. The benefits include:</p>
<ul>
<li><p><strong>Short-lived tokens:</strong> Reduces risk exposure.</p>
</li>
<li><p><strong>Automatic token rotation:</strong> No need to manually manage credentials.</p>
</li>
<li><p><strong>Granular access control:</strong> Vault policies can define fine-grained permissions based on GitLab project, branch, and environment.</p>
</li>
</ul>
<hr />
<h3 id="heading-heres-a-step-by-step-guide-to-set-up-the-kv-secrets-engine-with-gitlab"><strong>Here’s a step-by-step guide to set up the KV Secrets Engine with GitLab:</strong></h3>
<h4 id="heading-what-are-vault-secrets-engines"><strong>What Are Vault Secrets Engines?</strong></h4>
<p>Vault Secrets Engines are components of HashiCorp Vault that manage secrets and sensitive data. Each engine is mounted at a specific path in Vault and provides an API to interact with secrets. The KV Secrets Engine is widely used to store static key-value pairs, such as API tokens or passwords, making it ideal for GitLab integration. In this setup, GitLab Pipelines can fetch these secrets securely during runtime.</p>
<h4 id="heading-gitlabs-integration-with-the-kv-secrets-engine"><strong>GitLab’s Integration with the KV Secrets Engine</strong></h4>
<p>GitLab Pipelines integrates with HashiCorp Vault to retrieve secrets from the KV Secrets Engine using JSON Web Tokens (JWT) for authentication. Here’s the process:</p>
<ul>
<li><p><strong>Authentication:</strong> GitLab generates a JWT for each pipeline job, which Vault verifies using GitLab’s JWKS endpoint (<a target="_blank" href="https://gitlab.com/-/jwks">https://gitlab.com/-/jwks</a>). If valid, Vault provides a temporary token.</p>
</li>
<li><p><strong>Secrets Access:</strong> The pipeline uses this token to fetch secrets from the KV Secrets Engine. This ensures secrets remain secure and are never exposed in your repository or CI variables.</p>
</li>
</ul>
<hr />
<h3 id="heading-scenario"><strong>Scenario</strong></h3>
<ul>
<li><p><strong>GitLab:</strong> <code>https://gitlab.example.com</code> (self-hosted)</p>
</li>
<li><p><strong>Vault:</strong> <code>https://vault.example.com</code></p>
</li>
<li><p><strong>Goal:</strong> Fetch an <code>api_key</code> secret for project ID <code>42</code>, <code>main</code> branch</p>
</li>
</ul>
<hr />
<h3 id="heading-step-1-setting-up-vault"><strong>Step 1: Setting Up Vault</strong></h3>
<p>Vault must be configured to trust GitLab’s JWTs and grant access based on pipeline metadata.</p>
<h4 id="heading-1-enable-jwt-authentication"><strong>1. Enable JWT Authentication</strong></h4>
<pre><code class="lang-bash">vault auth <span class="hljs-built_in">enable</span> jwt
</code></pre>
<h4 id="heading-2-configure-the-jwt-backend"><strong>2. Configure the JWT Backend</strong></h4>
<p>Link Vault to your GitLab instance:</p>
<pre><code class="lang-bash">vault write auth/jwt/config \
    jwks_url=<span class="hljs-string">"https://gitlab.example.com/-/jwks"</span> \
    bound_issuer=<span class="hljs-string">"https://gitlab.example.com"</span> \
    default_role=<span class="hljs-string">"project-42-role"</span>
</code></pre>
<ul>
<li><p><code>jwks_url</code> – GitLab’s public key endpoint for JWT verification.</p>
</li>
<li><p><code>bound_issuer</code> – Ensures JWTs come from your GitLab instance.</p>
</li>
<li><p><code>default_role</code> – Optional fallback role (override in pipeline if needed).</p>
</li>
</ul>
<blockquote>
<p><strong>Note:</strong> For GitLab.com, use <code>jwks_url="https://gitlab.com/-/jwks"</code> and <code>bound_issuer="https://gitlab.com"</code>.</p>
</blockquote>
<h4 id="heading-3-create-a-role"><strong>3. Create a Role</strong></h4>
<p>Define a role to map GitLab pipeline metadata to Vault policies:</p>
<pre><code class="lang-bash">vault write auth/jwt/role/project-42-role \
    role_type=<span class="hljs-string">"jwt"</span> \
    policies=<span class="hljs-string">"project-42-policy"</span> \
    token_ttl=<span class="hljs-string">"5m"</span> \
    token_max_ttl=<span class="hljs-string">"10m"</span> \
    bound_claims.project_id=<span class="hljs-string">"42"</span> \
    bound_claims.ref=<span class="hljs-string">"main"</span> \
    bound_claims.ref_type=<span class="hljs-string">"branch"</span> \
    bound_claims.ref_protected=<span class="hljs-string">"true"</span>
</code></pre>
<blockquote>
<p><code>bound_claims</code> – Restricts access to project <code>42</code>, <code>main</code> branch, and protected refs.</p>
</blockquote>
<h4 id="heading-4-create-a-policy"><strong>4. Create a Policy</strong></h4>
<p>Grant read access to specific secrets:</p>
<pre><code class="lang-bash">vault policy write project-42-policy - &lt;&lt;EOF
path <span class="hljs-string">"secret/data/project-42/*"</span> {
    capabilities = [<span class="hljs-string">"read"</span>]
}
EOF
</code></pre>
<h4 id="heading-5-enable-secret-engine-and-store-a-secret"><strong>5. Enable Secret Engine and Store a Secret</strong></h4>
<p>Enable the KV Secrets Engine in Vault:</p>
<pre><code class="lang-bash">vault secrets <span class="hljs-built_in">enable</span> -path=secret kv-v2
</code></pre>
<blockquote>
<p>version 2 is recommended for features like versioning</p>
</blockquote>
<p>Add the secret to Vault:</p>
<pre><code class="lang-bash">vault kv put secret/project-42/api api_key=<span class="hljs-string">"s3cr3t-k3y"</span>
</code></pre>
<blockquote>
<p><strong>Note:</strong> Uses KV v2 (<code>secret/data/</code>) for versioning support.</p>
</blockquote>
<h4 id="heading-6-enable-audit-logging"><strong>6. Enable Audit Logging</strong></h4>
<p>Track secret access:</p>
<pre><code class="lang-bash">vault audit <span class="hljs-built_in">enable</span> file file_path=/var/<span class="hljs-built_in">log</span>/vault_audit.log
</code></pre>
<hr />
<h2 id="heading-step-2-gitlab-pipeline-configuration"><strong>Step 2: GitLab Pipeline Configuration</strong></h2>
<p>GitLab offers multiple ways to authenticate with Vault using JWTs, depending on your version and needs.</p>
<h3 id="heading-option-1-manual-way-with-cijobjwt-gitlab-134-1312"><strong>Option 1: Manual Way with</strong> <code>CI_JOB_JWT</code> (GitLab 13.4-13.12)</h3>
<p>For older GitLab versions, use CI_JOB_JWT manually:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">deploy_job:</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">alpine:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">VAULT_ADDR:</span> <span class="hljs-string">"https://vault.example.com"</span>
    <span class="hljs-attr">VAULT_CACERT:</span> <span class="hljs-string">"/etc/ssl/certs/vault-ca.crt"</span>
  <span class="hljs-attr">before_script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">apk</span> <span class="hljs-string">add</span> <span class="hljs-string">--no-cache</span> <span class="hljs-string">vault</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">if</span> [ <span class="hljs-string">-z</span> <span class="hljs-string">"$CI_JOB_JWT"</span> ]<span class="hljs-string">;</span> <span class="hljs-string">then</span> <span class="hljs-string">echo</span> <span class="hljs-string">"JWT missing!"</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">exit</span> <span class="hljs-number">1</span><span class="hljs-string">;</span> <span class="hljs-string">fi</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">VAULT_TOKEN=$(vault</span> <span class="hljs-string">write</span> <span class="hljs-string">-field=token</span> <span class="hljs-string">auth/jwt/login</span> <span class="hljs-string">role=project-42-role</span> <span class="hljs-string">jwt=$CI_JOB_JWT)</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">API_KEY=$(vault</span> <span class="hljs-string">kv</span> <span class="hljs-string">get</span> <span class="hljs-string">-field=api_key</span> <span class="hljs-string">secret/project-42/api)</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">echo</span> <span class="hljs-string">"API Key retrieved successfully"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">./deploy.sh</span> <span class="hljs-string">--key</span> <span class="hljs-string">"$API_KEY"</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">if:</span> <span class="hljs-string">'$CI_COMMIT_BRANCH == "main" &amp;&amp; $CI_COMMIT_REF_PROTECTED == "true"'</span>
  <span class="hljs-attr">environment:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">production</span>
</code></pre>
<blockquote>
<p><strong>How It Works:</strong> <em>CI_JOB_JWT</em> (introduced in 13.4) is sent to Vault to obtain a token, then used to fetch the secret.</p>
<ul>
<li><p>CI_JOB_JWT is a predefined variable in GitLab 13.4+ containing a signed JWT with pipeline metadata (e.g., project_id, ref).</p>
</li>
<li><p>The script sends it to Vault’s auth/jwt/login endpoint, gets a Vault token, and uses it to fetch the secret.</p>
</li>
<li><p>No secrets or id_tokens - all logic is manual.</p>
</li>
</ul>
</blockquote>
<h3 id="heading-option-2-manual-way-with-idtokens-gitlab-140"><strong>Option 2: Manual Way with</strong> <code>id_tokens</code> (GitLab 14.0+)</h3>
<p>Use id_tokens for a custom JWT ($VAULT_ID_TOKEN), handling Vault calls manually:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">deploy_job:</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">alpine:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">VAULT_ADDR:</span> <span class="hljs-string">"https://vault.example.com"</span>
    <span class="hljs-attr">VAULT_CACERT:</span> <span class="hljs-string">"/etc/ssl/certs/vault-ca.crt"</span>
  <span class="hljs-attr">id_tokens:</span>
    <span class="hljs-attr">VAULT_ID_TOKEN:</span>
      <span class="hljs-attr">aud:</span> <span class="hljs-string">"https://vault.example.com"</span>
  <span class="hljs-attr">before_script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">apk</span> <span class="hljs-string">add</span> <span class="hljs-string">--no-cache</span> <span class="hljs-string">vault</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">if</span> [ <span class="hljs-string">-z</span> <span class="hljs-string">"$VAULT_ID_TOKEN"</span> ]<span class="hljs-string">;</span> <span class="hljs-string">then</span> <span class="hljs-string">echo</span> <span class="hljs-string">"ID token missing!"</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">exit</span> <span class="hljs-number">1</span><span class="hljs-string">;</span> <span class="hljs-string">fi</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">VAULT_TOKEN=$(vault</span> <span class="hljs-string">write</span> <span class="hljs-string">-field=token</span> <span class="hljs-string">auth/jwt/login</span> <span class="hljs-string">role=project-42-role</span> <span class="hljs-string">jwt=$VAULT_ID_TOKEN)</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">export</span> <span class="hljs-string">API_KEY=$(vault</span> <span class="hljs-string">kv</span> <span class="hljs-string">get</span> <span class="hljs-string">-field=api_key</span> <span class="hljs-string">secret/project-42/api)</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">echo</span> <span class="hljs-string">"API Key retrieved successfully"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">./deploy.sh</span> <span class="hljs-string">--key</span> <span class="hljs-string">"$API_KEY"</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">if:</span> <span class="hljs-string">'$CI_COMMIT_BRANCH == "main" &amp;&amp; $CI_COMMIT_REF_PROTECTED == "true"'</span>
  <span class="hljs-attr">environment:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">production</span>
</code></pre>
<blockquote>
<p><strong>How It Works:</strong></p>
</blockquote>
<ul>
<li><p><strong>ID_TOKENS</strong> defines <em>VAULT_ID_TOKEN</em> as a JWT with a custom aud claim (<a target="_blank" href="https://vault.example.com/">https://vault.example.com</a>).</p>
</li>
<li><p>The script manually sends this JWT to Vault’s auth/jwt/login endpoint to get a token, then fetches the secret.</p>
</li>
<li><p>No secrets keyword—full control is in the script.</p>
</li>
</ul>
<h3 id="heading-option-3-modern-way-with-secrets-gitlab-140"><strong>Option 3: Modern Way with</strong> <code>secrets</code> (GitLab 14.0+)</h3>
<p>The streamlined approach using secrets:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">deploy_job:</span>
  <span class="hljs-attr">image:</span> <span class="hljs-string">alpine:latest</span>
  <span class="hljs-attr">variables:</span>
    <span class="hljs-attr">VAULT_ADDR:</span> <span class="hljs-string">"https://vault.example.com"</span>
    <span class="hljs-attr">VAULT_CACERT:</span> <span class="hljs-string">"/etc/ssl/certs/vault-ca.crt"</span>
  <span class="hljs-attr">id_tokens:</span>
    <span class="hljs-attr">VAULT_ID_TOKEN:</span>
      <span class="hljs-attr">aud:</span> <span class="hljs-string">"https://vault.example.com"</span>
  <span class="hljs-attr">secrets:</span>
    <span class="hljs-attr">API_KEY:</span>
      <span class="hljs-attr">vault:</span> <span class="hljs-string">secret/project-42/api/api_key</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">if</span> [ <span class="hljs-string">-z</span> <span class="hljs-string">"$API_KEY"</span> ]<span class="hljs-string">;</span> <span class="hljs-string">then</span> <span class="hljs-string">echo</span> <span class="hljs-string">"Secret not injected!"</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">exit</span> <span class="hljs-number">1</span><span class="hljs-string">;</span> <span class="hljs-string">fi</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">echo</span> <span class="hljs-string">"API Key retrieved successfully"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">./deploy.sh</span> <span class="hljs-string">--key</span> <span class="hljs-string">"$API_KEY"</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">if:</span> <span class="hljs-string">'$CI_COMMIT_BRANCH == "main" &amp;&amp; $CI_COMMIT_REF_PROTECTED == "true"'</span>
  <span class="hljs-attr">environment:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">production</span>
</code></pre>
<blockquote>
<p><strong>How It Works:</strong></p>
</blockquote>
<ul>
<li><p><strong>id_tokens</strong> generates a JWT tailored for Vault (with a custom aud claim).</p>
</li>
<li><p><strong>secrets</strong> handles authentication and secret injection automatically.</p>
</li>
</ul>
<h4 id="heading-sidenote-why-idtokens-and-secrets-are-preferred-over-cijobjwt"><strong>Sidenote: Why id_tokens and secrets Are Preferred Over CI_JOB_JWT</strong></h4>
<ul>
<li><p><strong>Security Improvements:</strong> id_tokens allow for specifying an audience (aud), reducing the risk of token misuse.</p>
</li>
<li><p><strong>Flexibility:</strong> id_tokens provide a cleaner integration with external authentication providers, allowing better access control.</p>
</li>
<li><p><strong>Modern Best Practices:</strong> The secrets method automates secret injection, reducing the need for manual script handling.</p>
</li>
<li><p><strong>Deprecation of CI_JOB_JWT:</strong> GitLab is phasing out CI_JOB_JWT in favor of id_tokens. Update your pipeline configurations accordingly!</p>
</li>
</ul>
<hr />
<h3 id="heading-best-practices"><strong>Best Practices</strong></h3>
<h4 id="heading-secure-variables"><strong>Secure Variables</strong></h4>
<ul>
<li><p>Store <em>VAULT_ADDR</em> and <em>VAULT_CACERT</em> as protected, masked GitLab CI/CD variables in Settings &gt; CI/CD &gt; Variables.</p>
</li>
<li><p>Mark them as protected to limit access to protected branches (e.g., main).</p>
</li>
<li><p>Use masked variables for sensitive data to prevent accidental logging.</p>
</li>
<li><p>Always use verified images</p>
</li>
</ul>
<h4 id="heading-restrict-pipeline-execution"><strong>Restrict Pipeline Execution</strong></h4>
<ul>
<li><p>Use rules with <em>$CI_COMMIT_REF_PROTECTED</em> to limit secrets are only accessed in protected branches or tags.</p>
</li>
<li><p>Avoid running on untrusted branches (e.g., feature branches) to prevent JWT misuse.</p>
</li>
</ul>
<h4 id="heading-environment-tracking"><strong>Environment Tracking</strong></h4>
<ul>
<li>Define an environment for visibility in GitLab’s Environments dashboard.</li>
</ul>
<h4 id="heading-error-handling"><strong>Error Handling</strong></h4>
<ul>
<li><p>Validate JWTs and fail fast if missing or Vault calls fail.</p>
</li>
<li><p>Suppress errors (2&gt;/dev/null) to avoid leaking sensitive info.</p>
</li>
</ul>
<h3 id="heading-manual-cijobjwt-best-practices"><strong>Manual <em>CI_JOB_JWT</em> Best Practices</strong></h3>
<h4 id="heading-minimize-dependencies"><strong>Minimize Dependencies</strong></h4>
<ul>
<li><p>Use a lightweight image (e.g., alpine) and cache vault in a custom runner image:</p>
</li>
<li><p>Cache dependencies in a custom runner image to speed up pipelines:</p>
</li>
</ul>
<pre><code class="lang-dockerfile">
<span class="hljs-keyword">FROM</span> alpine:latest
<span class="hljs-keyword">RUN</span><span class="bash"> apk add --no-cache vault</span>
</code></pre>
<h4 id="heading-secure-jwt-handling"><strong>Secure JWT handling</strong></h4>
<ul>
<li><p>Never log (or echo) <em>$CI_JOB_JWT</em> - it’s sensitive and could be exploited if exposed.</p>
</li>
<li><p>Pass it directly to Vault without intermediate storage.</p>
</li>
<li><p>If avoiding the Vault CLI, use curl for portability</p>
</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> VAULT_TOKEN=$(curl -s --request POST --data <span class="hljs-string">"{\"jwt\": \"<span class="hljs-variable">$CI_JOB_JWT</span>\", \"role\": \"project-42-role\"}"</span> <span class="hljs-variable">$VAULT_ADDR</span>/v1/auth/jwt/login | jq -r .auth.client_token)
</code></pre>
<h3 id="heading-manual-idtokens-without-secrets-best-practices"><strong>Manual <em>id_tokens</em> (without <em>secrets</em>) Best Practices</strong></h3>
<h4 id="heading-audience-specificity-amp-jwt-validation"><strong>Audience Specificity &amp; JWT Validation</strong></h4>
<ul>
<li><p>Set aud in id_tokens to match Vault’s expected audience.</p>
</li>
<li><p>Set the aud in id_tokens to match your Vault server’s URL exactly, ensuring the JWT is purpose-specific.</p>
</li>
<li><p>Check <em>$VAULT_ID_TOKEN</em> presence in before_script.</p>
</li>
</ul>
<h4 id="heading-secrets"><strong>Secrets</strong></h4>
<ul>
<li><p>Use fully qualified paths (e.g., <em>secret/project-42/api/api_key</em>) in secrets to avoid ambiguity with Vault mounts.</p>
</li>
<li><p>Only define secrets you need in the job—don’t pull unnecessary ones.</p>
</li>
</ul>
<h4 id="heading-manual-flexibility"><strong>Manual Flexibility</strong></h4>
<p>Use for custom logic (e.g., dynamic roles):</p>
<pre><code class="lang-bash">
<span class="hljs-built_in">export</span> VAULT_TOKEN=$(vault write -field=token auth/jwt/login role=<span class="hljs-string">"<span class="hljs-variable">${CI_PROJECT_ID}</span>-role"</span> jwt=<span class="hljs-variable">$VAULT_ID_TOKEN</span>)
</code></pre>
<h3 id="heading-modern-secrets-best-practices"><strong>Modern secrets Best Practices</strong></h3>
<ul>
<li><p>Use exact secret paths (e.g., <em>secret/project-42/api/api_key</em>).</p>
</li>
<li><p>Fetch only required secrets per job.</p>
</li>
</ul>
<h3 id="heading-dont-forget-also-vault-best-practices"><strong>Don't forget also Vault Best Practices</strong></h3>
<h4 id="heading-use-short-lived-tokens"><strong>Use Short-Lived Tokens</strong></h4>
<ul>
<li><p>Set token_ttl (e.g., 5m) and token_max_ttl (e.g., 10m) to limit exposure.</p>
</li>
<li><p>Grant minimal capabilities (e.g., read) to specific paths.</p>
</li>
<li><p>Enable audit logs in Vault to monitor access.</p>
</li>
</ul>
<h3 id="heading-why-these-practices-matter"><strong>Why These Practices Matter</strong></h3>
<ul>
<li><p><strong>Security:</strong> Short TTLs, bound claims, and protected variables minimize risks of token or secret exposure.</p>
</li>
<li><p><strong>Reliability:</strong> Error handling and validation prevent silent failures.</p>
</li>
<li><p><strong>Maintainability:</strong> Audit logs and environment tracking simplify debugging and compliance.</p>
</li>
</ul>
<hr />
<h2 id="heading-comparison-of-methods"><strong>Comparison of Methods</strong></h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Method</strong></td><td><strong>GitLab Version</strong></td><td><strong>JWT Variable</strong></td><td><strong>Pros</strong></td><td><strong>Cons</strong></td><td><strong>Use case</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Manual <code>CI_JOB_JWT</code></td><td>13.4-13.12</td><td><code>CI_JOB_JWT</code></td><td>Works on older versions</td><td>No <code>aud</code> customization</td><td>Legacy GitLab instances</td></tr>
<tr>
<td>Manual <code>id_tokens</code></td><td>14.0+</td><td><code>VAULT_ID_TOKEN</code></td><td>Custom <code>aud</code>, flexible logic</td><td>Requires manual scripting</td><td>Custom authentication setups</td></tr>
<tr>
<td>Modern <code>secrets</code></td><td>14.0+</td><td><code>VAULT_ID_TOKEN</code></td><td>Simple, automatic</td><td>Less control over process</td><td>Best for most CI/CD workflows</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-testing-and-validation"><strong>Testing and Validation</strong></h2>
<ul>
<li><p><strong>Pipeline Logs:</strong> Look for “API Key retrieved successfully” and ensure no sensitive data (e.g., $VAULT_TOKEN) leaks.</p>
</li>
<li><p><strong>Vault Audit:</strong> Verify in /var/log/vault_audit.log that only authorized roles access secrets.</p>
</li>
<li><p><strong>Debugging:</strong></p>
<ul>
<li><p><strong>Manual methods:</strong> Add vault token lookup to inspect token capabilities.</p>
</li>
<li><p><strong>Modern method:</strong> Check $API_KEY presence.</p>
</li>
</ul>
</li>
<li><p><strong>Dry Run:</strong> Test on a non-protected branch to confirm rules block execution.</p>
</li>
</ul>
<hr />
<h2 id="heading-troubleshooting"><strong>Troubleshooting</strong></h2>
<ul>
<li><p><strong>"invalid role":</strong> Ensure role name matches (<em>project-42-role</em>) and bound_claims align with pipeline metadata.</p>
</li>
<li><p><strong>"x509: certificate signed by unknown authority":</strong> Set <em>VAULT_CACERT</em> or use <em>VAULT_SKIP_VERIFY=true</em> (testing only).</p>
</li>
<li><p><strong>"Secret not found"</strong>: Verify path (<em>secret/project-42/api</em> vs. <em>secret/data/project-42/api</em>) and KV version.</p>
</li>
</ul>
<hr />
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Whether you’re stuck on GitLab 13.4 with CI_JOB_JWT, need manual control with id_tokens, or prefer the simplicity of secrets, this guide provides a secure, practical way to integrate GitLab pipelines with Vault. Apply the best practices to minimize risks and streamline your workflow. For self-signed certificates or custom setups, adjust the TLS handling as needed.</p>
<hr />
<h2 id="heading-need-expert-help-with-hashicorp-vault"><strong>Need Expert Help with HashiCorp Vault?</strong></h2>
<p>🚀 Struggling with <strong>Vault integration, security, or scaling</strong>? Whether you're using <strong>open-source Vault, Vault Enterprise, or Vault Cloud</strong>, I can help <strong>optimize your setup, enhance security, and streamline your GitLab CI/CD workflows.</strong></p>
<p><strong>Services I offer:</strong></p>
<p>✅ Secure and scalable Vault deployments ✅ CI/CD pipeline optimization with Vault integration ✅ Vault Enterprise and multi-cloud implementations ✅ Security best practices and compliance</p>
<p>📩 <strong>Let’s connect!</strong> Reach out on <a target="_blank" href="https://www.linkedin.com/in/miroslavmilak/"><strong>LinkedIn</strong></a> or schedule a <a target="_blank" href="https://calendly.com/myros/vault-consultation"><strong>free consultation</strong></a> and take your Vault setup to the next level.</p>
<hr />
<p>Happy deploying! 🚀</p>
]]></content:encoded></item></channel></rss>