Skip to content

Commit 280daa9

Browse files
lmicciniclaude
andcommitted
Add RestrictiveSecurityContext helper to pod package
Provides a centralized helper for building hardened container SecurityContext for unprivileged workloads: RunAsNonRoot, drops all capabilities, disables privilege escalation, and applies the RuntimeDefault seccomp profile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 734fbfd commit 280daa9

4 files changed

Lines changed: 162 additions & 5 deletions

File tree

modules/common/pod/security.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
Copyright 2026 Red Hat
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pod
18+
19+
import (
20+
corev1 "k8s.io/api/core/v1"
21+
"k8s.io/utils/ptr"
22+
)
23+
24+
// RestrictiveSecurityContext returns a hardened container SecurityContext
25+
// suitable for unprivileged workloads. It sets RunAsNonRoot, drops all
26+
// capabilities, disables privilege escalation, and applies the RuntimeDefault
27+
// seccomp profile. The provided uid is used for both RunAsUser and RunAsGroup.
28+
// Optional addCapabilities are added back after dropping ALL.
29+
func RestrictiveSecurityContext(uid int64, addCapabilities ...corev1.Capability) *corev1.SecurityContext {
30+
return RestrictiveSecurityContextWithGID(uid, uid, addCapabilities...)
31+
}
32+
33+
// RestrictiveSecurityContextWithGID is like RestrictiveSecurityContext but
34+
// allows specifying a different GID.
35+
func RestrictiveSecurityContextWithGID(uid, gid int64, addCapabilities ...corev1.Capability) *corev1.SecurityContext {
36+
caps := &corev1.Capabilities{
37+
Drop: []corev1.Capability{"ALL"},
38+
}
39+
if len(addCapabilities) > 0 {
40+
caps.Add = addCapabilities
41+
}
42+
return &corev1.SecurityContext{
43+
RunAsUser: ptr.To(uid),
44+
RunAsGroup: ptr.To(gid),
45+
RunAsNonRoot: ptr.To(true),
46+
ReadOnlyRootFilesystem: ptr.To(true),
47+
AllowPrivilegeEscalation: ptr.To(false),
48+
Capabilities: caps,
49+
SeccompProfile: &corev1.SeccompProfile{
50+
Type: corev1.SeccompProfileTypeRuntimeDefault,
51+
},
52+
}
53+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Copyright 2026 Red Hat
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pod
18+
19+
import (
20+
"testing"
21+
22+
corev1 "k8s.io/api/core/v1"
23+
)
24+
25+
func TestRestrictiveSecurityContext(t *testing.T) {
26+
var uid int64 = 42457
27+
sc := RestrictiveSecurityContext(uid)
28+
29+
if sc.RunAsUser == nil || *sc.RunAsUser != uid {
30+
t.Errorf("expected RunAsUser %d, got %v", uid, sc.RunAsUser)
31+
}
32+
if sc.RunAsGroup == nil || *sc.RunAsGroup != uid {
33+
t.Errorf("expected RunAsGroup %d, got %v", uid, sc.RunAsGroup)
34+
}
35+
if sc.RunAsNonRoot == nil || !*sc.RunAsNonRoot {
36+
t.Error("expected RunAsNonRoot true")
37+
}
38+
if sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation {
39+
t.Error("expected AllowPrivilegeEscalation false")
40+
}
41+
if sc.Capabilities == nil || len(sc.Capabilities.Drop) != 1 || sc.Capabilities.Drop[0] != "ALL" {
42+
t.Errorf("expected Capabilities.Drop [ALL], got %v", sc.Capabilities)
43+
}
44+
if sc.SeccompProfile == nil || sc.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault {
45+
t.Errorf("expected SeccompProfile RuntimeDefault, got %v", sc.SeccompProfile)
46+
}
47+
if sc.ReadOnlyRootFilesystem == nil || !*sc.ReadOnlyRootFilesystem {
48+
t.Error("expected ReadOnlyRootFilesystem true")
49+
}
50+
}
51+
52+
func TestRestrictiveSecurityContextWithGID(t *testing.T) {
53+
var uid int64 = 42415
54+
var gid int64 = 42416
55+
sc := RestrictiveSecurityContextWithGID(uid, gid)
56+
57+
if sc.RunAsUser == nil || *sc.RunAsUser != uid {
58+
t.Errorf("expected RunAsUser %d, got %v", uid, sc.RunAsUser)
59+
}
60+
if sc.RunAsGroup == nil || *sc.RunAsGroup != gid {
61+
t.Errorf("expected RunAsGroup %d, got %v", gid, sc.RunAsGroup)
62+
}
63+
if sc.RunAsNonRoot == nil || !*sc.RunAsNonRoot {
64+
t.Error("expected RunAsNonRoot true")
65+
}
66+
if sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation {
67+
t.Error("expected AllowPrivilegeEscalation false")
68+
}
69+
}
70+
71+
func TestRestrictiveSecurityContextNoAddCaps(t *testing.T) {
72+
sc := RestrictiveSecurityContext(42457)
73+
if sc.Capabilities.Add != nil {
74+
t.Errorf("expected no Add capabilities, got %v", sc.Capabilities.Add)
75+
}
76+
}
77+
78+
func TestRestrictiveSecurityContextWithAddCaps(t *testing.T) {
79+
sc := RestrictiveSecurityContext(42457, "NET_BIND_SERVICE", "CHOWN")
80+
81+
if len(sc.Capabilities.Drop) != 1 || sc.Capabilities.Drop[0] != "ALL" {
82+
t.Errorf("expected Drop [ALL], got %v", sc.Capabilities.Drop)
83+
}
84+
if len(sc.Capabilities.Add) != 2 {
85+
t.Fatalf("expected 2 Add capabilities, got %d", len(sc.Capabilities.Add))
86+
}
87+
if sc.Capabilities.Add[0] != "NET_BIND_SERVICE" || sc.Capabilities.Add[1] != "CHOWN" {
88+
t.Errorf("expected Add [NET_BIND_SERVICE CHOWN], got %v", sc.Capabilities.Add)
89+
}
90+
}

modules/common/serviceaccount/projected_token.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,23 @@ const (
3737
// time-limited service account token, the cluster CA certificate, and
3838
// the pod namespace. Use it together with AutomountServiceAccountToken=false
3939
// on the PodSpec to replace the default long-lived automounted token with a
40-
// short-lived one.
41-
func KubeAPIAccessVolume() corev1.Volume {
40+
// short-lived one. An optional expirationSeconds overrides the default
41+
// token lifetime (1 hour).
42+
func KubeAPIAccessVolume(expirationSeconds ...int64) corev1.Volume {
43+
expiration := DefaultTokenExpirationSeconds
44+
if len(expirationSeconds) > 0 && expirationSeconds[0] > 0 {
45+
expiration = expirationSeconds[0]
46+
}
4247
return corev1.Volume{
4348
Name: KubeAPIAccessVolumeName,
4449
VolumeSource: corev1.VolumeSource{
4550
Projected: &corev1.ProjectedVolumeSource{
46-
DefaultMode: ptr.To[int32](0o444),
51+
DefaultMode: ptr.To[int32](0444),
4752
Sources: []corev1.VolumeProjection{
4853
{
4954
ServiceAccountToken: &corev1.ServiceAccountTokenProjection{
5055
Path: "token",
51-
ExpirationSeconds: ptr.To(DefaultTokenExpirationSeconds),
56+
ExpirationSeconds: ptr.To(expiration),
5257
},
5358
},
5459
{

modules/common/serviceaccount/projected_token_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func TestKubeAPIAccessVolume(t *testing.T) {
2727

2828
g.Expect(vol.Name).To(Equal(KubeAPIAccessVolumeName))
2929
g.Expect(vol.Projected).NotTo(BeNil())
30-
g.Expect(*vol.Projected.DefaultMode).To(Equal(int32(0o444)))
30+
g.Expect(*vol.Projected.DefaultMode).To(Equal(int32(0444)))
3131

3232
sources := vol.Projected.Sources
3333
g.Expect(sources).To(HaveLen(3))
@@ -47,6 +47,15 @@ func TestKubeAPIAccessVolume(t *testing.T) {
4747
g.Expect(sources[2].DownwardAPI.Items[0].FieldRef.FieldPath).To(Equal("metadata.namespace"))
4848
}
4949

50+
func TestKubeAPIAccessVolumeCustomExpiration(t *testing.T) {
51+
g := NewWithT(t)
52+
53+
var customExpiration int64 = 7200
54+
vol := KubeAPIAccessVolume(customExpiration)
55+
56+
g.Expect(*vol.Projected.Sources[0].ServiceAccountToken.ExpirationSeconds).To(Equal(customExpiration))
57+
}
58+
5059
func TestKubeAPIAccessVolumeMount(t *testing.T) {
5160
g := NewWithT(t)
5261

0 commit comments

Comments
 (0)