, ,

VKS Package Repositories, Registry Secrets and Private Registries (VCAP-VKS Exam Series, Part 23)

Objective 4.8 hinges on knowing which of four separate authentication and trust paths a failed image pull belongs to. This Part installs the standard package repository, places registry secrets where kapp controller can actually read them, and relocates the whole repository into an air-gapped Harbor.

VCAP-VKS Exam Series · Part 23 of 34

A candidate on a coaching call last month asked me why his Prometheus package kept failing with an authentication error while his application pods were pulling images from that same Harbor without a complaint. He had created one secret, put it in the namespace where his application ran, and assumed that covered the cluster. It covered exactly one of four separate authentication and trust paths, and objective 4.8 expects you to keep all four apart in your head.

Key takeaways: Objective 4.8, published wording Manage package repositories, standard packages, registry secrets, and private registries. A package repository belongs in tkg-system, because a PackageRepository added there publishes Package and PackageMetadata objects into every namespace on the cluster instead of one. A registry secret used by kapp-controller also belongs in tkg-system and must carry a matching SecretExport, or the fetch fails with an authorization error while your pods keep pulling happily. Node level trust for a private registry certificate is a third thing entirely, set at cluster provisioning time through the trust variable, not by any secret you create afterwards. Headline command: vcf package repository add standard-package-repo –url projects.packages.broadcom.com/vsphere/supervisor/packages/2025.8.19/vks-standard-packages:v2025.8.19 -n tkg-system.
Who this is for: A candidate who installed a package repository in Part 22 as a means of getting the autoscaler onto a cluster, and now has to treat repositories, packages and registries as objects in their own right. This Part covers Objective 4.8. Terms defined on first use: VKS is vSphere Kubernetes Service, the product formerly named TKG Service or TKGS, and because the rename is unfinished on VCF 9.0 you will keep seeing tkg in namespace names, CRD groups and documentation URLs; Carvel is the open source packaging toolchain VKS uses, with imgpkg for moving image bundles and kapp-controller for reconciling them; a PackageRepository is a bundle of package definitions; a PackageInstall is one package reconciled onto a cluster; a SecretExport is the Carvel object that makes a secret visible to other namespaces; imagePullSecrets is the Kubernetes pod field naming credentials for a registry.

Two package mechanisms living in one release

Start with the thing that will waste your study time if nobody warns you. Broadcom now documents two ways to get a standard package onto a VKS cluster, and only one of them exists on the estate the exam is built against. VKS 3.5 introduced an add-on management layer with AddonRepository and AddonRepositoryInstall objects, an embedded repository shipped on every cluster, and an override annotation for pointing at your own copy. VCF 9.0 shipped VKS 3.3, which has none of those CRDs. If you search for how to install a package today you will land on the 3.5 pages first, follow them, and get a CRD not found error on a perfectly healthy cluster.

Popular study advice says read the newest documentation because it is the most correct. For this objective that advice is wrong, and it is wrong in a way that costs you an item. Exam content is based on VCF 9.0, so the mechanism you need in your fingers is the Carvel one: a PackageRepository object in tkg-system, PackageInstall objects reconciled by kapp-controller, and secrets managed by the secretgen controller. Learn the 3.5 add-on model afterwards, as an operator, not as a candidate.

# Versions everything below was run against. VCF 9.0, vCenter 9.0.0.0 build 24755230 Supervisor version v1.31.4+vmware.wcp.2 VKS 3.3.1, ClusterClass builtin-generic-v3.1.0 kubectl v1.32.2, kubectl-vsphere plugin 9.0.0 VCF CLI v9.0.0, imgpkg v0.44.0 Harbor Supervisor Service at harbor.lab.local, from Part 19 Target cluster vks-lifecycle-01 in ns-platform-a, VKr v1.32.0 # Credentials come from the shell, never from a file in git. export HARBOR_USER=svc-vks-puller read -s HARBOR_PASS && export HARBOR_PASS kubectl vsphere login –server 10.50.10.20 –tanzu-kubernetes-cluster-name vks-lifecycle-01 –tanzu-kubernetes-cluster-namespace ns-platform-a –vsphere-username $VSPHERE_USER # Preflight 1. kapp-controller has to be up or nothing below reconciles. kubectl get pods -n tkg-system -l app=kapp-controller NAME READY STATUS RESTARTS AGE kapp-controller-6d9c4b8f7c-2xk9n 2/2 Running 0 9d # Preflight 2. Prove which mechanism this cluster actually has. kubectl api-resources –api-group=addons.kubernetes.vmware.com error: unable to retrieve the complete list of server APIs: no matches for group addons.kubernetes.vmware.com kubectl api-resources –api-group=packaging.carvel.dev NAME SHORTNAMES APIVERSION NAMESPACED KIND packageinstalls pkgi packaging.carvel.dev/v1alpha1 true PackageInstall packagerepositories pkgr packaging.carvel.dev/v1alpha1 true PackageRepository

