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()