I’m trying to start a single-node cluster without kubeadm, but can’t seem to get service account tokens mounted into any containers.
I’m using a very simple test pod spec for this test:
apiVersion: v1
kind: Pod
metadata:
name: debug
spec:
hostNetwork: true
containers:
- name: alpine
image: docker.io/library/alpine:3.23.3
command:
- /bin/sh
args:
- '-c'
- 'sleep 3600'
volumeMounts:
- name: service-account
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: service-account
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 360
I’m using the default namespace and service account:
kubectl apply -f tests/k8s/debug.yaml
When I go seek out the service account token, it’s not there:
$ kubectl exec debug -- ls -la /var/run/secrets/kubernetes.io/serviceaccount
total 0
drwxrwxrwx 1 root root 0 Apr 16 19:42 .
drwxr-xr-t 1 root root 28 Apr 16 19:42 ..
Debugging steps
Cranked verbosity on the API server to determine whether it was issuing tokens, and it is:
I0416 19:32:14.348730 1 httplog.go:134] "HTTP" verb="POST" URI="/api/v1/namespaces/default/serviceaccounts/default/token" latency="3.061416ms" userAgent="kubelet/v1.35.4 (linux/arm64) kubernetes/7b8c6cf" audit-ID="301b8b51-1542-4407-a6c9-012b4a420799" srcIP="192.168.18.3:45340" apf_pl="system" apf_fs="system-nodes" apf_iseats=1 apf_fseats=0 apf_additionalLatency="0s" apf_execution_time="2.760375ms" resp=201
Turned up the kubelet verbosity to make sure it was setting up the projected volume as expected, and it is:
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.146224 1030 kubelet.go:2601] "SyncLoop RECONCILE" source="api" pods=["default/debug"]
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.203544 1030 desired_state_of_world.go:310] "expected volume SELinux label context" volume="service-account" label=""
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.203564 1030 desired_state_of_world.go:330] "volume does not support SELinux context mount, clearing the expected label" volume="service-account"
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.203569 1030 desired_state_of_world_populator.go:318] "Added volume to desired state" pod="default/debug" volumeName="service-account" volumeSpecName="service-account"
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.244712 1030 reconciler_common.go:251] "operationExecutor.VerifyControllerAttachedVolume started for volume \"service-account\" (UniqueName: \"kubernetes.io/projected/d2b0fbb5-a130-4b1a-af7c-26e9368dd796-service-account\") pod \"debug\" (UID: \"d2b0fbb5-a130-4b1a-af7c-26e9368dd796\") " pod="default/debug"
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.345215 1030 reconciler_common.go:214] "Starting operationExecutor.MountVolume for volume \"service-account\" (UniqueName: \"kubernetes.io/projected/d2b0fbb5-a130-4b1a-af7c-26e9368dd796-service-account\") pod \"debug\" (UID: \"d2b0fbb5-a130-4b1a-af7c-26e9368dd796\") " pod="default/debug"
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.345239 1030 reconciler_common.go:225] "operationExecutor.MountVolume started for volume \"service-account\" (UniqueName: \"kubernetes.io/projected/d2b0fbb5-a130-4b1a-af7c-26e9368dd796-service-account\") pod \"debug\" (UID: \"d2b0fbb5-a130-4b1a-af7c-26e9368dd796\") " pod="default/debug"
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.345288 1030 projected.go:187] Setting up volume service-account for pod d2b0fbb5-a130-4b1a-af7c-26e9368dd796 at /var/lib/kubelet/pods/d2b0fbb5-a130-4b1a-af7c-26e9368dd796/volumes/kubernetes.io~projected/service-account
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.349140 1030 empty_dir_linux.go:99] Statfs_t of /var/lib/kubelet/pods/d2b0fbb5-a130-4b1a-af7c-26e9368dd796/volumes/kubernetes.io~projected/service-account: {Type:2435016766 Bsize:4096 Blocks:13699479 Bfree:12552315 Bavail:12458716 Files:0 Ffree:0 Fsid:{Val:[-1529449832 -899098667]} Namelen:255 Frsize:4096 Flags:4132 Spare:[0 0 0 0]}
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.349169 1030 empty_dir.go:338] pod d2b0fbb5-a130-4b1a-af7c-26e9368dd796: mounting tmpfs for volume wrapped_service-account
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.349180 1030 mount_linux.go:260] Mounting cmd (mount) with arguments (-t tmpfs -o size=2979602432,noswap tmpfs /var/lib/kubelet/pods/d2b0fbb5-a130-4b1a-af7c-26e9368dd796/volumes/kubernetes.io~projected/service-account)
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.351520 1030 atomic_writer.go:203] pod default/debug volume service-account: performed write of new data to ts data directory: /var/lib/kubelet/pods/d2b0fbb5-a130-4b1a-af7c-26e9368dd796/volumes/kubernetes.io~projected/service-account/..2026_04_16_19_32_14.91822510
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.351566 1030 operation_generator.go:614] "MountVolume.SetUp succeeded for volume \"service-account\" (UniqueName: \"kubernetes.io/projected/d2b0fbb5-a130-4b1a-af7c-26e9368dd796-service-account\") pod \"debug\" (UID: \"d2b0fbb5-a130-4b1a-af7c-26e9368dd796\") " pod="default/debug"
Apr 16 19:32:14 test0 kubelet[1030]: I0416 19:32:14.445621 1030 volume_manager.go:471] "All volumes are attached and mounted for pod" pod="default/debug"
I went to the host path where the projected volume is mounted, and it is also unsurprisingly empty:
$ sudo ls -al /var/lib/kubelet/pods/d2b0fbb5-a130-4b1a-af7c-26e9368dd796/volumes/kubernetes.io~projected/service-account
total 0
drwxrwxrwx. 1 root root 0 Apr 16 19:42 .
drwxr-xr-x. 1 root root 30 Apr 16 19:42 ..
I’m a little lost, I don’t really know where else to look. I’m happy to share the static pod spec for the API server, and the kubelet service unit file if you have a hunch. I’m really surprised this seems to be failing silently.
Updates
Adding more here as I figure things out:
I can inspect the mounts from the perspective of kubelet:
$ ps -e | grep kubelet
1017 ? 00:00:01 kubelet
$ cat /proc/1017/mounts | grep service-account
tmpfs /var/lib/kubelet/pods/d2b0fbb5-a130-4b1a-af7c-26e9368dd796/volumes/kubernetes.io~projected/service-account tmpfs rw,seclabel,relatime,size=2909768k,inode64,noswap 0 0
The tmpfs mount for the projected volume is indeed there. Is it normal for the kubelet to have its own mount namespace though? Wouldn’t that prevent the container runtime from seeing the contents of the projected volume directory? If I use the kubelet’s mount namespace, everything is there:
$ sudo nsenter --target=1017 -m
$ ls /var/lib/kubelet/pods/d2b0fbb5-a130-4b1a-af7c-26e9368dd796/volumes/kubernetes.io~projected
token
Is there something wrong with the way systemd is managing the service, creating a mount namespace for the kubelet process when it shouldn’t? Or is this expected?
Cluster information:
Kubernetes version: 1.35.4
Cloud being used: bare-metal
Installation method: custom
Host OS: Fedora 43
CNI and version: hostNetwork
CRI and version: CRI-O v1.35.2