, ,

VKS Registry and Trusted CA Errors in Container Deployment (VCAP-VKS Exam Series, Part 32)

Objective 5.3 as a runbook: separating x509 trust failures from registry authentication failures on a VKS cluster, the double encoded secret the Supervisor expects, and the version window where the fix applies but never reaches containerd.

VCAP-VKS Exam Series · Part 32 of 34
Key takeaways: objective 5.3 in the published guide reads, verbatim, Troubleshoot container deployment, registry, and trusted CA errors. Three surfaces carry an image pull and each one fails with different text: the node trust store validated by containerd, the credentials in a dockerconfigjson secret inside the workload cluster, and the registry decision about the project. Trust is repaired on the Supervisor, authentication is repaired in the workload cluster, and mixing the two is the single most expensive habit on this objective. The certificate the Supervisor expects is base64 encoded twice, not once, and the secret is inert until the cluster trust variable names its data key. Headline moves are kubectl describe pod for the error text, base64 -w 0 ca.crt | base64 -w 0 for the payload, and a forced MachineDeployment rollout, because on VKS 3.3.0 through 3.3.2 the trust change is applied and never reaches containerd. Across 24 induced pull failures on the reference estate, 9 lived in the node trust store and only 5 were actually the registry.
Who this is for: a platform engineer or vSphere admin who worked through Part 31 and now has clusters that provision cleanly. Objective 5.3 starts where cluster health stops mattering. Your Cluster object is Ready, every machine is Running, and a workload still will not start. The estate is unchanged from Part 31: VCF 9.0, one workload domain, three vSphere Zones, NSX VPC networking, Avi in the load balancing path, and the Harbor registry we installed as a Supervisor add on service back in Part 19.

Two passes. That is how many times a CA certificate has to go through base64 before a VKS cluster will accept it, and skipping the second pass is the most common reason a trust fix looks applied and changes nothing at all. Objective 5.3 lives almost entirely in the gap between applied and effective.

Part 31 left the estate with clusters that build. VM classes are bound, the cluster plan renders, the content library serves releases. This Part starts one layer above that, with a cluster that is completely healthy and a deployment that will not come up, because its image sits in a Harbor project the nodes have never been told to trust. Nothing in the cluster status will tell you that. VKS is the product formerly named TKG Service, and the naming still shows: the secret these controllers look for is named after the cluster with a user-trusted-ca-secret suffix, and plenty of the surrounding resources still carry tkg. That is current VCF 9.0 output, not legacy output.

Three trust surfaces and where a pull actually breaks

An image pull on a VKS cluster crosses three surfaces, and each one is owned by a different object in a different cluster. Kubelet asks containerd, the container runtime on the node, to fetch the image. Containerd opens a TLS session to the registry and validates the server certificate against the node operating system trust store. If validation passes, containerd presents credentials taken from a Kubernetes secret of type dockerconfigjson that lives in the workload cluster, in the same namespace as the pod. Only after both of those does the registry decide whether that account may pull from the project.

Trust and authentication fail with different error text and are repaired in different clusters. That sentence is most of the objective. A trust failure names x509 and never reaches the registry logic at all. An authentication failure produces an HTTP status, usually 401 or 403, which means the TLS session already succeeded and your CA work is done. Reading the first line of the kubelet event tells you which cluster to open, and reading it wrong sends you to the wrong kubeconfig for the next hour.

On VKS the node trust store is not something you edit. It is populated during node bootstrap from a Kubernetes secret held in the vSphere Namespace on the Supervisor, and the Cluster object only consumes that secret if its topology names the data key through the trust variable. Two objects, one on each side, and both have to agree on a string you chose yourself.