That second preflight is worth thirty seconds on any cluster you are handed. It answers the question of which documentation set applies before you type anything destructive, and it is the single fastest way to tell a 3.3 estate from a 3.5 one without asking anybody.

Standard package repository install and package discovery

Namespace choice is the whole procedure. Adding a PackageRepository to tkg-system makes every Package and PackageMetadata object it carries visible from every namespace on the cluster, without copies. Add the same repository to a per application namespace and those definitions are scoped to that namespace alone, so a second team creating a PackageInstall elsewhere sees nothing available and files a ticket. I have watched that happen twice, both times because somebody followed a blog that used a namespace called packages.

# Step 1. Add the standard package repository in tkg-system. vcf package repository add standard-package-repo –url projects.packages.broadcom.com/vsphere/supervisor/packages/2025.8.19/vks-standard-packages:v2025.8.19 -n tkg-system Adding package repository 'standard-package-repo' Validating provided settings for the package repository Creating package repository resource Waiting for package repository reconciliation Updating package repository 'standard-package-repo' Added package repository 'standard-package-repo' in namespace 'tkg-system' # Same object, declarative form, for the times you want it in git. cat <<EOF | kubectl apply -f – apiVersion: packaging.carvel.dev/v1alpha1 kind: PackageRepository metadata: name: standard-package-repo namespace: tkg-system spec: fetch: imgpkgBundle: image: projects.packages.broadcom.com/vsphere/supervisor/packages/2025.8.19/vks-standard-packages:v2025.8.19 EOF # Step 2. Verification. Green means Reconcile succeeded, nothing softer. kubectl get pkgr -n tkg-system NAME AGE DESCRIPTION standard-package-repo 94s Reconcile succeeded # Step 3. Discovery. Package names are cluster wide once the repo is in tkg-system. vcf package available list -n tkg-system NAME DISPLAY-NAME cert-manager.kubernetes.vmware.com cert-manager cluster-autoscaler.kubernetes.vmware.com cluster-autoscaler contour.kubernetes.vmware.com contour external-dns.kubernetes.vmware.com external-dns fluent-bit.kubernetes.vmware.com fluent-bit harbor.kubernetes.vmware.com harbor istio.kubernetes.vmware.com istio prometheus.kubernetes.vmware.com prometheus velero.kubernetes.vmware.com velero # Prove the cluster wide claim from a different namespace. kubectl get packagemetadata -n default | wc -l 15

That last check is the one I would run in front of an examiner. Fourteen package metadata rows plus a header, read from a namespace where nothing was ever installed, is the observable proof that tkg-system was the right home. Run the same command after adding the repository to a private namespace and you get one line, the header, and no explanation.

Registry secrets and their export boundary

Here is where the candidate on that call went wrong, and where most experienced admins go wrong, because Kubernetes has trained us that an image pull secret is a namespace scoped thing you attach to a pod. That is true for your workloads. It is not true for packages. kapp-controller does the fetching for a PackageInstall, it runs in tkg-system, and it reads credentials through the secretgen controller. A secret sitting in your application namespace is invisible to it.

Two objects are needed, not one. A dockerconfigjson secret in tkg-system, and a SecretExport in tkg-system whose name matches the secret exactly and whose toNamespaces list covers where the package will land. Miss the SecretExport and the secret exists, kubectl shows it happily, and the fetch still fails. That gap between a resource existing and a resource being usable is exactly the shape of trap this objective likes.

