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
11 changes: 6 additions & 5 deletions config/core/configmaps/features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ metadata:
app.kubernetes.io/component: controller
app.kubernetes.io/version: devel
annotations:
knative.dev/example-checksum: "e6ec6806"
knative.dev/example-checksum: "28e940d8"
data:
_example: |-
################################
Expand All @@ -43,9 +43,10 @@ data:
# Default SecurityContext settings to secure-by-default values
# if unset.
#
# This value will default to "enabled" in a future release,
# probably Knative 1.10
secure-pod-defaults: "disabled"
# Disabled - do nothing; no security options are applied
# SecureDefaultsOverridable - applies secure defaults without enforcing strict policies; sets RunAsNonRoot to true if not already specified
# Enabled - applies all security options required by the secure profile, including enforcing RunAsNonRoot
secure-pod-defaults: "secure-defaults-overridable"

# Indicates whether multi container support is enabled
#
Expand Down Expand Up @@ -130,7 +131,7 @@ data:
#
# WARNING: Cannot safely be disabled once enabled.
# See: https://knative.dev/docs/serving/feature-flags/#kubernetes-security-context
kubernetes.podspec-securitycontext: "disabled"
kubernetes.podspec-securitycontext: "enabled"

# Indicated whether sharing the process namespace via ShareProcessNamespace pod spec is allowed.
# This can be especially useful for sharing data from images directly between sidecars
Expand Down
26 changes: 23 additions & 3 deletions pkg/apis/config/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const (
// Allowed neither explicitly disables or enables a behavior.
// eg. allow a client to control behavior with an annotation or allow a new value through validation.
Allowed Flag = "Allowed"
// SecureDefaultsOverridable is used by secure-pod-defaults to apply secure defaults without enforcing strict policies;
// sets RunAsNonRoot to true if not already specified
SecureDefaultsOverridable Flag = "SecureDefaultsOverridable"
)

