<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Ivan Tsanev</title>
    <description>The latest articles on Forem by Ivan Tsanev (@ivantsanev13).</description>
    <link>https://forem.com/ivantsanev13</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3863634%2F13b893da-a2d4-44f8-ab7d-09124371c3e1.jpeg</url>
      <title>Forem: Ivan Tsanev</title>
      <link>https://forem.com/ivantsanev13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ivantsanev13"/>
    <language>en</language>
    <item>
      <title>Running Vault in Kubernetes — Lessons from a Homelab</title>
      <dc:creator>Ivan Tsanev</dc:creator>
      <pubDate>Mon, 06 Apr 2026 10:13:28 +0000</pubDate>
      <link>https://forem.com/ivantsanev13/running-vault-in-kubernetes-lessons-from-a-homelab-9m2</link>
      <guid>https://forem.com/ivantsanev13/running-vault-in-kubernetes-lessons-from-a-homelab-9m2</guid>
      <description>&lt;p&gt;This post is aimed at engineers running Vault on bare metal or self-hosted Kubernetes — managed cloud clusters handle some of this automatically, but on-prem you're on your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Vault in Kubernetes — Lessons from a Homelab
&lt;/h2&gt;

&lt;p&gt;Most Vault + Kubernetes tutorials cover the basics: install Vault, store a secret, inject it into a pod. This post covers what comes after that — persistent storage, unsealing, and getting secrets into pods properly using External Secrets Operator.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Vault in the First Place
&lt;/h2&gt;

&lt;p&gt;Kubernetes Secrets are not secret. They're base64 encoded — which is encoding, not encryption. Anyone with the right RBAC permissions can decode them in seconds. There's no audit trail, no rotation, no central management.&lt;/p&gt;

&lt;p&gt;Vault solves all of this. Secrets are encrypted at rest, every access is logged, and you can rotate credentials without touching your cluster.&lt;/p&gt;

&lt;p&gt;The problem is getting there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start with Persistent Storage
&lt;/h2&gt;

&lt;p&gt;The natural starting point is dev mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;devRootToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dev mode is convenient — Vault starts pre-initialized and pre-unsealed. Good for experimenting.&lt;/p&gt;

&lt;p&gt;I started there too. Then a network routing issue caused my cluster to restart. I was troubleshooting Tailscale subnet routing — trying to reach cluster services from outside my home network — which left the routing table in a broken state and eventually took down the whole cluster. After bringing everything back up, ESO couldn't sync anything. The secrets were gone because dev mode stores everything in memory.&lt;/p&gt;

&lt;p&gt;That's when persistent storage became obvious. Vault is deployed via the official HashiCorp Helm chart. Use a proper storage class from the start — the following are the relevant Helm values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;dataStorage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
    &lt;span class="na"&gt;storageClass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn&lt;/span&gt;   &lt;span class="c1"&gt;# replace with your storage class&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to know: switching from dev mode to production mode isn't a config swap. Dev mode doesn't create a PVC, but you do need to delete the existing StatefulSet first — otherwise Helm will try to update it in place and hit an immutable fields error. Delete it, redeploy with the new config, then initialize fresh with &lt;code&gt;vault operator init&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Unseal Problem
&lt;/h2&gt;

&lt;p&gt;Production Vault starts sealed after every restart. This is a security feature — if someone steals your disk, the data is useless without the unseal keys. But it also means every time your pod restarts, someone has to manually unseal it.&lt;/p&gt;

&lt;p&gt;The proper solution is auto-unseal via a cloud KMS (AWS KMS, Azure Key Vault, etc.). For a homelab, I built a simpler workaround — a Kubernetes CronJob that runs every minute and unseals Vault automatically.&lt;/p&gt;