# Step 4. Registry credential for kapp-controller. Password from the environment. kubectl create secret docker-registry regcred -n tkg-system –docker-server=harbor.lab.local –docker-username=$HARBOR_USER –docker-password=$HARBOR_PASS secret/regcred created # Step 5. Export it. Name and namespace must match the source secret exactly. cat <<EOF | kubectl apply -f – apiVersion: secretgen.carvel.dev/v1alpha1 kind: SecretExport metadata: name: regcred namespace: tkg-system spec: toNamespaces: – '*' EOF secretexport.secretgen.carvel.dev/regcred created # Verification. A generated fetch secret is the signal that the export landed. kubectl get secretexport -n tkg-system NAME AGE regcred 11s kubectl get secret -n tkg-system | grep fetch standard-package-repo-fetch-0 kubernetes.io/dockerconfigjson 1 9s # REAL FAILURE, reproduced by deleting the SecretExport and leaving the secret. kubectl delete secretexport regcred -n tkg-system kubectl get pkgr standard-package-repo -n tkg-system -o jsonpath='{.status.usefulErrorMessage}' ImagePullBackOff: fetching image: GET https://harbor.lab.local/v2/standard-packages/manifests/v2025.8.19: UNAUTHORIZED: unauthorized to access repository: standard-packages, action: pull

Read that error line carefully, because it is the fork in the road. UNAUTHORIZED means credentials, so you are in secret and SecretExport territory. An x509 error mentioning an unknown authority means certificate trust, which no secret in tkg-system will ever fix. Candidates who conflate those two spend twenty minutes recreating credentials for a problem that lives on the node.

Private registry trust baked into a cluster spec

Node level trust is provisioning time work. Every worker and control plane node in a VKS cluster runs containerd, and containerd will refuse a registry whose certificate chain it does not recognise, no matter what credentials you hand it. On the Cluster v1beta1 API the mechanism is the trust variable, populated with the name of a secret that carries the registry certificate authority. Change it on a running cluster and the machines roll, which is a rolling update in the Part 21 sense, with all the disruption budget considerations that came with it.

One exemption is worth memorising. When Harbor runs as a Supervisor Service, as it does on this lab estate from Part 19, the platform already arranges trust for it, so the additional certificate step is only needed for a registry that is not the platform registry, or for a certificate that no public authority signed. Broadcom states that plainly in the add-on repository procedure, and a scenario item that mentions the Harbor Supervisor Service is very likely testing whether you know to skip a step rather than add one.

# Step 6. Certificate authority secret, created in the SUPERVISOR namespace, # not in the workload cluster. This is the part people get backwards. kubectl config use-context ns-platform-a kubectl create secret generic vks-lifecycle-01-user-trusted-ca-secret -n ns-platform-a –from-file=harborca.crt=/tmp/harbor-ca.crt secret/vks-lifecycle-01-user-trusted-ca-secret created # Step 7. Reference it from the cluster topology variables. variables: – name: trust value: additionalTrustedCAs: – name: harborca.crt kubectl apply -f vks-lifecycle-01.yaml cluster.cluster.x-k8s.io/vks-lifecycle-01 configured # Verification on a node, once the roll completes. kubectl get machine -n ns-platform-a -l cluster.x-k8s.io/cluster-name=vks-lifecycle-01 NAME CLUSTER PHASE VERSION vks-lifecycle-01-cp-9r4tz vks-lifecycle-01 Running v1.32.0 vks-lifecycle-01-np-1-6b8f4-h2knd vks-lifecycle-01 Running v1.32.0 # REAL FAILURE, the file key and the variable name disagreeing. # Secret key was harbor-ca.crt, variable said harborca.crt. kubectl describe cluster vks-lifecycle-01 -n ns-platform-a | grep -A2 Warning Warning TopologyReconcileFailed variable trust: additionalTrustedCAs entry harborca.crt not found in secret vks-lifecycle-01-user-trusted-ca-secret

Name discipline saves you here. Secret name follows the cluster name, the key inside the secret is what the variable references, and the two are not the same string. I now write the key first, paste it into the variable, and only then create the secret, purely because I got this backwards once and spent a machine roll finding out.

flowchart TD
  A[Image pull or package fetch fails] --> B{Which component is pulling}
  B -->|Application pod| C[Check imagePullSecrets on pod or service account]
  B -->|kapp controller| D[Check secret in tkg system plus SecretExport]
  B -->|Node containerd| E[Check trust variable and trusted CA secret]
  B -->|vSphere Pod on Supervisor| F[Check Supervisor container registry entry]
  C --> G{Error text}
  D --> G
  E --> G
  F --> G
  G -->|UNAUTHORIZED| H[Credential problem, rebuild secret and export]
  G -->|x509 unknown authority| I[Trust problem, fix CA and roll the cluster]
