, ,

Secrets, Config and CI/CD Pipeline Migration to VKS (TKGI to VKS Series, Part 21)

Move plain secrets and ConfigMaps with Velero, re-seal sealed secrets against VKS, and give your CI/CD pipeline a real service account instead of a human login.

TKGI to VKS Series · Part 21 of 26

A token that lasts ten hours is the reason more pipeline migrations slip than any storage problem. Every time the kubectl vsphere plugin logs in to a VKS cluster it mints a JSON Web Token, a JWT, that expires in roughly ten hours. Copy that kubeconfig into a build runner and the first job after midnight meets a blunt 401. Secrets and config feel like the gentle wave of a TKGI to VKS move, and the plain ones genuinely are, yet the identity your pipeline carries and the sealed material committed to your GitOps repo are exactly where a tidy cutover turns messy.

Key takeaways: Plain Secrets and ConfigMaps move cleanly with Velero filtered to configmaps,secrets.
SealedSecrets do not travel, the source sealing key cannot decrypt on VKS, so you re-seal against the new controller cert.
A human kubectl vsphere login mints a JWT good for about 10 hours, wrong for pipelines, use a dedicated ServiceAccount token.
Rewrite image references off the old Harbor host and recreate the pull secret before the first pipeline run.
Headline command: velero restore create –from-backup cfg-secrets –include-resources configmaps,secrets
Who this is for: Platform engineers and pipeline owners who already stood up a VKS cluster, moved a stateless and a stateful app, and cut ingress and DNS across. Starting point assumed: a TKGI 1.18 source on NSX-T with UAA and LDAP, a Harbor registry, and a target of VKS on VCF 9 with Antrea and vSphere CSI. VKS is the vSphere Kubernetes Service, the Supervisor is the Kubernetes control plane vSphere runs, and Velero is the backup and restore tool that carries objects between clusters.

Where the migration stands now

Last part the web tier answered on its new ingress and DNS pointed at the VKS load balancer. Traffic is flowing, but two things still live only on TKGI: the secrets and config the apps read at start, and the pipeline that builds and deploys them. This part moves both. TKGI is Tanzu Kubernetes Grid Integrated, the outgoing platform, and VKS is where these workloads now run.

Order matters here. Move config and secrets first so a redeploy on VKS finds everything it needs, then repoint the pipeline so the next commit lands on VKS instead of the old cluster. Doing it the other way round gives you a pipeline that deploys into a namespace missing half its secrets, and pods that crash loop on a key that is not there.

Run this per namespace, not per cluster. Each namespace in the running example, webtier and payments and the shared platform namespace, carries its own secrets, config and pull credentials, and folding them into one giant restore makes a failure hard to read. I move one namespace end to end, verify it, then repeat. A wave that touches three namespaces becomes three small, boring restores instead of one large one you cannot reason about when it turns yellow.

Preflight and prerequisites

Before anything moves, prove three things: you can reach both clusters, Velero is installed on the target pointing at the same object store the source backups landed in, and the SealedSecrets controller exists on VKS if you use sealed material. A preflight that skips the object store check is how you discover at restore time that the target Velero is looking at an empty bucket.

# Tested against: VCF 9.0, VKS on Supervisor, Velero 1.17, kubectl vsphere plugin 9.0, TKGI 1.18 source kubectl vsphere login –server=sc-vks-01.corp.local –vsphere-username migops@corp.local –tanzu-kubernetes-cluster-name app-prod –tanzu-kubernetes-cluster-namespace app-prod-ns # Logged in successfully. # You have access to the following contexts: # app-prod velero version –client-only # Client: # Version: v1.17.0 # confirm the target Velero points at a real bucket velero backup-location get # NAME PROVIDER BUCKET/PREFIX PHASE LAST VALIDATED # default aws vks-velero/prod Available 2026-08-01 07:12:03

Inventory secrets and config on the source clusters

You cannot migrate what you have not counted. List every ConfigMap and Secret per namespace on each source cluster and separate the three kinds that behave differently: plain Opaque objects, sealed objects, and the service account tokens Kubernetes manages for you. Only the first kind is safe to copy verbatim.

kubectl config use-context tkgi-prod kubectl get secrets,configmaps -A –field-selector type!=kubernetes.io/service-account-token -o custom-columns=NS:.metadata.namespace,KIND:.kind,NAME:.metadata.name | head # NS KIND NAME # webtier ConfigMap app-config # webtier Secret db-credentials # webtier Secret tls-webtier # payments Secret registry-pull # how many plain Opaque secrets are you about to move kubectl get secrets -A –field-selector type=Opaque -o name | wc -l # 34 # find sealed material, which needs different handling kubectl get sealedsecrets -A -o name | wc -l # 12

Here is the mapping I keep taped to the migration runbook, and the one artifact from this part worth returning to. Read the type, then the method. It is the difference between a restore that just works and a controller log full of decrypt errors.

TypeCopies with VeleroMigration method
ConfigMapYesVelero, include-resources configmaps
Opaque SecretYesVelero, include-resources secrets
TLS SecretPartlyVelero, or reissue via cert-manager on VKS
SealedSecretNoRe-seal against the VKS controller cert
ServiceAccount token SecretNoRecreate on VKS, never copy
Registry pull SecretNoRecreate for the new Harbor robot account
External secret via ESONo copy neededRepoint the SecretStore and let it sync

Two categories catch people out during the count. Config that looks static often carries environment specifics, a database host or a feature flag that differs between the TKGI cluster and VKS, so copy the object and then edit the values that are cluster bound. TLS secrets are the other one: if cert-manager issued them, let cert-manager reissue on VKS against the same DNS name rather than carrying a certificate that is about to rotate anyway.

Migrating ConfigMaps and Secrets with Velero

Velero copies Kubernetes API objects between clusters. Filtered to configmaps and secrets it moves your plain material in seconds. One habit saves confusion: expect a PartiallyFailed status on a first restore into a namespace that already holds service account tokens. Velero skips objects that already exist and warns rather than overwriting, so a stale token from the old cluster produces a warning that reads alarming and means nothing for your app.

# on the source, back up only config and secrets for the webtier namespace velero backup create cfg-secrets-webtier –include-namespaces webtier –include-resources configmaps,secrets –wait # Backup completed with status: Completed. 4 items backed up. # on the target VKS cluster, restore the same slice velero restore create –from-backup cfg-secrets-webtier –include-resources configmaps,secrets –wait # Restore … status: PartiallyFailed # Warnings: # webtier: could not restore, Secret default-token-4f9q2 already exists # and differs from backed up version, skipping # that warning is only the stale source token. verify the app secret landed: kubectl get secret db-credentials -n webtier -o jsonpath='{.data}’ | wc -c # 214 # non-zero, keys restored
Note: Velero skips existing objects rather than overwriting them, with ServiceAccounts as a documented merge exception. If you edit a Secret on VKS and then run a restore, the restore will not clobber your edit. Delete the object first if you truly want the backed up value to win.

If your namespaces are named differently on VKS, Velero maps them with –namespace-mappings old:new on the restore, so a source namespace webtier can land as webtier-prod without editing a single manifest. To silence the stale token warning for good, back up with a label selector that matches only your application objects, since the default service account tokens carry no app labels and simply fall outside the backup.

Sealed secrets, external secrets and the re-seal problem

Here is where the tutorial default is wrong. Guides tell you Velero backs up the whole namespace, so your secrets come along for free. For a SealedSecret that is technically true and operationally useless. A SealedSecret is a Secret encrypted with the public half of a key pair that lives inside one cluster. Copy the encrypted blob to VKS and the controller there, holding a different private key, cannot read it. You get one line in the controller log, no key could decrypt secret, and the plaintext Secret never materialises.

Gotcha: Do not migrate the SealedSecrets private key from TKGI to VKS to make old blobs decrypt. It works, and it also drags a retired platform key into your new cluster and defeats the point of sealing per cluster. Re-seal instead. It costs minutes and leaves the new cluster holding its own key.

If you already run External Secrets Operator, ESO, the picture is easier. ESO does not store secret values in the cluster or in git, it fetches them from an external store such as Vault at runtime. Migration then means pointing a SecretStore on VKS at the same backend, not copying anything. For teams choosing now, that property is the argument for ESO over sealing: nothing cluster bound to re-encrypt when the next platform arrives.

