{"id":1015,"date":"2025-01-28T06:20:09","date_gmt":"2025-01-28T06:20:09","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=1015"},"modified":"2025-01-28T06:20:09","modified_gmt":"2025-01-28T06:20:09","slug":"vault-in-kubernetes","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=1015","title":{"rendered":"How to Setup Vault in Kubernetes- Beginners Tutorial"},"content":{"rendered":"<p>This article aims to explain each of the Kubernetes vault components and step-by-step guides to set up a <a href=\"https:\/\/devopscube.com\/setup-hashicorp-vault-beginners-guide\/\" rel=\"noreferrer noopener\">Vault server<\/a> in Kubernetes. Towards the end of the article, we will also discuss how an application can make use of the vault with a simple demo.<\/p>\n<p>As a beginner, creating components one by one while understanding the steps involved is a great way to learn about <a href=\"https:\/\/devopscube.com\/kubernetes-tutorials-beginners\/\" rel=\"noreferrer noopener\">Kubernetes<\/a> and Vault. Going step by step ensures that you can focus on understanding the &#8216;why&#8217; while learning the &#8216;how&#8217;.<\/p>\n<h2 id=\"creating-the-vault-server-in-kubernetes\">Creating the Vault Server in Kubernetes<\/h2>\n<p>As you know, Kubernetes default secret object is just base64 encoded. You need a good secret management tool and workflow to manage secret storage and retrieval for production uses cases.<\/p>\n<p><a href=\"https:\/\/www.vaultproject.io\/?ref=devopscube.com\" rel=\"noreferrer noopener\">Hashicorp Vault<\/a> is one of the best open-source secret management tools that has good integration with Kubernetes to store and retrieve secrets.<\/p>\n<p>In this setup, you will learn the following.<\/p>\n<p>[powerkit_toc title=&#8221;Table of Contents&#8221; depth=&#8221;1&#8243; min_count=&#8221;4&#8243; min_characters=&#8221;1000&#8243;]<\/p>\n<p>Under each category, I have explained why the specific Kubernetes object is used for the vault deployment.<\/p>\n<h2 id=\"vault-kubernetes-manifests\">Vault Kubernetes Manifests<\/h2>\n<p>All the Kubernetes YAML manifests used in this guide are <a href=\"https:\/\/github.com\/scriptcamp\/kubernetes-vault.git?ref=devopscube.com\" rel=\"noreferrer noopener\">hosted on Github<\/a>. Clone the repository for reference and implementation.<\/p>\n<pre><code>git clone https:\/\/github.com\/techiescamp\/kubernetes-vault.git<\/code><\/pre>\n<h2 id=\"vault-rbac-setup\">Vault RBAC Setup<\/h2>\n<p>Before we get started with the setup, I would like to go through some of the basic Kubernetes objects we would be using in this vault setup.<\/p>\n<ol>\n<li><strong>ClusterRoles:<\/strong> Kubernetes ClusterRoles are entities that have been assigned certain special permissions.<\/li>\n<li><strong>ServiceAccounts<\/strong>: <a href=\"https:\/\/devopscube.com\/kubernetes-api-access-service-account\/\" rel=\"noreferrer noopener\">Kubernetes ServiceAccounts<\/a> are identities assigned to entities such as pods to enable their interaction with the Kubernetes APIs using the role&#8217;s permissions.<\/li>\n<li><strong>ClusterRoleBindings<\/strong>: ClusterRoleBindings are entities that provide roles to accounts i.e. they grant permissions to service accounts.<\/li>\n<\/ol>\n<p>Vault server requires certain extra Kubernetes permissions to do its operations. Therefore, a ClusterRole is required (with the appropriate permissions) to be assigned to a ServiceAccount via a ClusterRoleBinding.<\/p>\n<p>Kubernetes by default has a ClusterRole created with the required permissions i.e. &#8216;<code>system:auth-delegator<\/code>&#8216; so it&#8217;s not required to be created again for this case. Service account and Role Binding are required to be created.<\/p>\n<p>Let&#8217;s create the required RBAC for Vault.<\/p>\n<p>Save the following manifest as <code>rbac.yaml<\/code><\/p>\n<pre><code>---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: vault\n  namespace: default\n\n---\napiVersion: rbac.authorization.k8s.io\/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: vault-server-binding\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: system:auth-delegator\nsubjects:\n- kind: ServiceAccount\n  name: vault\n  namespace: default\n<\/code><\/pre>\n<p>Create the service account and ClusterRolebinding.<\/p>\n<pre><code>kubectl apply -f rbac.yaml<\/code><\/pre>\n<h2 id=\"creating-vault-configmaps\">Creating Vault ConfigMaps<\/h2>\n<p>A ConfigMaps in Kubernetes lets us mount files on containers without the need to make changes to the Dockerfile or rebuilding the container image. <\/p>\n<p>This feature is extremely helpful in cases where configurations have to be modified or created through files.<\/p>\n<p>Vault requires a configuration file with appropriate parameters to start its servers.<\/p>\n<p>Save the following manifest as <code>configmap.yaml<\/code><\/p>\n<pre><code>apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: vault-config\n  namespace: default\ndata:\n  extraconfig-from-values.hcl: |-\n    disable_mlock = true\n    ui = true\n    \n    listener \"tcp\" {\n      tls_disable = 1\n      address = \"[::]:8200\"\n      cluster_address = \"[::]:8201\"\n    }\n    storage \"file\" {\n      path = \"\/vault\/data\"\n    }\n<\/code><\/pre>\n<p>Create the configmap<\/p>\n<pre><code>kubectl apply -f configmap.yaml\n<\/code><\/pre>\n<p>It&#8217;s important to understand the idea behind the parameters inside the config map. Some of them are explained below. For a more exhaustive list of options: &#8211; refer to the <a href=\"https:\/\/www.vaultproject.io\/docs?ref=devopscube.com\" rel=\"noreferrer noopener\">official vault documentation<\/a>.<\/p>\n<ol>\n<li><strong>disable_mlock:<\/strong> Executing mlock syscall prevents memory from being swapped to<\/li>\n<li><strong>disk: <\/strong>This option disables the server from executing the mlock syscall.<\/li>\n<li><strong>ui: <\/strong>Enables the built-in web UI.<\/li>\n<li><strong>listener: <\/strong>Configures how Vault is listening for API requests.<\/li>\n<li><strong>storage: <\/strong>Configures the storage backend where Vault data is stored.<\/li>\n<\/ol>\n<h2 id=\"deploy-vault-services\">Deploy Vault Services<\/h2>\n<p>Services in Kubernetes are the objects that pods use to communicate with each other. <code>ClusterIP<\/code> type services are usually used for inter-pod communication. <\/p>\n<p>There are two types of ClusterIP services<\/p>\n<ol>\n<li>Headless Services<\/li>\n<li>Services<\/li>\n<\/ol>\n<p>Normal Kubernetes services act as load balancers and follow round-robin logic to distribute loads. Headless services don&#8217;t act like load balancers.<\/p>\n<p>Also, normal services are assigned IPs by Kubernetes whereas Headless services are not.<\/p>\n<p>For the vault server, we will create a headless service for internal usage. It will be very useful when we scale the vault to multiple replicas. <\/p>\n<p>A non-headless service will be created for UI as we want to load balance requests to the replicas when accessing the UI.<\/p>\n<p>Vault <strong>exposes its UI at port 8200<\/strong>.  We will use a non-headless service of type NodePort as we want to access this endpoint from outside Kubernetes Cluster.<\/p>\n<p>Save the following manifest as <code>services.yaml<\/code>. It has both service and headless service definitions.<\/p>\n<pre><code>---\n# Service for Vault Server\napiVersion: v1\nkind: Service\nmetadata:\n  name: vault\n  namespace: default\n  labels:\n    app.kubernetes.io\/name: vault\n    app.kubernetes.io\/instance: vault\n  annotations:\nspec:\n  type: NodePort  \n  publishNotReadyAddresses: true\n  ports:\n    - name: http\n      port: 8200\n      targetPort: 8200\n      nodePort: 32000\n    - name: https-internal\n      port: 8201\n      targetPort: 8201\n  selector:\n    app.kubernetes.io\/name: vault\n    app.kubernetes.io\/instance: vault\n    component: server\n\n---\n# Headless Service\napiVersion: v1\nkind: Service\nmetadata:\n  name: vault-internal\n  namespace: default\n  labels:\n    app.kubernetes.io\/name: vault\n    app.kubernetes.io\/instance: vault\n  annotations:\nspec:\n  clusterIP: None\n  publishNotReadyAddresses: true\n  ports:\n    - name: \"http\"\n      port: 8200\n      targetPort: 8200\n    - name: https-internal\n      port: 8201\n      targetPort: 8201\n  selector:\n    app.kubernetes.io\/name: vault\n    app.kubernetes.io\/instance: vault\n    component: server\n<\/code><\/pre>\n<p>Create the services.<\/p>\n<pre><code>kubectl apply -f services.yaml<\/code><\/pre>\n<h3 id=\"understanding-publishnotreadyaddresses\">Understanding publishNotReadyAddresses:<\/h3>\n<p>It&#8217;s a configuration option inside the headless services manifest file.<\/p>\n<p>By default, Kubernetes includes pods under a service only when the pod is in the &#8220;ready&#8221; state.<\/p>\n<p>The &#8220;<code>publishNotReadyAddresses<\/code>&#8221; option changes this behavior by including pods that may or may not be in the ready state. You can see the list of pods in the ready state by doing &#8220;kubectl get pods&#8221;.<\/p>\n<h2 id=\"need-for-vault-statefulset\">Need for Vault StatefulSet<\/h2>\n<p>StatefulSet is the Kubernetes object used to manage stateful applications. <\/p>\n<p>It&#8217;s preferred over deployments for this use case as it provides guarantees about the ordering and uniqueness of these Pods i.e. the management of volumes is better with stateful sets.<\/p>\n<p>This section is critical to get a deeper understanding of the vault.<\/p>\n<p>As a beginner, it is important to understand why we want to deploy a Statefulset and not Deployments. After all, our focus is on understanding the &#8216;why&#8217; along with learning the &#8216;how&#8217;.<\/p>\n<h3 id=\"why-do-we-need-statefulset\">Why do We Need Statefulset?<\/h3>\n<p>Vault is a stateful application i.e. it stores data (like configurations, secrets, metadata of vault operations) inside a volume. If the data is stored in memory, then the data will get erased once the pod restarts. <\/p>\n<p>Also, Vault may have to be scaled to more than one pod in caseload increases. <\/p>\n<p>All these operations have to be done in such a way that data consistency is maintained across vault pods like <code>vault-0<\/code>, <code>vault-1<\/code>, <code>vault-2<\/code>.<\/p>\n<p>How can we achieve this in Kubernetes? Think and then read ahead!<\/p>\n<p>Vault implements continuous replication of data across all its pods. So when data is written on <code>vault-0<\/code> it gets replicated into <code>vault-1<\/code>. <code>vault-2<\/code> replicates data from <code>vault-3<\/code>. And so on&#8230;<\/p>\n<p>The thing to understand here is that <code>vault-1<\/code> needs to know where to look for <code>vault-0<\/code>.<em> <\/em>Otherwise, How will the replication happen?<\/p>\n<ol>\n<li>How will it know from where to fetch data for the replication process?<\/li>\n<li>How will vault-1 know where to look for vault-0?<\/li>\n<li>How will vault-2 know where to look for vault-1?<\/li>\n<\/ol>\n<p>Let&#8217;s try to answer these questions now.<\/p>\n<p>In case of deployments &amp; stateful sets, pods are always assigned a unique name that can be used to look for the pods.<\/p>\n<p>In the <strong>case of deployments,<\/strong> pods are always assigned a unique name but this unique name <strong>changes after the pod are deleted &amp; recreated<\/strong>. So it&#8217;s not useful to identify any pod.<\/p>\n<pre><code>Case of deployments:\nname of pod initially: vault-7c6c5fd47c-fkvpf \nname of pod after it gets deleted &amp; recreated: vault-c5f7c6dfk4-7pfcv \nHere, pod name got changed.<\/code><\/pre>\n<p>In the <strong>case of the stateful set <\/strong>&#8211; each pod is assigned a unique name and this <strong>unique name stays with it even if the pod is deleted <\/strong>&amp; recreated.<\/p>\n<pre><code>Case of statefulsets:\nname of pod initially: vault-0\nname of pod after it gets deleted &amp; recreated: vault-0\nHere, pod name remained the same.<\/code><\/pre>\n<p>That&#8217;s why we want to use a stateful set here i.e. so that we can reach any pod without any discrepancies.<\/p>\n<h3 id=\"beyond-vault\">Beyond Vault<\/h3>\n<p>These concepts of Statefulset &amp; deployments are not unique to Vault, if you explore &#8211; you&#8217;ll find that many popular Kubernetes tools such as Elasticsearch, Postgresql use stateful sets and not deployments due to the same logic.<\/p>\n<h2 id=\"deploy-vault-statefulset\"><strong>Deploy Vault StatefulSet<\/strong><\/h2>\n<p>First, let&#8217;s create the Statefulset. I have added an explanation for the vault Statefulset as well.<\/p>\n<p>Save the following manifest as <code>statefulset.yaml<\/code><\/p>\n<pre><code>apiVersion: apps\/v1\nkind: StatefulSet\nmetadata:\n  name: vault\n  namespace: default\n  labels:\n    app.kubernetes.io\/name: vault\n    app.kubernetes.io\/instance: vault\nspec:\n  serviceName: vault-internal\n  replicas: 1\n  selector:\n    matchLabels:\n      app.kubernetes.io\/name: vault\n      app.kubernetes.io\/instance: vault\n      component: server\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io\/name: vault\n        app.kubernetes.io\/instance: vault\n        component: server\n    spec:\n      serviceAccountName: vault\n      securityContext:\n        runAsNonRoot: true\n        runAsGroup: 1000\n        runAsUser: 100\n        fsGroup: 1000\n      volumes:\n        - name: config\n          configMap:\n            name: vault-config\n        - name: home\n          emptyDir: {}\n      containers:\n        - name: vault          \n          image: hashicorp\/vault:1.18.0\n          imagePullPolicy: IfNotPresent\n          command:\n          - \"\/bin\/sh\"\n          - \"-ec\"\n          args: \n          - |\n            cp \/vault\/config\/extraconfig-from-values.hcl \/tmp\/storageconfig.hcl;\n            [ -n \"${HOST_IP}\" ] &amp;&amp; sed -Ei \"s|HOST_IP|${HOST_IP?}|g\" \/tmp\/storageconfig.hcl;\n            [ -n \"${POD_IP}\" ] &amp;&amp; sed -Ei \"s|POD_IP|${POD_IP?}|g\" \/tmp\/storageconfig.hcl;\n            [ -n \"${HOSTNAME}\" ] &amp;&amp; sed -Ei \"s|HOSTNAME|${HOSTNAME?}|g\" \/tmp\/storageconfig.hcl;\n            [ -n \"${API_ADDR}\" ] &amp;&amp; sed -Ei \"s|API_ADDR|${API_ADDR?}|g\" \/tmp\/storageconfig.hcl;\n            [ -n \"${TRANSIT_ADDR}\" ] &amp;&amp; sed -Ei \"s|TRANSIT_ADDR|${TRANSIT_ADDR?}|g\" \/tmp\/storageconfig.hcl;\n            [ -n \"${RAFT_ADDR}\" ] &amp;&amp; sed -Ei \"s|RAFT_ADDR|${RAFT_ADDR?}|g\" \/tmp\/storageconfig.hcl;\n            \/usr\/local\/bin\/docker-entrypoint.sh vault server -config=\/tmp\/storageconfig.hcl     \n          securityContext:\n            allowPrivilegeEscalation: false\n          env:\n            - name: HOSTNAME\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.name\n            - name: VAULT_ADDR\n              value: \"http:\/\/127.0.0.1:8200\"\n            - name: VAULT_API_ADDR\n              value: \"http:\/\/$(POD_IP):8200\"\n            - name: SKIP_CHOWN\n              value: \"true\"\n            - name: SKIP_SETCAP\n              value: \"true\"\n            - name: VAULT_CLUSTER_ADDR\n              value: \"https:\/\/$(HOSTNAME).vault-internal:8201\"\n            - name: HOME\n              value: \"\/home\/vault\"\n          volumeMounts:\n            - name: data\n              mountPath: \/vault\/data  \n            - name: config\n              mountPath: \/vault\/config\n            - name: home\n              mountPath: \/home\/vault\n          ports:\n            - containerPort: 8200\n              name: http\n            - containerPort: 8201\n              name: https-internal\n            - containerPort: 8202\n              name: http-rep\n          readinessProbe:\n            exec:\n              command: [\"\/bin\/sh\", \"-ec\", \"vault status -tls-skip-verify\"]\n            failureThreshold: 2\n            initialDelaySeconds: 5\n            periodSeconds: 5\n            successThreshold: 1\n            timeoutSeconds: 3\n  volumeClaimTemplates:\n    - metadata:\n        name: data\n      spec:\n        accessModes:\n          - ReadWriteOnce\n        resources:\n          requests:\n             storage: 1Gi\n<\/code><\/pre>\n<p>Create the Statefulset.<\/p>\n<pre><code>kubectl apply -f statefulset.yaml<\/code><\/pre>\n<p>The Statefulset YAML of the vault has a lot of components such as configmap mounts, security context, probes, etc.<\/p>\n<blockquote><p><strong>Note:<\/strong> Vault remains in a non ready state of now. It needs to be unsealed to become in ready state.  Feel free to skip to next section to unseal vault. For production use cases, <a href=\"https:\/\/learn.hashicorp.com\/collections\/vault\/auto-unseal?ref=devopscube.com\" rel=\"noreferrer noopener\">auto unseal <\/a>options should be used.<\/p><\/blockquote>\n<p>Let us dive deeper and understand what each part is doing.<\/p>\n<p><strong>Configurations: <\/strong>Vault&#8217;s extra configuration file is being mounted at <code>\/vault\/config\/extraconfig-from-values.hcl<\/code> and is then being copied to <code>\/tmp\/storageconfig.hcl <\/code>where it is used when the process starts.<\/p>\n<pre><code>cp \/vault\/config\/extraconfig-from-values.hcl \/tmp\/storageconfig.hcl;\n\/usr\/local\/bin\/docker-entrypoint.sh vault server -config=\/tmp\/storageconfig.hcl    <\/code><\/pre>\n<p><strong>ServiceAccount: <\/strong>Since we want the vault server to have the required permissions of &#8216;system:auth-delegator&#8217;. A service account has been assigned to the pod.<\/p>\n<pre><code> serviceAccountName: vault<\/code><\/pre>\n<p><strong>SecurityContext:<\/strong> Running pods with privileged processes pose a security risk. This is because running a process with root on a pod is identical to running a process with root on the host node. <\/p>\n<p>Ideally, pods should be run keeping in mind isolation between containers and host nodes. Therefore, here pods have been instructed to run as non-root users.<\/p>\n<pre><code>securityContext:\n    runAsNonRoot: true\n    runAsGroup: 1000\n    runAsUser: 100\n    fsGroup: 1000<\/code><\/pre>\n<p><strong>Probes:<\/strong> Probes ensure that the vault does not get stuck in a loop due to any bug and can be restarted automatically in case an unexpected error comes up.<\/p>\n<pre><code>readinessProbe:\n    exec:\n        command: [\"\/bin\/sh\", \"-ec\", \"vault status -tls-skip-verify\"]\n    failureThreshold: 2\n    initialDelaySeconds: 5\n    periodSeconds: 5\n    successThreshold: 1\n    timeoutSeconds: 3<\/code><\/pre>\n<p><strong>VolumeClaimTemplates: <\/strong>A template by which a stateful set can create volumes for replicas.<\/p>\n<pre><code>volumeClaimTemplates:\n    - metadata:\n        name: data\n      spec:\n        accessModes:\n          - ReadWriteOnce\n        resources:\n          requests:\n             storage: 1Gi<\/code><\/pre>\n<p>The vault setup is complete. The next step is to unseal and initialize the vault.<\/p>\n<h2 id=\"unseal-initialise-vault\">Unseal &amp; <strong>Initialise Vault<\/strong><\/h2>\n<p>Initialization is the process by which Vault&#8217;s storage starts preparation to receive data. <\/p>\n<p>Vault generates an in-memory master key and applies Shamir&#8217;s secret sharing algorithm to disassemble that master key into multiple keys. These keys are called &#8220;unseal keys&#8221;.<\/p>\n<p>So to initialize the vault, first, we need to unseal the vault using the unseal keys.<\/p>\n<p><strong>Step 1:<\/strong> First, we are creating tokens and keys to start using the vault.<\/p>\n<blockquote><p>Note:  Pleaset install jq if it is not installed on your system.<\/p><\/blockquote>\n<pre><code>kubectl exec vault-0 -- vault operator init -key-shares=1 -key-threshold=1 -format=json &gt; keys.json\n\nVAULT_UNSEAL_KEY=$(cat keys.json | jq -r \".unseal_keys_b64[]\")\necho $VAULT_UNSEAL_KEY\n\nVAULT_ROOT_KEY=$(cat keys.json | jq -r \".root_token\")\necho $VAULT_ROOT_KEY<\/code><\/pre>\n<p><strong>Step 2: <\/strong>Unseal is the state at which the vault can construct keys that are required to decrypt the data stored inside it.<\/p>\n<pre><code>kubectl exec vault-0 -- vault operator unseal $VAULT_UNSEAL_KEY<\/code><\/pre>\n<blockquote><p><strong>Important Note:<\/strong> If the Vault pod restarts, it gets sealed autoamtically. You will need to unseal vault again using the above unseal command. Also, if you run more than one replicaset, you need to login to all pods and execute the unseal commands. For production use cases, there are <a href=\"https:\/\/learn.hashicorp.com\/collections\/vault\/auto-unseal?ref=devopscube.com\" rel=\"noreferrer noopener\">auto-unseal <\/a>options available.<\/p><\/blockquote>\n<h2 id=\"login-access-vault-ui\">Login &amp; Access Vault UI<\/h2>\n<p>You can log in to Vault using UI and CLI.<\/p>\n<blockquote><p><strong>Note:<\/strong> In production setup, vault UI and login will be secured using LDAP, okta  or other secure mechanisms that follows standard security guidelines.<\/p><\/blockquote>\n<p>The following command will remotely execute the vault log command as we have the <code>VAULT_ROOT_KEY<\/code> set locally in the environment variable.<\/p>\n<pre><code>kubectl exec vault-0 -- vault login $VAULT_ROOT_KEY<\/code><\/pre>\n<p>The above command will display a token. Save the token as you will need it to log in to the vault UI.<\/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-29-9.png\" class=\"kg-image\" alt=\"Kubernetes Vault login token\" loading=\"lazy\" width=\"509\" height=\"296\"><\/figure>\n<p>As we have created a service on nodeport <code>32000<\/code>, we will be able to access the Vault UI using any of the Node IPs on port 32000. You can use the saved token to log in to the UI.<\/p>\n<p>For example,<\/p>\n<pre><code>http:\/\/33.143.55.228:32000<\/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-30-7.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"1650\" height=\"888\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-30-7.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/03\/image-30-7.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/03\/image-30-7.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-30-7.png 1650w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"creating-vault-secrets\">Creating Vault Secrets<\/h2>\n<p>We can create secrets using CLI as well as the UI. We will use the CLI to create secrets.<\/p>\n<p>To use the vault CLI, we need to exec into the vault pod.<\/p>\n<pre><code>kubectl exec -it vault-0 -- \/bin\/sh<\/code><\/pre>\n<h3 id=\"create-secrets\">Create secrets<\/h3>\n<p>There are multiple secret engines (Databases, <a href=\"https:\/\/devopscube.com\/setup-consul-cluster-guide\/\" rel=\"noreferrer noopener\">Consul<\/a>, AWS, etc). Refer to the <a href=\"https:\/\/www.vaultproject.io\/docs\/secrets?ref=devopscube.com\" rel=\"noreferrer noopener\">official documentation<\/a> to know more about the supported secret engines.<\/p>\n<p>Here, we are utilizing <a href=\"https:\/\/www.vaultproject.io\/docs\/secrets\/kv\/kv-v2?ref=devopscube.com\" rel=\"noreferrer noopener\">key-value engine v2<\/a>. It has advanced capabilities to keep multiple versions of the same keys. v1 can manage only a single version at a time.<\/p>\n<p>Let&#8217;s enable the Key value engine.<\/p>\n<pre><code>vault secrets enable -version=2 -path=\"demo-app\" kv<\/code><\/pre>\n<p>Create a secret in key-value format and list it. The id (key) is <code>name<\/code> and secret(value)  would be <code>devopscube<\/code>. Path is<code> demo-app\/user01<\/code><\/p>\n<pre><code>vault kv put demo-app\/user01 name=devopscube\nvault kv get demo-app\/user01 <\/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-32-3.png\" class=\"kg-image\" alt=\"Kubernetes vault create secret as key value pair.\" loading=\"lazy\" width=\"454\" height=\"421\"><\/figure>\n<h3 id=\"create-a-policy\">Create a Policy<\/h3>\n<p>By default, the secret path has the deny policy enabled. We need to explicitly add a policy to read\/write\/delete the secrets.<\/p>\n<p>The following policy dictates that the entity be allowed the read operation for secrets stored under &#8220;<code>demo-app<\/code>&#8220;. Execute it to create the policy<\/p>\n<pre><code>vault policy write demo-policy - &lt;&lt;EOH\npath \"demo-app\/*\" {\n  capabilities = [\"read\"]\n}\nEOH<\/code><\/pre>\n<p>You can list and validate the policy.<\/p>\n<pre><code>vault policy list<\/code><\/pre>\n<h2 id=\"enable-vault-kubernetes-authentication-method\">Enable Vault Kubernetes Authentication Method<\/h2>\n<p>For Kubernetes pods to interact with Vault and get the secrets, it needs a vault token. The kubernetes auth method with the pod service account makes it easy for pods to retrieve secrets from the vault.<\/p>\n<p>In this way, any pod which has been assigned the &#8220;<code>vault<\/code>&#8221; as the <a href=\"https:\/\/devopscube.com\/create-kubernetes-role\/\" rel=\"noreferrer noopener\">service account<\/a> &#8211; will be able to read these secrets without requiring any vault token.<\/p>\n<p>Let&#8217;s enable the kubernetes auth method.<\/p>\n<pre><code>vault auth enable kubernetes<\/code><\/pre>\n<p>You can view and enable auth methods from the vault UI as well.<\/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-33-4.png\" class=\"kg-image\" alt=\"vault UI enable auth methods.\" loading=\"lazy\" width=\"821\" height=\"386\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/image-33-4.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-33-4.png 821w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>We have attached a service account with a ClusterRole to the vault Statefulset. The following command configures the service account token to enable the vault server to make API calls to Kubernetes using the token, Kubernetes URL and the cluster API CA certificate.<\/p>\n<p><code>KUBERNETES_PORT_443_TCP_ADDR<\/code> is the env variable inside the pod that returns the internal API endpoint.<\/p>\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\" \\\n  kubernetes_ca_cert=@\/var\/run\/secrets\/kubernetes.io\/serviceaccount\/ca.crt<\/code><\/pre>\n<blockquote><p><strong>NOTE: <\/strong>Use the <code>disable_iss_validation=true<\/code> option in the above command to disables issuer validation, this allowa Vault to skip checking the iss issuer claim in JWT tokens. <\/p>\n<p>Avoid using in production environment, this is only for testing.<\/p><\/blockquote>\n<p>Now we have to <strong>create a vault approle<\/strong> that binds a Kubernetes service account, namespace, and vault policies. This way vault server knows if a specific service account is authorized to read the stored secrets.<\/p>\n<p>Let&#8217;s create a service account that can be used by application pods to retrieve secrets from the vault.<\/p>\n<pre><code>kubectl create serviceaccount vault-auth<\/code><\/pre>\n<p>Let&#8217;s create a vault <code>approle<\/code> named <code>webapp<\/code> and bind a service account named <code>vault-auth <\/code>in the <code>default<\/code> namespace. Also, we are attaching the <code>demo-policy<\/code> we have created which has read access to a secret.<\/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=demo-policy \\\n        ttl=72h<\/code><\/pre>\n<p>Now, any pod in the default namespace with a <code>vault-auth<\/code> service account can request the secret under demo-policy.<\/p>\n<h2 id=\"fetching-secrets-stored-in-vault-with-service-accounts\">Fetching Secrets Stored in Vault With Service Accounts<\/h2>\n<p>The objective is that a pod should be able to fetch secrets from the vault without it being fed any vault token i.e. it should use the service account token to make the request to the vault.<\/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\/vault-1.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"903\" height=\"615\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/03\/vault-1.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/vault-1.png 903w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Here is how it works.<\/p>\n<ol>\n<li>A <a href=\"https:\/\/jwt.io\/introduction?ref=devopscube.com\" rel=\"noreferrer noopener\">JWT token<\/a> (Service account token) from the pod is passed to the vault server.<\/li>\n<li>Vault server requests the Kubernetes API server to get the service account and namespace attached to the JWT token.<\/li>\n<li>The Kubernetes API server returns the namespace and service account details.<\/li>\n<li>Vault server validates if the service account is authorized to read secrets using the attached policies.<\/li>\n<li>After validation, the vault server returns a vault token.<\/li>\n<li>In another API call, the vault token is passed with a secret path to retrieve the secrets.<\/li>\n<\/ol>\n<p>To demonstrate this, first, we will deploy a pod named <code>vault-client<\/code> with <code>vault-auth<\/code> service account in the <code>default<\/code> namespace.<\/p>\n<p>Save the manifest as <code>pod.yaml<\/code><\/p>\n<pre><code>---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: vault-client\n  namespace: default\nspec:\n  containers:\n  - image: nginx:latest\n    name: nginx\n  serviceAccountName: vault-auth<\/code><\/pre>\n<p>Deploy the pod.<\/p>\n<pre><code>kubectl apply -f pod.yaml<\/code><\/pre>\n<p>Now, take the exec session of the pod, so that we can make the API requests and see if it is able to authenticate to the vault server and retrieve the required secrets.<\/p>\n<pre><code>kubectl exec -it vault-client -- \/bin\/bash<\/code><\/pre>\n<p>Follow the steps given below.<\/p>\n<p><strong>Step 1:<\/strong>  Make an API request to the vault server using the service account token (JWT) to acquire a client token.<\/p>\n<p>First, install jq if it&#8217;s not installed on your system using the following command.<\/p>\n<pre><code>apt-get update &amp;&amp; apt-get install -y jq<\/code><\/pre>\n<p>Then, save the service account token to a variable <code>jwt_token<\/code><\/p>\n<pre><code>jwt_token=$(cat \/var\/run\/secrets\/kubernetes.io\/serviceaccount\/token)<\/code><\/pre>\n<p>Make the API call using curl. Replace <code>http:\/\/35.193.55.248:32000<\/code> with your vault URL.<\/p>\n<pre><code>curl --request POST \\\n    --data '{\"jwt\": \"'$jwt_token'\", \"role\": \"webapp\"}' \\\n    http:\/\/35.193.55.248:32000\/v1\/auth\/kubernetes\/login | jq<\/code><\/pre>\n<p>The above API request will return a JSON containing the <code>client_token<\/code>. Using this token, secrets can be read.<\/p>\n<p>Sample JSON (beautified) output:<\/p>\n<pre><code>{\n  \"request_id\": \"87440e92-cbe9-8357-059d-c4ecdfd58e84\",\n  \"lease_id\": \"\",\n  \"renewable\": false,\n  \"lease_duration\": 0,\n  \"data\": null,\n  \"wrap_info\": null,\n  \"warnings\": null,\n  \"auth\": {\n    \"client_token\": \"s.r2kKKaYnGR48CR9aWvoV8skx\",\n    \"accessor\": \"bqqCCmrxS58TiTyafH9udHm7\",\n    \"policies\": [\n      \"default\",\n      \"demo-policy\"\n    ],\n    \"token_policies\": [\n      \"default\",\n      \"demo-policy\"\n    ]\n  }\n}<\/code><\/pre>\n<p>Now we will use the client_token to make an API call and fetch the stored secrets under <code>demo-app<\/code>. Replace the token and vault URL with relevant values.<\/p>\n<pre><code>curl -H \"X-Vault-Token: s.bSRl7TNajYxWvA7WiLdTQS7Z\" \\\n     -H \"X-Vault-Namespace: vault\" \\\n     -X GET http:\/\/35.193.55.248:32000\/v1\/demo-app\/data\/user01?version=1 | jq<\/code><\/pre>\n<p>The above API request will return a JSON containing the secrets under &#8216;data&#8217;.<\/p>\n<p>Sample output (beautified)<\/p>\n<pre><code>{\n  \"request_id\": \"d2a2fc9c-6eca-9a12-a4a8-8799230e9449\",\n  \"lease_id\": \"\",\n  \"renewable\": false,\n  \"lease_duration\": 0,\n  \"data\": {\n    \"data\": {\n      \"name\": \"devopscube\"\n    }\n  }\n}<\/code><\/pre>\n<h2 id=\"inject-secrets-to-pods-using-vault-agent\">Inject Secrets to Pods Using Vault Agent<\/h2>\n<p>Using a vault injector, you can inject secrets into Kubernetes pods. We have a detailed guide on setting up a vault injector that explains the whole workflow.<\/p>\n<p>Refer: <a href=\"https:\/\/devopscube.com\/vault-agent-injector-tutorial\/\" rel=\"noreferrer noopener\">Vault Agent Injector Tutorial<\/a><\/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-15.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-15.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/03\/image-7-15.png 969w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<h2 id=\"possible-errors-troubleshooting\">Possible Errors &amp; Troubleshooting<\/h2>\n<p>If you delete the stateful set and redeploy the vault again,  don&#8217;t run the init command again. It might throw the following error.<\/p>\n<pre><code>Error initializing: Error making API request.\n\nURL: PUT http:\/\/127.0.0.1:8200\/v1\/sys\/init\nCode: 400. Errors:<\/code><\/pre>\n<p>It happens because by design when you delete a Statefulset the volumes attached to the pod won&#8217;t get deleted. This way you have a chance to copy the data.<\/p>\n<p>So the PVC will have the initialized configs. So you just have to use the existing key.json and use the secret token to unseal the vault without initialization. Or, you can delete the PVC entirely and redeploy the vault with a new PVC.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>By now, you have learned to set up, configure and utilize vault in Kubernetes. It&#8217;s a great starting point and there are more concepts and workflows that you can explore.<\/p>\n<p>When it comes to <strong>vault production implementation<\/strong>, there are many considerations and standard security practices to be followed.<\/p>\n<p>Also, when used with application pods to retrieve secrets, you can use <a href=\"https:\/\/www.vaultproject.io\/docs\/platform\/k8s\/injector?ref=devopscube.com\" rel=\"noreferrer noopener\">Vault injector<\/a> to inject secrets in the pod. We will publish the vault injector tutorial in the upcoming posts.<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/vault-in-kubernetes\/\" target=\"_blank\" rel=\"noopener noreferrer\">How to Setup Vault in Kubernetes- Beginners Tutorial \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/vault-in-kubernetes\/<\/p>\n","protected":false},"author":1,"featured_media":1016,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1015","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\/1015","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=1015"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/1015\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/1016"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1015"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1015"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1015"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}