Argo CD doesn’t store secrets in Git by default because Git is a plaintext log, and secrets shouldn’t be there.

Let’s see how this actually looks. Imagine you have a Kubernetes Secret object. Normally, you’d commit this YAML to Git.

apiVersion: v1
kind: Secret
metadata:
  name: my-db-credentials
  namespace: default
type: Opaque
data:
  username: dXNlcm5hbWU= # base64 encoded 'username'
  password: cGFzc3dvcmQ= # base64 encoded 'password'

If this Secret is in your Git repository, anyone with access to that repository can base64 -d the values and get your plaintext credentials. That’s bad.

Sealed Secrets, a project from Bitnami, solves this by encrypting your Kubernetes Secret before it gets committed to Git. Argo CD can then decrypt it in-cluster and create the actual Kubernetes Secret object.

Here’s the workflow:

  1. You create a regular Kubernetes Secret YAML locally.
  2. You use the kubeseal CLI tool to encrypt this Secret. This kubeseal command encrypts your secret using a public key that’s held by the Sealed Secrets controller running in your cluster. The resulting YAML contains an encrypted blob, not plaintext data.
  3. You commit this sealed secret YAML to your Git repository.
  4. Argo CD, watching your Git repository, sees the sealed secret YAML.
  5. Argo CD applies this sealed secret YAML to your cluster.
  6. The Sealed Secrets controller, which has the corresponding private key, intercepts the sealed secret. It decrypts the blob and creates a regular, unencrypted Kubernetes Secret object in your cluster.

Let’s get hands-on. First, you need to install the Sealed Secrets controller in your cluster. You can do this with Helm:

helm install sealed-secrets bitnami/sealed-secrets \
  --namespace kube-system \
  --create-namespace

This installs the controller and its necessary RBAC roles. The controller has a public key that kubeseal will use. You can fetch this public key using:

kubectl get secret -n kube-system sealed-secrets-key -o jsonpath='{.data.tls\.crt}' | base64 --decode > pub-cert.pem

Now, let’s create a sample secret and seal it.

# my-db-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: my-db-credentials
  namespace: default
type: Opaque
data:
  username: dXNlcm5hbWU= # base64 encoded 'username'
  password: cGFzc3dvcmQ= # base64 encoded 'password'

Use kubeseal to encrypt this:

kubeseal < my-db-secret.yaml --cert pub-cert.pem > sealed-secret.yaml

The sealed-secret.yaml file will look something like this:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: my-db-credentials
  namespace: default
spec:
  encryptedData:
    password: AgC2n... # this is the encrypted blob
    username: AgCaV... # this is the encrypted blob
  template:
    metadata:
      creationTimestamp: null
      name: my-db-credentials
      namespace: default
status: {}

Notice encryptedData – this is the magic. This sealed-secret.yaml is what you commit to Git.

Now, configure Argo CD to sync this sealed-secret.yaml. Add it to your Argo CD Application’s source directory.

# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  destination:
    namespace: default
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://github.com/your-org/your-repo.git
    targetRevision: HEAD
    path: path/to/your/sealed-secrets # where sealed-secret.yaml lives
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

When Argo CD applies sealed-secret.yaml, the Sealed Secrets controller in kube-system (or wherever you installed it) will detect it. Because the controller has the private key, it can decrypt spec.encryptedData and create the actual Kubernetes Secret object named my-db-credentials in the default namespace.

The core idea is that kubeseal encrypts the secret using the controller’s public key. The controller, running inside the cluster, is the only entity with the corresponding private key. This means the sensitive data is never exposed in Git, and only the controller can reconstitute the original Kubernetes Secret.

The template section within the SealedSecret allows you to specify metadata for the resulting Kubernetes Secret. This is useful for setting labels, annotations, or even the type of the Secret that will be created. For example, if you wanted to create a kubernetes.io/tls secret, you’d structure your original secret YAML accordingly.

What most people don’t realize is that the kubeseal command requires access to the controller’s public certificate. If this certificate changes (e.g., you rotate the keys for Sealed Secrets), you’ll need to re-seal all your secrets. The kubeseal --controller-install-cert flag can be used to fetch the certificate directly from a running controller if you don’t have it saved.

After this, you’ll likely want to explore how to manage TLS secrets with Sealed Secrets.

Want structured learning?

Take the full Argocd course →