Four authentication and trust paths, and the two error strings that tell them apart.

Registry secret placement lookup

This table is the artifact from this Part worth pinning above your desk. Four consumers, four homes, four different failures. Nothing else in objective 4.8 generates as many wrong answers as putting the right object in the wrong place.

ConsumerObject and where it livesFailure if missing
Application poddockerconfigjson secret in the workload namespace, named in imagePullSecrets or on the service accountImagePullBackOff on that pod only
kapp-controller, packages and repositoriesdockerconfigjson secret in tkg-system plus a matching SecretExportPackageRepository stuck, UNAUTHORIZED in usefulErrorMessage, workloads unaffected
containerd on cluster nodesCA in a secret in the Supervisor namespace, referenced by the cluster trust variablex509 certificate signed by unknown authority, every pull from that registry, cluster wide
vSphere Pods on the SupervisorContainer Registries entry under Supervisor Configure in the vSphere ClientSupervisor Pod never starts, VKS clusters keep working, which hides the cause

Air-gapped relocation with imgpkg

Part 9 built the local content library for the air-gapped Kubernetes release exercise. Packages need their own relocation, because a content library moves node images and has nothing to say about Carvel bundles. imgpkg copy handles both halves, a pull to a tarball on a machine that has internet access, and a push into your registry from a machine that does not.

# Step 8. On the connected jump host, pull the whole bundle to a tar. imgpkg copy -b projects.packages.broadcom.com/vsphere/supervisor/packages/2025.8.19/vks-standard-packages:v2025.8.19 –to-tar ./standard-packages.tar copy | exporting 138 images… copy | will export projects.packages.broadcom.com/vsphere/supervisor/packages/2025.8.19/vks-standard-packages copy | exported 138 images copy | writing layers… Succeeded ls -lh standard-packages.tar -rw-r–r– 1 pranay pranay 11G Aug 14 09:41 standard-packages.tar # Step 9. On the isolated side, push into Harbor. imgpkg copy –tar standard-packages.tar –to-repo harbor.lab.local/standard-packages/vks-standard-packages –registry-ca-cert-path /tmp/harbor-ca.crt Succeeded # Step 10. Repoint the repository. Nothing else in the object changes. kubectl -n tkg-system patch pkgr standard-package-repo –type merge -p '{"spec":{"fetch":{"imgpkgBundle":{"image":"harbor.lab.local/standard-packages/vks-standard-packages:v2025.8.19"}}}}' packagerepository.packaging.carvel.dev/standard-package-repo patched kubectl get pkgr -n tkg-system NAME AGE DESCRIPTION standard-package-repo 41m Reconcile succeeded # ROLLBACK. Patch the image back to the public URL and the repo reconciles again # within one sync cycle. No PackageInstall is disturbed, because installed # packages keep the digests they already resolved. kubectl -n tkg-system patch pkgr standard-package-repo –type merge -p '{"spec":{"fetch":{"imgpkgBundle":{"image":"projects.packages.broadcom.com/vsphere/supervisor/packages/2025.8.19/vks-standard-packages:v2025.8.19"}}}}'

Budget the time honestly. On a 1 Gb link into the lab, the four stages below took 63 minutes end to end, and the tarball landed at 11 GB for 138 images. Nobody in a change window expects a package repository move to take an hour, so tell them before you start rather than after.

Air-gapped repository relocation, measured Minutes per stage, 11 GB bundle, 138 images, 1 Gb link, VKS 3.3.1 imgpkg pull to tar 26 min imgpkg push to Harbor 31 min Registry trust and secret 2 min Repository reconcile 4 min Total 63 minutes Transfer is 90 percent of it. Configuration is 6 minutes of the hour.

Failures I hit, with their error lines

