Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/k8s/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func (c *contextDialer) startDialerPod(ctx context.Context) (err error) {
Stdin: true,
StdinOnce: true,
Command: []string{"socat", "-u", "-", "OPEN:/dev/null"},
SecurityContext: defaultSecurityContext(client),
SecurityContext: defaultSecurityContext(),
},
},
DNSPolicy: coreV1.DNSClusterFirst,
Expand Down
2 changes: 1 addition & 1 deletion pkg/k8s/persistent_volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func runWithVolumeMounted(ctx context.Context, podImage string, podCommand []str
MountPath: volumeMntPoint,
},
},
SecurityContext: defaultSecurityContext(client),
SecurityContext: defaultSecurityContext(),
},
},
Volumes: []corev1.Volume{{
Expand Down
64 changes: 41 additions & 23 deletions pkg/k8s/security_context.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,61 @@
package k8s

import (
"github.com/Masterminds/semver"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)

var oneTwentyFour = semver.MustParse("1.24")

// defaultPodSecurityContext returns a PodSecurityContext that satisfies the
// Kubernetes "restricted" pod security profile (requires k8s >= 1.25; this
// project tracks k8s client-go v0.35 / k8s 1.35).
//
// SeccompProfile is set at both pod and container level (see defaultSecurityContext)
// as defence-in-depth: pod-level covers all containers by default, container-level
// ensures compliance even if a pod-level context is ever overridden downstream.
//
// RunAsGroup: 0 (root group) is retained on non-OpenShift to preserve compatibility
// with Tekton buildpack tasks that mount volumes with group ownership 0.
// This does not violate the restricted profile (which checks UID, not GID) but is
// tracked for remediation in https://github.com/knative/func/issues/3517.
func defaultPodSecurityContext() *corev1.PodSecurityContext {
// change ownership of the mounted volume to the first non-root user uid=1000
runAsNonRoot := true
seccompProfile := &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}

if IsOpenShift() {
return nil
// On OpenShift, SCCs manage RunAsUser/RunAsGroup/FSGroup; setting them
// here would conflict with the namespace's SCC UID range policy.
// Only set the fields required by the restricted PSA profile.
return &corev1.PodSecurityContext{
RunAsNonRoot: &runAsNonRoot,
SeccompProfile: seccompProfile,
}
}

runAsUser := int64(1001)
runAsGroup := int64(0) // Match Tekton buildpack task group
runAsGroup := int64(0) // Match Tekton buildpack task group; see doc comment above.
fsGroup := int64(1002) // Keep FSGroup for volume ownership
return &corev1.PodSecurityContext{
RunAsUser: &runAsUser,
RunAsGroup: &runAsGroup,
FSGroup: &fsGroup,
RunAsNonRoot: &runAsNonRoot,
SeccompProfile: seccompProfile,
RunAsUser: &runAsUser,
RunAsGroup: &runAsGroup,
FSGroup: &fsGroup,
}
}

func defaultSecurityContext(client *kubernetes.Clientset) *corev1.SecurityContext {
// defaultSecurityContext returns a container SecurityContext that satisfies the
// Kubernetes "restricted" pod security profile.
// SeccompProfile is set unconditionally; RuntimeDefault has been GA since k8s 1.25.
func defaultSecurityContext() *corev1.SecurityContext {
privileged := false
runAsNonRoot := true
sc := &corev1.SecurityContext{
Privileged: new(bool),
AllowPrivilegeEscalation: new(bool),
allowPrivilegeEscalation := false
return &corev1.SecurityContext{
Privileged: &privileged,
AllowPrivilegeEscalation: &allowPrivilegeEscalation,
RunAsNonRoot: &runAsNonRoot,
Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}},
SeccompProfile: nil,
}
if info, err := client.ServerVersion(); err == nil {
var v *semver.Version
v, err = semver.NewVersion(info.String())
if err == nil && v.Compare(oneTwentyFour) >= 0 {
sc.SeccompProfile = &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}
}
// SeccompProfile is also set at pod level; both levels are set intentionally
// for defence-in-depth (see defaultPodSecurityContext doc comment).
SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault},
}
return sc
}
147 changes: 147 additions & 0 deletions pkg/k8s/security_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package k8s

import (
"testing"

corev1 "k8s.io/api/core/v1"
)

// Note: SetOpenShiftForTest mutates a package-level bool without a mutex.
// These tests must not be run with t.Parallel() until that is addressed.
// See openshift.go:SetOpenShiftForTest.

