, ,

Identity, RBAC and Harbor on VKS (TKGI to VKS Series, Part 15)

How to replace TKGI UAA with vCenter SSO, map old LDAP groups to vSphere Namespace roles, grant developers scoped RBAC, and install Harbor as a Supervisor Service on your first VKS cluster.

TKGI to VKS Series · Part 15 of 26

Our dev lead asked one question in the migration standup that stalled the room: with UAA gone, how do my engineers actually log in to the new cluster and push an image. That is what this part wires, the plumbing that turns an empty VKS cluster into one a team can use.

TL;DR · Key Takeaways

  • VKS (vSphere Kubernetes Service) authenticates through vCenter Single Sign-On by default, using the VCF CLI. You do not rebuild UAA, and for most teams you should not federate an external identity provider on day one.
  • vSphere Namespace permissions, Can edit, Can view and Owner, propagate into every cluster in that namespace as Kubernetes RBAC. Set them on the namespace, not per cluster.
  • Owner is the only role that lets you create namespaces with kubectl, and it works for vCenter SSO users only, not for external OIDC users. Federating everything to mimic old LDAP quietly breaks that self-service.
  • Harbor installs as a Supervisor Service from the vSphere Client, needs Contour or an NGINX load balancer, and the Supervisor trusts it automatically as long as the tlsSecretLabels stay set to managed-by vmware-vRegistry.
  • Headline flow: kubectl vsphere login binds the vSphere identity, then a scoped RoleBinding grants each developer only what they need.
Who this is for: platform engineers and Kubernetes operators who stood up the first VKS cluster last part and now need identity, RBAC and a private registry before any workload moves. You have Supervisor admin rights, the VCF CLI and the kubectl vSphere plugin installed, and a TKGI estate still running UAA and LDAP.

Two identity models, and which to standardize on

Last part we stood up dev-tkg-01 in the dev-apps namespace. It runs, but nobody except the platform admin can reach it, and it cannot pull a private image. Fixing that starts with a choice between two authentication models, and the choice matters more than it looks.

On TKGI, logins went through UAA (User Account and Authentication, the OAuth2 identity server that fronted Enterprise PKS) wired to your LDAP directory. VKS drops UAA entirely. By default it authenticates through vCenter Single Sign-On (SSO, the vSphere identity service), which already integrates with Active Directory and LDAP, and you drive it with the VCF CLI. The second model is an external OIDC (OpenID Connect) provider, federated into the Supervisor through Pinniped, the Kubernetes authentication service that runs a Supervisor component plus a Concierge component on each VKS cluster.

Two details about that federation matter in practice. That Concierge authorises requests inside each VKS cluster, so when you register an OIDC provider on the Supervisor, every existing and future cluster picks up the change with no per cluster step. Access changes also settle on the token lifetime rather than instantly, so revoking a group or rotating a role takes effect at the next login or refresh, not the moment you press enter. Plan cutovers and revocations around that lag instead of assuming an immediate switch.

Here is where the tutorial instinct goes wrong. Every quickstart shows you how to register an external OIDC provider, so migrating teams reach for it to recreate the UAA and LDAP experience. On this estate that was a mistake, because one capability silently disappears when a user comes in over OIDC instead of vCenter SSO, and it is the capability platform teams lean on most.

Login time by authentication modelSeconds from command to authenticated context, reference estatesec061218vCenter SSO2AD via SSO3External OIDC15
Figure 1. Measured login times. vCenter SSO returns a context in about 2 seconds; the external OIDC browser round trip took roughly 15 on the same workstation, most of it the interactive redirect.

Speed is the minor point. This table is the one to keep, because it lays out the capability difference that actually drives the decision.

DimensionvCenter SSO (default)External OIDC via Pinniped
How you log inVCF CLI, no browserBrowser OIDC redirect
Backing directoryAD or LDAP through vCenterYour own IdP
Owner role, namespace create by kubectlSupportedNot supported
Best forPlatform and DevOps operatorsDeveloper federation at scale

Table 1. Identity model trade off. The Owner row is the one that reversed my first design.

Contrarian call: do not federate everything to match UAA. vCenter SSO already reads your AD and LDAP, and it is the only path that keeps the Owner role. Bring developers in over OIDC only if you must, and keep platform admins on vCenter SSO.

Preflight before wiring identity and Harbor

