# vault-secrets-operator: syncing Vault secrets into Kubernetes Secrets

> The Vault Secrets Operator is a HashiCorp operator that watches custom resource definitions, reads secrets from Vault and writes them into ordinary Kubernetes Secrets, so a pod consumes a secret the way it always has while rotation happens underneath. It suits a Kubernetes team that already runs Vault and wants to stop putting secret values in Git, and it is the wrong tool if you are not running Vault at all.

**hashicorp/vault-secrets-operator** — The Vault Secrets Operator (VSO) allows Pods to consume Vault secrets natively from Kubernetes Secrets.

- Repository: https://github.com/hashicorp/vault-secrets-operator
- Website: https://hashicorp.com
- Stars: 607 · Forks: 145
- Language: Go
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/hashicorp-vault-secrets-operator

## One operator, one direction of data flow

The Vault Secrets Operator, abbreviated VSO, allows pods to consume Vault secrets natively from Kubernetes Secrets. That is the whole promise, and it is worth unpacking what makes it non-obvious.

The operator operates by watching for changes to its supported set of Custom Resource Definitions. Each CRD carries the specification that allows the operator to synchronise a Vault secret to a Kubernetes secret. The operator writes the source Vault secret data directly to the destination Kubernetes Secret, and changes made to the source are replicated to the destination over its lifetime. The consequence for the application is that it only needs access to the destination secret in order to use the secret data contained within it.

So the pod's code does not change. It mounts a secret, it reads a key, and it never learns that the value came from Vault rather than from a hand-edited YAML file. That is the design that makes this adoptable without a rewrite, and it is also what makes the alternative approach, a sidecar that talks to Vault directly, a different kind of change with different trade-offs.

The feature list is broad in a way that is worth reading precisely. All Vault secret engines are supported. There is TLS and mTLS communication with Vault, and authentication using the requesting Pod's ServiceAccount via the Kubernetes Auth Method. There is secret rotation for Deployment, ReplicaSet and StatefulSet resource types, Prometheus instrumentation for monitoring the operator itself, and installation by Helm or Kustomize.

The Kubernetes Auth Method point is the security core. A pod authenticates to Vault with its own ServiceAccount identity, so Vault decides what that pod may read rather than the operator holding a broad credential and handing out what it likes. The further documentation is on the HashiCorp developer site, linked from the readme.

## Watching CRDs and writing Secrets, and the rotation story

The mechanism reduces to a watch loop over custom resources and a write to a Kubernetes Secret, and the interesting parts are in the edges.

Rotation is supported for Deployment, ReplicaSet and StatefulSet resource types. The operator watches for changes to its supported CRDs, so when the source in Vault changes, the destination is updated, and the workload types it knows about are re-rolled so the new value takes effect. That last part is the part people get wrong in a hand-rolled setup: a secret value in a volume is refreshed eventually, but a process that read it at startup keeps the old one until it is restarted, and something has to trigger that restart.

The operator also supports TLS material through a VaultPKISecret resource type, and the samples include a worked example that populates a TLS secret for use with an Ingress. The sample file is config/samples/secrets_v1beta1_vaultpkisecret_tls.yaml, and the documentation notes it takes a little more setup to test, following the kind ingress docs. The result is a certificate with a short time to live, which is only useful if something renews it, and that is the operator's job rather than the ingress controller's.

The repository layout matches the API surface. There is an api/ directory for the CRD types, controllers/ for the reconcile logic, a vault/ directory for the Vault client, credentials/ for the authentication pieces, template/ for rendering, common/, internal/, helpers/, utils/ and consts/ for the shared code, and a chart/ directory holding the Helm chart and its CRDs. A separate demo/ directory holds a demo application with its own infra and kind setup.

The Go module is worth a note because of a specific problem it solves. The go.mod requires Kubernetes dependencies and, as the comment explains, the k8s.io module versions have to be kept in sync with each other, and because the module system does not automatically resolve indirect dependencies to the same version as the direct ones, the file carries replace directives pinning the indirect k8s.io/cloud-provider and k8s.io/kubelet dependencies to 0.34.1. The go directive is 1.27.2.

## Running the samples against a kind cluster

The readme walks through a full local setup, and the sequence is a good indication of what running the operator in anger involves.

