ESO with HashiCorp Vault

ArgoCD enables the management of Kubernetes resources declaratively, using code that lives in a git repository. Although most resources can be handled this way, secrets should never be committed, meaning ArgoCD cannot handle them. To solve this, secrets can be stored in HashiCorp Vault and turned into Kubernetes secret objects using the External Secrets Operator (ESO).

This article presents the setup I use in my Kubernetes cluster.

HashiCorp Vault

Install

HashiCorp Vault can be installed using ArgoCD using the following manifest:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: vault
  namespace: argocd
spec:
  project: default
  destination:
    server: https://kubernetes.default.svc
    namespace: vault
  sources:
    - repoURL: https://helm.releases.hashicorp.com
      chart: vault
      targetRevision: 0.33.0
      helm:
        valueFiles:
          - $values/vault/values.yml
    - repoURL: your-repo-with-values # placeholder: replace with the repo holding your values.yml
      targetRevision: HEAD
      ref: values

Here, values.yml is provided in a separate repo as per this article

Once Vault is installed and its keys/token generated, an access policy for ESO needs to be added.

Secret engine

In Secrets, enable a new engine of type kv and give it a path such as k8s. Once done, a secret can be added, for example one named testsecret with a key-value pair testkey: testvalue.

ACL policy

In Access control, create a new ACL policy named, for example, eso with the following policy

path "k8s/data/*" {
  capabilities = ["read"]
}

Here, the path k8s is that of the kv secret engine created above.

Authentication method

In Access control create a new authentication method of type Kubernetes with eponymous path.

Settings can be left as default except Kubernetes API URL which is to be set to https://kubernetes.default.svc:443

Once the method is created, a role is to be created in the corresponding tab of the method, with the following settings

  • name: eso
  • Alias name source: serviceaccount_name
  • Bound service account names: *
  • Bound service account namespaces: *

Moreover, on the same role configuration page, under Tokens, set Generated Token’s Policies to eso. This allows the eso role to access the eso kv secret engine created above.

Note that using * for both the service account names and namespaces lets any service account in the cluster log in with this role, and thereby read every secret under k8s. This is convenient on a single-user cluster, but these can be restricted to the service account ESO authenticates with.

External Secrets Operator

Install

The External Secrets Operator can also be installed via ArgoCD:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: external-secrets
  namespace: argocd
spec:
  project: default
  destination:
    server: https://kubernetes.default.svc
    namespace: external-secrets
  source:
    repoURL: https://charts.external-secrets.io
    chart: external-secrets
    targetRevision: 2.6.0
  syncPolicy:
    syncOptions:
      - ServerSideApply=true

With ESO installed, a ClusterSecretStore object can be created using the following manifest:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: vault
spec:
  provider:
    vault:
      server: http://vault.vault:8200
      path: k8s
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes
          role: eso # Must match ACL policy created above

External Secret

With Vault and ESO installed and configured, an ExternalSecret object can be created using manifests such as the following:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: test-externalsecret
  namespace: test
spec:
  refreshInterval: 5m0s
  secretStoreRef:
    name: vault
    kind: ClusterSecretStore
  target:
    name: test-secret-from-vault
    creationPolicy: Orphan
  dataFrom:
    - extract:
        key: testsecret

Note that the creationPolicy is set to Orphan so that the secret can continue existing independently from the liveness of Vault or ESO.

This should result in:

$ kubectl get externalsecrets.external-secrets.io -n test test-externalsecret
NAME                  STORETYPE            STORE   REFRESH INTERVAL   STATUS         READY   LAST SYNC
test-externalsecret   ClusterSecretStore   vault   5m0s               SecretSynced   True    3m21s

One can then confirm that a secret has indeed been created:

$ kubectl get secrets -n test
NAME                     TYPE     DATA   AGE
test-secret-from-vault   Opaque   1      5m17s