How to use HashiCorp Vault and ArgoCD for GitOps

How to use HashiCorp Vault and ArgoCD for GitOps: ArgoCD + Vault + argocd-vault-plugin

Context

There are various ways to manage your secrets. HashiCorp Vault (Vault from here on) is a widely known option, and it was the one I had in mind. For the Humble project, I used it as the core secret management system alongside Kubernetes secrets themselves. In this post I’d like to share how I use Vault with ArgoCD to deploy secrets across my system.

Injecting Vault secrets into Kubernetes pods via sidecar

I followed the official article. The idea is to enable Kubernetes authentication in Vault, bind a Kubernetes Service Account to a role, then set that role to allow pods using the Service Account to read the secrets in a scoped manner.

I installed it with the official Helm chart for Vault via Terraform: init-resources.tf.

The dependency here is longhorn for the persistent block storage that backs Vault:

depends_on       = [helm_release.longhorn]

Remember to switch the injector support on in the values.yaml:

injector:
  enabled: true

Vault will be up and running in a few seconds. We initialize and unseal it first, step by step:

kubectl exec -n vault -ti vault-0 /bin/sh
vault operator init
vault operator unseal

Now we define the app policy, which should also be provisioned via Terraform:

resource "vault_policy" "postgresql_read_only" {
  depends_on = [helm_release.vault]
  name       = "postgresql_read_only"
  policy     = <<EOT
  path "secret/postgresql/*" {
    capabilities = ["read"]
}
EOT
}

First, enable the Kubernetes authentication:

vault auth enable kubernetes

If you’re on one of the control plane nodes of your cluster, the quickest way to set this up is:

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
vault write auth/kubernetes/role/myapp \
   bound_service_account_names=app \
   bound_service_account_namespaces=demo \
   policies=app \
   ttl=1h

Otherwise, you will have to get the JWT token and the ca.crt from one of your Service Accounts:

export VAULT_SA_NAME=$(kubectl get sa vault-auth -o jsonpath="{.secrets[*]['name']}")
export SA_JWT_TOKEN=$(kubectl get secret $VAULT_SA_NAME -o go-template='{{ .data.token }}' | base64 --decode)
export SA_CA_CRT=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}' | base64 --decode)
export K8S_HOST=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.server}')
vault write auth/kubernetes/config \
   token_reviewer_jwt="$SA_JWT_TOKEN" \
   kubernetes_host="$K8S_HOST" \
   kubernetes_ca_cert="$SA_CA_CRT"

Let’s create a role and bind it to our Service Account:

vault write auth/kubernetes/role/postgresql_read_only \
   bound_service_account_names=apps \
   bound_service_account_namespaces=apps \
   policies=postgresql_read_only \
   ttl=1h

Now let’s put some secrets in there:

vault kv put secret/postgresql/data \
      username='databaseuser' \
      password='suP3rsec(et!' \
      ttl='30s'

Now we can inject it into our pods. See the documentation at https://www.vaultproject.io/docs/platform/k8s/injector.

As an example, in my ArgoCD apps folder I injected the secret as the DSN env var so Ory Kratos uses it as the connection string to the PostgreSQL database: ory-kratos.yaml.

And let the sidecar do its job.

Using argocd-vault-plugin

Injecting the secrets into pods is straightforward. But what if we want to inject them on the fly into other resources like Kubernetes secrets or custom resources? That is where argocd-vault-plugin helps.

To set this up, first install the executable binary in an initContainer. The Helm chart values include this:

repoServer:
    metrics:
        enabled: true
        serviceMonitor:
            enabled: true
    image:
        tag: v2.0.0
    volumes:
    - name: custom-tools
      emptyDir: {}
    initContainers:
    - name: download-tools
      image: alpine:3.8
      command: [sh, -c]
      args:
        - >-
          wget -O argocd-vault-plugin
          https://github.com/IBM/argocd-vault-plugin/releases/download/v1.1.1/argocd-vault-plugin_1.1.1_linux_amd64 &&
          chmod +x argocd-vault-plugin &&
          mv argocd-vault-plugin /custom-tools/
      volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools
    volumeMounts:
    - name: custom-tools
      mountPath: /usr/local/bin/argocd-vault-plugin
      subPath: argocd-vault-plugin

Next, we configure the plugin so ArgoCD knows how to use it:

server:
  config:
    configManagementPlugins: |-
      - name: argocd-vault-plugin
        generate:
          command: ["argocd-vault-plugin"]
          args: ["generate", "./"]
      - name: argocd-vault-plugin-helm
        init:
          command: [sh, -c]
          args: ["helm dependency build"]
        generate:
          command: ["sh", "-c"]
          args: ["helm template $ARGOCD_APP_NAME . | argocd-vault-plugin generate -"]

Here is the values.yaml file: argocd.yaml.

Let’s put a new secret:

vault kv put secret/humble/demo \
  username='locmai' \
  ttl='30s'

Now create a policy and a role:

kubectl exec -ti vault-0 /bin/sh
cat <<EOF > /home/vault/humble-policy.hcl
path "secret/humble/*" {
  capabilities = ["read"]
}
EOF
vault policy write humble /home/vault/humble-policy.hcl
vault write auth/kubernetes/role/myhumbledemo \
   bound_service_account_names=default \
   bound_service_account_namespaces=argocd \
   policies=humble \
   ttl=1h

Now we can put it all together with ArgoCD. Define a Kubernetes secret manifest that we would like to inject the secret value into:

# demo/secrets.yaml
kind: Secret
apiVersion: v1
metadata:
  name: humble-example-secret
  namespace: argocd
  annotations:
    avp.kubernetes.io/path: "secret/humble/data/demo"
type: Opaque
stringData:
  username: <username>

The spec above tells the plugin to read the secret/humble/demo path (/data/ is the new path pattern for the kv-v2 engine) and inject the secret with the key username into the <username> placeholder.

From the ArgoCD application, we can configure it to use the plugin like this:

spec:
  source:
    path: 'demo'
    repoURL: [email protected]:locmai/humble.git
    targetRevision: main
    plugin:
      name: argocd-vault-plugin
      env:
        - name: VAULT_ADDR
          value: http://vault-ui.vault.svc.cluster.local:8200
        - name: AVP_TYPE
          value: vault
        - name: AVP_AUTH_TYPE
          value: k8s
        - name: AVP_K8S_ROLE
          value: argocd-server

And the secret is injected, nice and easy.

Other GitOps secrets approaches: https://argo-cd.readthedocs.io/en/stable/operator-manual/secret-management/.