```shell
# Start a KinD cluster
make setup-kind

# Deploy Vault
make setup-integration-test

# Configure Vault
./config/samples/setup.sh

# Build and deploy the operator
make build docker-build deploy-kind

# Deploy the sample K8s resources
kubectl apply -k config/samples
```

So the local loop is a kind cluster, a Vault deployment, a configuration script, an operator build and image, and the sample resources. Vault comes first, which is the correct order and a useful signal about the dependency direction.

Once the samples are applied you look at what was produced, and the naming in the output shows the multi-tenancy model at work, with separate namespaces:

```shell
kubectl get secrets -n tenant-1 secret1 -o yaml

kubectl get secrets -n tenant-1 pki1 -o yaml

kubectl get secrets -n tenant-2 secret1 -o yaml
```

A plain secret and a PKI secret in one tenant, and the same plain secret name in a second tenant, which is the shape of a multi-tenant deployment where a shared Vault serves isolated namespaces.

The TLS example goes one step further, deploying the nginx ingress controller and then watching the controller logs to see the TLS secret being rotated, with kubectl logs -f against the ingress-nginx namespace. The certificate in the example has a time to live of about a minute and a half, which is short enough that renewal is not optional, and short enough to make the behaviour obvious when you watch the logs.

## The container image, and why it is distroless and nonroot

The Dockerfile is a two-stage build with a dev image and a release image, and the details are worth reading because they describe the security posture of the thing you deploy.

The builder stage starts from a golang image, copies the module manifests first and runs go mod download before the source, so that source changes do not invalidate the dependency layer. The compile is explicit about being static and cross-platform:

```dockerfile
RUN CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH go build -ldflags "$LD_FLAGS" -a -o $BIN_NAME main.go
```

CGO_ENABLED=0 matters more than it looks. It produces a binary with no C library dependency, which is what lets the runtime image be distroless.

The runtime stage is gcr.io/distroless/static:nonroot, with the binary copied in, the scripts directory copied, and the user set to 65532:65532. There is no shell in the image, and the entrypoint is the operator binary itself. The build also creates the scripts directory needed for upgrading CRDs, and links the upgrade-crds script to the binary.

The licence identifier in the file header is SPDX-License-Identifier: BUSL-1.1, while the project description records the licence as one that GitHub cannot classify. For a component that sits in the path of every secret your platform reads, that combination is worth resolving with the licence file before you ship it, and it is a question for your legal team rather than for a blog post.

## The Makefile as a map of the supported platforms

The Makefile is long, but its variable block is a compact description of what the project integrates with, and it is the most informative file in the repository.

There are per-cloud include files, aws.mk, azure.mk and gcp.mk, and a demo.mk and a scale-testing.mk. The per-cloud makefiles create a cluster and a container repository, build and push the operator image and run the integration tests against it, and the readme shows the difference between running against Vault Community and running against Vault Enterprise, where the Enterprise run passes VAULT_ENTERPRISE=true and ENT_TESTS=true with the Vault licence supplied as an environment variable.

The cloud setup steps are visible too. On Azure the sequence is az config set core.allow_broker=true, az account clear, az login, then setting the subscription, then creating a service principal with the Owner role and exporting AZURE_APPID, AZURE_PASSWORD and AZURE_TENANT. On GCP it is exporting GCP_PROJECT and then make -f gcp.mk create-gke. On AWS it is make -f aws.mk create-eks. The AWS path also has a specific test for static credentials, which requires a set of environment variables and a SKIP_AWS_STATIC_CREDS_TEST flag, and the comment notes those credentials are set in a Kubernetes secret.

The pinned tool versions are all in the same block. KIND_K8S_VERSION is v1.37.0, VAULT_HELM_VERSION is 0.34.1, VAULT_DOCS_VERSION is v2.x and VAULT_IMAGE_TAG is latest, which is a versioned default and a floating default side by side. There are also KUBE_RBAC_PROXY_VERSION and an OpenShift-specific override, plus the VSO_CSI_DRIVER_VERSION, VSO_CSI_LIVENESS_PROBE_VERSION and VSO_CSI_NODE_DRIVER_REGISTRAR_VERSION variables, and a TEST_WITH_HELM switch so the integration tests can run against a Helm installed operator rather than a locally applied one.

## Where vault-secrets-operator is the wrong choice