func TestDefaultPodSecurityContext_NonOpenShift(t *testing.T) {
cleanup := SetOpenShiftForTest(false)
defer cleanup()

sc := defaultPodSecurityContext()
if sc == nil {
t.Fatal("expected non-nil PodSecurityContext on non-OpenShift")
}
if sc.RunAsNonRoot == nil || !*sc.RunAsNonRoot {
t.Error("expected RunAsNonRoot=true")
}
if sc.SeccompProfile == nil {
t.Fatal("expected SeccompProfile to be set")
}
if sc.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault {
t.Errorf("expected SeccompProfile.Type=RuntimeDefault, got %v", sc.SeccompProfile.Type)
}
if sc.RunAsUser == nil || *sc.RunAsUser == 0 {
t.Error("expected non-zero RunAsUser on non-OpenShift")
}
if sc.FSGroup == nil {
t.Error("expected FSGroup to be set on non-OpenShift")
}
}

func TestDefaultPodSecurityContext_OpenShift(t *testing.T) {
cleanup := SetOpenShiftForTest(true)
defer cleanup()

sc := defaultPodSecurityContext()
if sc == nil {
t.Fatal("expected non-nil PodSecurityContext on OpenShift")
}
if sc.RunAsNonRoot == nil || !*sc.RunAsNonRoot {
t.Error("expected RunAsNonRoot=true on OpenShift")
}
if sc.SeccompProfile == nil {
t.Fatal("expected SeccompProfile to be set on OpenShift")
}
if sc.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault {
t.Errorf("expected SeccompProfile.Type=RuntimeDefault, got %v", sc.SeccompProfile.Type)
}
// On OpenShift SCCs manage UID/GID; these must not be set.
if sc.RunAsUser != nil {
t.Errorf("expected RunAsUser to be nil on OpenShift, got %d", *sc.RunAsUser)
}
if sc.RunAsGroup != nil {
t.Errorf("expected RunAsGroup to be nil on OpenShift, got %d", *sc.RunAsGroup)
}
if sc.FSGroup != nil {
t.Errorf("expected FSGroup to be nil on OpenShift, got %d", *sc.FSGroup)
}
}

func TestDefaultSecurityContext(t *testing.T) {
sc := defaultSecurityContext()
if sc == nil {
t.Fatal("expected non-nil SecurityContext")
}
if sc.Privileged == nil || *sc.Privileged {
t.Error("expected Privileged=false (explicit)")
}
if sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation {
t.Error("expected AllowPrivilegeEscalation=false")
}
if sc.RunAsNonRoot == nil || !*sc.RunAsNonRoot {
t.Error("expected RunAsNonRoot=true")
}
if sc.Capabilities == nil {
t.Fatal("expected Capabilities to be set")
}
if len(sc.Capabilities.Drop) == 0 || sc.Capabilities.Drop[0] != "ALL" {
t.Error("expected Capabilities.Drop=[ALL]")
}
if sc.SeccompProfile == nil {
t.Fatal("expected SeccompProfile to be set")
}
if sc.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault {
t.Errorf("expected SeccompProfile.Type=RuntimeDefault, got %v", sc.SeccompProfile.Type)
}
}

// TestRestrictedProfileCompliance verifies both security context helpers together
// satisfy all four fields required by the Kubernetes "restricted" pod security
// profile on both OpenShift and vanilla Kubernetes clusters.
//
// Note: this validates Go struct fields only. End-to-end admission validation
// against a real restricted namespace is covered by the integration test suite
// (make test-full).
func TestRestrictedProfileCompliance(t *testing.T) {
for _, openshift := range []bool{false, true} {
openshift := openshift
name := "non-openshift"
if openshift {
name = "openshift"
}
t.Run(name, func(t *testing.T) {
cleanup := SetOpenShiftForTest(openshift)
defer cleanup()

pod := defaultPodSecurityContext()
ctr := defaultSecurityContext()

// restricted requires: allowPrivilegeEscalation=false (container level)
if ctr.AllowPrivilegeEscalation == nil || *ctr.AllowPrivilegeEscalation {
t.Error("restricted violation: AllowPrivilegeEscalation must be false")
}
// restricted requires: capabilities.drop must include ALL (container level)
hasDropAll := false
if ctr.Capabilities != nil {
for _, cap := range ctr.Capabilities.Drop {
if cap == corev1.Capability("ALL") {
hasDropAll = true
break
}
}
}
if !hasDropAll {
t.Error("restricted violation: capabilities.drop must include ALL")
}
// restricted requires: runAsNonRoot=true (pod or container level)
podHasRunAsNonRoot := pod != nil && pod.RunAsNonRoot != nil && *pod.RunAsNonRoot
ctrHasRunAsNonRoot := ctr.RunAsNonRoot != nil && *ctr.RunAsNonRoot
if !podHasRunAsNonRoot && !ctrHasRunAsNonRoot {
t.Error("restricted violation: runAsNonRoot must be true at pod or container level")
}
// restricted requires: seccompProfile (pod or container level)
podHasSeccomp := pod != nil && pod.SeccompProfile != nil
ctrHasSeccomp := ctr.SeccompProfile != nil
if !podHasSeccomp && !ctrHasSeccomp {
t.Error("restricted violation: seccompProfile must be set at pod or container level")
}
})
}
}
Loading