Skip to content

Commit 734fbfd

Browse files
lmicciniclaude
andcommitted
Add projected SA token volume helpers to serviceaccount package
Add KubeAPIAccessVolume() and KubeAPIAccessVolumeMount() helpers that create a projected volume replacing the default automounted SA token with a time-limited one (1h expiry). Use together with AutomountServiceAccountToken=false on the PodSpec. This enables operators to harden workloads by disabling the default long-lived automounted token while still providing K8s API access via a short-lived projected token at the standard mount path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 800f26c commit 734fbfd

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Licensed under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License.
4+
You may obtain a copy of the License at
5+
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package serviceaccount
16+
17+
import (
18+
corev1 "k8s.io/api/core/v1"
19+
"k8s.io/utils/ptr"
20+
)
21+
22+
const (
23+
// KubeAPIAccessVolumeName is the name of the projected volume that
24+
// replaces the default automounted service account token.
25+
KubeAPIAccessVolumeName = "kube-api-access"
26+
27+
// KubeAPIAccessMountPath is the standard mount path for the projected
28+
// service account token volume, matching the default automount path.
29+
KubeAPIAccessMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
30+
31+
// DefaultTokenExpirationSeconds is the default expiration for the
32+
// projected service account token (1 hour).
33+
DefaultTokenExpirationSeconds int64 = 3600
34+
)
35+
36+
// KubeAPIAccessVolume returns a projected Volume that provides a
37+
// time-limited service account token, the cluster CA certificate, and
38+
// the pod namespace. Use it together with AutomountServiceAccountToken=false
39+
// on the PodSpec to replace the default long-lived automounted token with a
40+
// short-lived one.
41+
func KubeAPIAccessVolume() corev1.Volume {
42+
return corev1.Volume{
43+
Name: KubeAPIAccessVolumeName,
44+
VolumeSource: corev1.VolumeSource{
45+
Projected: &corev1.ProjectedVolumeSource{
46+
DefaultMode: ptr.To[int32](0o444),
47+
Sources: []corev1.VolumeProjection{
48+
{
49+
ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
50+
Path: "token",
51+
ExpirationSeconds: ptr.To(DefaultTokenExpirationSeconds),
52+
},
53+
},
54+
{
55+
ConfigMap: &corev1.ConfigMapProjection{
56+
LocalObjectReference: corev1.LocalObjectReference{
57+
Name: "kube-root-ca.crt",
58+
},
59+
Items: []corev1.KeyToPath{
60+
{
61+
Key: "ca.crt",
62+
Path: "ca.crt",
63+
},
64+
},
65+
},
66+
},
67+
{
68+
DownwardAPI: &corev1.DownwardAPIProjection{
69+
Items: []corev1.DownwardAPIVolumeFile{
70+
{
71+
Path: "namespace",
72+
FieldRef: &corev1.ObjectFieldSelector{
73+
APIVersion: "v1",
74+
FieldPath: "metadata.namespace",
75+
},
76+
},
77+
},
78+
},
79+
},
80+
},
81+
},
82+
},
83+
}
84+
}
85+
86+
// KubeAPIAccessVolumeMount returns a read-only VolumeMount for the
87+
// projected service account token volume at the standard automount path.
88+
func KubeAPIAccessVolumeMount() corev1.VolumeMount {
89+
return corev1.VolumeMount{
90+
Name: KubeAPIAccessVolumeName,
91+
MountPath: KubeAPIAccessMountPath,
92+
ReadOnly: true,
93+
}
94+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
Licensed under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License.
4+
You may obtain a copy of the License at
5+
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package serviceaccount
16+
17+
import (
18+
"testing"
19+
20+
. "github.com/onsi/gomega" //revive:disable:dot-imports
21+
)
22+
23+
func TestKubeAPIAccessVolume(t *testing.T) {
24+
g := NewWithT(t)
25+
26+
vol := KubeAPIAccessVolume()
27+
28+
g.Expect(vol.Name).To(Equal(KubeAPIAccessVolumeName))
29+
g.Expect(vol.Projected).NotTo(BeNil())
30+
g.Expect(*vol.Projected.DefaultMode).To(Equal(int32(0o444)))
31+
32+
sources := vol.Projected.Sources
33+
g.Expect(sources).To(HaveLen(3))
34+
35+
g.Expect(sources[0].ServiceAccountToken).NotTo(BeNil())
36+
g.Expect(sources[0].ServiceAccountToken.Path).To(Equal("token"))
37+
g.Expect(*sources[0].ServiceAccountToken.ExpirationSeconds).To(Equal(DefaultTokenExpirationSeconds))
38+
39+
g.Expect(sources[1].ConfigMap).NotTo(BeNil())
40+
g.Expect(sources[1].ConfigMap.Name).To(Equal("kube-root-ca.crt"))
41+
g.Expect(sources[1].ConfigMap.Items).To(HaveLen(1))
42+
g.Expect(sources[1].ConfigMap.Items[0].Key).To(Equal("ca.crt"))
43+
44+
g.Expect(sources[2].DownwardAPI).NotTo(BeNil())
45+
g.Expect(sources[2].DownwardAPI.Items).To(HaveLen(1))
46+
g.Expect(sources[2].DownwardAPI.Items[0].Path).To(Equal("namespace"))
47+
g.Expect(sources[2].DownwardAPI.Items[0].FieldRef.FieldPath).To(Equal("metadata.namespace"))
48+
}
49+
50+
func TestKubeAPIAccessVolumeMount(t *testing.T) {
51+
g := NewWithT(t)
52+
53+
mount := KubeAPIAccessVolumeMount()
54+
55+
g.Expect(mount.Name).To(Equal(KubeAPIAccessVolumeName))
56+
g.Expect(mount.MountPath).To(Equal(KubeAPIAccessMountPath))
57+
g.Expect(mount.ReadOnly).To(BeTrue())
58+
}

0 commit comments

Comments
 (0)