Skip to main content
In this lesson we encrypt Kubernetes Secrets with Bitnami Sealed Secrets so they can be safely stored in Git and only be decrypted by the Sealed Secrets controller running in the target cluster. We will work inside the sealed-secret directory of the cgoa-demos repository.
The image shows a GitHub repository interface with files and folders listed, including updates and commit details. It highlights the use of the Smarty language.
In that directory you can see a Deployment, a Service, and a standard Kubernetes Secret file that was recently updated.
The image shows a file directory from a web-based code repository, featuring a folder called "sealed-secrets" with YAML files related to deployment and services. The repository interface displays recent updates, including modifications to a "secret.yml" file.
Important: a Kubernetes Secret stores values Base64-encoded, not encrypted. Anyone with the file can decode the values. Example of a plain Secret YAML (values are Base64-encoded):
apiVersion: v1
kind: Secret
metadata:
  name: app-crds
data:
  apikey: emFDRUxnTC0wa1l5ZmM4bVZM3dzQWF3
  username: YWRtaW4=
  password: cGFzc3dvcmQ=
Because GitOps workflows keep manifests in Git, never commit plain (or only Base64-encoded) Secrets. Instead, encrypt secrets for storage in Git with a tool that only the cluster controller can decrypt — for example, Bitnami Sealed Secrets. Overview — how this works
  • The Sealed Secrets controller in the cluster holds a private key.
  • You fetch the controller’s public certificate and use kubeseal to encrypt a regular Secret YAML into a SealedSecret.
  • The SealedSecret is safe to commit to Git because it contains only encrypted data.
  • When applied to a cluster, the controller decrypts it and creates a standard Kubernetes Secret for pods to consume.
Step 1 — get the Sealed Secrets controller public certificate The kubeseal client needs the controller’s public cert to encrypt secrets. Fetch it from the namespace where the controller is installed (commonly kube-system):
  1. List secrets in the controller namespace:
kubectl -n kube-system get secret
  1. Extract and decode the controller certificate (replace <SECRET_NAME> with the secret name you found):
kubectl -n kube-system get secret <SECRET_NAME> -o json \
  | jq -r '.data["tls.crt"]' \
  | base64 --decode > ss-public.crt
Notes:
  • The bracket notation ["tls.crt"] is required because the key contains a dot.
  • ss-public.crt will contain the controller public certificate in PEM format, e.g.:
-----BEGIN CERTIFICATE-----
MIIzEDCArSgAwIBAgIQNydvhgVzGKbSCcZ...
-----END CERTIFICATE-----
The image shows a terminal window in a code editor displaying encoded or encrypted text.
Step 2 — generate the Secret YAML locally (without applying it) Use kubectl create secret with a dry-run to produce the Secret YAML file:
kubectl create secret generic app-crds \
  --from-literal=apikey=zaCELGm8vAyarYr4Rx-Af50DDqtlx \
  --from-literal=username=admin-dev-group \
  --from-literal='password=pa$$w0rd123' \
  -o yaml --dry-run=client > mysql-password.yaml
Tip: quote literal values that include shell-sensitive characters (like $) to prevent expansion, as shown for the password. Generated Secret (simplified):
apiVersion: v1
kind: Secret
metadata:
  name: app-crds
data:
  apikey: emFRUXnTC0wAlmbm4ZWM3dzQWF3
  username: YWRtaW4tZGV2LWxpcw==
  password: cGEtJHcwdmQxMjM=
Step 3 — encrypt the Secret with kubeseal Use the public certificate and choose a scope for the resulting SealedSecret (cluster-wide or namespace):
kubeseal --cert ss-public.crt --scope cluster-wide -o yaml \
  < mysql-password.yaml > sealed-secret.yaml
This reads the cleartext Secret from stdin and writes a SealedSecret resource to sealed-secret.yaml. A SealedSecret uses spec.encryptedData (not data) and is safe to commit. Example (truncated):
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: app-crds
  annotations:
    sealedsecrets.bitnami.com/cluster-wide: "true"
spec:
  encryptedData:
    apikey: AgAWtLEpdg/433zTXjKhuIrubYubbDA1or7Mm4UmMw0m/...
    password: AgB6ZlGxWrdZYrMUrYTM...
  template:
    metadata:
      annotations:
        sealedsecrets.bitnami.com/cluster-wide: "true"
Do not commit plain Secret YAML files to Git. Commit only the SealedSecret YAML so secrets remain encrypted in the repository.
Step 4 — commit the SealedSecret to Git and deploy with Argo CD
  • Commit sealed-secret.yaml to the repository path monitored by Argo CD.
  • Configure an Argo CD Application pointing at the repo/path containing the SealedSecret (you can create the application’s namespace via Argo CD or beforehand).
  • Sync the Argo CD Application.
When Argo CD applies the SealedSecret, the Sealed Secrets controller intercepts it, decrypts the encryptedData with its private key, and creates a regular Kubernetes Secret in the target namespace for your application. Argo CD UI — application view
The image shows a user interface of Argo CD, displaying a list of applications with their synchronization and health statuses. It includes details for each application, like repository, target revision, and last sync time.
You can inspect application relationships and sync/health status in Argo CD.
The image shows the interface of Argo CD, depicting the application status and a visual representation of application resources and their relationships. It indicates a "Healthy" app health and a "Synced" sync status.
Step 5 — verify the Secret in the cluster
  1. Verify namespaces (you should see secret-app if Argo CD created it):
kubectl get ns
  1. Check resources in the application namespace:
kubectl -n secret-app get all
  1. Inspect the created Secret (the Sealed Secrets controller creates a normal Secret resource — values are still Base64-encoded per Kubernetes format, but represent the decrypted values from the original Secret):
kubectl get secret app-crds -n secret-app -o json
Example output (truncated):
{
  "apiVersion": "v1",
  "data": {
    "apikey": "emFDRUnTC0wAlmbmM4bZVM3dzQWF3",
    "password": "cGFzc3dvcmQ=",
    "username": "YWRtaW4="
  },
  "kind": "Secret",
  "metadata": {
    "name": "app-crds",
    "namespace": "secret-app",
    "annotations": {
      "sealedsecrets.bitnami.com/cluster-wide": "true"
    },
    "ownerReferences": [
      {
        "apiVersion": "bitnami.com/v1alpha1",
        "controller": true,
        "kind": "SealedSecret",
        "name": "app-crds"
      }
    ]
  },
  "type": "Opaque"
}
The image shows a screenshot of a Bitnami interface displaying encrypted credentials such as username, password, and API key, noting they are managed with Bitnami Sealed Secrets.
Key takeaways
  • SealedSecrets are safe to commit to Git because they store only encrypted data.
  • The cluster’s Sealed Secrets controller holds the private key and will decrypt SealedSecrets to create real Kubernetes Secrets in the target namespace.
  • Applications consume the resulting Secret as usual.
Quick command reference
TaskCommand
List secrets in controller namespacekubectl -n kube-system get secret
Extract controller certkubectl -n kube-system get secret <SECRET_NAME> -o json | jq -r '.data["tls.crt"]' | base64 --decode > ss-public.crt
Create a Secret YAML (dry-run)kubectl create secret generic app-crds --from-literal=apikey=... --from-literal=username=... --from-literal='password=...' -o yaml --dry-run=client > mysql-password.yaml
Encrypt Secret to SealedSecretkubeseal --cert ss-public.crt --scope cluster-wide -o yaml < mysql-password.yaml > sealed-secret.yaml
Verify created Secret in clusterkubectl get secret app-crds -n secret-app -o json
Alternatives and further reading This concludes the demo.

Watch Video

Practice Lab