Tested on, and every command below assumes: VCF 9.0, single vCenter, one workload domain, three vSphere Zones Supervisor enabled on the workload domain, NSX VPC networking, Avi in the load balancing path VKS 3.3.2 first and VKS 3.3.3 second, both on purpose, because the difference decides step 5 VKr v1.32 node images, kubectl v1.32, kubectl vsphere plugin shipped with the Supervisor Harbor running as a Supervisor add on service, one private project, self signed CA $ kubectl config use-context app-prod $ kubectl get pods -n shop NAME READY STATUS RESTARTS AGE shop-web-6c8f7d9b4d-2xk9v 0/1 ImagePullBackOff 0 4m12s shop-web-6c8f7d9b4d-nn4rt 0/1 ImagePullBackOff 0 4m12s
Where 24 induced pull failures actually lived Series lab estate, VCF 9.0, Harbor as a Supervisor add on service, faults injected one at a time 0 3 6 9 9 7 5 3 Node trust store Supervisor secret Pull credentials Workload cluster Registry decision Harbor project Name resolution DNS or routing Two thirds of the sample was repaired outside the workload cluster, which is where most people start looking.
Scope note: general Kubernetes pull semantics and Harbor project administration are owned elsewhere in the catalogue and get one clause each here. For Harbor itself, read the Harbor complete guide. For the VKS product walkthrough underneath this exam series, read the vSphere Kubernetes Service complete guide. Everything below is the exam facing part.

Numbered repair procedure for a failing pull

Preflight is short. You need the registry CA certificate in PEM form, cluster edit rights in the vSphere Namespace on the Supervisor, and a maintenance window, because the correct fix rolls nodes. If you cannot roll nodes today, stop at step 4 and schedule the rest, because a half applied trust change is indistinguishable from no change.

Step 1, separate a trust failure from an authentication failure

Read the kubelet event before anything else. Two outputs, two different repairs, and they are visible in the first clause of the message.

$ kubectl describe pod shop-web-6c8f7d9b4d-2xk9v -n shop | tail -5 Events: Type Reason Age From Message Warning Failed 38s (x4 over 4m) kubelet Failed to pull image "harbor.lab.local/shop/web:1.4.2": failed to resolve reference: failed to do request: Head "https://harbor.lab.local/v2/shop/web/manifests/1.4.2": tls: failed to verify certificate: x509: certificate signed by unknown authority Warning Failed 38s (x4 over 4m) kubelet Error: ErrImagePull # the other shape, same pod phase, completely different repair Warning Failed 12s kubelet Failed to pull image "harbor.lab.local/shop/web:1.4.2": unexpected status from HEAD request to https://harbor.lab.local/v2/shop/web/manifests/1.4.2: 401 Unauthorized # and the chain the node will actually see, run from any host on the same segment $ openssl s_client -connect harbor.lab.local:443 -showcerts </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates subject=CN = harbor.lab.local issuer=C = IN, O = Lab, CN = lab-root-ca notBefore=Feb 11 09:22:41 2026 GMT notAfter=Feb 11 09:22:41 2027 GMT

An x509 line means go to the Supervisor. A 401 means stay in the workload cluster. If openssl reports an issuer you do not recognise, someone reissued the Harbor certificate and the nodes are still carrying the old root, which presents as x509 on a cluster that worked yesterday.

Step 2, build the double encoded secret in the vSphere Namespace

Kubernetes already base64 encodes the value of a data field. VKS expects the value of that field to itself be a base64 encoding of the PEM. Hence two passes. A single pass produces a secret that applies without complaint and decodes on the node into garbage.

$ base64 -w 0 ca.crt | base64 -w 0 > ca.double.b64 $ wc -c ca.double.b64 2916 ca.double.b64 # trusted-ca-secret.yaml apiVersion: v1 kind: Secret metadata: name: app-prod-user-trusted-ca-secret namespace: ns-app-prod type: Opaque data: harbor-ca: TFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PQ== $ kubectl config use-context supervisor.lab.local $ kubectl apply -f trusted-ca-secret.yaml -n ns-app-prod secret/app-prod-user-trusted-ca-secret created

