diff --git a/config/core/300-resources/configuration.yaml b/config/core/300-resources/configuration.yaml index 535230ad0b69..602634d3c2d7 100644 --- a/config/core/300-resources/configuration.yaml +++ b/config/core/300-resources/configuration.yaml @@ -478,6 +478,18 @@ spec: description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows. type: integer format: int64 + seccompProfile: + description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows. + type: object + required: + - type + properties: + localhostProfile: + description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied." + type: string terminationMessagePath: description: 'Optional: Path at which the file to which the container''s termination message will be written is mounted into the container''s filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.' type: string diff --git a/config/core/300-resources/revision.yaml b/config/core/300-resources/revision.yaml index 17f1f3c35e15..f3b13bbd2bf9 100644 --- a/config/core/300-resources/revision.yaml +++ b/config/core/300-resources/revision.yaml @@ -457,6 +457,18 @@ spec: description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows. type: integer format: int64 + seccompProfile: + description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows. + type: object + required: + - type + properties: + localhostProfile: + description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied." + type: string terminationMessagePath: description: 'Optional: Path at which the file to which the container''s termination message will be written is mounted into the container''s filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.' type: string diff --git a/config/core/300-resources/service.yaml b/config/core/300-resources/service.yaml index e2030b368455..d487e37d958f 100644 --- a/config/core/300-resources/service.yaml +++ b/config/core/300-resources/service.yaml @@ -482,6 +482,18 @@ spec: description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows. type: integer format: int64 + seccompProfile: + description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows. + type: object + required: + - type + properties: + localhostProfile: + description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied." + type: string terminationMessagePath: description: 'Optional: Path at which the file to which the container''s termination message will be written is mounted into the container''s filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.' type: string diff --git a/config/core/configmaps/features.yaml b/config/core/configmaps/features.yaml index 49f687a8c8d9..cc124eeba294 100644 --- a/config/core/configmaps/features.yaml +++ b/config/core/configmaps/features.yaml @@ -22,7 +22,7 @@ metadata: app.kubernetes.io/component: controller app.kubernetes.io/version: devel annotations: - knative.dev/example-checksum: "691a192e" + knative.dev/example-checksum: "d3565159" data: _example: |- ################################ @@ -40,6 +40,13 @@ data: # this example block and unindented to be in the data block # to actually change the configuration. + # 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" + # Indicates whether multi container support is enabled # # WARNING: Cannot safely be disabled once enabled. diff --git a/hack/schemapatch-config.yaml b/hack/schemapatch-config.yaml index f767e8e882ed..e0030b4dcb84 100644 --- a/hack/schemapatch-config.yaml +++ b/hack/schemapatch-config.yaml @@ -286,6 +286,7 @@ k8s.io/api/core/v1.SecurityContext: - RunAsGroup - RunAsNonRoot - RunAsUser + - SeccompProfile k8s.io/api/core/v1.Capabilities: fieldMask: - Add diff --git a/pkg/apis/config/features.go b/pkg/apis/config/features.go index 90bad722cdd2..655188ec4f9b 100644 --- a/pkg/apis/config/features.go +++ b/pkg/apis/config/features.go @@ -70,6 +70,7 @@ func defaultFeaturesConfig() *Features { PodSpecInitContainers: Disabled, PodSpecDNSPolicy: Disabled, PodSpecDNSConfig: Disabled, + SecurePodDefaults: Disabled, TagHeaderBasedRouting: Disabled, AutoDetectHTTP2: Disabled, } @@ -99,6 +100,7 @@ func NewFeaturesConfigFromMap(data map[string]string) (*Features, error) { asFlag("kubernetes.podspec-persistent-volume-write", &nc.PodSpecPersistentVolumeWrite), asFlag("kubernetes.podspec-dnspolicy", &nc.PodSpecDNSPolicy), asFlag("kubernetes.podspec-dnsconfig", &nc.PodSpecDNSConfig), + asFlag("secure-pod-defaults", &nc.SecurePodDefaults), asFlag("tag-header-based-routing", &nc.TagHeaderBasedRouting), asFlag("queueproxy.mount-podinfo", &nc.QueueProxyMountPodInfo), asFlag("autodetect-http2", &nc.AutoDetectHTTP2)); err != nil { @@ -134,6 +136,7 @@ type Features struct { QueueProxyMountPodInfo Flag PodSpecDNSPolicy Flag PodSpecDNSConfig Flag + SecurePodDefaults Flag TagHeaderBasedRouting Flag AutoDetectHTTP2 Flag } diff --git a/pkg/apis/config/features_test.go b/pkg/apis/config/features_test.go index 77c1c3305e27..bad1cbeefa2e 100644 --- a/pkg/apis/config/features_test.go +++ b/pkg/apis/config/features_test.go @@ -72,6 +72,7 @@ func TestFeaturesConfiguration(t *testing.T) { PodSpecSchedulerName: Enabled, PodSpecDNSPolicy: Enabled, PodSpecDNSConfig: Enabled, + SecurePodDefaults: Enabled, TagHeaderBasedRouting: Enabled, }), data: map[string]string{ @@ -88,6 +89,7 @@ func TestFeaturesConfiguration(t *testing.T) { "kubernetes.podspec-schedulername": "Enabled", "kubernetes.podspec-dnspolicy": "Enabled", "kubernetes.podspec-dnsconfig": "Enabled", + "secure-pod-defaults": "Enabled", "tag-header-based-routing": "Enabled", }, }, { diff --git a/pkg/apis/serving/fieldmask.go b/pkg/apis/serving/fieldmask.go index ab8724b49f60..cc59b95f383c 100644 --- a/pkg/apis/serving/fieldmask.go +++ b/pkg/apis/serving/fieldmask.go @@ -208,6 +208,9 @@ 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.PodSpecPriorityClassName != config.Disabled { out.PriorityClassName = in.PriorityClassName @@ -591,6 +594,19 @@ func PodSecurityContextMask(ctx context.Context, in *corev1.PodSecurityContext) out := new(corev1.PodSecurityContext) + if config.FromContextOrDefaults(ctx).Features.SecurePodDefaults == config.Enabled { + // Allow to opt out of more-secure defaults if SecurePodDefaults is enabled. + // This aligns with defaultSecurityContext in revision_defaults.go. + if in.SeccompProfile != nil { + seccomp := in.SeccompProfile.Type + if seccomp == corev1.SeccompProfileTypeRuntimeDefault || seccomp == corev1.SeccompProfileTypeUnconfined { + out.SeccompProfile = &corev1.SeccompProfile{ + Type: seccomp, + } + } + } + } + if config.FromContextOrDefaults(ctx).Features.PodSpecSecurityContext == config.Disabled { return out } diff --git a/pkg/apis/serving/fieldmask_test.go b/pkg/apis/serving/fieldmask_test.go index d4cc35363ba7..f5b14a28185b 100644 --- a/pkg/apis/serving/fieldmask_test.go +++ b/pkg/apis/serving/fieldmask_test.go @@ -728,6 +728,51 @@ func TestPodSecurityContextMask_FeatureEnabled(t *testing.T) { } } +func TestPodSecurityContextMask_SecureEnabled(t *testing.T) { + // Ensure that users can opt out of better security by explicitly + // requesting the Kubernetes default, which is "Unconfined". + want := &corev1.PodSecurityContext{ + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeUnconfined, + }, + } + + 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.SeccompProfileTypeUnconfined, + }, + } + + ctx := config.ToContext(context.Background(), + &config.Config{ + Features: &config.Features{ + SecurePodDefaults: config.Enabled, + 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("PostSecurityContextMask (-want, +got):", diff) + } +} + func TestSecurityContextMask(t *testing.T) { mtype := corev1.UnmaskedProcMount want := &corev1.SecurityContext{ diff --git a/pkg/apis/serving/v1/revision_defaults.go b/pkg/apis/serving/v1/revision_defaults.go index 354b12d89d6f..8acbf3446fd1 100644 --- a/pkg/apis/serving/v1/revision_defaults.go +++ b/pkg/apis/serving/v1/revision_defaults.go @@ -72,6 +72,10 @@ func (rs *RevisionSpec) SetDefaults(ctx context.Context) { applyDefaultContainerNames(rs.PodSpec.InitContainers, containerNames, defaultInitContainerName) for idx := range rs.PodSpec.Containers { rs.applyDefault(ctx, &rs.PodSpec.Containers[idx], cfg) + rs.defaultSecurityContext(rs.PodSpec.SecurityContext, &rs.PodSpec.Containers[idx], cfg) + } + for idx := range rs.PodSpec.InitContainers { + rs.defaultSecurityContext(rs.PodSpec.SecurityContext, &rs.PodSpec.InitContainers[idx], cfg) } } @@ -158,6 +162,57 @@ func (*RevisionSpec) applyProbes(container *corev1.Container) { } } +// Upgrade SecurityContext for this container and the Pod definition to use settings +// for the `restricted` profile when the feature flag is enabled. +// This does not currently set `runAsNonRoot` for the restricted profile, because +// that feels harder to default safely. +func (rs *RevisionSpec) defaultSecurityContext(psc *corev1.PodSecurityContext, container *corev1.Container, cfg *config.Config) { + if cfg.Features.SecurePodDefaults != config.Enabled { + return + } + + if psc == nil { + psc = &corev1.PodSecurityContext{} + } + + updatedSC := container.SecurityContext + + if updatedSC == nil { + updatedSC = &corev1.SecurityContext{} + } + + if updatedSC.AllowPrivilegeEscalation == nil { + updatedSC.AllowPrivilegeEscalation = ptr.Bool(false) + } + if psc.SeccompProfile == nil || psc.SeccompProfile.Type == "" { + if updatedSC.SeccompProfile == nil { + updatedSC.SeccompProfile = &corev1.SeccompProfile{} + } + if updatedSC.SeccompProfile.Type == "" { + updatedSC.SeccompProfile.Type = corev1.SeccompProfileTypeRuntimeDefault + } + } + if updatedSC.Capabilities == nil { + updatedSC.Capabilities = &corev1.Capabilities{} + updatedSC.Capabilities.Drop = []corev1.Capability{"ALL"} + // Default in NET_BIND_SERVICE to allow binding to low-numbered ports. + needsLowPort := false + for _, p := range container.Ports { + if p.ContainerPort < 1024 { + needsLowPort = true + break + } + } + if updatedSC.Capabilities.Add == nil && needsLowPort { + updatedSC.Capabilities.Add = []corev1.Capability{"NET_BIND_SERVICE"} + } + } + + if *updatedSC != (corev1.SecurityContext{}) { + container.SecurityContext = updatedSC + } +} + func applyDefaultContainerNames(containers []corev1.Container, containerNames sets.String, defaultContainerName string) { // Default container name based on ContainerNameFromTemplate value from configmap. // In multi-container or init-container mode, add a numeric suffix, avoiding clashes with user-supplied names. diff --git a/pkg/apis/serving/v1/revision_defaults_test.go b/pkg/apis/serving/v1/revision_defaults_test.go index e56ec9c52ec8..332fecfb4d9d 100644 --- a/pkg/apis/serving/v1/revision_defaults_test.go +++ b/pkg/apis/serving/v1/revision_defaults_test.go @@ -835,6 +835,194 @@ func TestRevisionDefaulting(t *testing.T) { }, }, }, + }, { + name: "Default security context with feature enabled", + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logger) + s.OnConfigChanged(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: autoscalerconfig.ConfigName}}) + s.OnConfigChanged(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: config.DefaultsConfigName}}) + s.OnConfigChanged( + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: config.FeaturesConfigName}, + Data: map[string]string{"secure-pod-defaults": "Enabled"}, + }, + ) + + return s.ToContext(ctx) + }, + in: &Revision{ + Spec: RevisionSpec{ + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Ports: []corev1.ContainerPort{{ + ContainerPort: 80, + }}, + }, { + Name: "sidecar", + SecurityContext: &corev1.SecurityContext{}, + }, { + Name: "special-sidecar", + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(true), + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + Drop: []corev1.Capability{}, + }, + }, + }}, + InitContainers: []corev1.Container{{ + Name: "special-init", + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(true), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeLocalhost, + LocalhostProfile: ptr.String("special"), + }, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }}, + }, + }, + }, + want: &Revision{ + Spec: RevisionSpec{ + ContainerConcurrency: ptr.Int64(config.DefaultContainerConcurrency), + TimeoutSeconds: ptr.Int64(config.DefaultRevisionTimeoutSeconds), + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Ports: []corev1.ContainerPort{{ + ContainerPort: 80, + }}, + ReadinessProbe: defaultProbe, + Resources: defaultResources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(false), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + Add: []corev1.Capability{"NET_BIND_SERVICE"}, + }, + }, + }, { + Name: "sidecar", + Resources: defaultResources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(false), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, { + Name: "special-sidecar", + Resources: defaultResources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(true), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + Drop: []corev1.Capability{}, + }, + }, + }}, + InitContainers: []corev1.Container{{ + Name: "special-init", + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(true), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeLocalhost, + LocalhostProfile: ptr.String("special"), + }, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"NET_ADMIN"}, + }, + }, + }}, + }, + }, + }, + }, { + name: "uses pod defaults in security context", + wc: func(ctx context.Context) context.Context { + s := config.NewStore(logger) + s.OnConfigChanged(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: autoscalerconfig.ConfigName}}) + s.OnConfigChanged(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: config.DefaultsConfigName}}) + s.OnConfigChanged( + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: config.FeaturesConfigName}, + Data: map[string]string{"secure-pod-defaults": "Enabled"}, + }, + ) + + return s.ToContext(ctx) + }, + in: &Revision{ + Spec: RevisionSpec{ + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Ports: []corev1.ContainerPort{{ + ContainerPort: 8080, + }}, + }}, + InitContainers: []corev1.Container{{ + Name: "init", + SecurityContext: &corev1.SecurityContext{}, + }}, + SecurityContext: &corev1.PodSecurityContext{ + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeUnconfined, + }, + }, + }, + }, + }, + want: &Revision{ + Spec: RevisionSpec{ + ContainerConcurrency: ptr.Int64(config.DefaultContainerConcurrency), + TimeoutSeconds: ptr.Int64(config.DefaultRevisionTimeoutSeconds), + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Ports: []corev1.ContainerPort{{ + ContainerPort: 8080, + }}, + ReadinessProbe: defaultProbe, + Resources: defaultResources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }}, + InitContainers: []corev1.Container{{ + Name: "init", + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }}, + SecurityContext: &corev1.PodSecurityContext{ + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeUnconfined, + }, + }, + }, + }, + }, }} for _, test := range tests { diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index ad0f7fde67f1..dc4a02ffea9f 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -126,6 +126,11 @@ toggle_feature kubernetes.podspec-securitycontext Disabled toggle_feature kubernetes.podspec-persistent-volume-write Disabled toggle_feature kubernetes.podspec-persistent-volume-claim Disabled +# RUN secure pod defaults test in a separate install. +toggle_feature secure-pod-defaults Enabled +go_test_e2e -timeout=3m ./test/e2e/securedefaults ${TEST_OPTIONS} || failed=1 +toggle_feature secure-pod-defaults Disabled + # Run HA tests separately as they're stopping core Knative Serving pods. # Define short -spoofinterval to ensure frequent probing while stopping pods. toggle_feature autocreateClusterDomainClaims true config-network || fail_test diff --git a/test/e2e/securedefaults/secure_pod_defaults_test.go b/test/e2e/securedefaults/secure_pod_defaults_test.go new file mode 100644 index 000000000000..af1498deede8 --- /dev/null +++ b/test/e2e/securedefaults/secure_pod_defaults_test.go @@ -0,0 +1,113 @@ +//go:build e2e +// +build e2e + +/* +Copyright 2023 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package securedefaults + +import ( + "testing" + + v1 "k8s.io/api/core/v1" + "knative.dev/pkg/ptr" + . "knative.dev/serving/pkg/testing/v1" + "knative.dev/serving/test" + v1test "knative.dev/serving/test/v1" +) + +func TestSecureDefaults(t *testing.T) { + if !test.ServingFlags.EnableAlphaFeatures { + t.Skip("Alpha features not enabled") + } + t.Parallel() + clients := test.Setup(t) + + names := test.ResourceNames{ + Service: test.ObjectNameForTest(t), + Image: test.HelloWorld, + } + + test.EnsureTearDown(t, clients, &names) + + t.Log("Creating a new Service") + + resources, err := v1test.CreateServiceReady(t, clients, &names) + if err != nil { + t.Fatalf("Failed to create service with default SecurityContext: %v: %v", names.Service, err) + } + + revisionSC := resources.Revision.Spec.Containers[0].SecurityContext + if revisionSC == nil { + t.Fatal("Container SecurityContext was nil, should have been defaulted.") + } + if len(revisionSC.Capabilities.Drop) != 1 || revisionSC.Capabilities.Drop[0] != "ALL" { + t.Errorf("Expected to Drop 'ALL' capability: %v", revisionSC.Capabilities) + } + if revisionSC.AllowPrivilegeEscalation == nil || *revisionSC.AllowPrivilegeEscalation { + t.Errorf("Expected allowPrivilegeEscalation: false, got %v", revisionSC.AllowPrivilegeEscalation) + } + if revisionSC.SeccompProfile == nil || revisionSC.SeccompProfile.Type != v1.SeccompProfileTypeRuntimeDefault { + t.Errorf("Expected seccompProfile to be RuntimeDefault, got: %v", revisionSC.SeccompProfile) + } +} + +func TestUnsafePermitted(t *testing.T) { + if !test.ServingFlags.EnableAlphaFeatures { + t.Skip("Alpha features not enabled") + } + t.Parallel() + clients := test.Setup(t) + + names := test.ResourceNames{ + Service: test.ObjectNameForTest(t), + Image: test.HelloWorld, + } + + test.EnsureTearDown(t, clients, &names) + + t.Log("Creating a new Service") + + withDefaultUnsafeContext := WithSecurityContext(&v1.SecurityContext{ + Capabilities: &v1.Capabilities{ + Drop: []v1.Capability{}, + }, + RunAsNonRoot: ptr.Bool(false), + AllowPrivilegeEscalation: ptr.Bool(true), + SeccompProfile: &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeUnconfined, + }, + }) + + resources, err := v1test.CreateServiceReady(t, clients, &names, withDefaultUnsafeContext) + if err != nil { + t.Fatalf("Failed to create service with explicit k8s default SecurityContext: %v: %v", names.Service, err) + } + + revisionSC := resources.Revision.Spec.Containers[0].SecurityContext + if revisionSC == nil { + t.Fatal("Container SecurityContext was nil, requested non-nil.") + } + if len(revisionSC.Capabilities.Drop) != 0 { + t.Errorf("Expected to Drop no capabilities (empty list): %v", revisionSC.Capabilities) + } + if revisionSC.AllowPrivilegeEscalation == nil || !*revisionSC.AllowPrivilegeEscalation { + t.Errorf("Expected allowPrivilegeEscalation: true, got %v", revisionSC.AllowPrivilegeEscalation) + } + if revisionSC.SeccompProfile == nil || revisionSC.SeccompProfile.Type != v1.SeccompProfileTypeUnconfined { + t.Errorf("Expected seccompProfile to be Unconfined, got: %v", revisionSC.SeccompProfile) + } +}