&lt;p&gt;First, store your unseal keys in a K8s Secret. When you initialize Vault with &lt;code&gt;vault operator init&lt;/code&gt;, it generates 5 unseal keys and requires any 3 of them to unseal. Store 3 of those keys here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic vault-unseal-keys &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;key1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;UNSEAL_KEY_1&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;key2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;UNSEAL_KEY_2&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;key3&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;UNSEAL_KEY_3&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; vault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then deploy the CronJob. It runs every minute, reads the keys from that Secret as environment variables, and passes them to &lt;code&gt;vault operator unseal&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CronJob&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-unsealer&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;jobTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-unsealer&lt;/span&gt;
            &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/vault:1.18.1&lt;/span&gt;
            &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/bin/sh&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-c&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;export VAULT_ADDR=http://vault.vault.svc:8200  # &amp;lt;service-name&amp;gt;.&amp;lt;namespace&amp;gt;.svc&lt;/span&gt;
              &lt;span class="s"&gt;vault operator unseal $KEY1&lt;/span&gt;
              &lt;span class="s"&gt;vault operator unseal $KEY2&lt;/span&gt;
              &lt;span class="s"&gt;vault operator unseal $KEY3&lt;/span&gt;
            &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KEY1&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-unseal-keys&lt;/span&gt;  &lt;span class="c1"&gt;# the Secret we created above&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KEY2&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-unseal-keys&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key2&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KEY3&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-unseal-keys&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key3&lt;/span&gt;
          &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth noting: storing unseal keys in the same cluster as Vault is circular from a security standpoint — if someone compromises the cluster, they have both. For a homelab it's an acceptable trade-off. In production, use cloud KMS where the unseal key lives outside the cluster entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  External Secrets Operator
&lt;/h2&gt;

&lt;p&gt;Vault stores your secrets. But your pods need Kubernetes Secrets. These are two different things and Vault alone doesn't bridge the gap.&lt;/p&gt;

&lt;p&gt;That's what External Secrets Operator (ESO) does. It watches for &lt;code&gt;ExternalSecret&lt;/code&gt; resources in your cluster and automatically syncs secrets from Vault into native K8s Secrets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vault (source of truth)
  → ESO watches ExternalSecret resources
  → Creates/updates K8s Secrets automatically
  → Pod consumes K8s Secret as env var or volume mount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 1 — Tell ESO how to connect to Vault&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;ClusterSecretStore&lt;/code&gt; is a cluster-wide resource that defines the connection to your secret backend — in this case Vault. Think of it as a named connection configuration that all your namespaces can reference.&lt;/p&gt;