Three strings matter and all three are yours to choose badly. Namespace must be the vSphere Namespace where the cluster lives. Secret name is free text but the controllers generate a default of cluster name plus user-trusted-ca-secret, and matching that convention saves you from the retirement fault described later. Data key, harbor-ca here, is the string the Cluster object will name in step 3.

Step 3, name the data key in the cluster trust variable

Applying a secret changes nothing on its own. Cluster topology has to reference it, and it references the data key, not the secret name. That indirection catches people who correctly assume the opposite.

# trust-var.yaml, merged into the existing Cluster spec: topology: variables: – name: trust value: additionalTrustedCAs: – name: harbor-ca $ kubectl patch cluster app-prod -n ns-app-prod –type merge –patch-file trust-var.yaml cluster.cluster.x-k8s.io/app-prod patched $ kubectl get cluster app-prod -n ns-app-prod -o jsonpath='{.spec.topology.variables[?(@.name=="trust")]}’ {"name":"trust","value":{"additionalTrustedCAs":[{"name":"harbor-ca"}]}}

Step 4, force the value onto the nodes

Here is the real failure, and it is the reason this Part exists. On VKS 3.3.0 through 3.3.2, updating certain Cluster variables or the Harbor Supervisor Service certificate does not trigger an automatic rolling update, and a related defect in the same window leaves containerd unrestarted after CA certificates are installed. Broadcom fixed both in VKS 3.3.3. Until you are on 3.3.3, the cluster reports a clean patch and the nodes never learn anything.

# the failure: patch accepted, machine ages unchanged, pulls still x509 $ kubectl get machines -n ns-app-prod NAME CLUSTER PHASE AGE VERSION app-prod-9jd7q-gmmtk app-prod Running 19d v1.32.0 app-prod-np1-kd88h-6b7c9c-4rt2n app-prod Running 19d v1.32.0 app-prod-np1-kd88h-6b7c9c-9wq5z app-prod Running 19d v1.32.0 # force a rollout by touching the machine template metadata $ kubectl patch machinedeployment app-prod-np1 -n ns-app-prod –type merge -p ‘{"spec":{"template":{"metadata":{"annotations":{"trust.rollout/ts":"2026-08-22T06:40:00Z"}}}}}’ machinedeployment.cluster.x-k8s.io/app-prod-np1 patched $ kubectl get machines -n ns-app-prod -w app-prod-np1-kd88h-77f4c8-hb2mn app-prod Provisioning 12s v1.32.0 app-prod-np1-kd88h-77f4c8-hb2mn app-prod Running 6m41s v1.32.0
Field note: I lost most of an afternoon to this on a 3.3.2 cluster, roughly three hours, because I trusted my own verification. Secret present, data key correct, trust variable readable from jsonpath, and every pull still x509. I re encoded the certificate twice, rebuilt the secret, and started drafting a support case before I noticed the machine ages had not moved since the previous week. One annotation patch and the replacement node was Running in 6 minutes and 41 seconds, pulling normally. The lesson I took into every cluster afterwards is that on this objective the last verification step is never the Cluster object, it is the age column on the machines.

Step 5, repair authentication inside the workload cluster

A 401 needs nothing from the Supervisor. Create the credential where the pod runs and attach it to the identity the pod uses. Read the robot account password from an environment variable so it never lands in shell history or a manifest.

$ kubectl config use-context app-prod $ kubectl create secret docker-registry harbor-pull –namespace shop –docker-server=harbor.lab.local –docker-username=’robot$shop+puller’ –docker-password="$HARBOR_ROBOT_PW" secret/harbor-pull created $ kubectl patch serviceaccount default -n shop -p ‘{"imagePullSecrets":[{"name":"harbor-pull"}]}’ serviceaccount/default patched

Verification, back out and a pull error lookup

Green looks like a pod that goes Running without a restart, on a node whose age is younger than your trust change. Delete the stuck pods rather than waiting out the backoff timer, which grows to five minutes and will make a working fix look broken.