// service annotations under features.knative.dev/*
Expand Down Expand Up @@ -86,7 +89,7 @@ func defaultFeaturesConfig() *Features {
PodSpecFieldRef: Disabled,
PodSpecNodeSelector: Disabled,
PodSpecRuntimeClassName: Disabled,
PodSpecSecurityContext: Disabled,
PodSpecSecurityContext: Enabled,
PodSpecShareProcessNamespace: Disabled,
PodSpecHostIPC: Disabled,
PodSpecHostPID: Disabled,
Expand All @@ -107,7 +110,7 @@ func defaultFeaturesConfig() *Features {
PodSpecInitContainers: Disabled,
PodSpecDNSPolicy: Disabled,
PodSpecDNSConfig: Disabled,
SecurePodDefaults: Disabled,
SecurePodDefaults: SecureDefaultsOverridable,
TagHeaderBasedRouting: Disabled,
AutoDetectHTTP2: Disabled,
}
Expand All @@ -124,7 +127,7 @@ func NewFeaturesConfigFromMap(data map[string]string) (*Features, error) {
asFlag("multi-container-probing", &nc.MultiContainerProbing),
asFlag("queueproxy.mount-podinfo", &nc.QueueProxyMountPodInfo),
asFlag("queueproxy.resource-defaults", &nc.QueueProxyResourceDefaults),
asFlag("secure-pod-defaults", &nc.SecurePodDefaults),
asSecurePodDefaultsFlag("secure-pod-defaults", &nc.SecurePodDefaults),
asFlag("tag-header-based-routing", &nc.TagHeaderBasedRouting),
asFlag(FeatureContainerSpecAddCapabilities, &nc.ContainerSpecAddCapabilities),
asFlag(FeaturePodSpecAffinity, &nc.PodSpecAffinity),
Expand Down Expand Up @@ -198,6 +201,7 @@ type Features struct {
}

// asFlag parses the value at key as a Flag into the target, if it exists.
// Only accepts Enabled, Disabled, and Allowed values.
func asFlag(key string, target *Flag) cm.ParseFunc {
return func(data map[string]string) error {
if raw, ok := data[key]; ok {
Expand All @@ -211,3 +215,19 @@ func asFlag(key string, target *Flag) cm.ParseFunc {
return nil
}
}

// asSecurePodDefaultsFlag parses the value at key as a Flag into the target, if it exists.
// Accepts Enabled, Disabled, Allowed, and SecureDefaultsOverridable values.
func asSecurePodDefaultsFlag(key string, target *Flag) cm.ParseFunc {
return func(data map[string]string) error {
if raw, ok := data[key]; ok {
for _, flag := range []Flag{Disabled, SecureDefaultsOverridable, Enabled} {
if strings.EqualFold(raw, string(flag)) {
*target = flag
return nil
}
}
}
return nil
}
}
58 changes: 56 additions & 2 deletions pkg/apis/config/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestFeaturesConfiguration(t *testing.T) {
PodSpecSchedulerName: Enabled,
PodSpecDNSPolicy: Enabled,
PodSpecDNSConfig: Enabled,
SecurePodDefaults: Enabled,
SecurePodDefaults: SecureDefaultsOverridable,
QueueProxyResourceDefaults: Enabled,
TagHeaderBasedRouting: Enabled,
}),
Expand All @@ -98,7 +98,7 @@ func TestFeaturesConfiguration(t *testing.T) {
"kubernetes.podspec-schedulername": "Enabled",
"kubernetes.podspec-dnspolicy": "Enabled",
"kubernetes.podspec-dnsconfig": "Enabled",
"secure-pod-defaults": "Enabled",
"secure-pod-defaults": "SecureDefaultsOverridable",
"queueproxy.resource-defaults": "Enabled",
"tag-header-based-routing": "Enabled",
},
Expand Down Expand Up @@ -714,6 +714,60 @@ func TestFeaturesConfiguration(t *testing.T) {
data: map[string]string{
"kubernetes.podspec-hostnetwork": "Disabled",
},
}, {
name: "secure-pod-defaults SecureDefaultsOverridable",
wantErr: false,
wantFeatures: defaultWith(&Features{
SecurePodDefaults: SecureDefaultsOverridable,
}),
data: map[string]string{
"secure-pod-defaults": "secure-defaults-overridable",
},
}, {
name: "secure-pod-defaults Disabled",
wantErr: false,
wantFeatures: defaultWith(&Features{
SecurePodDefaults: Disabled,
}),
data: map[string]string{
"secure-pod-defaults": "Disabled",
},
}, {
name: "invalid secure-pod-defaults value",
wantErr: false,
wantFeatures: defaultWith(&Features{
SecurePodDefaults: SecureDefaultsOverridable,
}),
data: map[string]string{
"secure-pod-defaults": "InvalidValue",
},
}, {
name: "multi-container cannot be set to Restricted",
wantErr: false,
wantFeatures: defaultWith(&Features{
MultiContainer: Enabled, // Should remain default since Restricted is not valid
}),
data: map[string]string{
"multi-container": "Restricted",
},
}, {
name: "PodSpecAffinity cannot be set to Restricted",
wantErr: false,
wantFeatures: defaultWith(&Features{
PodSpecAffinity: Disabled, // Should remain default since Restricted is not valid
}),
data: map[string]string{
"kubernetes.podspec-affinity": "Restricted",
},
}, {
name: "tag-header-based-routing cannot be set to Restricted",
wantErr: false,
wantFeatures: defaultWith(&Features{
TagHeaderBasedRouting: Disabled, // Should remain default since Restricted is not valid
}),
data: map[string]string{
"tag-header-based-routing": "Restricted",
},
}}

for _, tt := range configTests {
Expand Down
7 changes: 2 additions & 5 deletions pkg/apis/serving/fieldmask.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,6 @@ func PodSpecMask(ctx context.Context, in *corev1.PodSpec) *corev1.PodSpec {
}
if cfg.Features.PodSpecSecurityContext != config.Disabled {
out.SecurityContext = in.SecurityContext
} else if cfg.Features.SecurePodDefaults != config.Disabled {
// This is further validated in ValidatePodSecurityContext.
out.SecurityContext = in.SecurityContext
}
if cfg.Features.PodSpecShareProcessNamespace != config.Disabled {
out.ShareProcessNamespace = in.ShareProcessNamespace
Expand Down Expand Up @@ -664,7 +661,7 @@ func PodSecurityContextMask(ctx context.Context, in *corev1.PodSecurityContext)

out := new(corev1.PodSecurityContext)

if config.FromContextOrDefaults(ctx).Features.SecurePodDefaults == config.Enabled {
if config.FromContextOrDefaults(ctx).Features.SecurePodDefaults == config.Enabled || config.FromContextOrDefaults(ctx).Features.SecurePodDefaults == config.SecureDefaultsOverridable {
// Allow to opt out of more-secure defaults if SecurePodDefaults is enabled.
// This aligns with defaultSecurityContext in revision_defaults.go.
if in.SeccompProfile != nil {
Expand Down Expand Up @@ -749,7 +746,7 @@ func CapabilitiesMask(ctx context.Context, in *corev1.Capabilities) *corev1.Capa

if config.FromContextOrDefaults(ctx).Features.ContainerSpecAddCapabilities == config.Enabled {
out.Add = in.Add
} else if config.FromContextOrDefaults(ctx).Features.SecurePodDefaults == config.Enabled {
} else if config.FromContextOrDefaults(ctx).Features.SecurePodDefaults == config.Enabled || config.FromContextOrDefaults(ctx).Features.SecurePodDefaults == config.SecureDefaultsOverridable {
if len(in.Add) == 1 && in.Add[0] == "NET_BIND_SERVICE" {
out.Add = in.Add
} else {
Expand Down
121 changes: 119 additions & 2 deletions pkg/apis/serving/fieldmask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,20 @@ func TestPodSecurityContextMask(t *testing.T) {
},
}

want := &corev1.PodSecurityContext{}
ctx := context.Background()
want := &corev1.PodSecurityContext{
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
LocalhostProfile: nil,
},
}
ctx := config.ToContext(context.Background(),
&config.Config{
Features: &config.Features{
SecurePodDefaults: config.Enabled,
PodSpecSecurityContext: config.Disabled,
},
},
)

got := PodSecurityContextMask(ctx, in)

Expand Down Expand Up @@ -967,6 +979,111 @@ func TestPodSecurityContextMask_SecurePodDefaultsEnabled(t *testing.T) {
}
}

func TestPodSecurityContextMask_SecurePodDefaultsRestricted(t *testing.T) {
// Test that Restricted works the same as Enabled for masking purposes
want := &corev1.PodSecurityContext{
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
}

in := &corev1.PodSecurityContext{
SELinuxOptions: &corev1.SELinuxOptions{},
WindowsOptions: &corev1.WindowsSecurityContextOptions{},
SupplementalGroups: []int64{1},
Sysctls: []corev1.Sysctl{},
RunAsUser: ptr.Int64(1),
RunAsGroup: ptr.Int64(1),
RunAsNonRoot: ptr.Bool(true),
FSGroup: ptr.Int64(1),
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
}

ctx := config.ToContext(context.Background(),
&config.Config{
Features: &config.Features{
SecurePodDefaults: config.SecureDefaultsOverridable,
PodSpecSecurityContext: config.Disabled,
},
},
)

got := PodSecurityContextMask(ctx, in)

if &want == &got {
t.Error("Input and output share addresses. Want different addresses")
}

if diff, err := kmp.SafeDiff(want, got); err != nil {
t.Error("Got error comparing output, err =", err)
} else if diff != "" {
t.Error("PodSecurityContextMask (-want, +got):", diff)
}
}

func TestCapabilitiesMask_SecurePodDefaultsRestricted(t *testing.T) {
// Ensures users can only add NET_BIND_SERVICE or nil capabilities
tests := []struct {
name string
in *corev1.Capabilities
want *corev1.Capabilities
}{{
name: "empty",
in: &corev1.Capabilities{
Add: nil,
},
want: &corev1.Capabilities{
Add: nil,
},
}, {
name: "allows NET_BIND_SERVICE capability",
in: &corev1.Capabilities{
Add: []corev1.Capability{"NET_BIND_SERVICE"},
},
want: &corev1.Capabilities{
Add: []corev1.Capability{"NET_BIND_SERVICE"},
},
}, {
name: "prevents restricted fields",
in: &corev1.Capabilities{
Add: []corev1.Capability{"CHOWN"},
},
want: &corev1.Capabilities{
Add: nil,
},
}}

for _, test := range tests {
ctx := config.ToContext(context.Background(),
&config.Config{
Features: &config.Features{
SecurePodDefaults: config.SecureDefaultsOverridable,
},
},
)

t.Run(test.name, func(t *testing.T) {
got := CapabilitiesMask(ctx, test.in)

if &test.want == &got {
t.Error("Input and output share addresses. Want different addresses")
}

if diff, err := kmp.SafeDiff(test.want, got); err != nil {
t.Error("Got error comparing output, err =", err)
} else if diff != "" {
t.Error("CapabilitiesMask (-want, +got):", diff)
}

if got = CapabilitiesMask(ctx, nil); got != nil {
t.Errorf("CapabilitiesMask = %v, want: nil", got)
}
})
}
}

func TestSecurityContextMask(t *testing.T) {
mtype := corev1.UnmaskedProcMount
want := &corev1.SecurityContext{
Expand Down
30 changes: 20 additions & 10 deletions pkg/apis/serving/k8s_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1369,16 +1369,6 @@ func TestPodSpecFeatureValidation(t *testing.T) {
Paths: []string{"runtimeClassName"},
},
cfgOpts: []configOption{withPodSpecRuntimeClassNameEnabled()},
}, {
name: "PodSpecSecurityContext",
featureSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{},
},
err: &apis.FieldError{
Message: "must not set the field(s)",
Paths: []string{"securityContext"},
},
cfgOpts: []configOption{withPodSpecSecurityContextEnabled()},
}, {
name: "PriorityClassName",
featureSpec: corev1.PodSpec{
Expand Down Expand Up @@ -1474,6 +1464,26 @@ func TestPodSpecFeatureValidation(t *testing.T) {
}
}

func TestPodSpecSecurityContext_DefaultEnabled_AllowsPodSecurityContext(t *testing.T) {
ctx := context.Background()
// Ensure the feature is enabled in context
cfg := config.FromContextOrDefaults(ctx)
cfg.Features.PodSpecSecurityContext = config.Enabled
ctx = config.ToContext(ctx, cfg)

ps := corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: ptr.Bool(false),
},
Containers: []corev1.Container{{
Image: "busybox",
}},
}
if got := ValidatePodSpec(ctx, ps).Filter(apis.ErrorLevel); got != nil {
t.Fatalf("ValidatePodSpec() = %v, want nil", got)
}
}

func TestPodSpecFieldRefValidation(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading