From e4f5753e97b4a6fba6a4f13c0e5730b8885830ea Mon Sep 17 00:00:00 2001 From: "Subham K." Date: Fri, 4 Sep 2026 17:37:37 -0400 Subject: [PATCH 1/6] feat: restricted-compatible security context for the sdk sidecar The sdk sidecar container now drops all capabilities and uses the RuntimeDefault seccomp profile by default, so GameServers can run in namespaces enforcing the restricted Pod Security Standard. The full sidecar security context is configurable through the agones.image.sdk.securityContext Helm value, passed to the controller as JSON via SIDECAR_SECURITY_CONTEXT / --sidecar-security-context. Closes #4668 Signed-off-by: Subham K. --- cmd/controller/main.go | 31 ++++++++++-- cmd/controller/main_test.go | 35 +++++++++++++ install/helm/agones/templates/controller.yaml | 4 +- install/helm/agones/values.yaml | 11 +++++ install/yaml/install.yaml | 4 +- pkg/gameservers/controller.go | 28 ++++++++--- pkg/gameservers/controller_test.go | 45 ++++++++++++++++- .../en/docs/Guides/Best Practices/_index.md | 13 +++++ .../docs/Installation/Install Agones/helm.md | 26 ++++++++++ test/e2e/gameserver_test.go | 49 +++++++++++++++++++ 10 files changed, 230 insertions(+), 16 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 8483ec4be2..658ebb8d05 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. Ignored if --sidecar-security-context is set. 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,20 @@ 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 + } + + sc := &corev1.SecurityContext{} + if err := json.Unmarshal([]byte(s), sc); err != nil { + return nil, fmt.Errorf("invalid sidecar security context format: %w", err) + } + return sc, nil +} + // config stores all required configuration to create a game server controller. type config struct { PortRanges map[string]portallocator.PortRange @@ -456,7 +479,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..9acf3f1acd 100644 --- a/cmd/controller/main_test.go +++ b/cmd/controller/main_test.go @@ -18,11 +18,46 @@ 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 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) + }) + + 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..f05804f174 100644 --- a/install/helm/agones/values.yaml +++ b/install/helm/agones/values.yaml @@ -345,6 +345,17 @@ agones: memoryRequest: 0 memoryLimit: 0 alwaysPull: false + # Security context for the sdk server sidecar container. + # The default is compatible with the "restricted" Pod Security Standard. + 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..82a2fbdd4a 100644 --- a/pkg/gameservers/controller.go +++ b/pkg/gameservers/controller.go @@ -65,6 +65,8 @@ const ( grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" httpPortEnvVar = "AGONES_SDK_HTTP_PORT" passthroughPortEnvVar = "PASSTHROUGH" + // defaultSidecarRunAsUser is the UID the sidecar runs as when no security context is configured + defaultSidecarRunAsUser = 1000 ) // Extensions struct contains what is needed to bind webhook handlers @@ -87,7 +89,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 +123,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 +137,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 +148,7 @@ func NewController( sidecarCPURequest: sidecarCPURequest, sidecarMemoryLimit: sidecarMemoryLimit, sidecarMemoryRequest: sidecarMemoryRequest, - sidecarRunAsUser: sidecarRunAsUser, + sidecarSecurityContext: sidecarSecurityContext, sidecarRequestsRateLimit: sidecarRequestsRateLimit, alwaysPullSidecarImage: alwaysPullSidecarImage, sdkServiceAccount: sdkServiceAccount, @@ -811,13 +817,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..f89336bdf3 100644 --- a/site/content/en/docs/Guides/Best Practices/_index.md +++ b/site/content/en/docs/Guides/Best Practices/_index.md @@ -29,6 +29,19 @@ 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 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/gameserver_test.go b/test/e2e/gameserver_test.go index 90d1ccb7e1..7e4626357e 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,54 @@ 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) + 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() From fe25556c0ed18fb69c717d8b114225997096bc07 Mon Sep 17 00:00:00 2001 From: "Subham K." Date: Fri, 4 Sep 2026 21:58:40 -0400 Subject: [PATCH 2/6] chore: remove redundant comments Signed-off-by: Subham K. --- install/helm/agones/values.yaml | 2 -- pkg/gameservers/controller.go | 9 ++++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/install/helm/agones/values.yaml b/install/helm/agones/values.yaml index f05804f174..88de9b13ea 100644 --- a/install/helm/agones/values.yaml +++ b/install/helm/agones/values.yaml @@ -345,8 +345,6 @@ agones: memoryRequest: 0 memoryLimit: 0 alwaysPull: false - # Security context for the sdk server sidecar container. - # The default is compatible with the "restricted" Pod Security Standard. securityContext: allowPrivilegeEscalation: false runAsNonRoot: true diff --git a/pkg/gameservers/controller.go b/pkg/gameservers/controller.go index 82a2fbdd4a..104df0ab87 100644 --- a/pkg/gameservers/controller.go +++ b/pkg/gameservers/controller.go @@ -61,11 +61,10 @@ import ( ) const ( - sdkserverSidecarName = "agones-gameserver-sidecar" - grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" - httpPortEnvVar = "AGONES_SDK_HTTP_PORT" - passthroughPortEnvVar = "PASSTHROUGH" - // defaultSidecarRunAsUser is the UID the sidecar runs as when no security context is configured + sdkserverSidecarName = "agones-gameserver-sidecar" + grpcPortEnvVar = "AGONES_SDK_GRPC_PORT" + httpPortEnvVar = "AGONES_SDK_HTTP_PORT" + passthroughPortEnvVar = "PASSTHROUGH" defaultSidecarRunAsUser = 1000 ) From 0d632b9fc3b18f222a41a7c289160a3257782fe7 Mon Sep 17 00:00:00 2001 From: "Subham K." Date: Sat, 5 Sep 2026 01:41:11 -0400 Subject: [PATCH 3/6] test: use PortPolicy None in the restricted pod security e2e test The baseline and restricted Pod Security Standards forbid hostPort, so the GameServer in the test cannot use a Dynamic port. Document the constraint, and clarify the --sidecar-run-as-user help text. Signed-off-by: Subham K. --- cmd/controller/main.go | 2 +- site/content/en/docs/Guides/Best Practices/_index.md | 4 ++++ test/e2e/gameserver_test.go | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 658ebb8d05..06d3308645 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -286,7 +286,7 @@ 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. Ignored if --sidecar-security-context is set. 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") diff --git a/site/content/en/docs/Guides/Best Practices/_index.md b/site/content/en/docs/Guides/Best Practices/_index.md index f89336bdf3..d1fa8353b0 100644 --- a/site/content/en/docs/Guides/Best Practices/_index.md +++ b/site/content/en/docs/Guides/Best Practices/_index.md @@ -37,6 +37,10 @@ The Agones sdk sidecar container declares a security context that is compatible `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" >}}). + 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. diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index 7e4626357e..a5ff3beb10 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -555,6 +555,8 @@ func TestGameServerRestrictedPodSecurity(t *testing.T) { 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} gs.Spec.Template.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{ AllowPrivilegeEscalation: ptr.To(false), RunAsNonRoot: ptr.To(true), From c663ccc32633d768d2eeaf258719037d271afa69 Mon Sep 17 00:00:00 2001 From: "Subham K." Date: Sat, 5 Sep 2026 21:02:08 -0400 Subject: [PATCH 4/6] test: set a pod level seccomp profile in the restricted pod security e2e test On GKE Autopilot, Agones defaults the Pod seccomp profile to Unconfined unless the template sets one, which the restricted Pod Security Standard rejects. Document this, and log GameServer events when a GameServer fails to become Ready in e2e tests. Signed-off-by: Subham K. --- site/content/en/docs/Guides/Best Practices/_index.md | 4 ++++ test/e2e/framework/framework.go | 3 +++ test/e2e/gameserver_test.go | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/site/content/en/docs/Guides/Best Practices/_index.md b/site/content/en/docs/Guides/Best Practices/_index.md index d1fa8353b0..f67f35dbc2 100644 --- a/site/content/en/docs/Guides/Best Practices/_index.md +++ b/site/content/en/docs/Guides/Best Practices/_index.md @@ -41,6 +41,10 @@ The `baseline` and `restricted` standards also forbid `hostPort`, which the `Dyn 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. 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 a5ff3beb10..122e3db988 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -557,6 +557,10 @@ func TestGameServerRestrictedPodSecurity(t *testing.T) { 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), From ee11fd56d3ab70e02d799939a2cefadab72a34ed Mon Sep 17 00:00:00 2001 From: "Subham K." Date: Mon, 7 Sep 2026 19:21:11 -0400 Subject: [PATCH 5/6] fix: fall back to the default sidecar security context when Helm renders null Signed-off-by: Subham K. --- cmd/controller/main.go | 8 ++++++-- cmd/controller/main_test.go | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 06d3308645..7f3079f44b 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -464,10 +464,14 @@ func parseSidecarSecurityContext(s string, runAsUser int64) (*corev1.SecurityCon return gameservers.DefaultSidecarSecurityContext(runAsUser), nil } - sc := &corev1.SecurityContext{} - if err := json.Unmarshal([]byte(s), sc); err != 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) } + // Helm renders an unset value as "null", which unmarshals to nil. + if sc == nil { + return gameservers.DefaultSidecarSecurityContext(runAsUser), nil + } return sc, nil } diff --git a/cmd/controller/main_test.go b/cmd/controller/main_test.go index 9acf3f1acd..e8a2ea426c 100644 --- a/cmd/controller/main_test.go +++ b/cmd/controller/main_test.go @@ -30,7 +30,7 @@ import ( func TestParseSidecarSecurityContext(t *testing.T) { t.Parallel() - t.Run("empty falls back to the default", func(t *testing.T) { + 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) @@ -38,6 +38,10 @@ func TestParseSidecarSecurityContext(t *testing.T) { 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) { From 37f6e25efc114b6a1ad35999fc863618a6b4313d Mon Sep 17 00:00:00 2001 From: "Subham K." Date: Mon, 7 Sep 2026 19:24:52 -0400 Subject: [PATCH 6/6] chore: drop redundant comment in parseSidecarSecurityContext Signed-off-by: Subham K. --- cmd/controller/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 7f3079f44b..c02b3475aa 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -468,7 +468,6 @@ func parseSidecarSecurityContext(s string, runAsUser int64) (*corev1.SecurityCon if err := json.Unmarshal([]byte(s), &sc); err != nil { return nil, fmt.Errorf("invalid sidecar security context format: %w", err) } - // Helm renders an unset value as "null", which unmarshals to nil. if sc == nil { return gameservers.DefaultSidecarSecurityContext(runAsUser), nil }