$ kubectl delete pod -n shop -l app=shop-web $ kubectl get pods -n shop -o wide NAME READY STATUS RESTARTS AGE NODE shop-web-6c8f7d9b4d-8kk4d 1/1 Running 0 22s app-prod-np1-kd88h-77f4c8-hb2mn shop-web-6c8f7d9b4d-t9pm2 1/1 Running 0 22s app-prod-np1-kd88h-77f4c8-x9rc4 # back out: remove the variable, keep the secret until every node has been replaced $ kubectl patch cluster app-prod -n ns-app-prod –type json -p ‘[{"op":"remove","path":"/spec/topology/variables/3"}]’ # what deleting the secret too early looks like on the next machine message: ‘failed to resolve file source: secret not found: ns-app-prod/app-prod-user-trusted-ca-secret’ reason: DataSecretGenerationFailed type: BootstrapReady status: "False"

That last block is worth memorising. Broadcom documents the same DataSecretGenerationFailed condition as a known issue in the 3.3.0 through 3.3.2 window, where additional trusted CAs configured against a TanzuKubernetesCluster are lost once that object is retired and replaced by a Cluster resource. Bootstrap then fails on every new machine, so the symptom appears at the next scale up or upgrade rather than at the moment of breakage. If you inherit an estate with retired TanzuKubernetesCluster objects, check for the secret before you scale anything.

flowchart TD
  A[Pod stuck on ImagePullBackOff] --> B{Message names x509 or a 401}
  B -->|401| C[Create dockerconfigjson secret in the workload cluster]
  C --> D[Attach it to the pod service account]
  B -->|x509| E{CA secret exists in the vSphere Namespace}
  E -->|No| F[Encode the PEM twice and apply on the Supervisor]
  E -->|Yes| G{Trust variable names the data key}
  G -->|No| H[Patch topology variables and plan a rollout]
  G -->|Yes| I{Machine age newer than the trust change}
  I -->|No| J[Force a MachineDeployment rollout, 3.3.2 will not do it]
  I -->|Yes| K[Fault is on the registry, check project and robot role]
Four gates in cost order. Only the first branch stays inside the workload cluster.

Keep the table below. It is the artifact from this Part worth returning to, because every row pairs a literal string you will actually see with the surface that owns the repair.

Error you seeWhat it really meansSurface that fixes it
x509: certificate signed by unknown authorityNode trust store has no CA for the registrySupervisor: secret in the vSphere Namespace, trust variable, node rollout
401 Unauthorized on a HEAD requestTLS already succeeded, no usable credentialWorkload cluster: dockerconfigjson secret and the pod service account
403 or project not foundCredential is valid but has no pull role on that projectHarbor: project members or robot account permissions
CA applied, machine ages unchanged, still x509Variable change did not trigger a rolling update on VKS 3.3.0 to 3.3.2Forced MachineDeployment rollout, then move to VKS 3.3.3
Nodes rolled, CA installed, still x509Containerd not restarted after CA install, same version windowVKS upgrade to 3.3.3 or later, no YAML edit will help
DataSecretGenerationFailed, secret not foundTrusted CA secret lost when a TanzuKubernetesCluster was retiredRecreate the secret with the exact expected name before scaling
x509: certificate has expiredRegistry certificate rotated, nodes still hold the old rootReissue, replace the secret data, roll the nodes again
dial tcp: lookup harbor.lab.local: no such hostRegistry name does not resolve from the node networkCluster DNS configuration or the external-dns add on from Part 19

Now for the advice this Part contradicts. Search almost any forum for the x509 string and the top answer will tell you to connect to the node, drop the certificate into the operating system trust directory, refresh the trust store and restart containerd. That works. It works for about as long as the node lives, and it is the wrong answer here for two reasons. Every replacement machine comes up untrusted, so the fault reappears at the next upgrade, autoscale event or repair, usually to somebody else. Worse, the manual fix hides which of the two defects you were hitting, so you never learn whether the estate needs the VKS upgrade. Declarative repair through the trust variable is slower on the day and it is the only one that survives a node rollout, which is exactly the property the exam is testing for. Reserve node level intervention for proving a hypothesis, never for fixing production.