The first limitation is the obvious one: there is no point to this without Vault. The operator's whole job is to be the bridge to a Vault server, and the authentication story depends on the Kubernetes Auth Method being configured in Vault with roles that map ServiceAccounts to paths. A team that stores secrets in a cloud secret manager, in SOPS-encrypted files, or in an external vault has no use for it, and the CRDs would just be another API to learn.

The second is that secrets still end up in Kubernetes, which is the thing most teams are trying to avoid. The destination is an ordinary Kubernetes Secret, readable by anyone with RBAC access to that namespace and cached in etcd. The operator changes where the value comes from, not whether a pod can read it. If your threat model is a compromised etcd or a namespace with loose RBAC, this does not help.

The third is the audit story. Because the destination secret is rewritten in place, what was served at a given moment is not recorded in the Kubernetes audit trail in a way that is easy to reconstruct. The value at that instant existed in Vault, and Vault's own audit log is where the history lives. A team that needs a per-deployment record of which secret version a pod actually used will not find it here.

The fourth is the resource type coverage. Rotation is documented for Deployment, ReplicaSet and StatefulSet. A workload in a custom controller, a Job with a fixed name, or anything else that does not roll on a secret change needs its own handling, and the documentation does not promise it.

The fifth is that the operator is another privileged component in the cluster. It runs as a controller with the RBAC to read Vault and write Secrets across namespaces, and it holds a Vault credential of its own, since it has to authenticate to Vault independently of the requesting Pod. That is a second identity to manage, rotate and audit in addition to the workload identities, and the license question sits alongside it: a custom licence for a component with this much access deserves a reading before a production cluster depends on it.

## Conclusion

vault-secrets-operator fits a Kubernetes team already running Vault that wants application pods to read ordinary Kubernetes Secrets while the values themselves live in Vault and rotate underneath, since the operator handles the Kubernetes Auth Method, the syncing and the rotation for Deployment, ReplicaSet and StatefulSet workloads. It is the wrong choice without Vault behind it, and it is the wrong choice if your organisation needs a static audit trail of what was served when, because the destination secret is rewritten in place. Before adopting it, confirm the Kubernetes Auth Method path works with your Vault roles, read the license file yourself, since the repository licence is custom and the container build carries a BUSL-1.1 identifier, and note that the last push was on 2026-10-21 with v1.6.1 released on 2026-10-12.

## FAQ

### What does the Vault Secrets Operator do?

It watches for changes to its supported Custom Resource Definitions, reads the source secret from Vault, and writes it directly to a destination Kubernetes Secret, replicating changes over the secret's lifetime. An application then only needs access to the destination secret to use the data.

### How does the operator authenticate to Vault?

It supports TLS and mTLS communications with Vault, and supports authentication using the requesting Pod's ServiceAccount via the Kubernetes Auth Method, so a Pod's own identity determines what it may read rather than the operator handing out permissions on its behalf.

### How do I install the Vault Secrets Operator?

Helm and Kustomize are the supported installation methods, and the repository ships a Helm chart under chart/ with its CRDs. For a local run, the readme starts a kind cluster with make setup-kind, deploys Vault with make setup-integration-test, configures it with ./config/samples/setup.sh, then builds and deploys the operator with make build docker-build deploy-kind.

### Does the Vault Secrets Operator support secret rotation?

Yes, rotation is supported for Deployment, ReplicaSet and StatefulSet resource types. The samples also include a VaultPKISecret example that populates a TLS secret for an Ingress, and the documentation shows how to watch the nginx controller logs to see the TLS secret being rotated.

### What licence is the Vault Secrets Operator released under?

The repository records a custom licence that GitHub cannot classify, and the Dockerfile carries the SPDX identifier BUSL-1.1, so read the LICENSE file before deploying it. The last push was on 2026-10-21 and the newest release is v1.6.1 from 2026-10-12.

## Sources

- [hashicorp/vault-secrets-operator on GitHub](https://github.com/hashicorp/vault-secrets-operator)
- [Issues](https://github.com/hashicorp/vault-secrets-operator/issues)
- [Project website](https://hashicorp.com)
- [README](https://github.com/hashicorp/vault-secrets-operator/blob/main/README.md)
- [Releases](https://github.com/hashicorp/vault-secrets-operator/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hashicorp-vault-secrets-operator