# the failure, straight from the VKS controller after a blind copy kubectl logs -n kube-system deploy/sealed-secrets-controller | tail -2 # Error: no key could decrypt secret (db-credentials) # fix: fetch the VKS controller public cert and re-seal from the live Secret kubeseal –controller-namespace kube-system –fetch-cert > vks-pub-cert.pem kubectl get secret db-credentials -n webtier -o yaml | kubeseal –cert vks-pub-cert.pem –format yaml > db-credentials-sealed.yaml kubectl apply -f db-credentials-sealed.yaml # sealedsecret.bitnami.com/db-credentials created # controller now materialises the plaintext Secret kubectl get secret db-credentials -n webtier # NAME TYPE DATA AGE # db-credentials Opaque 2 6s

At twelve sealed objects you do not re-seal by hand twelve times. Loop the live Secrets through kubeseal with the VKS cert, write each sealed file into the GitOps repo, and let your delivery tool apply them, so the committed blobs are the new ones and the old cluster bound blobs stop being the source of truth. Treat the re-seal as a repo change, reviewed and merged, not a one off kubectl apply that nobody can reproduce later.

Pilot secrets and config wave, minutes per taskMeasured on the dev namespace, one operator, 12 sealed objectsConfigMaps restore3Plain Secrets restore4Image ref rewrite20Pipeline identity25SealedSecrets re-seal35 min
Plain objects are trivial, re-sealing twelve objects by hand is the long pole

Pipeline identity, tokens and kubeconfig

A build runner needs an identity that outlives a coffee break. Pinniped, the authentication component the Supervisor wires up for federated login, is built for humans at a keyboard, and the JWT a kubectl vsphere login hands back expires in about ten hours. That is fine for you at a terminal and wrong for a runner that deploys at 2am. Give the pipeline its own ServiceAccount, an SA, with a bound token scoped to the target namespace, and store that token in the CI secret store, never in git.

# symptom when a cached vsphere JWT expires overnight kubectl get pods -n webtier # error: You must be logged in to the server (Unauthorized) # fix: a dedicated ServiceAccount with a scoped rolebinding kubectl create serviceaccount ci-deployer -n webtier kubectl create rolebinding ci-deployer-edit –clusterrole=edit –serviceaccount=webtier:ci-deployer -n webtier # mint a bound token for the runner, read the value from an env var, not git TOKEN=$(kubectl create token ci-deployer -n webtier –duration=8h) kubectl config set-credentials ci-deployer –token="$TOKEN" kubectl –user=ci-deployer get pods -n webtier # NAME READY STATUS RESTARTS AGE # app-7c9f1a2b3c 1/1 Running 0 40s

Bound tokens have a ceiling. A token minted with kubectl create token honours the cluster maximum, often 24 or 48 hours, so a job that runs longer than the token lives will still fail midway. For anything long running, prefer a token your CI platform projects and rotates, or a short token minted fresh at the start of each job rather than one cached for days. Name the kubeconfig context for the cluster and namespace it targets, because a runner with three contexts and the wrong current-context is its own quiet outage.

flowchart LR
  A[Git repo manifests] --> B[CI build stage]
  B --> C[Push image to VKS Harbor]
  C --> D[kubectl apply]
  D --> E[ServiceAccount token auth]
  E --> F[VKS namespace]
Pipeline path after cutover, identity by ServiceAccount token not a human login

One more thing the pipeline carries is the image reference. Your manifests point at the old Harbor, harbor.tkgi.corp.local, and the robot account that pulled from it does not exist on the new registry. Harbor is the container registry both platforms use. Rewrite the reference and recreate the pull secret, or every pod lands in ImagePullBackOff.

# repoint the image from the old Harbor to the new one kustomize edit set image harbor.tkgi.corp.local/webtier/app=harbor.vks.corp.local/webtier/app:1.8.3 kubectl apply -k overlays/vks kubectl get pods -n webtier # NAME READY STATUS RESTARTS # app-7c9f1a2b3c 0/1 ImagePullBackOff 0 kubectl describe pod app-7c9f1a2b3c -n webtier | grep -A1 Failed # Failed to pull image harbor.vks.corp.local/webtier/app:1.8.3: # unauthorized: authentication required # fix: create the pull secret for the new Harbor robot account kubectl create secret docker-registry harbor-vks –docker-server=harbor.vks.corp.local –docker-username=robot$webtier –docker-password="$HARBOR_TOKEN" -n webtier kubectl patch serviceaccount ci-deployer -n webtier -p ‘{"imagePullSecrets":[{"name":"harbor-vks"}]}’ # serviceaccount/ci-deployer patched