&lt;p&gt;ESO needs a token to authenticate against Vault. You store that token in a regular K8s Secret first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic vault-auth-token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;YOUR_VAULT_TOKEN&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference it in the &lt;code&gt;ClusterSecretStore&lt;/code&gt;. The &lt;code&gt;tokenSecretRef&lt;/code&gt; block points to that K8s Secret — &lt;code&gt;name&lt;/code&gt; is the name of the secret, and &lt;code&gt;key&lt;/code&gt; is the field inside it that holds the actual Vault token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterSecretStore&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-connection&lt;/span&gt;       &lt;span class="c1"&gt;# referenced by ExternalSecrets later&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;vault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://vault.vault.svc:8200"&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret"&lt;/span&gt;           &lt;span class="c1"&gt;# the KV engine mount name in Vault&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2"&lt;/span&gt;
      &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;tokenSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-auth-token&lt;/span&gt;   &lt;span class="c1"&gt;# K8s Secret that holds the Vault token&lt;/span&gt;
          &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;token&lt;/span&gt;               &lt;span class="c1"&gt;# the field inside that Secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — Store the secret in Vault&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before ESO can sync anything, the secret needs to exist in Vault. If the path doesn't exist yet, ESO will report a &lt;code&gt;SecretSyncedError&lt;/code&gt; status on the ExternalSecret resource — easy to miss if you're not watching. Create the secret first (make sure &lt;code&gt;VAULT_ADDR&lt;/code&gt; and &lt;code&gt;VAULT_TOKEN&lt;/code&gt; are set in your shell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://&amp;lt;vault-address&amp;gt;:8200
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-vault-token&amp;gt;
vault kv put secret/myproject/production/database &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-db-password"&lt;/span&gt;
&lt;span class="c"&gt;# "secret" here is the KV engine mount name, not a literal — adjust if yours is named differently&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 — Define what to sync&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;ExternalSecret&lt;/code&gt; tells ESO: fetch this specific secret from Vault and create a K8s Secret from it. It references the &lt;code&gt;ClusterSecretStore&lt;/code&gt; you defined above.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;remoteRef.key&lt;/code&gt; is the path to the secret in Vault. A common convention is to organize secrets by project and environment — &lt;code&gt;myproject/production/database&lt;/code&gt;, &lt;code&gt;myproject/staging/database&lt;/code&gt; — so different clusters or namespaces can point to the right environment just by changing the path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExternalSecret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database-credentials&lt;/span&gt;        &lt;span class="c1"&gt;# name of this ExternalSecret resource&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;refreshInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;               &lt;span class="c1"&gt;# how often ESO re-syncs from Vault&lt;/span&gt;
  &lt;span class="na"&gt;secretStoreRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-connection&lt;/span&gt;          &lt;span class="c1"&gt;# references the ClusterSecretStore above&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterSecretStore&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database-credentials&lt;/span&gt;      &lt;span class="c1"&gt;# name of the K8s Secret ESO will create&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-password&lt;/span&gt;        &lt;span class="c1"&gt;# key name in the resulting K8s Secret&lt;/span&gt;
      &lt;span class="na"&gt;remoteRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myproject/production/database&lt;/span&gt;
        &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;                   &lt;span class="c1"&gt;# the field inside that Vault secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ESO creates a K8s Secret called &lt;code&gt;database-credentials&lt;/code&gt; in the &lt;code&gt;production&lt;/code&gt; namespace and keeps it in sync. Update the value in Vault, the K8s Secret updates automatically within the refresh interval — without touching the cluster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Env Vars vs Volume Mounts
&lt;/h2&gt;

&lt;p&gt;Once the K8s Secret exists, you have two ways to consume it in a pod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment variables&lt;/strong&gt; are set at container start time and never update. If the secret rotates, the pod needs a restart to see the new value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume mounts&lt;/strong&gt; update automatically. When the K8s Secret changes, the mounted file updates inside the pod within roughly 60-90 seconds (kubelet sync period). No restart needed — as long as the app re-reads the file each time it needs the value rather than caching it at startup.&lt;/p&gt;

&lt;p&gt;Here's what it looks like in a full Deployment spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app:latest&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;credentials-vol&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/secrets&lt;/span&gt;    &lt;span class="c1"&gt;# files appear here inside the container&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;credentials-vol&lt;/span&gt;
          &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database-credentials&lt;/span&gt;   &lt;span class="c1"&gt;# the K8s Secret ESO created&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file name inside the pod matches the &lt;code&gt;secretKey&lt;/code&gt; field defined in the ExternalSecret — in this example &lt;code&gt;db-password&lt;/code&gt;, so the app reads &lt;code&gt;/etc/secrets/db-password&lt;/code&gt; as a plain file. For anything that rotates — database passwords, API keys — volume mounts are the right choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The full working stack: Vault with Longhorn persistent storage, auto-unseal CronJob, ESO syncing secrets to K8s, pods consuming via volume mounts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't use dev mode for anything you care about — persistent storage from the start saves a lot of pain&lt;/li&gt;
&lt;li&gt;Initialize Vault before deploying anything that depends on it — ESO will report sync errors if Vault isn't ready&lt;/li&gt;
&lt;li&gt;Use volume mounts over env vars for secrets that rotate, and make sure your app reads the file on each use rather than caching it&lt;/li&gt;
&lt;li&gt;Test your unseal story before you need it at an inconvenient time&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>vault</category>
      <category>devops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