Error you seeCauseFix
UNAUTHORIZED: unauthorized to access repositorySecret present in tkg-system but no SecretExport, or the export name does not match the secretCreate the SecretExport with the identical name and namespace, then confirm a fetch secret appears
x509: certificate signed by unknown authorityRegistry certificate authority not trusted by containerd on the nodesAdd the CA secret in the Supervisor namespace, reference it from the trust variable, accept the machine roll
no matches for group addons.kubernetes.vmware.comFollowing VKS 3.5 add-on documentation on a VKS 3.3 clusterSwitch to the Carvel PackageRepository procedure, or upgrade VKS first and know that you did
No package metadata visible from a second namespaceRepository added to an application namespace instead of tkg-systemDelete and re-add with -n tkg-system, PackageInstalls elsewhere then resolve
variable trust: entry not found in secretKey inside the CA secret does not match the additionalTrustedCAs nameRecreate the secret with the key spelled exactly as the variable, no cluster rebuild needed
Package reconcile fails only after a VKr changePackage minor version bound to the Kubernetes minor version, per Part 22Repin the PackageInstall version to match the new release, then unpause
Clean end state: pkgr in tkg-system reporting Reconcile succeeded; fourteen package metadata entries readable from a namespace you never touched; a regcred secret and a matching SecretExport in tkg-system; a generated fetch secret present; the cluster trust variable naming a key that exists inside the CA secret; and one test PackageInstall reconciled from the private registry rather than the public one.

Exam focus for objective 4.8

EXAM FOCUS, objective 4.8: What this objective expects you to be able to do is place four different objects in four different namespaces and predict which failure each omission produces. Expect it in matching and drag and drop items pairing an error string to a remediation, in build list items ordering an air-gapped relocation, and in scenario multiple choice where a package fails while workloads succeed. Trap that catches experienced admins: assuming a working application image pull proves the cluster can fetch packages. It proves nothing about kapp-controller, which reads from tkg-system through a SecretExport your workload never needed. Second trap, smaller but sharper: node certificate trust is a provisioning time variable that rolls machines, so any answer implying you can fix an x509 error with a secret alone is wrong.
Objective checkpoint:

1. A PackageRepository in tkg-system reports UNAUTHORIZED while pods in the same cluster pull images from that registry without error. What is the most likely omission? Answer: a SecretExport for the registry secret in tkg-system. Pods use their own namespace secret; kapp-controller only sees credentials that secretgen has exported to it.

2. An operator adds the standard package repository to a namespace called apps and a colleague reports that no packages are available in namespace batch. Which single change fixes it for every namespace? Answer: add the repository in tkg-system instead. Package and PackageMetadata objects created there are visible cluster wide without duplication.

3. After pointing a repository at a self signed private registry, every node reports x509 certificate signed by unknown authority. Which action resolves it, and what is the operational consequence? Answer: add the certificate authority to the cluster trust variable, which triggers a rolling replacement of the nodes. No secret created inside the workload cluster changes what containerd trusts.

Repository and registry choices I would ship

War story, and it cost me ninety minutes I did not have. A customer cluster could not install cert-manager from their own Harbor. Credentials were right, I could docker login by hand from the jump host with the same account, and the CA was already trusted because Harbor was the platform registry. Every check passed and the fetch kept failing. What I had missed was that the Harbor project holding the relocated bundle had been created as private and the service account we were using had pull rights on a different project entirely. My login worked because my human account was a project admin somewhere else. Ninety minutes of Kubernetes debugging for a permission set two clicks deep in a registry UI. Since then the first thing I do on a package failure is pull the exact same reference with imgpkg, from a shell, using the service account credentials rather than mine.

Verdict for this estate. Put the standard package repository in tkg-system and nowhere else, and treat any suggestion of a per team repository namespace as a mistake to be argued out of the design. Create the registry secret and the SecretExport together in one manifest so the pair cannot drift, and export to all namespaces rather than a list, because a list is a future outage every time somebody adds a namespace. Relocate the bundle into Harbor even when you are not air-gapped, because the pull is faster, the versions stop moving under you, and you get one place to audit what your clusters are actually running. Avoid the pattern of trusting a private registry by editing nodes after provisioning; if the trust is not in the cluster spec, it does not survive a machine roll.

Tonight, in your own lab, do this. Create a registry secret in tkg-system without a SecretExport, add a repository pointing at a private Harbor project, and read the usefulErrorMessage field. Then add the export and watch a fetch secret appear. That single loop teaches the boundary better than any diagram. Objective 4.8 also leans on the Harbor work from the Harbor series and on the cluster mechanics in the VKS series, neither of which this Part repeats. Part 24 moves to snapshots inside VKS clusters. Full path is indexed on the VCAP-VKS exam guide pillar.

VCAP-VKS Exam Series · Part 23 of 34
« Previous: Part 22  |  Guide  |  Next: Part 24 »

References

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