KSOPS: a kustomize plugin that decrypts SOPS resources at build time
KSOPS - A Flexible Kustomize Plugin for SOPS Encrypted Resources
At a glance
- What is it?
- KSOPS is a kustomize KRM exec plugin that bridges kustomize and SOPS, so that encrypted Secrets and ConfigMaps can be managed, built and applied through the same GitOps flow as the rest of a Kubernetes manifest. It was written at Viaduct because no existing solution fitted their stack, and it is published as a Go module, a distroless container and a legacy exec plugin.
- Who is it for?
- Adopt KSOPS if you run GitOps and want encrypted manifests to go through the same kustomize build and apply path as everything else, because the plugin turns decryption into a build step rather than a separate step in your release pipeline. Do not adopt it if your pipeline already decrypts with the SOPS CLI outside kustomize, since the plugin exists precisely to remove that step.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 110 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The gap that started the project
The background section is short and explains the whole project.
At Viaduct, the team manages its Kubernetes resources via the GitOps pattern, but they could not find a solution compatible with their stack for managing secrets via the GitOps paradigm. So they built KSOPS to connect kustomize to SOPS and integrated it with Argo CD to safely manage secrets the same way they manage the rest of their Kubernetes manifests.
That is the problem in one sentence. GitOps wants everything in git, including the manifests, and everything applied from git, including the secrets. SOPS gives you encrypted files in git, but somebody has to decrypt them before the cluster sees them, and that somebody is usually a step in a CI pipeline, which is precisely the thing GitOps is meant to remove.
KSOPS moves the decryption into the kustomize build. The project is a kustomize KRM exec plugin for SOPS encrypted resources, and the README notes it can be used to decrypt any Kubernetes resource, although it is most commonly used to decrypt encrypted Kubernetes Secrets and ConfigMaps.
The result, in the README's words, is that KSOPS allows you to manage, build and apply encrypted manifests the same way you manage the rest of your Kubernetes manifests. The requirement list is short: kustomize, and gpg for the tutorial, since the example uses PGP keys.
The project is Go, licensed Apache-2.0, on the master branch, with the last push on 2026-07-10 and releases v4.4.0 in September 2025, then v4.5.0 and v4.5.1 on the same day in April 2026.
The plugin mechanism, and the manifest that drives it
KSOPS is a KRM exec plugin, and the way that works is worth understanding, because it is the part that has to keep working when kustomize changes.
A kustomize exec plugin is a binary that kustomize runs during a build, described by an annotation in the manifest that invokes it. The generator manifest the tutorial builds is short:
cat <<EOF > secret-generator.yaml
apiVersion: viaduct.ai/v1
kind: ksops
metadata:
# Specify a name
name: example-secret-generator
annotations:
config.kubernetes.io/function: |
exec:
# if the binary is in your PATH, you can do
path: ksops
# otherwise, path should be relative to manifest files, like
# path: ../../../ksops
files:
- ./secret.enc.yaml
EOFThe apiVersion and kind are the plugin's own, viaduct.ai/v1 and ksops, and the files list says which encrypted resources to decrypt. The annotation config.kubernetes.io/function is the standard exec hook, and the path is where the binary lives. The comment in the manifest covers the two cases that matter in practice, a binary on your PATH, or a path relative to the manifest files, which is what a container image needs.
Then the generator is referenced from an ordinary kustomization, in a generators list, and the build is run with the plugin flags:
kustomize build --enable-alpha-plugins --enable-exec .The README notes that in kustomize v2 and v3 the command was kustomize build --enable_alpha_plugins ., with a hyphen instead of a dash in the flag, which is a small detail that saves a confusing error message if you are following an older tutorial. The two flags are the practical cost of this approach: the plugin mechanism is still behind an alpha gate in kustomize, so a build that has to decrypt secrets cannot run with the default flags.
The README's own troubleshooting section is short and to the point. Validate that the ksops command is in your path, with command -v ksops, and if you would rather not install it there, make sure the path in the generator manifest is relative to the manifest files.
The eight step tutorial, and the encryption rules behind it
The getting started section walks through the whole flow, and the parts worth knowing are the encryption rules and the build step.
The files are encrypted with the SOPS CLI, using the configuration in .sops.yaml, which is where you decide what stays readable:
creation_rules:
- unencrypted_regex: "^(apiVersion|metadata|kind|type)$"
# Specify kms/pgp/etc encryption key
# This tutorial uses a local PGP key for encryption.
# DO NOT USE IN PRODUCTION ENV
pgp: "FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4"
# Optionally you can configure to use a providers key store
# kms: XXXXXX
# gcp_kms: XXXXXXThe unencrypted_regex is the decision that matters in a real repository. It leaves apiVersion, metadata, kind and type in the clear so that a human can still read the manifest, while the values are encrypted. The comments show the other key options, a provider key store, a KMS or a GCP KMS, and the warning that the tutorial's PGP key is not for production.
The rest of the flow is the standard SOPS one. A Secret is written with the data values base64 encoded, the SOPS CLI encrypts it into a .enc.yaml file, the generator manifest points at that encrypted file, and kustomize build with the plugin flags produces the decrypted output.
The tutorial also includes the local setup steps that are easy to skip and then miss. There are commands to verify that kustomize and gpg are installed, a source form of the install script that adds ksops to your shell's PATH, and make import-test-keys, which imports the project's PGP test keys so that the tutorial works without setting up your own key. The keys are also imported when you run make test.
Installation is a single line either way, with curl or wget piping the archive install script into bash, and a note that a Permission denied error means you should run it with sudo.
secretFrom: a Secret built from files, with no template
There is a second way to use the plugin, and it is the one that removes the most work.
KSOPS can generate a Kubernetes Secret directly from encrypted files or dotenv files, without a source template. The manifest for it looks like this:
apiVersion: viaduct.ai/v1
kind: ksops
metadata:
name: example-secret-generator
annotations:
config.kubernetes.io/function: |
exec:
path: ksops
secretFrom:
- metadata:
name: secret-name
labels:
app: foo
annotations:
kustomize.config.k8s.io/needs-hash: "false"
type: OpaqueThe secretFrom block replaces the files list, and each entry carries its own metadata, labels and type, so several secrets can be produced by one generator. The dotenv support comes from the joho/godotenv dependency, which means a .env file encrypted with SOPS can become the contents of a Secret without anyone writing a template or a script to reshape it.
The annotation kustomize.config.k8s.io/needs-hash set to false is the one line to notice, because it tells kustomize not to append a content hash to the name of the generated Secret. Generated resources normally get a hash suffix so that a change forces a rollout. For a Secret whose name is referenced by other manifests, that suffix breaks the references, and the annotation opts out of it. That is the kind of detail that turns into a long debugging session if you do not know why a Deployment cannot find its Secret.
Why the dependency list is enormous
The go.mod is short at the top and enormous underneath, and the reason is instructive.
The direct requirements are getsops/sops/v3 at v3.12.2, which is SOPS itself, joho/godotenv at v1.5.1 for the dotenv support, sigs.k8s.io/kustomize/api at v0.19.0 for the kustomize API, and sigs.k8s.io/yaml at v1.6.0. The module targets Go 1.25.0.
Then the indirect list runs to dozens of lines: the Google Cloud client libraries for KMS and IAM, the Azure SDK including keyvault, the filippo.io age and edwards25519 and hpke packages, the kpt functions SDK, and golang.org/x/sync. None of that is written by this project. It is inherited from SOPS, which supports many key management services, and a plugin that decrypts SOPS resources inherits the entire client surface of every backend SOPS can talk to.
That is worth knowing when you evaluate the project, because it affects the build image and the release artefacts. A plugin that pulls in a cloud SDK for every provider ends up with a dependency tree that has to be patched and audited like any other, and the build flags in the Makefile show the project is conscious of that: the Go build uses -trimpath and ldflags of -w -s, which strip the symbol table and DWARF data from the binary, and the Docker build sets CGO_ENABLED=0.
The Makefile is also where the plugin's own conventions live. PLUGIN_NAME is ksops, and the installation paths are derived from it: the plugin goes to the kustomize plugin directory under XDG_CONFIG_HOME, and there is a second path for the legacy exec form, with a -exec suffix. The prerequisites are staticcheck, kustomize, sops and go-mod-outdated, all installed on demand, and there is a help target that formats the available targets.
The container, Argo CD, and the release line
The Dockerfile is a careful cross-compilation build, and the comments in it explain why.
The base stage is a golang image with the Go version taken as a build argument, and a second stage brings in the tonistiigi cross-compilation image, pinned by digest, so the same build can produce binaries for every platform kustomize might run on. The builder stage runs xx-go to wrap the cross-compiler, runs make prereqs, verifies the kustomize binary, then runs make install and verifies the ksops binary as a static binary.
The comment that says to match Argo CD's build, placed next to the Go environment settings, is the important one. Argo CD runs its own kustomize build inside a container, and the plugin has to behave the same way there as it does on a laptop, so the build environment is deliberately aligned with the one Argo CD uses. The runtime stage is a distroless base image running as the nonroot user, with only the ksops binary, the CA certificates and git copied in, and its working directory is the binary's own directory.
The README's table of contents has an Argo CD Integration section, which is the deployment story, and the project is built around that integration rather than around a standalone use.
On versions, there is one small inconsistency worth noticing. The README has both a README.md and a README-legacy.md, and the Makefile defines a legacy plugin path alongside the modern one, so the project has kept the older exec plugin form working alongside the current one rather than dropping it.
The release line is v4.4.0 on 2025-09-05, then v4.5.0 and v4.5.1 published on the same day, 2026-04-13, which is the pattern of a patch and a release together. The last push was on 2026-07-10, so the tree is being worked on, and .goreleaser.yml in the repository root is how the binaries for those releases are built.
Editorial conclusion
Adopt KSOPS if you run GitOps and want encrypted manifests to go through the same kustomize build and apply path as everything else, because the plugin turns decryption into a build step rather than a separate step in your release pipeline. Do not adopt it if your pipeline already decrypts with the SOPS CLI outside kustomize, since the plugin exists precisely to remove that step. Before you deploy it, check that kustomize runs with the alpha plugin and exec flags enabled, because the plugin mechanism is still flagged as alpha in kustomize, and read the .sops.yaml creation rules carefully, because the unencrypted_regex there decides which fields of a Secret stay readable in git.
Frequently asked questions
What does KSOPS do in a kustomize build?
It is a kustomize KRM exec plugin for SOPS encrypted resources. A generator manifest of kind ksops lists the encrypted files, kustomize runs the ksops binary during the build, and the output is the decrypted manifest, so encrypted Secrets and ConfigMaps are built and applied through the same path as the rest of the Kubernetes manifests.
How do I install KSOPS?
Run the install script from the repository, with curl or wget piping it into bash, or build it from source with make install, which needs Go. The only stated requirement is kustomize, and the tutorial also expects gpg.
What flags does the kustomize build need for KSOPS?
The build is run with kustomize build --enable-alpha-plugins --enable-exec . The README notes that in kustomize v2 and v3 the equivalent command used a hyphen in the flag, kustomize build --enable_alpha_plugins ., because the plugin mechanism is still behind an alpha gate in kustomize.
Can KSOPS build a Secret from a dotenv file?
Yes. The secretFrom form of the generator takes files or dotenv files and produces a Secret directly, with per-secret metadata, labels and type, and the dependency list includes joho/godotenv for the dotenv parsing. The generated Secret carries the annotation kustomize.config.k8s.io/needs-hash set to false so that kustomize does not append a content hash to its name.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/viaduct-ai-kustomize-sops)Was this useful?