Four things must be true before you start. You can authenticate to the Supervisor with vCenter SSO. The dev-apps namespace and its cluster exist and are healthy. You have the VCF CLI and the kubectl vSphere plugin on your workstation. And for Harbor you have vCenter 8.0a or higher, a load balancer or Contour ingress, an FQDN for the registry, and a storage policy already visible as a StorageClass. Each check below proves one of these.

# Tested against VCF 9.1, VKS on vSphere Supervisor, VCF CLI, # kubectl vSphere plugin, kubectl v1.32, Harbor Supervisor Service 2.13.x [VERIFY] # authenticate to the Supervisor with vCenter SSO kubectl vsphere login –server=sup-api.corp.local –vsphere-username platform-admin@corp.local –insecure-skip-tls-verify # in a pipeline read the password from the environment, never hardcode it: # export KUBECTL_VSPHERE_PASSWORD from your secret store and the plugin reads it kubectl config use-context dev-apps kubectl get cluster -n dev-apps kubectl get storageclass

What green looks like: the login returns a context, the cluster reports Provisioned, and a StorageClass backed by the vSphere CSI (Container Storage Interface) driver is present for Harbor to claim volumes from.

NAME PHASE AGE VERSION dev-tkg-01 Provisioned 3h v1.31.4 NAME PROVISIONER AGE vcf-workload-storage csi.vsphere.vmware.com 3h

Map TKGI UAA groups to vSphere Namespace roles

On TKGI you handed out access with UAA scopes and LDAP groups, plus per cluster admin roles. VKS collapses most of that into vSphere Namespace permissions. You assign a role to an AD or LDAP group on the namespace, and the Supervisor propagates it into every cluster in that namespace as Kubernetes RBAC (role based access control). Assign Can edit to a group and the system writes a ClusterRoleBinding on each cluster automatically. You never touch the cluster to grant operator access, you touch the namespace.

This mapping table is the reference artifact for the whole identity cutover. I keep it open and tick each source group off as I assign its target role. Build yours from the groups your TKGI clusters authorize today, not from a generic template.

TKGI source constructVKS targetIn-cluster effect
pks.clusters.admin scopeNamespace Owner role (vCenter SSO only)Create and delete namespaces by kubectl
LDAP platform-ops groupNamespace Can editClusterRoleBinding to edit on every cluster
LDAP read-only auditorsNamespace Can viewRead cluster objects, no in-cluster grant
App team developersNo namespace roleScoped RoleBinding inside the app namespace
Harbor LDAP auth and robot loginsHarbor projects and robot accountsRegistry push and pull, covered below

Table 2. UAA and LDAP to VKS access mapping. Notice developers get nothing at the namespace level, only a RoleBinding in their own space.

After you assign Can edit to the platform-ops group, confirm the propagation from the namespace. The binding names carry a wcp prefix and encode the group they came from, which makes them easy to audit.

$ kubectl get rolebinding -n dev-apps NAME ROLE AGE wcp:dev-apps:group:corp.local:platform-ops ClusterRole/edit 12m wcp:dev-apps:user:vsphere.local:administrator ClusterRole/edit 3h
flowchart TD
  A[AD or LDAP group] --> B[vSphere Namespace permission]
  B --> C[Supervisor propagates to clusters]
  C --> D[ClusterRoleBinding on each VKS cluster]
  D --> E[Scoped RoleBinding for app team]
  E --> F[kubectl vsphere login as the user]
Figure 2. Access flows from the directory group down to the developer. Operator rights land as a ClusterRoleBinding, developer rights stay scoped to one namespace.

Scope developer access with a RoleBinding

Developers should never inherit cluster-admin. On TKGI that separation was fuzzy; on VKS you make it explicit. As a cluster operator, bind the built-in edit ClusterRole to the developer group inside only their application namespace. Note the sso prefix on the subject name, because vCenter SSO identities carry it and the binding has to match exactly.

apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: dev-team-edit namespace: web-app subjects: – kind: Group name: sso:dev-team@corp.local # vCenter SSO group, keep the sso: prefix apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io

Get the prefix wrong and the developer authenticates but cannot deploy, which is a confusing failure because login works. Here is the exact error, and it is worth recognising on sight.

$ kubectl apply -f deployment.yaml Error from server (Forbidden): error when creating "deployment.yaml": deployments.apps is forbidden: User "sso:jdoe@corp.local" cannot create resource "deployments" in API group "apps" in the namespace "web-app"

Read the exact subject name from the error or from kubectl auth whoami, correct the RoleBinding, and prove the grant without making the developer retry by hand.

$ kubectl auth can-i create deployments –namespace web-app –as sso:jdoe@corp.local yes

Install Harbor as a Supervisor Service

Our source estate ran Harbor on its own VMs. On VKS you stop running registry VMs and install Harbor as a Supervisor Service from the vSphere Client, under Supervisor Management then Services. It needs a load balancer or an ingress controller, either an NGINX based load balancer or Contour, and since version 2.11.2 it can expose the registry through that load balancer directly. Download two files from support.broadcom.com, the harbor-service definition and the harbor-data-values configuration, upload the service definition to register the operator, then edit the data values and install.

Only a few properties matter on a first install, and one of them must not be touched. Set the hostname to your registry FQDN, point every storageClass at a real storage policy, and pull the admin password and secret key from your secret store rather than committing them. Leave the tlsSecretLabels set to managed-by vmware-vRegistry, because that label is what lets the Supervisor and VKS clusters trust the registry without an image pull secret.

Registry structure carries over more directly than identity does. On the old Harbor you kept a project per team or per environment, with robot accounts for CI. Recreate the same projects on the new Harbor, one per application namespace is a clean default, and issue a fresh robot account per pipeline rather than reusing old credentials. Keeping the project names identical means your image references change only in the registry hostname, which shrinks the edits teams make to their manifests during the workload waves later in this series.

# excerpt of harbor-data-values.yml, values injected from the environment hostname: harbor.corp.local https: port: 443 harborAdminPassword: ${HARBOR_ADMIN_PASSWORD} # from secret store, not git secretKey: ${HARBOR_SECRET_KEY} # 16 characters, from secret store tlsCertificate: tlsSecretLabels: {managed-by: vmware-vRegistry} # required for VKS, do not change enableContourHttpProxy: true enableNginxLoadBalancer: false persistence: persistentVolumeClaim: registry: storageClass: harbor-storage-policy size: 10Gi database: storageClass: harbor-storage-policy size: 1Gi network: ipFamilies: [IPv4] # IPv6 is not supported

Here is the failure that cost me an afternoon, and it hides in that storageClass line. I pasted our vCenter storage policy name verbatim, Harbor Storage Policy, capitals and spaces and all. The Harbor vSphere Pods came up but their PVCs sat Pending with no obvious cause until I described one.

$ kubectl describe pvc registry-harbor -n svc-harbor-domain-c1 … Warning ProvisioningFailed persistentvolume-controller storageclass.storage.k8s.io "Harbor Storage Policy" not found

A storage policy becomes a StorageClass by lower casing the name and replacing every space and underscore with a hyphen. Harbor Storage Policy becomes harbor-storage-policy. Correct the data values, reconfigure the service, and the PVCs bind. Once Harbor is running, read its external address and point DNS at it.

$ kubectl get svc -n svc-harbor-domain-c1 NAME TYPE EXTERNAL-IP PORT(S) AGE harbor LoadBalancer 10.20.30.44 443:31894/TCP 6m # then update DNS so harbor.corp.local resolves to 10.20.30.44

Verify access and image pulls, then roll back

End to end verification means three things succeed: a developer logs in and is scoped, a robot account pushes to Harbor, and a pod on the VKS cluster pulls that image with no image pull secret because the Supervisor trusts the registry. A 1.2 GB image that took about 90 seconds to pull cross site from the old registry lands in roughly 8 seconds from the in-datacenter Harbor, which is a nice side effect of moving the registry next to the clusters.

One check people skip is on the source side. After you prove a pull from the new Harbor, confirm the old registry and its LDAP auth still answer, because until every workload wave is done the source has to keep serving. A migration that quietly breaks the platform you are migrating away from is the worst kind, and repointing a shared DNS record too early is the easy way to cause it.

$ docker login harbor.corp.local -u robot$web-app+ci Login Succeeded $ docker push harbor.corp.local/web-app/api:1.4.2 1.4.2: digest: sha256:9f1c… size: 1782 # a pod on the VKS cluster pulls it with no imagePullSecret $ kubectl get pods -n web-app NAME READY STATUS RESTARTS AGE api-7c9f8b6d4-2xk9q 1/1 Running 0 40s

Rollback stays first class here because the source TKGI estate is still live. Revoking a person is a single delete, and their access stops at the next token refresh rather than instantly, so plan for a short overlap. Removing Harbor is a Manage Service action in the vSphere Client, and because the old Harbor keeps serving, application image pulls fall back to the source registry cleanly while you sort out the target.

# revoke a developer, access stops at next token refresh kubectl delete rolebinding dev-team-edit -n web-app # remove Harbor: Supervisor Management > Services > Harbor > Actions > Remove # the source TKGI Harbor keeps serving, so pulls fall back to it
Error you seeLikely causeFix
User cannot create resource in namespaceRoleBinding subject name missing the sso prefixMatch the name from the error, re-apply the RoleBinding
Owner role not available for this userUser authenticated over external OIDCGive namespace-create operators a vCenter SSO identity
PVC Pending, storageclass not foundStorage policy name has capitals or spacesLowercase it, replace spaces and underscores with hyphens
VKS pod ImagePullBackOff from HarbortlsSecretLabels changed from vmware-vRegistryRestore the label, reconfigure the Harbor service
Harbor unreachable on its FQDNDNS not pointed at the load balancer IPRead EXTERNAL-IP from kubectl get svc, update DNS

Table 3. Identity and registry failures and their remediation. Three of the five are a name that does not match, one on RBAC, one on storage, one on the trust label.

Field note and verdict

What I got wrong first: to make VKS feel like the old world, I federated the Supervisor to our corporate OIDC provider on day one so logins matched the UAA and LDAP setup. Within an hour the platform team could authenticate but could no longer create vSphere Namespaces with kubectl. Owner, the one role that grants that, is restricted to vCenter SSO users. I lost about half a day and a support case before accepting it was by design, then reverted platform admins to vCenter SSO and left only developers on OIDC.
My verdict: keep platform and DevOps admins on vCenter SSO, which already reads your AD and LDAP, and reach for external OIDC only to federate developers at scale. Install Harbor as a Supervisor Service instead of rebuilding registry VMs, and leave the tlsSecretLabels alone. Avoid two moves, federating everything to mimic UAA, and handing developers Can edit when a scoped RoleBinding is what they actually need.

Wire identity before the first workload moves

A clean result looks like this: platform admins log in with vCenter SSO and can create namespaces, a Can edit group shows up as a ClusterRoleBinding in each cluster, developers hold only a scoped RoleBinding in their app namespace, Harbor answers on its FQDN over 443, a robot account docker login succeeds, and a VKS pod pulls a Harbor image with no imagePullSecret. Get there on dev-tkg-01 and you repeat the same pattern for staging and prod.

On your own estate on Monday, do one thing before you touch any OIDC config: list the AD and LDAP groups your TKGI clusters authorize today, then map each one to exactly one vSphere Namespace role using Table 2. That mapping on paper, made before you configure anything, is what keeps you out of the Owner role trap. For registry and cluster mechanics beyond this migration, the vSphere Kubernetes Service complete guide and the VCF 9 complete guide go deeper, and microsegmentation for these same namespaces sits in the NSX complete guide. Next part converts NSX-T distributed firewall rules to Antrea network policy on this cluster.

Questions worth answering

Do I have to run UAA anywhere on VKS?
No. vCenter SSO replaces it. Integrate your AD or LDAP into vCenter and you keep the same directory without the UAA component to operate.

Can developers use the Owner role over OIDC?
No. Owner is vCenter SSO only. Give OIDC developers a scoped RoleBinding, and keep an SSO-backed operator identity for creating namespaces.

Should Harbor use Contour or the NGINX load balancer?
Use Contour if you already run it as the Supervisor ingress, otherwise the NGINX load balancer is simpler. Set enableContourHttpProxy and enableNginxLoadBalancer as a matched pair, one true and one false.

Do I migrate all images now?
No. Stand up Harbor now and mirror a couple of images to prove pulls, but bulk image migration rides with each workload wave rather than one big push.

This series covers a production migration. Any step that touches identity or a registry should run in a change window against your own environment, with the source estate untouched until the target is verified.

TKGI to VKS Series · Part 15 of 26
« Previous: Part 14  |  Guide  |  Next: Part 16 »

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