diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 8483ec4be2..c02b3475aa 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -67,6 +67,7 @@ const ( sidecarMemoryRequestFlag = "sidecar-memory-request" sidecarMemoryLimitFlag = "sidecar-memory-limit" sidecarRunAsUserFlag = "sidecar-run-as-user" + sidecarSecurityContextFlag = "sidecar-security-context" sidecarRequestsRateLimitFlag = "sidecar-requests-rate-limit" sdkServerAccountFlag = "sdk-service-account" pullSidecarFlag = "always-pull-sidecar" @@ -206,7 +207,7 @@ func main() { gsController := gameservers.NewController(controllerHooks, health, ctlConf.PortRanges, ctlConf.SidecarImage, ctlConf.AlwaysPullSidecar, ctlConf.SidecarCPURequest, ctlConf.SidecarCPULimit, - ctlConf.SidecarMemoryRequest, ctlConf.SidecarMemoryLimit, ctlConf.SidecarRunAsUser, ctlConf.SidecarRequestsRateLimit, ctlConf.SdkServiceAccount, + ctlConf.SidecarMemoryRequest, ctlConf.SidecarMemoryLimit, ctlConf.SidecarSecurityContext, ctlConf.SidecarRequestsRateLimit, ctlConf.SdkServiceAccount, kubeClient, kubeInformerFactory, extClient, agonesClient, agonesInformerFactory) gsSetController := gameserversets.NewController(health, gsCounter, kubeClient, extClient, agonesClient, agonesInformerFactory, ctlConf.MaxCreationParallelism, ctlConf.MaxDeletionParallelism, ctlConf.MaxGameServerCreationsPerBatch, ctlConf.MaxGameServerDeletionsPerBatch, ctlConf.MaxPodPendingCount) @@ -253,6 +254,7 @@ func parseEnvFlags() config { viper.SetDefault(sidecarMemoryRequestFlag, "0") viper.SetDefault(sidecarMemoryLimitFlag, "0") viper.SetDefault(sidecarRunAsUserFlag, "1000") + viper.SetDefault(sidecarSecurityContextFlag, "") viper.SetDefault(sidecarRequestsRateLimitFlag, "500ms") viper.SetDefault(pullSidecarFlag, false) viper.SetDefault(sdkServerAccountFlag, "agones-sdk") @@ -284,7 +286,8 @@ func parseEnvFlags() config { pflag.String(sidecarCPURequestFlag, viper.GetString(sidecarCPURequestFlag), "Flag to overwrite the GameServer sidecar container's cpu request. Can also use SIDECAR_CPU_REQUEST env variable") pflag.String(sidecarMemoryLimitFlag, viper.GetString(sidecarMemoryLimitFlag), "Flag to overwrite the GameServer sidecar container's memory limit. Can also use SIDECAR_MEMORY_LIMIT env variable") pflag.String(sidecarMemoryRequestFlag, viper.GetString(sidecarMemoryRequestFlag), "Flag to overwrite the GameServer sidecar container's memory request. Can also use SIDECAR_MEMORY_REQUEST env variable") - pflag.Int32(sidecarRunAsUserFlag, viper.GetInt32(sidecarRunAsUserFlag), "Flag to indicate the GameServer sidecar container's UID. Can also use SIDECAR_RUN_AS_USER env variable") + pflag.Int32(sidecarRunAsUserFlag, viper.GetInt32(sidecarRunAsUserFlag), "Flag to indicate the GameServer sidecar container's UID. Only used when --sidecar-security-context is empty. Can also use SIDECAR_RUN_AS_USER env variable") + pflag.String(sidecarSecurityContextFlag, viper.GetString(sidecarSecurityContextFlag), `Optional. JSON encoded Kubernetes SecurityContext for the GameServer sidecar container. Defaults to a context compatible with the "restricted" Pod Security Standard. Can also use SIDECAR_SECURITY_CONTEXT env variable`) pflag.String(sidecarRequestsRateLimitFlag, viper.GetString(sidecarRequestsRateLimitFlag), "Flag to indicate the GameServer sidecar requests rate limit. Can also use SIDECAR_REQUESTS_RATE_LIMIT env variable") pflag.Bool(pullSidecarFlag, viper.GetBool(pullSidecarFlag), "For development purposes, set the sidecar image to have a ImagePullPolicy of Always. Can also use ALWAYS_PULL_SIDECAR env variable") pflag.String(sdkServerAccountFlag, viper.GetString(sdkServerAccountFlag), "Overwrite what service account default for GameServer Pods. Defaults to Can also use SDK_SERVICE_ACCOUNT") @@ -323,6 +326,7 @@ func parseEnvFlags() config { runtime.Must(viper.BindEnv(sidecarMemoryLimitFlag)) runtime.Must(viper.BindEnv(sidecarMemoryRequestFlag)) runtime.Must(viper.BindEnv(sidecarRunAsUserFlag)) + runtime.Must(viper.BindEnv(sidecarSecurityContextFlag)) runtime.Must(viper.BindEnv(sidecarRequestsRateLimitFlag)) runtime.Must(viper.BindEnv(pullSidecarFlag)) runtime.Must(viper.BindEnv(sdkServerAccountFlag)) @@ -381,6 +385,11 @@ func parseEnvFlags() config { logger.WithError(err).Fatalf("could not parse %s", sidecarRequestsRateLimitFlag) } + sidecarSecurityContext, err := parseSidecarSecurityContext(viper.GetString(sidecarSecurityContextFlag), int64(viper.GetInt32(sidecarRunAsUserFlag))) + if err != nil { + logger.WithError(err).Fatalf("could not parse %s", sidecarSecurityContextFlag) + } + portRanges, err := parsePortRanges(viper.GetString(additionalPortRangesFlag)) if err != nil { logger.WithError(err).Fatalf("could not parse %s", additionalPortRangesFlag) @@ -397,7 +406,7 @@ func parseEnvFlags() config { SidecarCPULimit: limitCPU, SidecarMemoryRequest: requestMemory, SidecarMemoryLimit: limitMemory, - SidecarRunAsUser: int(viper.GetInt32(sidecarRunAsUserFlag)), + SidecarSecurityContext: sidecarSecurityContext, SidecarRequestsRateLimit: requestsRateLimit, SdkServiceAccount: viper.GetString(sdkServerAccountFlag), AlwaysPullSidecar: viper.GetBool(pullSidecarFlag), @@ -448,6 +457,23 @@ func parsePortRanges(s string) (map[string]portallocator.PortRange, error) { return portRanges, nil } +// parseSidecarSecurityContext parses the JSON encoded sidecar security context, falling back to +// the default restricted-compatible context with the given UID when none is provided. +func parseSidecarSecurityContext(s string, runAsUser int64) (*corev1.SecurityContext, error) { + if strings.TrimSpace(s) == "" { + return gameservers.DefaultSidecarSecurityContext(runAsUser), nil + } + + var sc *corev1.SecurityContext + if err := json.Unmarshal([]byte(s), &sc); err != nil { + return nil, fmt.Errorf("invalid sidecar security context format: %w", err) + } + if sc == nil { + return gameservers.DefaultSidecarSecurityContext(runAsUser), nil + } + return sc, nil +} + // config stores all required configuration to create a game server controller. type config struct { PortRanges map[string]portallocator.PortRange @@ -456,7 +482,7 @@ type config struct { SidecarCPULimit resource.Quantity SidecarMemoryRequest resource.Quantity SidecarMemoryLimit resource.Quantity - SidecarRunAsUser int + SidecarSecurityContext *corev1.SecurityContext SidecarRequestsRateLimit time.Duration SdkServiceAccount string AlwaysPullSidecar bool diff --git a/cmd/controller/main_test.go b/cmd/controller/main_test.go index 90f7f2549a..e8a2ea426c 100644 --- a/cmd/controller/main_test.go +++ b/cmd/controller/main_test.go @@ -18,11 +18,50 @@ import ( "testing" agonesv1 "agones.dev/agones/pkg/apis/agones/v1" + "agones.dev/agones/pkg/gameservers" "agones.dev/agones/pkg/portallocator" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/ptr" ) +func TestParseSidecarSecurityContext(t *testing.T) { + t.Parallel() + + t.Run("empty or null falls back to the default", func(t *testing.T) { + sc, err := parseSidecarSecurityContext("", 1000) + require.NoError(t, err) + assert.Equal(t, gameservers.DefaultSidecarSecurityContext(1000), sc) + + sc, err = parseSidecarSecurityContext(" ", 2000) + require.NoError(t, err) + assert.Equal(t, int64(2000), *sc.RunAsUser) + + sc, err = parseSidecarSecurityContext("null", 3000) + require.NoError(t, err) + assert.Equal(t, gameservers.DefaultSidecarSecurityContext(3000), sc) + }) + + t.Run("json is used as is", func(t *testing.T) { + sc, err := parseSidecarSecurityContext(`{"runAsNonRoot":true,"runAsUser":2000,"runAsGroup":3000,"capabilities":{"drop":["ALL"]},"seccompProfile":{"type":"Localhost","localhostProfile":"profiles/agones.json"}}`, 1000) + require.NoError(t, err) + assert.Equal(t, &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + RunAsUser: ptr.To(int64(2000)), + RunAsGroup: ptr.To(int64(3000)), + Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}, + SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: ptr.To("profiles/agones.json")}, + }, sc) + }) + + t.Run("invalid json", func(t *testing.T) { + _, err := parseSidecarSecurityContext(`{"runAsUser":`, 1000) + require.Error(t, err) + }) +} + func TestControllerConfigValidation(t *testing.T) { t.Parallel() diff --git a/install/helm/agones/templates/controller.yaml b/install/helm/agones/templates/controller.yaml index 76d64e53a9..96248d3264 100644 --- a/install/helm/agones/templates/controller.yaml +++ b/install/helm/agones/templates/controller.yaml @@ -137,8 +137,8 @@ spec: value: {{ .Values.agones.image.sdk.memoryRequest | quote }} - name: SIDECAR_MEMORY_LIMIT value: {{ .Values.agones.image.sdk.memoryLimit | quote }} - - name: SIDECAR_RUN_AS_USER - value: "1000" + - name: SIDECAR_SECURITY_CONTEXT + value: {{ .Values.agones.image.sdk.securityContext | toJson | quote }} - name: SIDECAR_REQUESTS_RATE_LIMIT value: {{ .Values.agones.sdkServer.requestsRateLimit | quote }} - name: SDK_SERVICE_ACCOUNT diff --git a/install/helm/agones/values.yaml b/install/helm/agones/values.yaml index f205639f41..88de9b13ea 100644 --- a/install/helm/agones/values.yaml +++ b/install/helm/agones/values.yaml @@ -345,6 +345,15 @@ agones: memoryRequest: 0 memoryLimit: 0 alwaysPull: false + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1000 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault ping: name: agones-ping pullPolicy: IfNotPresent diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index c3b7696086..467d161adf 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -20787,8 +20787,8 @@ spec: value: "0" - name: SIDECAR_MEMORY_LIMIT value: "0" - - name: SIDECAR_RUN_AS_USER - value: "1000" + - name: SIDECAR_SECURITY_CONTEXT + value: "{\"allowPrivilegeEscalation\":false,\"capabilities\":{\"drop\":[\"ALL\"]},\"runAsNonRoot\":true,\"runAsUser\":1000,\"seccompProfile\":{\"type\":\"RuntimeDefault\"}}" - name: SIDECAR_REQUESTS_RATE_LIMIT value: "500ms" - name: SDK_SERVICE_ACCOUNT diff --git a/pkg/gameservers/controller.go b/pkg/gameservers/controller.go index f72e1bc592..104df0ab87 100644 --- a/pkg/gameservers/controller.go +++ b/pkg/gameservers/controller.go @@ -61,10 +61,11 @@ import ( ) const ( - sdkserverSidecarName = "agones-gameserver-sidecar" - grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" - httpPortEnvVar = "AGONES_SDK_HTTP_PORT" - passthroughPortEnvVar = "PASSTHROUGH" + sdkserverSidecarName = "agones-gameserver-sidecar" + grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" + httpPortEnvVar = "AGONES_SDK_HTTP_PORT" + passthroughPortEnvVar = "PASSTHROUGH" + defaultSidecarRunAsUser = 1000 ) // Extensions struct contains what is needed to bind webhook handlers @@ -87,7 +88,7 @@ type Controller struct { sidecarCPULimit resource.Quantity sidecarMemoryRequest resource.Quantity sidecarMemoryLimit resource.Quantity - sidecarRunAsUser int + sidecarSecurityContext *corev1.SecurityContext sidecarRequestsRateLimit time.Duration sdkServiceAccount string crdGetter apiextclientv1.CustomResourceDefinitionInterface @@ -121,7 +122,7 @@ func NewController( sidecarCPULimit resource.Quantity, sidecarMemoryRequest resource.Quantity, sidecarMemoryLimit resource.Quantity, - sidecarRunAsUser int, + sidecarSecurityContext *corev1.SecurityContext, sidecarRequestsRateLimit time.Duration, sdkServiceAccount string, kubeClient kubernetes.Interface, @@ -135,6 +136,10 @@ func NewController( gameServers := agonesInformerFactory.Agones().V1().GameServers() gsInformer := gameServers.Informer() + if sidecarSecurityContext == nil { + sidecarSecurityContext = DefaultSidecarSecurityContext(defaultSidecarRunAsUser) + } + c := &Controller{ controllerHooks: controllerHooks, sidecarImage: sidecarImage, @@ -142,7 +147,7 @@ func NewController( sidecarCPURequest: sidecarCPURequest, sidecarMemoryLimit: sidecarMemoryLimit, sidecarMemoryRequest: sidecarMemoryRequest, - sidecarRunAsUser: sidecarRunAsUser, + sidecarSecurityContext: sidecarSecurityContext, sidecarRequestsRateLimit: sidecarRequestsRateLimit, alwaysPullSidecarImage: alwaysPullSidecarImage, sdkServiceAccount: sdkServiceAccount, @@ -811,13 +816,21 @@ func (c *Controller) sidecar(gs *agonesv1.GameServer) corev1.Container { sidecar.ImagePullPolicy = corev1.PullAlways } - sidecar.SecurityContext = &corev1.SecurityContext{ + sidecar.SecurityContext = c.sidecarSecurityContext.DeepCopy() + + return sidecar +} + +// DefaultSidecarSecurityContext returns the default security context for the sidecar container, +// which is compatible with the `restricted` Pod Security Standard. +func DefaultSidecarSecurityContext(runAsUser int64) *corev1.SecurityContext { + return &corev1.SecurityContext{ AllowPrivilegeEscalation: ptr.To(false), RunAsNonRoot: ptr.To(true), - RunAsUser: ptr.To(int64(c.sidecarRunAsUser)), + RunAsUser: ptr.To(runAsUser), + Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}, + SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}, } - - return sidecar } // addGameServerHealthCheck adds the http health check to the GameServer container diff --git a/pkg/gameservers/controller_test.go b/pkg/gameservers/controller_test.go index 65a60b49d1..f4dc2a39c6 100644 --- a/pkg/gameservers/controller_test.go +++ b/pkg/gameservers/controller_test.go @@ -1622,6 +1622,8 @@ func TestControllerCreateGameServerPod(t *testing.T) { assert.False(t, *sidecarContainer.SecurityContext.AllowPrivilegeEscalation) assert.True(t, *sidecarContainer.SecurityContext.RunAsNonRoot) assert.Equal(t, *sidecarContainer.SecurityContext.RunAsUser, int64(sidecarRunAsUser)) + assert.Equal(t, []corev1.Capability{"ALL"}, sidecarContainer.SecurityContext.Capabilities.Drop) + assert.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, sidecarContainer.SecurityContext.SeccompProfile.Type) assert.Equal(t, fixture.Spec.Ports[0].HostPort, gsContainer.Ports[0].HostPort) assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, gsContainer.Ports[0].ContainerPort) @@ -2551,6 +2553,47 @@ func testWithNonZeroDeletionTimestamp(t *testing.T, f func(*Controller, *agonesv assert.Equal(t, fixture, result) } +func TestControllerSidecarSecurityContext(t *testing.T) { + t.Parallel() + + newGameServer := func() *agonesv1.GameServer { + gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, Spec: newSingleContainerSpec()} + gs.ApplyDefaults() + return gs + } + + t.Run("default security context", func(t *testing.T) { + c, _ := newFakeController() + sidecar := c.sidecar(newGameServer()) + + assert.Equal(t, DefaultSidecarSecurityContext(sidecarRunAsUser), sidecar.SecurityContext) + }) + + t.Run("custom security context", func(t *testing.T) { + c, _ := newFakeController() + c.sidecarSecurityContext = &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + RunAsUser: ptr.To(int64(2000)), + RunAsGroup: ptr.To(int64(3000)), + } + sidecar := c.sidecar(newGameServer()) + + assert.Equal(t, c.sidecarSecurityContext, sidecar.SecurityContext) + assert.Nil(t, sidecar.SecurityContext.Capabilities) + assert.Nil(t, sidecar.SecurityContext.SeccompProfile) + }) + + t.Run("each sidecar gets its own copy", func(t *testing.T) { + c, _ := newFakeController() + first := c.sidecar(newGameServer()) + second := c.sidecar(newGameServer()) + + *first.SecurityContext.RunAsUser = 2000 + assert.Equal(t, int64(sidecarRunAsUser), *second.SecurityContext.RunAsUser) + assert.Equal(t, int64(sidecarRunAsUser), *c.sidecarSecurityContext.RunAsUser) + }) +} + // newFakeController returns a controller, backed by the fake Clientset func newFakeController() (*Controller, agtesting.Mocks) { m := agtesting.NewMocks() @@ -2560,7 +2603,7 @@ func newFakeController() (*Controller, agtesting.Mocks) { map[string]portallocator.PortRange{agonesv1.DefaultPortRange: {MinPort: 10, MaxPort: 20}}, "sidecar:dev", false, resource.MustParse("0.05"), resource.MustParse("0.1"), - resource.MustParse("50Mi"), resource.MustParse("100Mi"), sidecarRunAsUser, 500*time.Millisecond, "sdk-service-account", + resource.MustParse("50Mi"), resource.MustParse("100Mi"), DefaultSidecarSecurityContext(sidecarRunAsUser), 500*time.Millisecond, "sdk-service-account", m.KubeClient, m.KubeInformerFactory, m.ExtClient, m.AgonesClient, m.AgonesInformerFactory) c.recorder = m.FakeRecorder return c, m diff --git a/site/content/en/docs/Guides/Best Practices/_index.md b/site/content/en/docs/Guides/Best Practices/_index.md index 69be830a0b..f67f35dbc2 100644 --- a/site/content/en/docs/Guides/Best Practices/_index.md +++ b/site/content/en/docs/Guides/Best Practices/_index.md @@ -29,6 +29,27 @@ If you are collecting [Metrics]({{< relref "metrics" >}}) using our standard Pro See [Creating a Cluster]({{< relref "Creating Cluster" >}}) for initial set up on your cloud provider. +{{% feature publishVersion="1.61.0" %}} +## Pod Security Standards + +The Agones sdk sidecar container declares a security context that is compatible with the `restricted` +[Pod Security Standard](https://kubernetes.io/docs/concepts/security/pod-security-standards/) by default, so +`GameServers` can be run in namespaces that enforce it. Your game server container, and any other containers in the +`GameServer` Pod template, need to declare their own compliant security contexts. + +The `baseline` and `restricted` standards also forbid `hostPort`, which the `Dynamic`, `Static` and `Passthrough` +port policies rely on, so `GameServers` in these namespaces need to use the `None` +[port policy]({{< ref "/docs/Reference/gameserver.md" >}}). + +On [GKE Autopilot]({{< ref "/docs/Installation/Creating Cluster/gke.md" >}}), Agones sets the Pod seccomp profile to +`Unconfined` unless the `GameServer` Pod template sets one, which the `restricted` standard rejects. Set +`securityContext.seccompProfile.type: RuntimeDefault` on the Pod template to run in these namespaces. + +The sidecar security context can be changed through the `agones.image.sdk.securityContext` +[Helm value]({{< ref "/docs/Installation/Install Agones/helm.md#configuration" >}}), for example to use a different seccomp +profile or group. +{{% /feature %}} + ## Redundant Clusters ### Allocate Across Clusters diff --git a/site/content/en/docs/Installation/Install Agones/helm.md b/site/content/en/docs/Installation/Install Agones/helm.md index f990176338..1e7ad8edcb 100644 --- a/site/content/en/docs/Installation/Install Agones/helm.md +++ b/site/content/en/docs/Installation/Install Agones/helm.md @@ -149,6 +149,7 @@ The following tables lists the configurable parameters of the Agones chart and t ### Container Images +{{% feature expiryVersion="1.61.0" %}} | Parameter | Description | Default | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | `agones.image.registry` | Global image registry for all the Agones system images | `us-docker.pkg.dev/agones-images/release` | @@ -168,6 +169,29 @@ The following tables lists the configurable parameters of the Agones chart and t | `agones.image.ping.pullPolicy` | Image pull policy for the ping service | `IfNotPresent` | | `agones.image.extensions.name` | Image name for extensions | `agones-extensions` | | `agones.image.extensions.pullPolicy` | Image pull policy for extensions | `IfNotPresent` | +{{% /feature %}} +{{% feature publishVersion="1.61.0" %}} +| Parameter | Description | Default | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| `agones.image.registry` | Global image registry for all the Agones system images | `us-docker.pkg.dev/agones-images/release` | +| `agones.image.tag` | Global image tag for all images | `{{< release-version >}}` | +| `agones.image.controller.name` | Image name for the controller | `agones-controller` | +| `agones.image.controller.pullPolicy` | Image pull policy for the controller | `IfNotPresent` | +| `agones.image.controller.pullSecret` | Image pull secret for the controller, allocator, sdk and ping image. Should be created both in `agones-system` and `default` namespaces | \`\` | +| `agones.image.sdk.name` | Image name for the sdk | `agones-sdk` | +| `agones.image.sdk.tag` | Image tag for the sdk | value of `agones.image.tag` | +| `agones.image.sdk.cpuRequest` | The [cpu request][cpu-constraints] for sdk server container | `30m` | +| `agones.image.sdk.cpuLimit` | The [cpu limit][cpu-constraints] for the sdk server container | `0` (none) | +| `agones.image.sdk.memoryRequest` | The [memory request][memory-constraints] for sdk server container | `0` (none) | +| `agones.image.sdk.memoryLimit` | The [memory limit][memory-constraints] for the sdk server container | `0` (none) | +| `agones.image.sdk.alwaysPull` | Tells if the sdk image should always be pulled | `false` | +| `agones.image.sdk.securityContext` | The [security context][security-context] for the sdk server container, passed to the container as-is. The default is compatible with the `restricted` [Pod Security Standard][pod-security-standards]. Set a key to `null` to remove it. Example:
securityContext:
  runAsUser: 2000 | see `values.yaml` | +| `agones.image.ping.name` | Image name for the ping service | `agones-ping` | +| `agones.image.ping.tag` | Image tag for the ping service | value of `agones.image.tag` | +| `agones.image.ping.pullPolicy` | Image pull policy for the ping service | `IfNotPresent` | +| `agones.image.extensions.name` | Image name for extensions | `agones-extensions` | +| `agones.image.extensions.pullPolicy` | Image pull policy for extensions | `IfNotPresent` | +{{% /feature %}} ### Agones Controller @@ -412,6 +436,8 @@ The following tables lists the configurable parameters of the Agones chart and t [nodeSelector]: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector [affinity]: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity [cpu-constraints]: https://kubernetes.io/docs/tasks/administer-cluster/manage-resources/cpu-constraint-namespace/ +[security-context]: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ +[pod-security-standards]: https://kubernetes.io/docs/concepts/security/pod-security-standards/ [memory-constraints]: https://kubernetes.io/docs/tasks/administer-cluster/manage-resources/memory-constraint-namespace/ [ping]: {{< ref "/docs/Guides/ping-service.md" >}} [service]: https://kubernetes.io/docs/concepts/services-networking/service/ diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index dcba85e42f..540b3388f5 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -238,6 +238,9 @@ func (f *Framework) CreateGameServerAndWaitUntilReady(t *testing.T, ns string, g readyGs, err := f.WaitForGameServerState(t, newGs, agonesv1.GameServerStateReady, f.WaitForState) if err != nil { + if readyGs != nil { + f.LogEvents(t, log, ns, readyGs) + } return readyGs, fmt.Errorf("waiting for %v GameServer instance readiness timed out (%v): %w", gs.Spec, gs.Name, err) } diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index 90d1ccb7e1..122e3db988 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -22,6 +22,7 @@ import ( "net" "os" "os/exec" + "slices" "sort" "strconv" "strings" @@ -532,6 +533,60 @@ func TestGameServerUnhealthyAfterReadyCrashWithGenericContainer(t *testing.T) { }, 3*time.Minute, 5*time.Second) } +// TestGameServerRestrictedPodSecurity checks that a GameServer becomes Ready in a namespace that +// enforces the restricted Pod Security Standard, which requires the sdk sidecar container to +// declare a compliant security context. +func TestGameServerRestrictedPodSecurity(t *testing.T) { + t.Parallel() + ctx := context.Background() + + namespace := fmt.Sprintf("restricted-%s", rand.String(5)) + require.NoError(t, framework.CreateNamespace(namespace)) + defer func() { + if derr := framework.DeleteNamespace(namespace); derr != nil { + t.Error(derr) + } + }() + + ns, err := framework.KubeClient.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) + require.NoError(t, err) + ns.ObjectMeta.Labels["pod-security.kubernetes.io/enforce"] = "restricted" + _, err = framework.KubeClient.CoreV1().Namespaces().Update(ctx, ns, metav1.UpdateOptions{}) + require.NoError(t, err) + + gs := framework.DefaultGameServer(namespace) + // the restricted standard forbids hostPort, so the port must be PortPolicy None + gs.Spec.Ports[0] = agonesv1.GameServerPort{Name: "udp-port", PortPolicy: agonesv1.None, ContainerPort: 7654, Protocol: corev1.ProtocolUDP} + // a pod level seccomp profile, as GKE Autopilot otherwise defaults it to Unconfined + gs.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ + SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}, + } + gs.Spec.Template.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.To(false), + RunAsNonRoot: ptr.To(true), + RunAsUser: ptr.To(int64(1000)), + Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}, + SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}, + } + + readyGs, err := framework.CreateGameServerAndWaitUntilReady(t, namespace, gs) + require.NoError(t, err) + + pod, err := framework.KubeClient.CoreV1().Pods(namespace).Get(ctx, readyGs.ObjectMeta.Name, metav1.GetOptions{}) + require.NoError(t, err) + + containers := slices.Concat(pod.Spec.InitContainers, pod.Spec.Containers) + i := slices.IndexFunc(containers, func(c corev1.Container) bool { return c.Name == "agones-gameserver-sidecar" }) + require.NotEqual(t, -1, i, "sdk sidecar container not found") + + sc := containers[i].SecurityContext + require.NotNil(t, sc) + assert.False(t, *sc.AllowPrivilegeEscalation) + assert.True(t, *sc.RunAsNonRoot) + assert.Equal(t, []corev1.Capability{"ALL"}, sc.Capabilities.Drop) + assert.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, sc.SeccompProfile.Type) +} + func TestGameServerPodCompletedAfterCleanExit(t *testing.T) { if !runtime.FeatureEnabled(runtime.FeatureSidecarContainers) { t.SkipNow()