The HomeLab Chronicles

Managing Wiz Secrets with Doppler and ESO

Managing Wiz Kubernetes Secrets with Doppler and External Secrets Operator

Deploying security tooling across multiple Kubernetes clusters is the easy part.

Managing secrets correctly across those clusters is where things get interesting.

When I rolled out the Wiz Kubernetes integration in my lab, I wanted it to reflect how I would approach this in a production environment. That meant I refused to:

  • Hardcode credentials in Helm values
  • Commit plaintext secrets to Git
  • Manually recreate secrets cluster by cluster

Instead, I built a repeatable GitOps pattern using Doppler, External Secrets Operator (ESO), and ArgoCD. I really like using Doppler since they are built for teams to collaborate and really provide a GitOps framework. They also offer a full featured free tier for a dev environment

The Problem

The Wiz Kubernetes integration requires two distinct secret types.

1. Wiz Credentials

clientId and clientToken used by the connector and broker to authenticate with the Wiz platform.

2. Image Pull Credentials

A dockerconfigjson secret so the sensor can pull images from Wiz's private registry.

These secrets must:

  • Exist before the Helm chart deploys
  • Be present on every cluster
  • Stay synchronized
  • Avoid manual drift

That combination is where most "quick setups" start to break down.

If secret handling is not intentional from day one, it becomes fragile very quickly.


The Architecture

I approached this with GitOps as the control plane. The goal was simple: after initial bootstrap, no human should ever touch a secret again. The only manual step is a one-time bootstrap per cluster.

Doppler as the Source of Truth

Doppler acts as the single source of truth for all secret material. I split secrets into separate projects:

  • wiz-token for Wiz credentials
  • sensor-image for registry credentials

Each concern gets its own boundary. This keeps blast radius small and permissions tight.

Cluster Bootstrap

On each cluster:

  • A bootstrap secret is created manually once. This is the Doppler service token that ESO uses to authenticate. It is the only secret that lives outside of Git.

From there:

  • A SecretStore connects ESO to the appropriate Doppler project.
  • ExternalSecret resources map Doppler keys into the exact Kubernetes Secret format the Helm chart expects.
  • ArgoCD ApplicationSets deploy everything across clusters with per-cluster overlays.

No kubectl loops. No manual patches. No hidden credentials floating around.

Example: SecretStore

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: doppler-wiz-token
  namespace: wiz-system
spec:
  provider:
    doppler:
      auth:
        secretRef:
          dopplerToken:
            name: doppler-bootstrap
            key: dopplerToken
      project: wiz-token
      config: prd

Example: ExternalSecret for Wiz Credentials

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: wiz-credentials
  namespace: wiz-system
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: doppler-wiz-token
    kind: SecretStore
  target:
    name: wiz-credentials
    creationPolicy: Owner
  data:
    - secretKey: clientId
      remoteRef:
        key: WIZ_CLIENT_ID
    - secretKey: clientToken
      remoteRef:
        key: WIZ_CLIENT_TOKEN

Example: ExternalSecret for Sensor Image Pull Secret

The sensor requires a Kubernetes secret of type:

type: kubernetes.io/dockerconfigjson

ESO handles this cleanly through templating:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: wiz-sensor-registry
  namespace: wiz-system
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: doppler-sensor-image
    kind: SecretStore
  target:
    name: wiz-sensor-registry
    creationPolicy: Owner
    template:
      type: kubernetes.io/dockerconfigjson
      data:
        .dockerconfigjson: "{{ .dockerconfigjson }}"
  data:
    - secretKey: dockerconfigjson
      remoteRef:
        key: SENSOR_IMAGE

No manual base64 encoding. No copy-paste gymnastics. No guessing at secret formats.


Key Design Decisions

Separate Doppler Projects Per Secret Domain

Wiz credentials and sensor registry credentials are isolated. Each project has its own service token and SecretStore. This enforces least privilege structurally, not just conceptually.

GitOps as the Control Plane

ArgoCD ApplicationSets deploy everything across clusters.

Adding a new cluster requires:

  • Creating one bootstrap secret
  • Adding the cluster to the ApplicationSet generator

That's it.

No rework. No secret recreation.

Kustomize Overlays for Cluster Behavior

Production runs the full stack, including the sensor. My dev cluster does not. A single overlay value controls that behavior. This keeps environments intentional instead of accidental.


The Result

Secrets refresh automatically every hour.

No credentials live in Git.

No manual secret recreation beyond the initial bootstrap.

Full visibility into sync status through ESO and ArgoCD.

The entire Wiz stack - connector, broker, admission controller, and sensor - deploys consistently across clusters with zero manual secret handling after the first setup.


Closing Thoughts

This is what GitOps should feel like.

Predictable. Auditable. Boring in the best possible way.

If your secret management process feels fragile, it probably is. Design the boundaries intentionally.