From 3adb98b28707fb1cea1c165e35094352b2457262 Mon Sep 17 00:00:00 2001 From: Thomas Anderson <127358482+zc-devs@users.noreply.github.com> Date: Sun, 26 Nov 2023 10:46:06 +0300 Subject: [PATCH] Simple security context options (Kubernetes) (#2550) --- .../22-backends/40-kubernetes.md | 4 + pipeline/backend/kubernetes/flags.go | 5 + pipeline/backend/kubernetes/kubernetes.go | 21 +- pipeline/backend/kubernetes/pod.go | 64 +++++- pipeline/backend/types/backend_kubernetes.go | 9 + pipeline/frontend/yaml/compiler/convert.go | 60 ++++-- .../frontend/yaml/linter/schema/schema.json | 198 ++++++++++++++---- .../frontend/yaml/types/backend_options.go | 9 + 8 files changed, 298 insertions(+), 72 deletions(-) diff --git a/docs/docs/30-administration/22-backends/40-kubernetes.md b/docs/docs/30-administration/22-backends/40-kubernetes.md index 2fc80dba2..a62c69cad 100644 --- a/docs/docs/30-administration/22-backends/40-kubernetes.md +++ b/docs/docs/30-administration/22-backends/40-kubernetes.md @@ -42,6 +42,10 @@ agent: Additional annotations to apply to worker pods. Must be a YAML object, e.g. `{"example.com/test-annotation":"test-value"}`. +- `WOODPECKER_BACKEND_K8S_SECCTX_NONROOT` (default: `false`) + + Determines if containers must be required to run as non-root users. + ## Job specific configuration ### Resources diff --git a/pipeline/backend/kubernetes/flags.go b/pipeline/backend/kubernetes/flags.go index 7a51e4291..6bb7ec1aa 100644 --- a/pipeline/backend/kubernetes/flags.go +++ b/pipeline/backend/kubernetes/flags.go @@ -57,6 +57,11 @@ var Flags = []cli.Flag{ Usage: "backend k8s additional worker pod annotations", Value: "", }, + &cli.BoolFlag{ + EnvVars: []string{"WOODPECKER_BACKEND_K8S_SECCTX_NONROOT"}, + Name: "backend-k8s-secctx-nonroot", + Usage: "`run as non root` Kubernetes security context option", + }, &cli.IntFlag{ EnvVars: []string{"WOODPECKER_CONNECT_RETRY_COUNT"}, Name: "connect-retry-count", diff --git a/pipeline/backend/kubernetes/kubernetes.go b/pipeline/backend/kubernetes/kubernetes.go index 2fcb1a760..216c8cb60 100644 --- a/pipeline/backend/kubernetes/kubernetes.go +++ b/pipeline/backend/kubernetes/kubernetes.go @@ -55,12 +55,16 @@ type kube struct { } type Config struct { - Namespace string - StorageClass string - VolumeSize string - StorageRwx bool - PodLabels map[string]string - PodAnnotations map[string]string + Namespace string + StorageClass string + VolumeSize string + StorageRwx bool + PodLabels map[string]string + PodAnnotations map[string]string + SecurityContext SecurityContextConfig +} +type SecurityContextConfig struct { + RunAsNonRoot bool } func configFromCliContext(ctx context.Context) (*Config, error) { @@ -73,6 +77,9 @@ func configFromCliContext(ctx context.Context) (*Config, error) { StorageRwx: c.Bool("backend-k8s-storage-rwx"), PodLabels: make(map[string]string), // just init empty map to prevent nil panic PodAnnotations: make(map[string]string), // just init empty map to prevent nil panic + SecurityContext: SecurityContextConfig{ + RunAsNonRoot: c.Bool("backend-k8s-secctx-nonroot"), + }, } // Unmarshal label and annotation settings here to ensure they're valid on startup if labels := c.String("backend-k8s-pod-labels"); labels != "" { @@ -191,7 +198,7 @@ func (e *kube) SetupWorkflow(ctx context.Context, conf *types.Config, taskUUID s // Start the pipeline step. func (e *kube) StartStep(ctx context.Context, step *types.Step, taskUUID string) error { - pod, err := Pod(e.config.Namespace, step, e.config.PodLabels, e.config.PodAnnotations, e.goos) + pod, err := Pod(e.config.Namespace, step, e.config.PodLabels, e.config.PodAnnotations, e.goos, e.config.SecurityContext) if err != nil { return err } diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index 0e9613448..c9d2fd43e 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -28,7 +28,7 @@ import ( "go.woodpecker-ci.org/woodpecker/pipeline/backend/types" ) -func Pod(namespace string, step *types.Step, labels, annotations map[string]string, goos string) (*v1.Pod, error) { +func Pod(namespace string, step *types.Step, labels, annotations map[string]string, goos string, secCtxConf SecurityContextConfig) (*v1.Pod, error) { var ( vols []v1.Volume volMounts []v1.VolumeMount @@ -142,6 +142,11 @@ func Pod(namespace string, step *types.Step, labels, annotations map[string]stri log.Trace().Msgf("Tolerations that will be used in the backend options: %v", beTolerations) } + beSecurityContext := step.BackendOptions.Kubernetes.SecurityContext + log.Trace().Interface("Security context", beSecurityContext).Msg("Security context that will be used for pods/containers") + podSecCtx := podSecurityContext(beSecurityContext, secCtxConf) + containerSecCtx := containerSecurityContext(beSecurityContext, step.Privileged) + pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, @@ -155,6 +160,7 @@ func Pod(namespace string, step *types.Step, labels, annotations map[string]stri NodeSelector: nodeSelector, Tolerations: tolerations, ServiceAccountName: serviceAccountName, + SecurityContext: podSecCtx, Containers: []v1.Container{{ Name: podName, Image: step.Image, @@ -165,9 +171,7 @@ func Pod(namespace string, step *types.Step, labels, annotations map[string]stri Env: mapToEnvVars(step.Environment), VolumeMounts: volMounts, Resources: resourceRequirements, - SecurityContext: &v1.SecurityContext{ - Privileged: &step.Privileged, - }, + SecurityContext: containerSecCtx, }}, ImagePullSecrets: []v1.LocalObjectReference{{Name: "regcred"}}, Volumes: vols, @@ -195,3 +199,55 @@ func volumeMountPath(i string) string { } return s[0] } + +func podSecurityContext(sc *types.SecurityContext, secCtxConf SecurityContextConfig) *v1.PodSecurityContext { + var ( + nonRoot *bool + user *int64 + group *int64 + fsGroup *int64 + ) + + if sc != nil && sc.RunAsNonRoot != nil { + if *sc.RunAsNonRoot { + nonRoot = sc.RunAsNonRoot // true + } + } else if secCtxConf.RunAsNonRoot { + nonRoot = &secCtxConf.RunAsNonRoot // true + } + + if sc != nil { + user = sc.RunAsUser + group = sc.RunAsGroup + fsGroup = sc.FSGroup + } + + if nonRoot == nil && user == nil && group == nil && fsGroup == nil { + return nil + } + + return &v1.PodSecurityContext{ + RunAsNonRoot: nonRoot, + RunAsUser: user, + RunAsGroup: group, + FSGroup: fsGroup, + } +} + +func containerSecurityContext(sc *types.SecurityContext, stepPrivileged bool) *v1.SecurityContext { + var privileged *bool + + if sc != nil && sc.Privileged != nil && *sc.Privileged { + privileged = sc.Privileged // true + } else if stepPrivileged { + privileged = &stepPrivileged // true + } + + if privileged == nil { + return nil + } + + return &v1.SecurityContext{ + Privileged: privileged, + } +} diff --git a/pipeline/backend/types/backend_kubernetes.go b/pipeline/backend/types/backend_kubernetes.go index 0b5947108..207764435 100644 --- a/pipeline/backend/types/backend_kubernetes.go +++ b/pipeline/backend/types/backend_kubernetes.go @@ -20,6 +20,7 @@ type KubernetesBackendOptions struct { ServiceAccountName string `json:"serviceAccountName,omitempty"` NodeSelector map[string]string `json:"nodeSelector,omitempty"` Tolerations []Toleration `json:"tolerations,omitempty"` + SecurityContext *SecurityContext `json:"securityContext,omitempty"` } // Resources defines two maps for kubernetes resource definitions @@ -51,3 +52,11 @@ const ( TolerationOpExists TolerationOperator = "Exists" TolerationOpEqual TolerationOperator = "Equal" ) + +type SecurityContext struct { + Privileged *bool `json:"privileged,omitempty"` + RunAsNonRoot *bool `json:"runAsNonRoot,omitempty"` + RunAsUser *int64 `json:"runAsUser,omitempty"` + RunAsGroup *int64 `json:"runAsGroup,omitempty"` + FSGroup *int64 `json:"fsGroup,omitempty"` +} diff --git a/pipeline/frontend/yaml/compiler/convert.go b/pipeline/frontend/yaml/compiler/convert.go index 3ec7ef46f..cda2de9fd 100644 --- a/pipeline/frontend/yaml/compiler/convert.go +++ b/pipeline/frontend/yaml/compiler/convert.go @@ -116,28 +116,9 @@ func (c *Compiler) createProcess(name string, container *yaml_types.Container, s } } - var tolerations []backend_types.Toleration - for _, t := range container.BackendOptions.Kubernetes.Tolerations { - tolerations = append(tolerations, backend_types.Toleration{ - Key: t.Key, - Operator: backend_types.TolerationOperator(t.Operator), - Value: t.Value, - Effect: backend_types.TaintEffect(t.Effect), - TolerationSeconds: t.TolerationSeconds, - }) - } - - // Kubernetes advanced settings + // Advanced backend settings backendOptions := backend_types.BackendOptions{ - Kubernetes: backend_types.KubernetesBackendOptions{ - Resources: backend_types.Resources{ - Limits: container.BackendOptions.Kubernetes.Resources.Limits, - Requests: container.BackendOptions.Kubernetes.Resources.Requests, - }, - ServiceAccountName: container.BackendOptions.Kubernetes.ServiceAccountName, - NodeSelector: container.BackendOptions.Kubernetes.NodeSelector, - Tolerations: tolerations, - }, + Kubernetes: convertKubernetesBackendOptions(&container.BackendOptions.Kubernetes), } memSwapLimit := int64(container.MemSwapLimit) @@ -223,3 +204,40 @@ func (c *Compiler) stepWorkdir(container *yaml_types.Container) string { } return path.Join(c.base, c.path, container.Directory) } + +func convertKubernetesBackendOptions(kubeOpt *yaml_types.KubernetesBackendOptions) backend_types.KubernetesBackendOptions { + resources := backend_types.Resources{ + Limits: kubeOpt.Resources.Limits, + Requests: kubeOpt.Resources.Requests, + } + + var tolerations []backend_types.Toleration + for _, t := range kubeOpt.Tolerations { + tolerations = append(tolerations, backend_types.Toleration{ + Key: t.Key, + Operator: backend_types.TolerationOperator(t.Operator), + Value: t.Value, + Effect: backend_types.TaintEffect(t.Effect), + TolerationSeconds: t.TolerationSeconds, + }) + } + + var securityContext *backend_types.SecurityContext + if kubeOpt.SecurityContext != nil { + securityContext = &backend_types.SecurityContext{ + Privileged: kubeOpt.SecurityContext.Privileged, + RunAsNonRoot: kubeOpt.SecurityContext.RunAsNonRoot, + RunAsUser: kubeOpt.SecurityContext.RunAsUser, + RunAsGroup: kubeOpt.SecurityContext.RunAsGroup, + FSGroup: kubeOpt.SecurityContext.FSGroup, + } + } + + return backend_types.KubernetesBackendOptions{ + Resources: resources, + ServiceAccountName: kubeOpt.ServiceAccountName, + NodeSelector: kubeOpt.NodeSelector, + Tolerations: tolerations, + SecurityContext: securityContext, + } +} diff --git a/pipeline/frontend/yaml/linter/schema/schema.json b/pipeline/frontend/yaml/linter/schema/schema.json index 2557cf182..1b8b8cd4e 100644 --- a/pipeline/frontend/yaml/linter/schema/schema.json +++ b/pipeline/frontend/yaml/linter/schema/schema.json @@ -14,28 +14,58 @@ "variables": { "description": "Use yaml aliases to define variables. Read more: https://woodpecker-ci.org/docs/usage/advanced-yaml-syntax" }, - "clone": { "$ref": "#/definitions/clone" }, - "skip_clone": { "type": "boolean" }, - "branches": { "$ref": "#/definitions/branches" }, - "when": { "$ref": "#/definitions/pipeline_when" }, - "steps": { "$ref": "#/definitions/step_list" }, - "pipeline": { "$ref": "#/definitions/step_list", "description": "deprecated, use steps" }, - "services": { "$ref": "#/definitions/services" }, - "workspace": { "$ref": "#/definitions/workspace" }, - "matrix": { "$ref": "#/definitions/matrix" }, - "platform": { "$ref": "#/definitions/platform" }, - "labels": { "$ref": "#/definitions/labels" }, + "clone": { + "$ref": "#/definitions/clone" + }, + "skip_clone": { + "type": "boolean" + }, + "branches": { + "$ref": "#/definitions/branches" + }, + "when": { + "$ref": "#/definitions/pipeline_when" + }, + "steps": { + "$ref": "#/definitions/step_list" + }, + "pipeline": { + "$ref": "#/definitions/step_list", + "description": "deprecated, use steps" + }, + "services": { + "$ref": "#/definitions/services" + }, + "workspace": { + "$ref": "#/definitions/workspace" + }, + "matrix": { + "$ref": "#/definitions/matrix" + }, + "platform": { + "$ref": "#/definitions/platform" + }, + "labels": { + "$ref": "#/definitions/labels" + }, "depends_on": { "type": "array", "minLength": 1, - "items": { "type": "string" } + "items": { + "type": "string" + } }, "runs_on": { "type": "array", "minLength": 1, - "items": { "type": "string" } + "items": { + "type": "string" + } }, - "version": { "type": "number", "default": 1 } + "version": { + "type": "number", + "default": 1 + } }, "definitions": { "clone": { @@ -74,20 +104,28 @@ "oneOf": [ { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "minLength": 1 }, - { "type": "string" } + { + "type": "string" + } ] }, "include": { "oneOf": [ { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "minLength": 1 }, - { "type": "string" } + { + "type": "string" + } ] } } @@ -97,8 +135,20 @@ "step_list": { "description": "The steps section defines a list of steps which will be executed serially, in the order in which they are defined. Read more: https://woodpecker-ci.org/docs/usage/pipeline-syntax", "oneOf": [ - { "type": "object", "additionalProperties": { "$ref": "#/definitions/step" }, "minProperties": 1 }, - { "type": "array", "items": { "$ref": "#/definitions/step" }, "minLength": 1 } + { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/step" + }, + "minProperties": 1 + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/step" + }, + "minLength": 1 + } ] }, "pipeline_when": { @@ -107,7 +157,9 @@ { "type": "array", "minLength": 1, - "items": { "$ref": "#/definitions/pipeline_when_condition" } + "items": { + "$ref": "#/definitions/pipeline_when_condition" + } }, { "$ref": "#/definitions/pipeline_when_condition" @@ -133,7 +185,9 @@ { "type": "array", "minLength": 1, - "items": { "$ref": "#/definitions/event_enum" } + "items": { + "$ref": "#/definitions/event_enum" + } }, { "$ref": "#/definitions/event_enum" @@ -163,7 +217,9 @@ "path": { "description": "Execute a step only on commit with certain files added/removed/modified. Read more: https://woodpecker-ci.org/docs/usage/pipeline-syntax#path", "oneOf": [ - { "type": "string" }, + { + "type": "string" + }, { "type": "array", "items": { @@ -264,7 +320,9 @@ { "type": "array", "minLength": 1, - "items": { "$ref": "#/definitions/step_when_condition" } + "items": { + "$ref": "#/definitions/step_when_condition" + } }, { "$ref": "#/definitions/step_when_condition" @@ -290,7 +348,9 @@ { "type": "array", "minLength": 1, - "items": { "$ref": "#/definitions/event_enum" } + "items": { + "$ref": "#/definitions/event_enum" + } }, { "$ref": "#/definitions/event_enum" @@ -311,7 +371,10 @@ { "type": "array", "minLength": 1, - "items": { "type": "string", "enum": ["success", "failure"] } + "items": { + "type": "string", + "enum": ["success", "failure"] + } }, { "type": "string", @@ -341,7 +404,9 @@ "path": { "description": "Execute a step only on commit with certain files added/removed/modified. Read more: https://woodpecker-ci.org/docs/usage/pipeline-syntax#path", "oneOf": [ - { "type": "string" }, + { + "type": "string" + }, { "type": "array", "items": { @@ -388,7 +453,9 @@ { "type": "array", "minLength": 1, - "items": { "type": "string" } + "items": { + "type": "string" + } }, { "type": "object", @@ -402,7 +469,9 @@ { "type": "array", "minLength": 1, - "items": { "type": "string" } + "items": { + "type": "string" + } } ] }, @@ -414,7 +483,9 @@ { "type": "array", "minLength": 1, - "items": { "type": "string" } + "items": { + "type": "string" + } } ] } @@ -440,10 +511,14 @@ "oneOf": [ { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "minLength": 1 }, - { "type": "string" } + { + "type": "string" + } ] }, "step_environment": { @@ -451,7 +526,9 @@ "oneOf": [ { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "minLength": 1 }, { @@ -467,13 +544,19 @@ "type": "array", "items": { "oneOf": [ - { "type": "string" }, + { + "type": "string" + }, { "type": "object", "required": ["source", "target"], "properties": { - "source": { "type": "string" }, - "target": { "type": "string" } + "source": { + "type": "string" + }, + "target": { + "type": "string" + } } } ] @@ -490,7 +573,9 @@ "step_volumes": { "description": "Mount files or folders from the host machine into your step container. Read more: https://woodpecker-ci.org/docs/usage/volumes", "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "minLength": 1 }, "step_directory": { @@ -502,7 +587,7 @@ "type": "object", "properties": { "kubernetes": { - "$ref": "#/definitions/step_backend_kubernetes_resources" + "$ref": "#/definitions/step_backend_kubernetes" } } }, @@ -512,6 +597,9 @@ "properties": { "resources": { "$ref": "#/definitions/step_backend_kubernetes_resources" + }, + "securityContext": { + "$ref": "#/definitions/step_backend_kubernetes_security_context" } } }, @@ -527,6 +615,27 @@ } } }, + "step_backend_kubernetes_security_context": { + "description": "Pods / containers security context. Read more: https://woodpecker-ci.org/docs/administration/backends/kubernetes", + "type": "object", + "properties": { + "privileged": { + "type": "boolean" + }, + "runAsNonRoot": { + "type": "boolean" + }, + "runAsUser": { + "type": "number" + }, + "runAsGroup": { + "type": "number" + }, + "fsGroup": { + "type": "number" + } + } + }, "step_kubernetes_resources_object": { "description": "A list of kubernetes resource mappings", "type": "object", @@ -556,7 +665,9 @@ "services": { "description": "Read more: https://woodpecker-ci.org/docs/usage/services", "type": "object", - "additionalProperties": { "$ref": "#/definitions/service" }, + "additionalProperties": { + "$ref": "#/definitions/service" + }, "minProperties": 1 }, "service": { @@ -597,7 +708,14 @@ "description": "expose ports to which other steps can connect to", "type": "array", "items": { - "oneOf": [{ "type": "number" }, { "type": "string" }] + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] }, "minLength": 1 } diff --git a/pipeline/frontend/yaml/types/backend_options.go b/pipeline/frontend/yaml/types/backend_options.go index 650b7de16..245f7e0d3 100644 --- a/pipeline/frontend/yaml/types/backend_options.go +++ b/pipeline/frontend/yaml/types/backend_options.go @@ -24,6 +24,7 @@ type KubernetesBackendOptions struct { ServiceAccountName string `yaml:"serviceAccountName,omitempty"` NodeSelector map[string]string `yaml:"nodeSelector,omitempty"` Tolerations []Toleration `yaml:"tolerations,omitempty"` + SecurityContext *SecurityContext `yaml:"securityContext,omitempty"` } type Resources struct { @@ -53,3 +54,11 @@ const ( TolerationOpExists TolerationOperator = "Exists" TolerationOpEqual TolerationOperator = "Equal" ) + +type SecurityContext struct { + Privileged *bool `yaml:"privileged,omitempty"` + RunAsNonRoot *bool `yaml:"runAsNonRoot,omitempty"` + RunAsUser *int64 `yaml:"runAsUser,omitempty"` + RunAsGroup *int64 `yaml:"runAsGroup,omitempty"` + FSGroup *int64 `yaml:"fsGroup,omitempty"` +}