Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Comment thread
GrowlyX marked this conversation as resolved.
Outdated
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")
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand Down Expand Up @@ -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
}
Comment thread
GrowlyX marked this conversation as resolved.

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
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions cmd/controller/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
4 changes: 2 additions & 2 deletions install/helm/agones/templates/controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions install/helm/agones/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions install/yaml/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 24 additions & 11 deletions pkg/gameservers/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -135,14 +136,18 @@ func NewController(
gameServers := agonesInformerFactory.Agones().V1().GameServers()
gsInformer := gameServers.Informer()

if sidecarSecurityContext == nil {
sidecarSecurityContext = DefaultSidecarSecurityContext(defaultSidecarRunAsUser)
}

c := &Controller{
controllerHooks: controllerHooks,
sidecarImage: sidecarImage,
sidecarCPULimit: sidecarCPULimit,
sidecarCPURequest: sidecarCPURequest,
sidecarMemoryLimit: sidecarMemoryLimit,
sidecarMemoryRequest: sidecarMemoryRequest,
sidecarRunAsUser: sidecarRunAsUser,
sidecarSecurityContext: sidecarSecurityContext,
sidecarRequestsRateLimit: sidecarRequestsRateLimit,
alwaysPullSidecarImage: alwaysPullSidecarImage,
sdkServiceAccount: sdkServiceAccount,
Expand Down Expand Up @@ -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
Expand Down
45 changes: 44 additions & 1 deletion pkg/gameservers/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions site/content/en/docs/Guides/Best Practices/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading