Summary
Since Kamaji adopted kubeadm v1.36 (edge-26.2.5+, following KEP-2305), the admin.conf client certificate is generated with O=kubeadm:cluster-admins instead of the previous O=system:masters. This creates a race condition that makes the {tcp}-admin-kubeconfig secret temporarily unusable after a tenant cluster is created or its certificates are rotated.
Root cause
The TCP reconciler writes the new admin.conf (with O=kubeadm:cluster-admins) to the management cluster secret. A consumer of this secret trying to authenticate against the tenant cluster then needs the kubeadm:cluster-admins → cluster-admin ClusterRoleBinding to exist inside that tenant. This CRB is created by PhaseClusterAdminRBAC in the soot manager — a separate controller loop that starts after the control plane becomes reachable, asynchronously relative to the TCP reconciler.
In the window between the TCP reconciler writing admin.conf and the soot manager completing PhaseClusterAdminRBAC, any caller using admin.conf to perform privileged operations in the tenant receives:
secrets is forbidden: User "kubernetes-admin" cannot create resource "secrets"
in API group "" in the namespace "kube-system"
The cert authenticates successfully (HTTP 403, not 401) — the issue is purely that O=kubeadm:cluster-admins requires a CRB that does not yet exist.
Why this wasn't an issue before
Before kubeadm v1.29 / KEP-2305, admin.conf used O=system:masters, which is a Kubernetes API-server internal short-circuit that bypasses RBAC entirely. No ClusterRoleBinding was required.
Relevant files
internal/resources/kubeconfig.go — KubeconfigResource.mutate calls kubeadm.CreateKubeconfig(AdminKubeConfigFileName, ...), which in kubeadm v1.36 emits O=kubeadm:cluster-admins.
internal/resources/kubeadm_phases.go — PhaseClusterAdminRBAC calls EnsureAdminClusterRoleBinding to create the CRB. Runs in the soot manager.
controllers/soot/manager.go — starts the soot manager, including PhaseClusterAdminRBAC. Runs after TCP reconciler.
Proposed fix
When generating admin.conf, substitute SuperAdminKubeConfigFileName as the cert configuration argument so the cert carries O=system:masters (same as super-admin.conf). The secret key is still named admin.conf; only the embedded cert's Organization field changes. This makes admin.conf unconditionally privileged without depending on ClusterRoleBinding ordering.
kubeconfigFileName := r.KubeConfigFileName
if kubeconfigFileName == kubeadmconstants.AdminKubeConfigFileName {
kubeconfigFileName = kubeadmconstants.SuperAdminKubeConfigFileName
}
kubeconfig, kcErr := kubeadm.CreateKubeconfig(kubeconfigFileName, crtKeyPair, config)
An alternative fix is to make PhaseClusterAdminRBAC a hard prerequisite before the TCP reconciler marks admin.conf as ready, or to add the admin-kubeconfig secret as a watch trigger for PhaseClusterAdminRBAC.
A PR with the proposed fix is attached.
Summary
Since Kamaji adopted kubeadm v1.36 (edge-26.2.5+, following KEP-2305), the
admin.confclient certificate is generated withO=kubeadm:cluster-adminsinstead of the previousO=system:masters. This creates a race condition that makes the{tcp}-admin-kubeconfigsecret temporarily unusable after a tenant cluster is created or its certificates are rotated.Root cause
The TCP reconciler writes the new
admin.conf(withO=kubeadm:cluster-admins) to the management cluster secret. A consumer of this secret trying to authenticate against the tenant cluster then needs thekubeadm:cluster-admins → cluster-adminClusterRoleBinding to exist inside that tenant. This CRB is created byPhaseClusterAdminRBACin the soot manager — a separate controller loop that starts after the control plane becomes reachable, asynchronously relative to the TCP reconciler.In the window between the TCP reconciler writing
admin.confand the soot manager completingPhaseClusterAdminRBAC, any caller usingadmin.confto perform privileged operations in the tenant receives:The cert authenticates successfully (HTTP 403, not 401) — the issue is purely that
O=kubeadm:cluster-adminsrequires a CRB that does not yet exist.Why this wasn't an issue before
Before kubeadm v1.29 / KEP-2305,
admin.confusedO=system:masters, which is a Kubernetes API-server internal short-circuit that bypasses RBAC entirely. No ClusterRoleBinding was required.Relevant files
internal/resources/kubeconfig.go—KubeconfigResource.mutatecallskubeadm.CreateKubeconfig(AdminKubeConfigFileName, ...), which in kubeadm v1.36 emitsO=kubeadm:cluster-admins.internal/resources/kubeadm_phases.go—PhaseClusterAdminRBACcallsEnsureAdminClusterRoleBindingto create the CRB. Runs in the soot manager.controllers/soot/manager.go— starts the soot manager, includingPhaseClusterAdminRBAC. Runs after TCP reconciler.Proposed fix
When generating
admin.conf, substituteSuperAdminKubeConfigFileNameas the cert configuration argument so the cert carriesO=system:masters(same assuper-admin.conf). The secret key is still namedadmin.conf; only the embedded cert's Organization field changes. This makesadmin.confunconditionally privileged without depending on ClusterRoleBinding ordering.An alternative fix is to make
PhaseClusterAdminRBACa hard prerequisite before the TCP reconciler marks admin.conf as ready, or to add the admin-kubeconfig secret as a watch trigger forPhaseClusterAdminRBAC.A PR with the proposed fix is attached.