Learning GitOps - FluxCD - Sealed Secrets
Episode 22 of 36

Learning GitOps - FluxCD - Sealed Secrets

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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.

What Are Sealed Secrets

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.

  • Public/private key encryption: the cluster has a key pair — the private key is stored by the controller, the public key is distributed to users.
  • Controller-based decryption: the controller reads the SealedSecret, decrypts it with the private key, then creates the native Secret.
  • Git-native: a SealedSecret is safe to commit because its content is ciphertext; a GitOps cluster can still apply manifests uniformly.

The difference from SOPS (episode 20) and ESO (episode 21) should be emphasized:

AspectSOPSESOSealed Secrets
Secret locationGit (ciphertext)External backendGit (SealedSecret)
Keys in the clusterNeeded (age/gpg/KMS)Backend credentialsOne controller key pair
Automatic rotationNoYesNo
ComplexityMediumHighLow

Installing the Controller and Kubeseal

The Sealed Secrets Controller

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:

clusters/prod/flux-system/sealed-secrets.yaml
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: true

On 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.

The Kubeseal CLI

Kubeseal is the CLI for sealing secrets. In online mode, kubeseal talks directly to the controller and fetches the public key:

Install kubeseal and fetch the certificate
kubeseal --version
kubeseal --controller-namespace sealed-secrets \
  --controller-name sealed-secrets \
  --fetch-cert > public-cert.pem

Certificate Management

--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.

Usage with Flux

Sealing a Secret

Create an ordinary Secret (locally, without applying it to the cluster), then seal it with kubeseal:

Seal a secret
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.yaml

The result is a SealedSecret manifest — note that the DB_PASSWORD value is already an encrypted AgBy3i4O... block:

clusters/prod/apps/sealedsecret.yaml
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: Opaque

Commit and Automatic Unsealing

Commit 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 GitOps Workflow

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.

Key Management

  • Certificate backup: back up the 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.
  • Key rotation: delete the key secret and restart the controller to create a new pair, then re-seal all the secrets. Because it's disruptive, do it in a planned manner.
  • Multi-cluster: each cluster has its own key pair — a SealedSecret for cluster A can't be opened in cluster B. If you want one SealedSecret to work in many clusters, restore the same key secret to all clusters; if you want isolation, leave each cluster independent.

Back up the key with:

Back up and restore keys
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-secrets

Closing

This 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:

  • Private key in the controller, public key in users: sealing doesn't need the private key.
  • Git as the secret home: a SealedSecret is safe to commit, unlike an ordinary Secret.
  • Smooth Flux integration: the controller is installed through a HelmRelease, the SealedSecret is applied by Flux, unsealing is automatic.
  • Key backup is mandatory: without sealed-secrets-key, no secret can be opened.
  • Understand the scope: the default binding is per name and namespace, adjust the scope as needed.

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!

Learning GitOps - FluxCD - Sealed Secrets | Learn FluxCD & GitOps