Verification, rollback and common failures

Green looks like this. Every app Secret and ConfigMap present in the target namespace with matching keys, the SealedSecrets controller materialising each sealed object with no decrypt error in its log, a pipeline run that authenticates with its ServiceAccount token and pushes to the new Harbor, and pods reporting Running rather than ImagePullBackOff. Prove the secret restore with kubectl get secret db-credentials -n webtier -o jsonpath='{.data}' and confirm the key count matches the source. Prove the pipeline identity by running one job end to end and watching it deploy without a human login anywhere in the logs.

Rollback is first class in a migration. Because you deleted nothing on TKGI, backing out is a pipeline and DNS decision, not a data recovery. Point the pipeline context back at the TKGI cluster, revert the image reference to the old Harbor, and if you had shifted traffic move DNS back. Nothing you did on VKS touched the source, which is the whole reason we migrate beside the old platform rather than in place.

SymptomCauseFix
Restore PartiallyFailed, skip warning on default-tokenStale source service account tokenBenign, confirm the app secret restored
no key could decrypt secret in controller logSealedSecret sealed with the source keyRe-seal with kubeseal –fetch-cert on VKS
You must be logged in to the server, UnauthorizedCached vsphere JWT expired near 10 hoursUse a bound ServiceAccount token
ImagePullBackOff, unauthorizedPull secret points at the old HarborRecreate docker-registry secret for new Harbor
CreateContainerConfigError, secret not foundConfigMap or Secret name changed on movekubectl get secret, fix the manifest reference
ESO SecretStore Invalid, auth failedSecretStore still points at the old Vault pathUpdate the SecretStore host and role on VKS

Field notes and a recommendation for pipeline migration

On the production cutover a Jenkins agent had cached an admin kubeconfig from my own kubectl vsphere login. Deploys ran clean all afternoon across all three namespaces. At 2am the nightly job failed, every stage, with You must be logged in to the server. I burned 40 minutes chasing RBAC and network before the timestamp on the token gave it away: it had expired at the ten hour mark, quietly, mid pipeline. Swapping to a bound ServiceAccount token ended it, and I have not shipped a pipeline on a human login since.

For pipeline identity, pick a dedicated ServiceAccount with a bound token, or a projected token if your CI platform speaks OIDC to the Supervisor. Avoid reusing a human kubectl vsphere login for anything automated. For secrets, if you already run a secret manager, pick ESO and repoint the SecretStore. If you run SealedSecrets, re-seal against the VKS controller and avoid dragging the old key across. Copying sealed blobs and hoping is the option to avoid, because it fails silently until a pod cannot start.

Clean result checklist: App Secrets and ConfigMaps present on VKS with matching keys.
Every SealedSecret re-sealed, controller log clean of decrypt errors.
Pipeline authenticates with a ServiceAccount token, no human JWT in any runner.
Image references and pull secrets point at the new Harbor.
A full pipeline run deploys to VKS and pods report Running.

Two questions come up every time. Do I need to migrate service account tokens? No. Kubernetes issues new ones on VKS, and copying the old ones only causes the skip warnings you saw. Recreate any token a tool depends on. Can I keep the old Harbor during transition? For a short window yes, if VKS can reach it and a pull secret exists on the target, but cut to the new registry before you decommission TKGI, or you will forget it is still load bearing.

For deeper background, the Velero migration toolchain part covers the backup mechanics this part builds on, the TKGI to VKS guide holds the full map, and the series hub links the related VKS and VCF 9 work.

On Monday, list the SealedSecrets in one source namespace with kubectl get sealedsecrets and re-seal them against your VKS controller. That single dry run tells you whether your GitOps repo is ready for the cutover.

TKGI to VKS Series · Part 21 of 26
« Previous: Part 20  |  Guide  |  Next: Part 22 »

References

Broadcom TechDocs, Understanding Authorization in vSphere Supervisor
Velero docs, Resource filtering for backup and restore
Pinniped docs, Using Pinniped for CI/CD cluster operations

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading