Thursday, July 4, 2024
HomeServerSecurityDeploy HashiCorp Vault for Secrets Management in Kubernetes

Deploy HashiCorp Vault for Secrets Management in Kubernetes

In a real Kubernetes production environment, you do not only have containers, but also other tightly-integrated collections of related components. The microservices act as mini-applications that work together as a system. In other words, a microservice is an application that runs on a server or virtual instance and responds to network requests. The main advantage of microservices is that it allows the admins to build specific applications for customers/tasks. The admins can then scale the services as desired.

It is therefore important to understand the fundamentals of Kubernetes microservices and the security features. Normally, Kubernetes allows one to store and manage sensitive information that includes passwords, OAuth tokens, SSH keys e.t.c. as Kubernetes secrets.

The idea of using secrets is to avoid exposing sensitive data in your application code since the Secrets can be deployed independently of the pods using them. There are 3 ways in which a pod can use a secret. There are:

  • As files in a volume mounted on one or more of its containers.
  • As container environment variable
  • In a container image

Once the Kubernetes cluster grows, it becomes hard to manage the secrets. HashiCorp Vault acts as a centrally managed service that handles encryption and storage of secrets in the entire infrastructure. It allows one to store and control access to tokens, passwords, certificates, and encryption keys to protect secrets and other sensitive data using UI, CLI, or HTTP API. Vault is a complex tool with several pieces working together.

The below diagram shows the Vault architecture:

HashiCorp Vault for Secrets Management

There are several features associated with HashiCorp Vault, these are:

  • Open Source – it is available for download and can be used locally or in the cloud.
  • Identity-based security – it deeply integrates with trusted identities to automate access to secrets, data, and systems
  • Application & machine identity – it allows one to secure applications and systems with machine identity and automate credential issuance, rotation e.t.c

HashiCorp vault is commonly used for:

  • Secret management: where you can centrally store and manage the secrets
  • Dynamic secrets: that are generated on demand. They are unique to clients instead of using static secrets.
  • Kubernetes secrets: where you can securely inject the secrets in your application stack
  • Database Credential rotation: you can rotate the database password automatically using the Vault’s database engine.
  • Automated PKI infrastructure: quickly create X.509 certificates on demand.
  • Identity-based access: ability to authenticate to other clouds, systems, and endpoints using trusted identities.

In this guide, we will learn how to deploy HashiCorp Vault for Secrets Management in Kubernetes

Getting Started

For this guide, you need the following:

We will also use the below tools:

  • Kubectl
  • Helm v3
  • Vault CLI

Kubectl can be installed using the command:

curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin

Allow kubectl to access the cluster:

# For k0s
export KUBECONFIG=/var/lib/k0s/pki/admin.conf

Check the available nodes:

# kubectl get nodes
NAME     STATUS   ROLES           AGE     VERSION
master   Ready    control-plane   2m54s   v1.24.3+k0s
node1    Ready    <none>          112s    v1.24.3+k0s
node2    Ready    <none>          113s    v1.24.3+k0s
node3    Ready    <none>          108s    v1.24.3+k0s

Step 1 – Create a Persistent Storage for Vault

Vault requires one to create a Storage class with a Persistent volume configured. Begin by creating the namespace and set it as default:

kubectl create namespace vault
kubectl config set-context --current --namespace vault

Example on manual storage class creation (Only for reference)

If you have an existing default StorageClass configured in your cluster you can skip this step.

Create a storage class;

vim storageClass.yml

Add the below lines to the file:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: my-local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

Create the storage class and set it as default.

kubectl create -f storageClass.yml
kubectl patch storageclass my-local-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

Then create a PV using the storage class:

vim vault-pv.yml

Add the lines below:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-local-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: my-local-storage
  local:
    path: /mnt/disk/vol1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node1

With the node affinity set to node1, create the storage path on node1 as shown.

DIRNAME="vol1"
sudo mkdir -p /mnt/disk/$DIRNAME 
sudo chcon -Rt svirt_sandbox_file_t /mnt/disk/$DIRNAME
sudo chmod 777 /mnt/disk/$DIRNAME

Apply the manifest:

kubectl create -f vault-pv.yml

Step 2 – Run HashiCorp Vault on Kubernetes

This guide offers the easiest way to deploy Vault on Kubernetes using Helm. First, ensure that helm is installed on your system.

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
chmod 700 get_helm.sh
sudo ./get_helm.sh

Once installed, verify as shown:

$ helm version
version.BuildInfo{Version:"v3.9.2", GitCommit:"1addefbfe665c350f4daf868a9adc5600cc064fd", GitTreeState:"clean", GoVersion:"go1.17.12"}

Git clone into the official Vault helm chart maintained by Hashicorp:

git clone https://github.com/hashicorp/vault-helm.git
cd vault-helm

In the directory, you can customize the values.yaml as desired, then install Vault as shown:

helm install vault . --namespace vault --values values.yaml

Verify if the pods are running:

