{"id":872,"date":"2021-08-11T05:23:04","date_gmt":"2021-08-11T05:23:04","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=872"},"modified":"2021-08-11T05:23:04","modified_gmt":"2021-08-11T05:23:04","slug":"vault-agent-injector-tutorial","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=872","title":{"rendered":"Vault Agent Injector Tutorial: Setup Init &amp; Sidecar Agents"},"content":{"rendered":"<p>In this vault agent injector tutorial, I will show you exactly how to use a Hashicorp vault agent configuration to inject agents and render secrets into a kubernetes pod.<\/p>\n<p>I have covered the setup by step guide to implement kubernetes vault agent pods to dynamically retrieve secrets from the vault server<\/p>\n<p>Towards the end of the article, I have added vault agent templating examples using annotations, configmaps, and environment variables.<\/p>\n<p>Also, if you want to understand how pods authenticate to <a href=\"https:\/\/devopscube.com\/setup-hashicorp-vault-beginners-guide\/\" rel=\"noreferrer noopener\">vault server<\/a>, refer to this <a href=\"https:\/\/devopscube.com\/vault-in-kubernetes\/\" rel=\"noreferrer noopener\">beginner&#8217;s vault guide on Kubernetes<\/a>.<\/p>\n<p>Here is what you will learn from this article.<\/p>\n<h2 id=\"application-secret-formats\">Application Secret Formats<\/h2>\n<p>It is standard security practice to isolate secrets from code, and the developers should not worry about where the secrets come from. Generally, applications expect secrets in a file format in a specific location.<\/p>\n<p>The common formats in which applications expect secrets are,<\/p>\n<ol>\n<li>A config file (Text file with newline strings.)<\/li>\n<li>Json\/Yaml File<\/li>\n<li>Environemnt variables<\/li>\n<\/ol>\n<p>The format depends on the application and teams who design the CI\/CD process.<\/p>\n<p>For example, a java spring boot application property file can be <code>application.properties<\/code> with the following contents.<\/p>\n<pre><code>spring.datasource.url=jdbc:mysql:\/\/localhost:3306\/myDb\nspring.datasource.username=myuser\nspring.datasource.password=secretpassword\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver<\/code><\/pre>\n<p>When it comes to CI\/CD process, you cannot commit the secrets to the source code or even have it as Kubernetes secret object. Therefore, you need an efficient mechanism like a vault injector for applications to consume secrets securely.<\/p>\n<p>I\u2019ll also show you how to use the Vault injector and all vault agent configurations to place or inject the secrets into a pod from the vault server in the required formats for the application.<\/p>\n<h2 id=\"what-is-vault-agent-injector\">What is Vault Agent Injector?<\/h2>\n<p>Vault Agent Injector is a controller (custom implementation) that can add sidecar and <a href=\"https:\/\/devopscube.com\/kubernetes-init-containers\/\">init containers<\/a> to <a href=\"https:\/\/devopscube.com\/kubernetes-pod\/\">kubernetes pods<\/a> in runtime.<\/p>\n<p>The job of the init container is to authenticate and retrieve secrets from the vault server using the pod <a href=\"https:\/\/devopscube.com\/kubernetes-api-access-service-account\/\" rel=\"noreferrer noopener\">service account<\/a> place them in a shared location (In memory volume) where the application container can access them.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-7-57.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"969\" height=\"459\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-7-57.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-7-57.png 969w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>You can use this implementation for kubernetes standalone pods, <a href=\"https:\/\/devopscube.com\/kubernetes-deployment-tutorial\/\" rel=\"noreferrer noopener\">deployments<\/a>, Statefuset, and <a href=\"https:\/\/devopscube.com\/create-kubernetes-jobs-cron-jobs\/\" rel=\"noreferrer noopener\">Kubernetes jobs<\/a>.<\/p>\n<h2 id=\"how-does-vault-injector-work\">How Does Vault Injector Work<\/h2>\n<p>The Vault Agent Injector is a <a href=\"https:\/\/kubernetes.io\/docs\/reference\/access-authn-authz\/admission-controllers\/?ref=devopscube.com\" rel=\"noreferrer noopener\">Kubernetes Mutation Webhook Controller<\/a>.<\/p>\n<p>Meaning, it is a custom piece of code (controller) and a webhook that gets deployed in kubernetes that intercepts pod events like create and update to check if any agent-specific annotation is applied to the pod.<\/p>\n<p>For example, if a pod gets deployed with an annotation.&#8221;<code>vault.hashicorp.com\/agent-inject: 'true'<\/code>&#8220;, here is what happens.<\/p>\n<ol>\n<li>Custom <code>MutatingWebhookConfiguration<\/code> sends a webhook with all pod information to the injector controller deployment.<\/li>\n<li>Then the controller modifies the Pod spec in runtime to introduce a sidecar and init container agents to the actual pod specification.<\/li>\n<li>Controller then returns the modifed object for object validation.<\/li>\n<li>After validation the modified pod spec gets deloyed with a sidecar and init container.<\/li>\n<\/ol>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-329.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"802\" height=\"551\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-329.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-329.png 802w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>So when the pod comes up, it will have the application container, a sidecar, and a init container.<\/p>\n<p>The init container is <strong>responsible for retrieving the secrets<\/strong>. In addition, a sidecar container is required if your application uses <strong>dynamic secrets<\/strong>. Dynamic secrets are secrets that are created on-demand with expiration time. The sidecar container ensures that the latest secrets are present inside the pod after every secret renewal.<\/p>\n<p>If your application does not use dynamic secrets, then the sidecar container is not required.<\/p>\n<p>You can read more about <a href=\"https:\/\/www.hashicorp.com\/resources\/what-are-dynamic-secrets-why-do-i-need-them?ref=devopscube.com\" rel=\"noreferrer noopener\">dynamic secrets here<\/a>.<\/p>\n<h2 id=\"prerequisites-github-repository\">Prerequisites &amp; Github Repository<\/h2>\n<p>To follow this tutorial, you need a running vault server on kubernetes.<\/p>\n<p>If you don&#8217;t have a vault server, follow the <a href=\"https:\/\/devopscube.com\/vault-in-kubernetes\/\" rel=\"noreferrer noopener\">vault server setup guide on Kubernetes<\/a>, which explains all the Kubberenetes components involved in vault setup.<\/p>\n<p>Vault server and vault injector Kubernetes manifests are part of the following Github repository.<\/p>\n<pre><code>https:\/\/github.com\/scriptcamp\/kubernetes-vault<\/code><\/pre>\n<h2 id=\"deploy-vault-agent-injector\">Deploy Vault Agent Injector<\/h2>\n<p>As explained earlier, vault inject is a controller code that listens to a mutation webhook. The injector controller is responsible for modifying the pod spec to add sidecar and init containers.<\/p>\n<p>You can deploy the injector with one command.<\/p>\n<p>cd into the vault-injector-manifests directory of the cloned repository and execute the following command.<\/p>\n<pre><code>kubectl apply -f .<\/code><\/pre>\n<p>If you are a helm user, you can install both vault server and injector using a single helm chart.<\/p>\n<pre><code>helm repo add hashicorp https:\/\/helm.releases.hashicorp.com\nhelm repo update\nhelm install vault hashicorp\/vault<\/code><\/pre>\n<p>Anyways, I will go through all the manifest files. Go to the next section if you want to skip the explanation.<\/p>\n<p>All the vault injector components get deployed in the <code>default<\/code> namespace.<\/p>\n<p>Let&#8217;s get started.<\/p>\n<p>Create <code>rbac.yaml<\/code> and copy the following manifest. It creates the following.<\/p>\n<ol>\n<li><code>vault-injector<\/code> service account<\/li>\n<li>vault-agent-injector-clusterrole with persmissions to <code>mutatingwebhookconfigurations<\/code> as we will be deploying a mutating webhook.<\/li>\n<\/ol>\n<pre><code>---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: vault-agent-injector\n  namespace: default\n  labels:\n    app.kubernetes.io\/name: vault-agent-injector\n    app.kubernetes.io\/instance: vault\n\n---\napiVersion: rbac.authorization.k8s.io\/v1\nkind: ClusterRole\nmetadata:\n  name: vault-agent-injector-clusterrole\n  labels:\n    app.kubernetes.io\/name: vault-agent-injector\n    app.kubernetes.io\/instance: vault \nrules:\n- apiGroups: [\"admissionregistration.k8s.io\"]\n  resources: [\"mutatingwebhookconfigurations\"]\n  verbs: \n    - \"get\"\n    - \"list\"\n    - \"watch\"\n    - \"patch\"\n\n---\napiVersion: rbac.authorization.k8s.io\/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: vault-agent-injector-binding\n  labels:\n    app.kubernetes.io\/name: vault-agent-injector\n    app.kubernetes.io\/instance: vault  \nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: vault-agent-injector-clusterrole\nsubjects:\n- kind: ServiceAccount\n  name: vault-agent-injector\n  namespace: default<\/code><\/pre>\n<p>Execute the RBAC manifest.<\/p>\n<pre><code>kubectl apply -f rbac.yaml<\/code><\/pre>\n<p>Create <code>mutating-webhook.yaml<\/code> and copy the following manifest. This webhook is responsible for intercepting and sending pod events to the injector controller. It sends the webhook to the injector controller&#8217;s service endpoint on <code>\/mutate<\/code> path.<\/p>\n<pre><code>---\napiVersion: admissionregistration.k8s.io\/v1\nkind: MutatingWebhookConfiguration\nmetadata:\n  name: vault-agent-injector-cfg\n  labels:\n    app.kubernetes.io\/name: vault-agent-injector\n    app.kubernetes.io\/instance: vault\nwebhooks:\n  - name: vault.hashicorp.com\n    sideEffects: None\n    admissionReviewVersions:\n    - \"v1beta1\"\n    - \"v1\"\n    clientConfig:\n      service:\n        name: vault-agent-injector-svc\n        namespace: default\n        path: \"\/mutate\"\n      caBundle: \"\"\n    rules:\n      - operations: [\"CREATE\", \"UPDATE\"]\n        apiGroups: [\"\"]\n        apiVersions: [\"v1\"]\n        resources: [\"pods\"]\n    failurePolicy: Ignore<\/code><\/pre>\n<p>Create the webhook.<\/p>\n<pre><code>kubectl apply -f mutating-webhook.yaml<\/code><\/pre>\n<p>Create a <code>deployment.yaml<\/code> using the following manifest file.<\/p>\n<ol>\n<li>This deployment assumes you have vault server running on default namespace with service endpoint <code>http:\/\/vault.default.svc:8200<\/code><\/li>\n<li>we are using the latest vault injector image <code>hashicorp\/vault-k8s:0.11.0<\/code> (At the time of writing this blog)<\/li>\n<\/ol>\n<pre><code>---\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: vault-agent-injector\n  namespace: default\n  labels:\n    app.kubernetes.io\/name: vault-agent-injector\n    app.kubernetes.io\/instance: vault\n    component: webhook\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app.kubernetes.io\/name: vault-agent-injector\n      app.kubernetes.io\/instance: vault\n      component: webhook\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io\/name: vault-agent-injector\n        app.kubernetes.io\/instance: vault\n        component: webhook\n    spec:\n      \n      affinity:\n        podAntiAffinity:\n          requiredDuringSchedulingIgnoredDuringExecution:\n            - labelSelector:\n                matchLabels:\n                  app.kubernetes.io\/name: vault-agent-injector\n                  app.kubernetes.io\/instance: \"vault\"\n                  component: webhook\n              topologyKey: kubernetes.io\/hostname\n      serviceAccountName: \"vault-agent-injector\"\n      hostNetwork: false\n      securityContext:\n        runAsNonRoot: true\n        runAsGroup: 1000\n        runAsUser: 100\n      containers:\n        - name: sidecar-injector\n          \n          image: \"hashicorp\/vault-k8s:0.11.0\"\n          imagePullPolicy: \"IfNotPresent\"\n          securityContext:\n            allowPrivilegeEscalation: false\n          env:\n            - name: AGENT_INJECT_LISTEN\n              value: :8080\n            - name: AGENT_INJECT_LOG_LEVEL\n              value: info\n            - name: AGENT_INJECT_VAULT_ADDR\n              value: http:\/\/vault.default.svc:8200\n            - name: AGENT_INJECT_VAULT_AUTH_PATH\n              value: auth\/kubernetes\n            - name: AGENT_INJECT_VAULT_IMAGE\n              value: \"hashicorp\/vault:1.8.0\"\n            - name: AGENT_INJECT_TLS_AUTO\n              value: vault-agent-injector-cfg\n            - name: AGENT_INJECT_TLS_AUTO_HOSTS\n              value: vault-agent-injector-svc,vault-agent-injector-svc.default,vault-agent-injector-svc.default.svc\n            - name: AGENT_INJECT_LOG_FORMAT\n              value: standard\n            - name: AGENT_INJECT_REVOKE_ON_SHUTDOWN\n              value: \"false\"\n            - name: AGENT_INJECT_CPU_REQUEST\n              value: \"250m\"\n            - name: AGENT_INJECT_CPU_LIMIT\n              value: \"500m\"\n            - name: AGENT_INJECT_MEM_REQUEST\n              value: \"64Mi\"\n            - name: AGENT_INJECT_MEM_LIMIT\n              value: \"128Mi\"\n            - name: AGENT_INJECT_DEFAULT_TEMPLATE\n              value: \"map\"\n            - name: AGENT_INJECT_TEMPLATE_CONFIG_EXIT_ON_RETRY_FAILURE\n              value: \"true\"\n            \n          args:\n            - agent-inject\n            - 2&gt;&amp;1\n          livenessProbe:\n            httpGet:\n              path: \/health\/ready\n              port: 8080\n              scheme: HTTPS\n            failureThreshold: 2\n            initialDelaySeconds: 5\n            periodSeconds: 2\n            successThreshold: 1\n            timeoutSeconds: 5\n          readinessProbe:\n            httpGet:\n              path: \/health\/ready\n              port: 8080\n              scheme: HTTPS\n            failureThreshold: 2\n            initialDelaySeconds: 5\n            periodSeconds: 2\n            successThreshold: 1\n            timeoutSeconds: 5<\/code><\/pre>\n<p>Create the deployment.<\/p>\n<pre><code>kubectl apply -f deployment.yaml<\/code><\/pre>\n<p>Create a <code>service.yaml<\/code> with the following manifest. The mutation webhook will use this service endpoint.<\/p>\n<pre><code>---\napiVersion: v1\nkind: Service\nmetadata:\n  name: vault-agent-injector-svc\n  namespace: default\n  labels:\n    app.kubernetes.io\/name: vault-agent-injector\n    app.kubernetes.io\/instance: vault\nspec:\n  ports:\n  - name: https\n    port: 443\n    targetPort: 8080\n  selector:\n    app.kubernetes.io\/name: vault-agent-injector\n    app.kubernetes.io\/instance: vault\n    component: webhook<\/code><\/pre>\n<p>Create the service endpoint.<\/p>\n<pre><code>kubectl apply -f service.yaml<\/code><\/pre>\n<p>Now we have all the vault agent injector components installed.<\/p>\n<h2 id=\"create-vault-secrets-policy\">Create Vault Secrets &amp; Policy<\/h2>\n<p>To demonstrate vault agent injector functionality, I will create the following.<\/p>\n<ol>\n<li>A set of secrets using vault kv engine<\/li>\n<li>Vault policy to read the secrets.<\/li>\n<li>Enable vault kubernetes authentication.<\/li>\n<li>Create a vault role to bind vault policy and kubernetes service account (vault-auth).<\/li>\n<li>Create a vault-auth kubernetes service account to be used for vault server authentication.<\/li>\n<\/ol>\n<blockquote><p><strong>Note:<\/strong> I assume you have unsealed and  loged in to vault using the vault token. If not please follow the vault server setup guide mentioned in the pre-requites and perform steps till vault login.<\/p><\/blockquote>\n<p>Exec into vault pod.<\/p>\n<pre><code>kubectl exec -it vault-0 -- \/bin\/sh  <\/code><\/pre>\n<p>Enable the vault kv engine (key-value store).<\/p>\n<pre><code>vault secrets enable -version=2 -path=\"kv\" kv<\/code><\/pre>\n<p>Create two secrets under <code>kv\/dev\/apps\/service01<\/code> path. <code>appkey<\/code> &amp; <code>apptoken<\/code><\/p>\n<pre><code>vault kv put kv\/dev\/apps\/service01 appkey=\"zsdkfjhj4534\" apptoken=\"zsdasdfaskfjhj4534\" <\/code><\/pre>\n<p>Create a vault policy named svc-policy that allowed read operation on secrets under <code>kv\/data\/dev\/apps\/service01<\/code> path.<\/p>\n<pre><code>vault policy write svc-policy - &lt;&lt;EOH\npath \"kv\/data\/dev\/apps\/service01\" {\n  capabilities = [\"read\"]\n}\nEOH<\/code><\/pre>\n<p>Enable Kubernetes authentication.<\/p>\n<blockquote><p><strong>Note: <\/strong>If you have done this as part of the vault setup on kubernetes, you can ignore the following two commands.<\/p><\/blockquote>\n<pre><code>vault auth enable kubernetes<\/code><\/pre>\n<pre><code>vault write auth\/kubernetes\/config \\\n    token_reviewer_jwt=\"$(cat \/var\/run\/secrets\/kubernetes.io\/serviceaccount\/token)\" \\\n    kubernetes_host=\"https:\/\/$KUBERNETES_PORT_443_TCP_ADDR:443\" \\ kubernetes_ca_cert=@\/var\/run\/secrets\/kubernetes.io\/serviceaccount\/ca.crt<\/code><\/pre>\n<p>Create a vault role named <code>webapp<\/code> that binds <code>svc-policy<\/code> and <code>vault-auth<\/code> kubernetes service account.<\/p>\n<pre><code>vault write auth\/kubernetes\/role\/webapp \\\n        bound_service_account_names=vault-auth \\\n        bound_service_account_namespaces=default \\\n        policies=svc-policy \\\n        ttl=72h<\/code><\/pre>\n<p>Now, exit the pod exec session and create a kubernetes service account named <code>vault-auth<\/code>. Vault agents will use this service account to authenticate to the vault server and retrieve the required secrets.<\/p>\n<pre><code>kubectl create serviceaccount vault-auth<\/code><\/pre>\n<h2 id=\"injecting-secrets-with-vault-agents\">Injecting Secrets With Vault Agents<\/h2>\n<p>Here is what you should know about injecting secrets with vault agents.<\/p>\n<ol>\n<li>The vault agent injector uses pod annotations to decide whether vault agents should be injected into pods.<\/li>\n<li>There are many supported annotations. Please refer the <a href=\"https:\/\/www.vaultproject.io\/docs\/platform\/k8s\/injector\/annotations?ref=devopscube.com\" rel=\"noreferrer noopener\">official documenation<\/a> to know about all the supported annotations.<\/li>\n<li>If you are not using dyanmic secrets, you can disable the sidecar agent using an annotation.<\/li>\n<li>By default the vault agents write the secrets to<code> \/vault\/secrets\/<\/code> path. Which is a shared in memory pod volume.<\/li>\n<\/ol>\n<p>First, we will look at a vault injector example using a simple pod definition with the vault agent annotations and see if the sidecar and init container gets injected into the pods.<\/p>\n<p>Save the following manifest as <code>pod.yaml<\/code>. It deploys an Nginx container.<\/p>\n<pre><code>---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: webapp\n  namespace: default\n  annotations:\n    vault.hashicorp.com\/agent-inject: 'true'\n    vault.hashicorp.com\/role: 'webapp'\n    vault.hashicorp.com\/agent-inject-secret-config.txt: 'kv\/dev\/apps\/service01'      \nspec:\n  containers:\n  - image: nginx:latest\n    name: nginx\n  serviceAccountName: vault-auth<\/code><\/pre>\n<p>As you can see, we have three annotations.<\/p>\n<ol>\n<li><code>vault.hashicorp.com\/agent-inject: 'true'<\/code> &#8211; This annotation enables sidecar and init containers.<\/li>\n<li><code>vault.hashicorp.com\/role: 'webapp<\/code>&#8216; : Assigns the webapp role we created earlier.<\/li>\n<li><code>vault.hashicorp.com\/agent-inject-secret-config.txt: 'kv\/dev\/apps\/service01'<\/code>&#8211; Vault path where the secrets reside.<\/li>\n<\/ol>\n<p>Let&#8217;s deploy the pod. It creates a pod named <strong>webapp<\/strong>.<\/p>\n<pre><code>kubectl apply -f pod.yaml<\/code><\/pre>\n<p>If you look at the injector pod logs, you will see a log showing the request sent from mutation webhook to vault injector service endpoint.<\/p>\n<pre><code>handler: Request received: Method=POST URL=\/mutate?timeout=10s<\/code><\/pre>\n<p>To validate vault agents, let&#8217;s describe the pod and check the events.<\/p>\n<pre><code>kubectl describe pod webapp<\/code><\/pre>\n<p>If you check the output events, you will notice three containers getting created.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-3-63.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"626\" height=\"247\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-3-63.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-3-63.png 626w\"><\/figure>\n<p>Now let&#8217;s check the init container logs and see if it could connect to the vault server and retrieve secrets.<\/p>\n<pre><code>kubectl logs webapp -c vault-agent-init<\/code><\/pre>\n<p>On successful execution, you will see the following output. You can see a message saying the secrets rendered to <code>\/vault\/secrets\/config.txt<\/code><\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-4-55.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"562\" height=\"298\"><\/figure>\n<p>Now you might wonder how did <code>\/vault\/secrets\/config.txt<\/code> path come in to picture?<\/p>\n<p>By default, the vault agent writes all the secrets in \/vault\/secrets\/ path, an in-memory volume that all the containers can access in the pod.<\/p>\n<p>The filename config.txt came from the annotation <strong>vault.hashicorp.com\/agent-inject-secret-config.txt<\/strong>. Any name you give after <strong>-secret<\/strong> will be considered as the secret file name.<\/p>\n<p>Now, let&#8217;s exec into the Nginx app container and see if we can access the secret file.<\/p>\n<pre><code>kubectl exec webapp -c nginx -- cat \/vault\/secrets\/config.txt<\/code><\/pre>\n<p>You should be getting the following output.<\/p>\n<pre><code>data: map[appkey:zsdkfjhj4534 apptoken:zsdasdfaskfjhj4534]\nmetadata: map[created_time:2021-08-08T11:29:42.495211138Z deletion_time: destroyed:false version:1]<\/code><\/pre>\n<p>As you can see, we have the secrets in the file. This means the vault agent is working as expected.<\/p>\n<p>However, the <strong>output is not in a format that<\/strong> the <strong>application can use<\/strong>. As discussed in the introduction, every application expects the secret config in a specific format. A text file with newline strings, a JSON file, or a YAML file.<\/p>\n<p>To achieve this, the vault agent provides templating where you can render secrets in required formats. I will show all the methods with an Nginx deployment as an example.<\/p>\n<h2 id=\"vault-agent-template-example\">Vault Agent Template Example<\/h2>\n<p>You can use vault templates to render secrets in required formats. In this example, we will see how to use templates in deployment annotation.<\/p>\n<p>Save the following manifest as <code>deployment-template.yaml<\/code>. It is a simple nginx deployment with vault agent configs.<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: nginx\n  labels:\n    app: nginx\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      annotations:\n        vault.hashicorp.com\/agent-inject: 'true'\n        vault.hashicorp.com\/role: 'webapp'\n        vault.hashicorp.com\/agent-pre-populate-only: 'true'\n        vault.hashicorp.com\/agent-inject-secret-config.txt: 'kv\/dev\/apps\/service01'\n        vault.hashicorp.com\/agent-inject-template-config.txt: |\n          {{ with secret \"kv\/dev\/apps\/service01\" }}\n          [DEFAULT]\n          LogLevel = DEBUG\n          [DATABASE]\n          Address=127.0.0.1\n          Port=3306\n          User={{ .Data.data.appkey }}\n          Password={{ .Data.data.apptoken }}\n          Database=app\n          {{ end }}\n      labels:\n        app: nginx\n    spec:\n      serviceAccountName: vault-auth\n      containers:\n        - name: nginx\n          image: nginx:latest<\/code><\/pre>\n<p>Here if you see, I have added an annotation named <strong>vault.hashicorp.com\/agent-pre-populate-only: &#8216;true&#8217;<\/strong>. It disables the sidecar agent, and only init container gets deployed in the pod along with Nginx.<\/p>\n<p><strong>vault.hashicorp.com\/agent-inject-template-config.txt<\/strong> annotation contains the secret file template with newline config.<\/p>\n<pre><code>{{ with secret \"kv\/dev\/apps\/service01\" }}\n[DEFAULT]\nLogLevel = DEBUG\n[DATABASE]\nAddress=127.0.0.1\nPort=3306\nUser={{ .Data.data.appkey }}\nPassword={{ .Data.data.apptoken }}\nDatabase=app\n{{ end }}<\/code><\/pre>\n<p>It starts with the <code>with<\/code> block with the secret path and ends with <code>end<\/code> keywords. All the substitution should happen between these blocks.<\/p>\n<p>Here we are substituting the user and password values using <code>{{ .Data.data.appkey }}<\/code> &amp;  <code>{{ .Data.data.apptoken }}<\/code>. Where <strong>appkey<\/strong> and <strong>apptoken<\/strong> are the vault secret keys. Vault agent substitutes the actual values in run-time.<\/p>\n<p>Let&#8217;s create the deployment.<\/p>\n<pre><code>kubectl apply -f deployment-template.yaml<\/code><\/pre>\n<p>If you exec into the Nginx pod created by the deployment, you will see the rendered config.txt file with user and password substituted with actual values from the vault server.<\/p>\n<pre><code>kubectl exec nginx-7c897d75cb-mvjft -c nginx \"--\" cat \/vault\/secrets\/config.txt<\/code><\/pre>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-5-65.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"628\" height=\"247\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-5-65.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-5-65.png 628w\"><\/figure>\n<h2 id=\"vault-agent-configmap-example\">Vault Agent Configmap example<\/h2>\n<p>You can add the secret file template as a configmap also. Along with the template, you need to add a few vault agent configs as well.<\/p>\n<p>Here is an example deployment with configmap that contains the vault agent template and configs.<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: nginx-configmap\n  labels:\n    app: nginx\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      annotations:\n        vault.hashicorp.com\/agent-inject: 'true'\n        vault.hashicorp.com\/agent-pre-populate-only: 'true'\n        vault.hashicorp.com\/agent-configmap: 'my-configmap'\n      labels:\n        app: nginx\n    spec:\n      serviceAccountName: vault-auth\n      containers:\n        - name: nginx\n          image: nginx:latest\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: my-configmap\ndata:\n  config-init.hcl: |\n      \"auto_auth\" = {\n        \"method\" = {\n          \"config\" = {\n            \"role\" = \"webapp\"\n          }\n          \"type\" = \"kubernetes\"\n        }\n\n        \"sink\" = {\n          \"config\" = {\n            \"path\" = \"\/home\/vault\/.token\"\n          }\n\n          \"type\" = \"file\"\n        }\n      }\n\n      \"exit_after_auth\" = true\n      \"pid_file\" = \"\/home\/vault\/.pid\"\n\n      \"template\" = {\n        \"contents\" = \"{{ with secret \\\"kv\/dev\/apps\/service01\\\" }} \\n User={{ .Data.data.appkey }} \\n Password={{ .Data.data.apptoken }} \\n config=testing\\n env=dev\\n {{ end }}\"\n        \"destination\" = \"\/vault\/secrets\/db-creds\"\n      }\n\n      \"vault\" = {\n        \"address\" = \"http:\/\/vault.default.svc:8200\"\n      }<\/code><\/pre>\n<p>In the configmap spec, you can see the filename as <code>config-init.hc<\/code>l. It is for vault init agent. If you are also using a sidecar agent, then you need to create one more <code>config.hcl<\/code> in the configmap with the same configuration as <code>config-init.hcl<\/code><\/p>\n<p>If you notice, other than the <strong>template content<\/strong>, we have other vault agent configs like vault <strong>address<\/strong>, <strong>role<\/strong>, <strong>type,<\/strong> etc., as part of the configmap.<\/p>\n<p>Also, we are using the same template content we used before, but here, we are using <strong><code>\\n<\/code><\/strong> for next line.<\/p>\n<p>You can create the deployment and check the secret file.<\/p>\n<h2 id=\"vault-agent-environment-variable-example\">Vault Agent Environment Variable Example<\/h2>\n<p>There is no direct way using vault agents t make the secrets available as environment variables. However, there is a workaround to do it.<\/p>\n<p>We can use the regular vault template to create a file with an export command for all the environment variables. Then in the deployment, we can use the command arguments to source this file so that all the export commands get executed. The secrets will be available as environments variable for the application.<\/p>\n<p>Create a file named <code>deployment-env.yaml<\/code> using the following manifest. I used a basic ubuntu image for demonstration purposes.<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: nginx\n  labels:\n    app: nginx\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      annotations:\n        vault.hashicorp.com\/agent-inject: 'true'\n        vault.hashicorp.com\/role: 'webapp'\n        vault.hashicorp.com\/agent-pre-populate-only: 'true'\n        vault.hashicorp.com\/agent-inject-secret-database-config: 'kv\/dev\/apps\/service01'\n        vault.hashicorp.com\/agent-inject-template-database-config: |\n          {{ with secret \"kv\/dev\/apps\/service01\" -}}\n            export appkey=\"{{ .Data.data.appkey }}\"\n            export apptoken=\"{{ .Data.data.apptoken }}\"\n          {{- end }}\n      labels:\n        app: nginx\n    spec:\n      serviceAccountName: vault-auth\n      containers:\n        - name: nginx\n          image: ubuntu:latest\n          command: [\"\/bin\/bash\"]\n          args:\n            ['-c', 'source \/vault\/secrets\/database-config &amp;&amp; env &gt; \/vault\/secrets\/test &amp;&amp; tail -f \/dev\/null']\n<\/code><\/pre>\n<p>If the above deployments,<\/p>\n<ol>\n<li>In the annotation we have the template with a export commands for <strong>appkey<\/strong> and <strong>apptoken<\/strong>. You can have multiple export commands like this<\/li>\n<li>In command arguments, we are sourcing the \/vault\/secrets\/database-config<\/li>\n<li>Ther we are writing the env variables to<code> \/vault\/secrets\/test<\/code> for validation. This is just for testing this setup beacuae if you exec it a different session those variable will not be available.<\/li>\n<li>The tail -f \/dev\/null keeps the container running as we are using the base ubuntu image.<\/li>\n<\/ol>\n<p>Create the deployment.<\/p>\n<pre><code>kubectl apply -f deployment-env.yaml<\/code><\/pre>\n<p>Now, if you check the <code>\/vault\/secrets\/test<\/code> file, you will see both <strong>appkey<\/strong> and <strong>apptoken<\/strong> as environment variables. Replace <code>nginx-767dbbd58b-bsxks<\/code> with your pod name.<\/p>\n<pre><code>kubectl exec -it nginx-767dbbd58b-bsxks -c nginx -- cat \/vault\/secrets\/test<\/code><\/pre>\n<h2 id=\"vault-injector-troubleshooting-issues\">Vault Injector Troubleshooting &amp; Issues<\/h2>\n<p>Following are the issues I have faced during the vault injector setup.<\/p>\n<ol>\n<li><strong>Vault agent Template works only after a first pod restart:<\/strong>&#8211; The agent was not able to render the template when the pod gets deployed. However, when I restarted the pod, it worked. I suspect it to be a node resource issue as I was using small k8 nodes. Later when i increse the the node resources, i never got the error.<\/li>\n<li><strong>Templated rendered empty secret values: <\/strong>In vault documentation, for rendering secrets, the systax is given as <code>.Data.key<\/code>. It didnt work. Somewhere if found that we have to add data to the syntax. eg, <code>.Data.data.apptoken<\/code><\/li>\n<li><strong>error authenticating: error=&#8221;context deadline exceeded&#8221; : <\/strong> This happends when the vault agent is not able to connect to the vault server or the service account doest have persmissions to read the secrets. Check the vault URL, policy, role and service account mapping.<\/li>\n<li><strong>Private GKE connectivity issues: <\/strong>In few forums I found people discussing about connectivity issues (Timeout errors in MutatingWebhookConfiguration) from master to injector controller pods due to firewall issues. I can be solved by adding a custom firewall rule from master to nodes on port 8080. Check out the <a href=\"https:\/\/github.com\/hashicorp\/vault-k8s\/issues\/46?ref=devopscube.com\" rel=\"noreferrer noopener\">discussion thread here<\/a>.<\/li>\n<\/ol>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>Using Kubernetes with a vault for secret management, a vault injector is a great way to introduce secrets to pods.<\/p>\n<p>This way application doesn&#8217;t need to be aware of the secret management system. It just has to consume the secrets from a designated file path.<\/p>\n<p>Also, the production vault server should be highly available. Vault server unavailability could result in application failure if there is a pod restart or scaling activity.<\/p>\n<p>Next, in this vault series, we will look at solutions like Bank vault and Kubernetes external secrets.<\/p>\n<p>You may be using a different approach in managing secrets or using solutions like Hashicorp vault.<\/p>\n<p>Either way, leave a comment.<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/vault-agent-injector-tutorial\/\" target=\"_blank\" rel=\"noopener noreferrer\">Vault Agent Injector Tutorial: Setup Init &amp;amp; Sidecar Agents \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/vault-agent-injector-tutorial\/<\/p>\n","protected":false},"author":1,"featured_media":873,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-872","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/872","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=872"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/872\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/873"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=872"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=872"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=872"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}