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:
- You create a regular Kubernetes
SecretYAML locally. - You use the
kubesealCLI tool to encrypt thisSecret. Thiskubesealcommand 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. - You commit this sealed secret YAML to your Git repository.
- Argo CD, watching your Git repository, sees the sealed secret YAML.
- Argo CD applies this sealed secret YAML to your cluster.
- The Sealed Secrets controller, which has the corresponding private key, intercepts the sealed secret. It decrypts the blob and creates a regular, unencrypted Kubernetes
Secretobject 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.