# kubectl get pods -n vault
NAME                                   READY   STATUS    RESTARTS   AGE
vault-0                                0/1     Running   0          75s
vault-agent-injector-57f567889-js274   1/1     Running   0          80s

You will also have a PVC created using the default storage class.

# kubectl get pvc
NAME           STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS       AGE
data-vault-0   Bound    my-local-pv   10Gi       RWO            my-local-storage   9s

Initialize the vault cluster using the command:

kubectl exec -ti --namespace vault vault-0 vault operator init

The command should return the initial token and unseal keys as shown;

kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
Unseal Key 1: vjwrDznfPk/7kHWY8L4OQL4PwXSuYFo3z45lt5SHolxj
Unseal Key 2: mnBo0TJGqDI1Qld18gM4kg6b58GYjLzKMAWaSX9uVwEg
Unseal Key 3: 6QWSei7R7re4sFlyz7os1TNpdxoJzpFOCvmhk09xIMWD
Unseal Key 4: iGZm2RiEQK3//RtUosUftb5dFU1R1YlqZmLQJJk7+I1I
Unseal Key 5: cnC9fyyxb4cBgKAKUbjTXT2R+y0CmyP/Ve7AlNvKZbut

Initial Root Token: hvs.QqliIhOLvhZq0nTCbKLAZXor

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated root key. Without at least 3 keys to
reconstruct the root key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.

Step 3 – Configuring HashiCorp Vault Kubernetes Service

Once HashiCorp Vault has been deployed, a ClusterIP service will be deployed as well running on ports 8200 and 8201.

# kubectl get svc
NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
vault                      ClusterIP   10.102.36.175   <none>        8200/TCP,8201/TCP   21m
vault-agent-injector-svc   ClusterIP   10.103.189.23   <none>        443/TCP             21m
vault-internal             ClusterIP   None            <none>        8200/TCP,8201/TCP   21m

In order to access this service remotely, we need to configure the service as NodePort, LoadBalancer, or use an Ingress service.

In this guide, we will configure the service as a LoadBalancer. Begin by installing MetalLB using our guide below:

Now edit the HashiCorp Vault service using the command:

kubectl edit svc/vault

In the opened file, change the type to LoadBalancer:

...... 
  sessionAffinity: None
  type: LoadBalancer
.......

Save the file and check the external IP Address associated with the service:

# kubectl get svc
NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)                         AGE
vault                      LoadBalancer   10.102.36.175   192.168.205.40   8200:30996/TCP,8201:30620/TCP   146m
vault-agent-injector-svc   ClusterIP      10.103.189.23   <none>           443/TCP                         146m
vault-internal             ClusterIP      None            <none>           8200/TCP,8201/TCP               146m

Now you can access the service using Load Balancer on http://external_IP:8200

HashiCorp Vault for Secrets Management in Kubernetes 2

Unseal vault by providing 3 unseal keys then proceed and provide the initial root token to be authenticated to the dashboard.

HashiCorp Vault for Secrets Management in Kubernetes 3

On successful authentication, you will see this Vault dashboard

HashiCorp Vault for Secrets Management in Kubernetes 4

Step 4 – Connect to Vault from Kubernetes

To connect to the vault, you need to start an interactive shell on the pod vault-0 using the command:

kubectl exec -it vault-0 /bin/sh

Login to the vault using the root token

$ vault login 
Token (will be hidden):  <root initial token>

Check the status:

/ $ vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.11.2
Build Date      2022-07-29T09:48:47Z
Storage Type    file
Cluster Name    vault-cluster-3d5e5ad2
Cluster ID      be268c68-646d-e4bd-9acf-c20c2ace1a91
HA Enabled      false

A. Creating a secret

For this guide, we will create a secret that will be used by applications later at internal/database/config. First, enable a kv secret engine

/ $ vault secrets enable -path=internal kv-v2
Success! Enabled the kv-v2 secrets engine at: internal/

Create the secret with the variables such as username and password to be used later:

vault kv put internal/database/config username="test_db" password="Passw0rd"

Verify the creation using the command:

vault kv get internal/database/config

Sample Output:

======== Secret Path ========
internal/data/database/config

======= Metadata =======
Key                Value
---                -----
created_time       2022-08-11T09:12:52.86722059Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

====== Data ======
Key         Value
---         -----
password    Passw0rd!
username    test_db

Proceed and create a policy called internal-app that enables create, read, update, delete and list capabilities:

vault policy write internal-app - <<EOH
path "internal/data/database/config" {
  capabilities = ["create", "read", "update", "delete", "list"]
}
EOH

B. Enable Kubernetes Authentication

To enable the clients to authenticate using a Kubernetes Service Account Token, enable the Kubernetes authentication method:

vault auth enable kubernetes

Vault will now accept the service token from any client within your Kubernetes cluster. This is done by querying a configured Kubernetes endpoint. We will configure the authentication method to use the below variables:

vault write auth/kubernetes/config \
        token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
        kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
        kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