Exam focus, objective 5.3

Objective 5.3, what it actually tests: whether you can read one line of error text and name both the failing surface and the cluster that owns the repair, and whether you know that a declared trust change only becomes real when a node is replaced. Expect scenario style multiple choice with a kubelet event pasted into the stem, matching items pairing an error string with a remediation, and build list items asking you to order secret creation, variable reference and rollout correctly. Drag and drop suits the encode, apply, reference, roll sequence particularly well. Multiple selection items tend to ask which two artifacts together make a trusted pull work, which is testing the secret and variable pairing rather than recall. The single trap that catches experienced admins: treating one CA fix as covering the whole pull path. Trust does not authenticate and credentials do not establish trust, and an option that adds an imagePullSecret to cure an x509 error is a distractor built for people who have skimmed. Second trap, close behind, is any answer that repairs the node directly. If an option involves connecting to a node, be suspicious of it.

Objective checkpoint

1. A VKS cluster pulls from a public registry without issue, but every pull from the internal Harbor fails with certificate signed by unknown authority. A secret holding the CA already exists in the vSphere Namespace. Which single change most likely completes the fix? (a) re encode the certificate with one base64 pass, (b) add the secret data key to the cluster trust variable and allow a node rollout, (c) add an imagePullSecret to the deployment, (d) mark the registry as insecure.
Answer: b. A secret is inert until the topology names its data key, and the value only reaches containerd through a new node.
2. Pods report ImagePullBackOff and the kubelet event ends with 401 Unauthorized. Node trust already includes the registry CA. Where does the repair live?
Answer: in the workload cluster. A 401 proves the TLS session completed, so the missing artifact is a dockerconfigjson secret referenced by the pod or its service account. No Supervisor object is involved.
3. After adding a trust variable on a cluster running VKS 3.3.2, machines show the same names and ages as before and pulls still fail with x509. Best next action? (a) re encode the certificate, (b) delete and recreate the secret, (c) force a rolling update of the node pool and plan a move to VKS 3.3.3, (d) restart the Cluster API controllers.
Answer: c. Unchanged machine ages are the signature of the known issue where variable updates do not trigger a rollout in that version window.

Trust repair order I use before escalating

My recommendation is narrow and I hold it firmly. Read the error, pick the cluster, then work in one direction only: secret, variable, rollout, verify on a new node. Never repair a node by hand, and never verify a trust fix by looking at the Cluster object, because the Cluster object will happily report a change that never left the Supervisor. If your estate is anywhere in the VKS 3.3.0 to 3.3.2 window, schedule the move to 3.3.3 before you spend another afternoon like the one I described, because two separate defects in that window both present as a fix that did not work.

A clean end state on this objective is small enough to check in a minute: pods Running with zero restarts, a machine age younger than the trust change, the trust variable readable from jsonpath, the CA secret still present in the vSphere Namespace, and an image pull secret attached to the service account only where the registry demands credentials. If all five are true you are done, and if any one is false you already know which surface to open.

Tonight, in your own lab, break it deliberately. Apply the CA secret with a single base64 pass instead of two, watch the pull fail with exactly the same x509 line as a missing secret, and note how little the error told you. That is the discrimination objective 5.3 asks for. Work through the rest of the series from the guide, and if you arrived here from a TKGI estate, the migration path is covered separately in the TKGI to VKS guide.

VCAP-VKS Exam Series · Part 32 of 34
« Previous: Part 31  |  Guide  |  Next: Part 33 »

References

VMware Cloud Foundation VKS Administrator exam guide, 3V0-24.25
VMware vSphere Kubernetes Service 3.3.3 and earlier release notes, fixed and known issues
Configuring a VKS cluster with a self signed container registry, William Lam

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