Synced but Degraded: onboarding a Helm app into ArgoCD with Sealed Secrets
I had my News Feed service running on my k3s homelab via a plain helm install, and Uptime Kuma already under ArgoCD. Time to bring News Feed under GitOps too — and to do it properly: secrets encrypted in git, not plaintext. That “properly” is where twenty minutes of education happened.
Secrets are the hard part of GitOps
Section titled “Secrets are the hard part of GitOps”GitOps means git is the source of truth for the cluster. But you can’t put plaintext DB passwords in a git repo. The standard answer is Sealed Secrets: a controller holds a private key that never leaves the cluster; you encrypt your Secret against its public key with kubeseal; you commit the ciphertext (a SealedSecret); the controller decrypts it into a real Secret in-cluster. Git only ever holds encrypted data — safe even in a public repo, because only that cluster’s controller can unseal it.
The setup
Section titled “The setup”Briefly, the moving parts:
- Installed the
sealed-secretscontroller + thekubesealCLI. - Sealed News Feed’s Secret (
newsfeed-secrets, keysDB_PASSWORD/JWT_SECRET/RABBITMQ_*) and committed the encryptedSealedSecretto my GitOps repo. - Added a
secrets.createtoggle to the Helm chart so ArgoCD could tell it “don’t make your own Secret — the SealedSecret handles it.” - Wrote a multi-source ArgoCD
Application: one source pulls the Helm chart (secrets.create=false), the other pulls the sealed secret. Applied it.
Synced… but Degraded
Section titled “Synced… but Degraded”NAME SYNC STATUS HEALTH STATUSnewsfeed Synced DegradedSynced = ArgoCD applied everything from git. Degraded = something under it is unhealthy. But every pod was Running. So what was degraded?
The SealedSecret’s own status had the answer:
Message: Resource "newsfeed-secrets" already exists and is not managed by SealedSecretMy earlier manual helm install had already created newsfeed-secrets. And Sealed Secrets deliberately refuses to overwrite a Secret it doesn’t own — a safety feature, so it can’t clobber something another tool manages. So the SealedSecret sat there unhealthy, ArgoCD aggregated that into Degraded, and yet the app kept serving fine (the old Secret still held the right values).
The trap: deleting the Secret wasn’t enough
Section titled “The trap: deleting the Secret wasn’t enough”Obvious fix — delete the old Secret so the controller can create its own:
kubectl delete secret newsfeed-secrets -n newsfeed…and it didn’t come back. The controller logs explained why:
"Error updating, giving up" ... "already exists and is not managed by SealedSecret""update suppressed, no changes in spec"Two things had happened:
- The controller had given up — it exhausted its retries earlier, while the Secret still existed.
- I then tried to nudge it by adding an annotation to the SealedSecret. It ignored me: “update suppressed, no changes in spec.” The controller only re-processes a SealedSecret when its spec changes — an annotation is metadata, not spec. My nudge was a no-op.
The fix: restart the controller
Section titled “The fix: restart the controller”A restart forces a full reconcile of every SealedSecret from scratch — an initial sync, not an “update” it can suppress:
kubectl rollout restart deploy sealed-secrets-controller -n kube-systemSeconds later:
$ kubectl get secret newsfeed-secrets -n newsfeedNAME TYPE DATA AGEnewsfeed-secrets Opaque 4 36s # created fresh by the controller, owned by it
$ kubectl get application newsfeed -n argocdNAME SYNC STATUS HEALTH STATUSnewsfeed Synced HealthyLessons
Section titled “Lessons”- Cutover order matters. Migrating an existing Secret to Sealed Secrets? Delete the old Secret before applying the SealedSecret. Do it the other way and the controller hits “already exists,” gives up, and won’t retry on its own.
- Sealed Secrets won’t clobber a Secret it doesn’t own. A great safety property that’s mildly infuriating mid-migration — until you know it’s the reason.
- The controller reconciles on spec changes, not metadata. “Just annotate it to trigger a re-sync” doesn’t work here. Change the spec, or restart the controller.
Synced≠Healthy. ArgoCD applied everything from git (Synced) but a child resource was unhealthy (Degraded). Read both columns — then follow the unhealthy resource’s own status to the real cause.
The app was never down through any of this. But properly onboarding one service to GitOps taught me more about the Sealed Secrets ownership model in twenty minutes than the docs did — which is exactly what a homelab is for.