Securing secrets directly in Git with Bitnami Sealed Secrets: public-private key encryption, controller and kubeseal installation, the GitOps flow with FluxCD, and certificate backup and rotation for multi-cluster scenarios.

In episode 21 you learned the External Secrets Operator — secrets are stored in an external backend and referenced from Git. But not every team has Vault or AWS, and sometimes Git itself is the most practical medium: small secrets, small teams, or organizational constraints. For this scenario there's a simpler alternative to SOPS: Sealed Secrets.
This episode covers Bitnami's Sealed Secrets: how public-private key encryption works, controller and kubeseal installation, the usage flow with FluxCD, and key management for backup, rotation, and multi-cluster.
Sealed Secrets uses an elegant idea: you "seal" a Kubernetes Secret into a SealedSecret that can only be opened by a specific cluster. It isn't a human who opens it, but the controller.
The difference from SOPS (episode 20) and ESO (episode 21) should be emphasized:
| Aspect | SOPS | ESO | Sealed Secrets |
|---|---|---|---|
| Secret location | Git (ciphertext) | External backend | Git (SealedSecret) |
| Keys in the cluster | Needed (age/gpg/KMS) | Backend credentials | One controller key pair |
| Automatic rotation | No | Yes | No |
| Complexity | Medium | High | Low |
The controller is installed through the official sealed-secrets Helm chart from Bitnami Labs. As an ordinary Kubernetes application, the chart is declared as a HelmRepository and HelmRelease so Flux installs it:
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: sealed-secrets
namespace: flux-system
spec:
interval: 10m
url: https://bitnami-labs.github.io/sealed-secrets
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: sealed-secrets
namespace: flux-system
spec:
interval: 10m
chart:
spec:
chart: sealed-secrets
sourceRef:
kind: HelmRepository
name: sealed-secrets
targetNamespace: sealed-secrets
install:
createNamespace: trueOn first run, the controller creates the sealed-secrets-key Secret containing the private key. This private key never leaves the cluster — only the public key is shared for the sealing process. Check the controller status with kubectl get pods -n sealed-secrets.
Kubeseal is the CLI for sealing secrets. In online mode, kubeseal talks directly to the controller and fetches the public key:
kubeseal --version
kubeseal --controller-namespace sealed-secrets \
--controller-name sealed-secrets \
--fetch-cert > public-cert.pem--fetch-cert downloads the public key as a certificate. Store public-cert.pem in the repo or share it with CI so the team can seal without direct cluster access (offline mode). This certificate is public — it isn't a secret.
Create an ordinary Secret (locally, without applying it to the cluster), then seal it with kubeseal:
kubectl create secret generic api-db \
--namespace apps \
--from-literal=DB_PASSWORD=sup3rsecret \
--dry-run=client -o yaml > secret.yaml
kubeseal --format yaml < secret.yaml > sealed-secret.yamlThe result is a SealedSecret manifest — note that the DB_PASSWORD value is already an encrypted AgBy3i4O... block:
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: api-db
namespace: apps
annotations:
sealedsecrets.bitnami.com/namespace-wide: "true"
spec:
encryptedData:
DB_PASSWORD: AgBy3i4OcX4j...
template:
metadata:
name: api-db
namespace: apps
type: OpaqueCommit sealed-secret.yaml to Git as usual. Flux pulls the manifests, applies the SealedSecret to the cluster, and the controller decrypts it into the api-db Secret used by the application. No manual step after the commit.
Important
A SealedSecret is bound to the name and namespace by default. Use the namespace-wide or cluster-wide scope when the secret will be used under a different name or in a different namespace, so it doesn't fail when the name changes.
The complete cycle becomes: a developer creates a temporary Secret, seals it, commits the SealedSecret, reviews it through a pull request, Flux applies it, the controller unseals it, the application reads it. All the secret values that were ever created can be removed after sealing — don't keep the raw secret.yaml file on disk or in Git.
sealed-secrets-key Secret to a safe place (e.g. SOPS-encrypted or in Vault). Losing the private key means every SealedSecret can never be opened again.Back up the key with:
kubectl get secret sealed-secrets-key -n sealed-secrets \
-o yaml > sealed-secrets-key-backup.yaml
kubectl apply -f sealed-secrets-key-backup.yaml -n sealed-secretsThis episode completes the secret management series: SOPS for field encryption, ESO for external backends, and Sealed Secrets for cluster-based encryption that still keeps secrets in Git. With Sealed Secrets, developers can write secrets to Git without fear of leaking, and Flux remains the only path for applying configuration.
The key takeaways:
sealed-secrets-key, no secret can be opened.We can now secure secrets in one cluster. But real organizations have many clusters — staging, production, per region. In the next episode, episode 23, we'll learn Multi-Cluster Management: cluster strategies, bootstrapping many clusters with Flux, cross-cluster dependencies, and Cluster API and vcluster integration. See you in episode 23!