Sealed Secrets Explained: Secure Kubernetes Secrets the Right Way

In this video, we break down how Sealed Secrets works, walk through a full end-to-end demo, and explain when it’s a good fit — and when it’s not.

Transcript
In this video, we're looking at Sealed Secrets, a Kubernetes controller that lets you store encrypted secrets in Git.
In GitOps, your repo is the source of truth for everything in your cluster, but you can't store secrets in Git as plain text. That's the problem that Sealed Secrets solves. It was one of the first Kubernetes-native ways to do it.
Out of the box, Kubernetes gives you a native object called a Secret. A Secret is just a way to store sensitive values inside the cluster. Secrets integrate cleanly with pods. You can inject them as environment variables or mount them as files. From the application's point of view, this works fine. The app just reads a value at runtime.
The problem is what Kubernetes doesn't solve. In a GitOps workflow, you'd want your secrets defined in Git alongside everything else. But Kubernetes Secrets are just Base64 encoded. That's not encryption, it's encoding. Anyone with access to your repo can decode them. So you can't safely commit them to Git. And if you can't commit them, they don't belong in a GitOps workflow.
That's the gap that Sealed Secrets fills.
Sealed Secrets consists of a Kubernetes controller and a CLI tool called kubeseal.
Here's how it works.
First, you install the Sealed Secrets controller in your cluster, typically in the kube-system namespace or a custom one. The controller generates a key pair: a public key that's used by kubeseal to encrypt secrets, and a private key that's used to decrypt secrets within the cluster or Kubernetes environment.
This is asymmetric encryption. You can encrypt secrets anywhere kubeseal is available, your laptop, CI/CD, wherever, but only the controller can decrypt them.
Once that's in place, the workflow is simple. You create a normal Kubernetes Secret manifest. You pipe that into kubeseal, which encrypts the secret values using the cluster's public key and outputs a SealedSecret object. You commit only the SealedSecret to Git. The original Secret never touches your repo.
Your GitOps tool, Flux, Argo CD, or whatever you're using, applies the SealedSecret object to the cluster. The Sealed Secrets controller sees it, decrypts the secret values using the cluster's private key, and creates a normal Kubernetes Secret. Your application then references the Secret like normal.
Encrypted before Git. Decrypted inside the cluster. Git-safe Kubernetes secrets.
Let's walk through this in action. Nothing crazy, just enough to see this workflow end to end.
Now I've got a simple Flask app that reads a DB password environment variable. We've deployed it into a Minikube cluster where a Kubernetes Secret gets mounted as environment variables into the pod. The app picks it up and displays it.
Obviously, you'd never render a password on a webpage in production. This is just so we can visually confirm that Sealed Secrets successfully decrypted and injected the value into our container.
First, we install the Sealed Secrets controller. There are a couple of ways to do this. We're going to use Helm. Copy this, and we'll install it in the kube-system namespace.
Now you should see it successfully install. I already had it installed. We can check and see that it's running with kubectl get pods -n kube-system, and we can see that our Sealed Secrets controller is running.
Next, the kubeseal CLI. We're going to use Homebrew. I already had it installed, so it shows installed and up to date.
Now we can confirm that we can talk to the controller and fetch its public key. You can use kubeseal --fetch-cert.
If your controller is in a different namespace than kube-system, you'd use flags like --controller-namespace, but the defaults work fine here.
First, we'll define a standard Kubernetes Secret. This is the raw material we want to protect. Notice the data field. While it's Base64 encoded, it's effectively plain text.
Now we can pipe that raw Secret directly into kubeseal. It communicates with the controller in our cluster to encrypt the data.
And there it is, a SealedSecret. It looks like a standard Kubernetes Secret object, but the kind is SealedSecret and the values have been encrypted.
This is fully safe to commit to Git, since it can only be decrypted by the private key living in the cluster.
I can actually delete the original Secret manifest. Keeping that around would defeat the whole point.
Now we apply the SealedSecret. In a real GitOps setup, you'd commit this to Git and let Flux or Argo CD apply it automatically. We're doing it manually here just to show the mechanics.
The controller turns it back into a normal Kubernetes Secret. There's our Base64 encoded DB password. Let's go ahead and decode that.
And there's our plain text DB password.
Now restart the deployment so the pod picks up the new Secret. Refresh. And there it is. Your app reads it just like any other Secret.
The key is where encryption happens. Encrypted before Git. Decrypted inside the cluster.
Before using this in production, there are a few limitations you need to understand.
Sealed Secrets are tied to a specific cluster key. They can also be scoped to a specific namespace and name. That means two things.
If you lose the controller's private key, those secrets are gone forever.
And if you move clusters, you have to reseal them.
Rotation is manual. So this isn't a great fit for secrets that change often, dynamic credentials, or syncing secrets across many clusters.
If those requirements apply to you, you'll want a different pattern, like using a native Kubernetes operator that syncs from an external secrets manager. Tools like the Infisical Operator handle that workflow. We'll link it in the description.
So when is Sealed Secrets a good fit?
It works best for GitOps-first teams running small to medium clusters that primarily need static secrets. If your workflows are simple and you want zero external dependencies, Sealed Secrets fits nicely. It does one job and it does it predictably.
As mentioned earlier, it's not the right tool for setups with high rotation requirements, centralized secrets management, multi-cluster environments, or dynamic credentials. Once you need those things, you're past what Sealed Secrets was designed to solve.
That's where other patterns and other tools start to make sense. Secrets CSI drivers, agent injection, and operators that sync from external platforms like Infisical.
Sealed Secrets solves a very specific problem: keeping secrets out of Git while staying declarative. It does that cleanly and predictably.
For teams early in Kubernetes or GitOps, this is a great place to start.
In future videos, we'll look at other Kubernetes secrets patterns and the tradeoffs between different approaches. Because once requirements grow, how you manage secrets starts to matter a lot.
Thanks for watching. I'll drop links to all relevant documentation in the description, and we'll see you in the next one.
Starting with Infisical is simple, fast, and free.