Create a role(internal-app) for the Kubernetes auth method to be used:

vault write auth/kubernetes/role/internal-app \
        bound_service_account_names=internal-app \
        bound_service_account_namespaces=vault \
        policies=internal-app \
        ttl=24h

The role will connect to a Kubernetes service account internal-app in the vault namespace and the tokens returned are valid for 24 hours.

Now exit the shell on vault-0 pod:

/ $ exit

C. Create a Kubernetes service account

We defined a Kubernetes service account named internal-app but the service account does not exist yet. We need to create the service account

kubectl apply \
  --filename=-<<EOH
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: internal-app
  namespace: vault
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: role-tokenreview-binding
  namespace: vault
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
- kind: ServiceAccount
  name: internal-app
  namespace: vault
EOH

Apply a cluster role binding for the ExternalSecrets’ service account with the role auth-delegator.

kubectl apply \
  --filename=-<<EOH
---
apiVersion: v1
kind: Secret
metadata:
  name: vault-auth-secret
  annotations:
    kubernetes.io/service-account.name: internal-app
type: kubernetes.io/service-account-token
EOH

Verify if the service account has been created:

# kubectl get sa
NAME                   SECRETS   AGE
default                0         102m
internal-app           0         58m
vault                  0         102m
vault-agent-injector   0         102m

Step 5 – Test Secret Injection to an Application

To demonstrate the secret injection, we will deploy a simple application using the manifest:

vim deployment-01-orgchart.yml

Add the lines below to the file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orgchart
  labels:
    app: vault-agent-injector-demo
spec:
  selector:
    matchLabels:
      app: vault-agent-injector-demo
  replicas: 1
  template:
    metadata:
      annotations:
      labels:
        app: vault-agent-injector-demo
    spec:
      serviceAccountName: internal-app
      containers:
        - name: orgchart
          image: jweissig/app:0.0.1

Apply the manifest using the command:

kubectl apply --filename deployment-01-orgchart.yml

Check if the pod is running:

$ kubectl get pods
NAME                                   READY   STATUS    RESTARTS      AGE
orgchart-56f7795cc5-cn7dp              1/1     Running   0             6s
vault-0                                0/1     Running   2 (57s ago)   19m
vault-agent-injector-dbc799985-sbwk4   1/1     Running   0             19m

The Vault-Agent injector traces the deployments that define specific annotations, none of them currently exist in our deployment. Verify that with the command:

$ kubectl exec orgchart-56f7795cc5-cn7dp --container orgchart -- ls /vault/secrets
ls: /vault/secrets: No such file or directory
command terminated with exit code 1

The Vault-Agent injector will only modify a deployment if it contains a specified set of annotations. We will create the annotations and patch them into the deployment.

vim deployment-02-inject-secrets.yml

Add the below lines to the file:

spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "internal-app"
        vault.hashicorp.com/agent-inject-secret-database-config.txt: "internal/data/database/config"

In the above file:

  • agent-inject: set to true enables the Vault Agent injector service
  • role: the Vault Kubernetes authentication role created before. It maps to the k8S service account
  • agent-inject-secret-FIlEPATH: this is the path of the file, database-config.txt written to /vault/secrets

Now path the deployment:

kubectl patch deployment orgchart --patch "$(cat deployment-02-inject-secrets.yml)"

After this, you will have the deployment with two containers orgchart and the Vault Agent container, with the name vault-agent

# kubectl get pods
NAME                                   READY   STATUS    RESTARTS        AGE
orgchart-77d65d6b59-28xfj          2/2     Running   0               11m
vault-0                                1/1     Running   3 (7m34s ago)   33m
vault-agent-injector-dbc799985-sbwk4   1/1     Running   0               33m

View the logs in the container:

kubectl logs orgchart-77d65d6b59-28xfj  --container vault-agent

Sample Output:

HashiCorp Vault for Secrets Management in Kubernetes

Verify if the secret has been written to the container;

kubectl exec orgchart-77d65d6b59-28xfj --container orgchart -- cat /vault/secrets/database-config.txt

Sample Output:

HashiCorp Vault for Secrets Management in Kubernetes 1

From the above output, we can verify that HashiCorp Vault is working as desired since we have the secrets injected into the container. For more details, on how to customize a template view the guide on:

Books For Learning Kubernetes Administration:

Closing Thoughts

At this point, you should be able to deploy HashiCorp Vault for Secrets Management in Kubernetes. This will eliminate the overheads involved when managing application/service secrets in your Kubernetes environment. I hope this was significant

Related:

Nicole Veronica Rubhabha
Nicole Veronica Rubhabha
A highly competent and organized individual DotNet developer with a track record of architecting and developing web client-server applications. Recognized as a personable, dedicated performer who demonstrates innovation, communication, and teamwork to ensure quality and timely project completion. Expertise in C#, ASP.Net, MVC, LINQ, EF 6, Web Services, SQL Server, MySql, Web development,
RELATED ARTICLES

Most Popular

Recent Comments