Injecting secrets from Vault into Helm charts with ArgoCD
Tags: vault, kubernetes, argocd, helm
Published: 2022-12-07 06:34:06


Managing secrets in Kubernetes isn’t a trivial topic. As is usual with Kubernetes, there are always many ways to achieve the desired goal and it’s often a problem to choose the right one for our case. In this article, I will show you one of ways to use ArgoCD with the help of [Vault Plugin](https://argocd-vault-plugin.readthedocs.io/en/stable/) to inject secrets into Helm Charts. So, as you can guess, this may be one of the best ways to inject secrets if you are already using ArgoCD and Vault in your project.


## Prerequisites

The basic knowledge of Kubernetes and Helm is obvious here. For ArgoCD and Vault I will try to guide you step by step in this article.
We will use the CLI wherever possible, so I recommend you install all the following:

```bash
# Helm
brew install helm

# Vault
brew tap hashicorp/tap
brew install hashicorp/tap/vault

#ArgoCD
brew install argocd
```

For structure, we will create 2 namespaces. The first one - technical, will be for Vault and ArgoCD instances, in the second one, we will install target Helm charts with injected secrets.

```bash
# namespace for Vault & ArgoCD
kubectl create ns toolbox

# namespace for resoruces installed by ArgoCD
kubectl create ns sandbox
```

I also encourage you to install [kubectx + kubens](https://github.com/ahmetb/kubectx) to navigate Kubernetes easily.


## Vault installation

For the beginning select `toolboox` namespace

```bash
kubens toolbox
```

To install Vault we will use the official [Helm chart](https://github.com/hashicorp/vault-helm) provided by HashiCorp. For simplicity, install it in developer mode. In dev mode, Vault doesn't need to be initialized or unsealed, but remember, it's only for development or experimentation. Never, ever run a dev mode in production

```bash
helm install vault hashicorp/vault --set "server.dev.enabled=true"
```

Vault can be configured via HTTP API, UI, or CLI. To operate Vault from local CLI establish port forwarding to `vault-0` Pod, and setup Vault server address for already installed Vault CLI. 

```bash
# port forwarding in separate terminal window
kubectl port-forward -n toolbox vault-0 8200

# login into Vault. Use 'root' token to authenticate into Vault
export VAULT_ADDR=http://127.0.0.1:8200
vault login 

```

I also encourage you to explore browser UI interface of Vault. You have to use this same `root` token generated in dev mode. 


## Vault setup

Vault uses [Secrets Engines](https://developer.hashicorp.com/vault/docs/secrets) to store, generate, or encrypt data. The basic Secret Engine for storing static secrets is [Key-Value](https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2) engine. Let’s create one sample secret that we’ll inject later into Helm Charts.

```bash
# enable kv-v2 engine in Vault
vault secrets enable kv-v2

# create kv-v2 secret with two keys
vault kv put kv-v2/demo user="secret_user" password="secret_password"

# create policy to enable reading above secret
vault policy write demo - <<EOF
path "kv-v2/data/demo" {
  capabilities = ["read"]
}
EOF
```

Now we need to create a role that will authenticate ArgoCD in Vault. We said that Vault has Secrets Engines component. [Auth methods](https://developer.hashicorp.com/vault/docs/auth) are another type of component in Vault but for assigning identity and a set of policies to user/app.  As we are using Kubernetes platforms, we need to focus on [Kubernetes Auth Method](https://developer.hashicorp.com/vault/docs/auth/kubernetes) to configure Vault accesses. Let’s configure this auth method.

```bash
# enable Kubernetes Auth Method
vault auth enable kubernetes

# get Kubernetes host address
K8S_HOST="https://$( kubectl exec vault-0 -- env | grep KUBERNETES_PORT_443_TCP_ADDR| cut -f2 -d'='):443"

# get Service Account token from Vault Pod
SA_TOKEN=$(kubectl exec vault-0 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# get Service Account CA certificate from Vault Pod
SA_CERT=$(kubectl exec vault-0 -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt)

# configure Kubernetes Auth Method
vault write auth/kubernetes/config \
    token_reviewer_jwt=$SA_TOKEN \
    kubernetes_host=$K8S_HOST \
    kubernetes_ca_cert=$SA_CERT

# create authenticate Role for ArgoCD
vault write auth/kubernetes/role/argocd \
  bound_service_account_names=argocd-repo-server \
  bound_service_account_namespaces=toolbox \
  policies=demo \
  ttl=48h
```

That's all for now in Vault. Once you have created all components, you can try to find them in the browser interface [http://localhost:8200/](http://localhost:8200/)


## ArgoCD & Vault Plugin Installation

Time for the main actor of this article - [Argo CD Vault Plugin](https://github.com/argoproj-labs/argocd-vault-plugin) It will be responsible for injecting secrets from the Vault into Helm Charts. In addition to Helm Charts, this plugin can handle secret injections into pure Kubernetes manifests or `Kustomize` templates. Here we will focus only on Helm Charts. Different sources required different installations, which you can find in plugin documentation.  

What makes plugin documentation less clear is that it can be installed in two ways:

- Installation via `argocd-cm` ConfigMap (old option, deprecated from version `2.6.0` of ArgoCD)
- Installation via a sidecar container (new option, supported from version `2.4.0` of ArgoCD)

Since the old option will be not supported in future releases, I will install the ArgoCD Vault Plugin using a sidecar container. In order to properly install and configure ArgoCD, we need to follow a few steps:

Before all make sure you are still in `toolbox` namespace where we want to place Vault, ArgoCD, and all stuff for Vault plugin.

```bash
kubens toolbox
```

1. Create k8s `Secret` with authorization configuration that Vault plugin will use.

```yaml
kind: Secret
apiVersion: v1
metadata:
  name: argocd-vault-plugin-credentials
type: Opaque
stringData:
  AVP_AUTH_TYPE: "k8s"
  AVP_K8S_ROLE: "argocd"
  AVP_TYPE: "vault"
  VAULT_ADDR: "http://vault.toolbox:8200"
```

Make sure you set the proper Vault address and role name.

2. Create k8s `ConfigMap` with Vault plugin configuration that will be mounted in the sidecar container, and overwrite default processing of Helm Charts on ArgoCD. Look carefully at this configuration file. Under `init command` you can see that we add Bitnami Helm repo and execute ``helm dependency build``. It is required if Charts installed by you use dependencies charts. You can customize or get rid of it if your Charts haven’t any dependencies.  

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cmp-plugin
data:
  plugin.yaml: |
    apiVersion: argoproj.io/v1alpha1
    kind: ConfigManagementPlugin
    metadata:
      name: argocd-vault-plugin-helm
    spec:
      allowConcurrency: true
      discover:
        find:
          command:
            - sh
            - "-c"
            - "find . -name 'Chart.yaml' && find . -name 'values.yaml'"
      init:
       command:
          - bash
          - "-c"
          - |
            helm repo add bitnami https://charts.bitnami.com/bitnami
            helm dependency build
      generate:
        command:
          - bash
          - "-c"
          - |
            helm template $ARGOCD_APP_NAME -n $ARGOCD_APP_NAMESPACE -f <(echo "$ARGOCD_ENV_HELM_VALUES") . |
            argocd-vault-plugin generate -s toolbox:argocd-vault-plugin-credentials -
      lockRepo: false
```

1. Finally, we have to install ArgoCD from the official [Helm Chart](https://github.com/argoproj/argo-helm) but with extra configuration that provides modifications required to install Vault plugin via sidecar container. 

```yaml
repoServer:
  rbac:
    - verbs:
        - get
        - list
        - watch
      apiGroups:
        - ''
      resources:
        - secrets
        - configmaps
  initContainers:
    - name: download-tools
      image: registry.access.redhat.com/ubi8
      env:
        - name: AVP_VERSION
          value: 1.11.0
      command: [sh, -c]
      args:
        - >-
          curl -L https://github.com/argoproj-labs/argocd-vault-plugin/releases/download/v$(AVP_VERSION)/argocd-vault-plugin_$(AVP_VERSION)_linux_amd64 -o argocd-vault-plugin &&
          chmod +x argocd-vault-plugin &&
          mv argocd-vault-plugin /custom-tools/
      volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools

  extraContainers:
    - name: avp-helm
      command: [/var/run/argocd/argocd-cmp-server]
      image: quay.io/argoproj/argocd:v2.4.8
      securityContext:
        runAsNonRoot: true
        runAsUser: 999
      volumeMounts:
        - mountPath: /var/run/argocd
          name: var-files
        - mountPath: /home/argocd/cmp-server/plugins
          name: plugins
        - mountPath: /tmp
          name: tmp-dir
        - mountPath: /home/argocd/cmp-server/config
          name: cmp-plugin
        - name: custom-tools
          subPath: argocd-vault-plugin
          mountPath: /usr/local/bin/argocd-vault-plugin

  volumes:
    - configMap:
        name: cmp-plugin
      name: cmp-plugin
    - name: custom-tools
      emptyDir: {}
    - name: tmp-dir
      emptyDir: {}

# If you face issue with ArgoCD CRDs installation, then uncomment below section to disable it
#crds:
#  install: false
```

Save that Helm values as `argocd-helm-values.yaml` and execute below commands:

```bash
# once againe make sure to use proper namespace
kubens toolbox

# install ArgoCD with provided vaules
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd -n toolbox -f argocd-helm-values.yaml
```

All of the above configurations you can find in dedicated [GitHub repo](https://github.com/luafanti/arogcd-vault-plugin-with-helm)

If all went well, you should see similar list of Pods in `toolbox` namespace. Note that `argocd-repo-server` has sidecar container `avp-helm`



![pod-list-in-lens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38e8mqc21tw4f64rcgvu.png)


## Install your resources with secrets injection

Is the time for a final check of our setup and installation of our Helm Charts.

Firstly, let’s try to authorize against ArgoCD. To obtain `admin` user password execute the below command:

```bash
kubectl -n toolbox get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
```

As with Vault, with ArgoCD we will also be working partly with the CLI and partly web UI. 

```bash
# port forwarding in separate terminal window
kubectl port-forward svc/argocd-server 8080:80

# authorize ArgoCD CLI
argocd login localhost:8080 --username admin --password $(kubectl get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
```

As our demo Chart we will use my debug Spring Boot application from [GitHub repo](https://github.com/luafanti/spring-boot-debug-app). It’s simple web server that exposes a few debugging endpoints. Application has Helm templates and ArgoCD Application definition under `/infra` directory. To deploy this stack to k8s with Argo we need to apply ArgoCD `Application` CRD. Below full code sample, which you can also explore [here](https://github.com/luafanti/spring-boot-debug-app/blob/main/infra/argocd/argocd-application-with-vault-secrets.yaml).

 

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: demo
spec:
  destination:
    namespace: sandbox
    server: https://kubernetes.default.svc
  project: default
  source:
    path: infra/helm
    repoURL: https://github.com/luafanti/spring-boot-debug-app
    targetRevision: main
    plugin:
      env:
        - name: HELM_VALUES
          value: |
            serviceAccount:
              create: true
            image:
              repository: luafanti/spring-boot-debug-app
              tag: main
              pullPolicy: IfNotPresent
            replicaCount: 1
            resources:
              memoryRequest: 256Mi
              memoryLimit: 512Mi
              cpuRequest: 500m
              cpuLimit: 1 
            probes:
              liveness:
                initialDelaySeconds: 15
                path: /actuator/health/liveness
                failureThreshold: 3
                successThreshold: 1
                timeoutSeconds: 3
                periodSeconds: 5
              readiness:
                initialDelaySeconds: 15
                path: /actuator/health/readiness
                failureThreshold: 3
                successThreshold: 1
                timeoutSeconds: 3
                periodSeconds: 5
            ports:
              http:
                name: http
                value: 8080
              management:
                name: management
                value: 8081
            envs:
              - name: VAULT_SECRET_USER
                value: <path:kv-v2/data/demo#user>
              - name: VAULT_SECRET_PASSWORD
                value: <path:kv-v2/data/demo#password>
            log:
              level:
                spring: "info"
                service: "info"
  syncPolicy: {}
```

You can see in line [54](https://github.com/luafanti/spring-boot-debug-app/blob/fa3f747b2e9f5cd47676d210fc2a79d01b74b2b5/infra/argocd/argocd-application-with-vault-secrets.yaml#L54) & [56](https://github.com/luafanti/spring-boot-debug-app/blob/fa3f747b2e9f5cd47676d210fc2a79d01b74b2b5/infra/argocd/argocd-application-with-vault-secrets.yaml#L56) placeholders with pattern `<path:vault_secret_path#secret_key>` where Vault Plugin will inject the actual value from Vault secret. 
I also encourage you to compare this definition file with a definition without secret injection and without using Vault Plugin [here](https://github.com/luafanti/spring-boot-debug-app/blob/main/infra/argocd/argocd-application.yaml). You should notice that `source` property is different when we use secrets injection. When we want to leverage on Vault plugin we need to define our Argo `Application` with source [plugin](https://github.com/luafanti/spring-boot-debug-app/blob/main/infra/argocd/argocd-application-with-vault-secrets.yaml#L14) and pass Helm Values using env `HELM_VALUES`

Let’s install this Argo `Application` and sync them.

```bash
# make sure you are in namespace where Argo has benn installed
kubens toolbox

# once you download soruce from GIT repo
kubectl apply -f infra/argocd/argocd-application-with-vault-secrets.yaml

# List ArgoCD applications
argocd app list

# Sync application
argocd app sync toolbox/demo
```

Once synchronization is finished, you should see beautiful green-full screen in ArgoCD UI


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uannx74gs4iswwuc8ygs.png)

Verify if injection works.

```bash
# use port other than 8080 as the tunnel to Argo already uses this port
kubectl port-forward -n sandbox svc/demo-spring-debug-app 8090:8080

# check injected envs 'VAULT_SECRET_PASSWORD' 'VAULT_SECRET_USER' in debug app 
chrome http://localhost:8090/envs
```

One of the greatest things about the plugin is that if the value changes in Vault, ArgoCD will notice these changes and display `OutOfSync` status. Let's prove it.

```bash
# update secrets in Vault
vault kv put kv-v2/demo user="secret_user_new" password="secret_password_new"

# refresh application as well with target manifests cache
argocd app get toolbox/demo --hard-refresh
```

After `Hard refresh` you should see that your Argo Application back to status `OutOfSync` what is expected during Vault secret update. Thanks to this mechanism, you don't have to worry about losing control of keeping your secrets up to date.


## Troubleshooting & possible problems

- make sure your Vault secrets don’t disappear from Vault. In this guide, we use Vault in `dev mode` so secrets are stored in-memory. After cluster reboot all Vault objects will disappear.
- if you would like to use different namespace names for Vault/ArgoCD etc. make sure you adjust your configuration files properly, especially [HERE](https://github.com/luafanti/arogcd-vault-plugin-with-helm/blob/main/argocd-installation/argocd-vault-plugin-cmp.yaml#L32) & [HERE](https://github.com/luafanti/arogcd-vault-plugin-with-helm/blob/840e8e96ec483739668237166b57cb83d73895dd/argocd-installation/argocd-vault-plugin-credentials.yaml#L10)
- Sometimes if you install ArgoCD multiple times in your cluster you can face error related to CRDs. You can uncomment [this section](https://github.com/luafanti/arogcd-vault-plugin-with-helm/blob/840e8e96ec483739668237166b57cb83d73895dd/argocd-installation/argocd-helm-values.yaml#L57) to resolve it.