Simple security context options (Kubernetes) (#2550)

This commit is contained in:
Thomas Anderson 2023-11-26 10:46:06 +03:00 committed by GitHub
parent ffb3bd806c
commit 3adb98b287
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 298 additions and 72 deletions

View file

@ -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"}`. 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 ## Job specific configuration
### Resources ### Resources

View file

@ -57,6 +57,11 @@ var Flags = []cli.Flag{
Usage: "backend k8s additional worker pod annotations", Usage: "backend k8s additional worker pod annotations",
Value: "", 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{ &cli.IntFlag{
EnvVars: []string{"WOODPECKER_CONNECT_RETRY_COUNT"}, EnvVars: []string{"WOODPECKER_CONNECT_RETRY_COUNT"},
Name: "connect-retry-count", Name: "connect-retry-count",

View file

@ -61,6 +61,10 @@ type Config struct {
StorageRwx bool StorageRwx bool
PodLabels map[string]string PodLabels map[string]string
PodAnnotations map[string]string PodAnnotations map[string]string
SecurityContext SecurityContextConfig
}
type SecurityContextConfig struct {
RunAsNonRoot bool
} }
func configFromCliContext(ctx context.Context) (*Config, error) { 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"), StorageRwx: c.Bool("backend-k8s-storage-rwx"),
PodLabels: make(map[string]string), // just init empty map to prevent nil panic 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 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 // Unmarshal label and annotation settings here to ensure they're valid on startup
if labels := c.String("backend-k8s-pod-labels"); labels != "" { 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. // Start the pipeline step.
func (e *kube) StartStep(ctx context.Context, step *types.Step, taskUUID string) error { 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 { if err != nil {
return err return err
} }

View file

@ -28,7 +28,7 @@ import (
"go.woodpecker-ci.org/woodpecker/pipeline/backend/types" "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 ( var (
vols []v1.Volume vols []v1.Volume
volMounts []v1.VolumeMount 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) 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{ pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: podName, Name: podName,
@ -155,6 +160,7 @@ func Pod(namespace string, step *types.Step, labels, annotations map[string]stri
NodeSelector: nodeSelector, NodeSelector: nodeSelector,
Tolerations: tolerations, Tolerations: tolerations,
ServiceAccountName: serviceAccountName, ServiceAccountName: serviceAccountName,
SecurityContext: podSecCtx,
Containers: []v1.Container{{ Containers: []v1.Container{{
Name: podName, Name: podName,
Image: step.Image, Image: step.Image,
@ -165,9 +171,7 @@ func Pod(namespace string, step *types.Step, labels, annotations map[string]stri
Env: mapToEnvVars(step.Environment), Env: mapToEnvVars(step.Environment),
VolumeMounts: volMounts, VolumeMounts: volMounts,
Resources: resourceRequirements, Resources: resourceRequirements,
SecurityContext: &v1.SecurityContext{ SecurityContext: containerSecCtx,
Privileged: &step.Privileged,
},
}}, }},
ImagePullSecrets: []v1.LocalObjectReference{{Name: "regcred"}}, ImagePullSecrets: []v1.LocalObjectReference{{Name: "regcred"}},
Volumes: vols, Volumes: vols,
@ -195,3 +199,55 @@ func volumeMountPath(i string) string {
} }
return s[0] 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,
}
}

View file

@ -20,6 +20,7 @@ type KubernetesBackendOptions struct {
ServiceAccountName string `json:"serviceAccountName,omitempty"` ServiceAccountName string `json:"serviceAccountName,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"` NodeSelector map[string]string `json:"nodeSelector,omitempty"`
Tolerations []Toleration `json:"tolerations,omitempty"` Tolerations []Toleration `json:"tolerations,omitempty"`
SecurityContext *SecurityContext `json:"securityContext,omitempty"`
} }
// Resources defines two maps for kubernetes resource definitions // Resources defines two maps for kubernetes resource definitions
@ -51,3 +52,11 @@ const (
TolerationOpExists TolerationOperator = "Exists" TolerationOpExists TolerationOperator = "Exists"
TolerationOpEqual TolerationOperator = "Equal" 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"`
}

View file

@ -116,28 +116,9 @@ func (c *Compiler) createProcess(name string, container *yaml_types.Container, s
} }
} }
var tolerations []backend_types.Toleration // Advanced backend settings
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
backendOptions := backend_types.BackendOptions{ backendOptions := backend_types.BackendOptions{
Kubernetes: backend_types.KubernetesBackendOptions{ Kubernetes: convertKubernetesBackendOptions(&container.BackendOptions.Kubernetes),
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,
},
} }
memSwapLimit := int64(container.MemSwapLimit) 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) 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,
}
}

View file

@ -14,28 +14,58 @@
"variables": { "variables": {
"description": "Use yaml aliases to define variables. Read more: https://woodpecker-ci.org/docs/usage/advanced-yaml-syntax" "description": "Use yaml aliases to define variables. Read more: https://woodpecker-ci.org/docs/usage/advanced-yaml-syntax"
}, },
"clone": { "$ref": "#/definitions/clone" }, "clone": {
"skip_clone": { "type": "boolean" }, "$ref": "#/definitions/clone"
"branches": { "$ref": "#/definitions/branches" }, },
"when": { "$ref": "#/definitions/pipeline_when" }, "skip_clone": {
"steps": { "$ref": "#/definitions/step_list" }, "type": "boolean"
"pipeline": { "$ref": "#/definitions/step_list", "description": "deprecated, use steps" }, },
"services": { "$ref": "#/definitions/services" }, "branches": {
"workspace": { "$ref": "#/definitions/workspace" }, "$ref": "#/definitions/branches"
"matrix": { "$ref": "#/definitions/matrix" }, },
"platform": { "$ref": "#/definitions/platform" }, "when": {
"labels": { "$ref": "#/definitions/labels" }, "$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": { "depends_on": {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "type": "string" } "items": {
"type": "string"
}
}, },
"runs_on": { "runs_on": {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "type": "string" } "items": {
"type": "string"
}
}, },
"version": { "type": "number", "default": 1 } "version": {
"type": "number",
"default": 1
}
}, },
"definitions": { "definitions": {
"clone": { "clone": {
@ -74,20 +104,28 @@
"oneOf": [ "oneOf": [
{ {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"minLength": 1 "minLength": 1
}, },
{ "type": "string" } {
"type": "string"
}
] ]
}, },
"include": { "include": {
"oneOf": [ "oneOf": [
{ {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"minLength": 1 "minLength": 1
}, },
{ "type": "string" } {
"type": "string"
}
] ]
} }
} }
@ -97,8 +135,20 @@
"step_list": { "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", "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": [ "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": { "pipeline_when": {
@ -107,7 +157,9 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "$ref": "#/definitions/pipeline_when_condition" } "items": {
"$ref": "#/definitions/pipeline_when_condition"
}
}, },
{ {
"$ref": "#/definitions/pipeline_when_condition" "$ref": "#/definitions/pipeline_when_condition"
@ -133,7 +185,9 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "$ref": "#/definitions/event_enum" } "items": {
"$ref": "#/definitions/event_enum"
}
}, },
{ {
"$ref": "#/definitions/event_enum" "$ref": "#/definitions/event_enum"
@ -163,7 +217,9 @@
"path": { "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", "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": [ "oneOf": [
{ "type": "string" }, {
"type": "string"
},
{ {
"type": "array", "type": "array",
"items": { "items": {
@ -264,7 +320,9 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "$ref": "#/definitions/step_when_condition" } "items": {
"$ref": "#/definitions/step_when_condition"
}
}, },
{ {
"$ref": "#/definitions/step_when_condition" "$ref": "#/definitions/step_when_condition"
@ -290,7 +348,9 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "$ref": "#/definitions/event_enum" } "items": {
"$ref": "#/definitions/event_enum"
}
}, },
{ {
"$ref": "#/definitions/event_enum" "$ref": "#/definitions/event_enum"
@ -311,7 +371,10 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "type": "string", "enum": ["success", "failure"] } "items": {
"type": "string",
"enum": ["success", "failure"]
}
}, },
{ {
"type": "string", "type": "string",
@ -341,7 +404,9 @@
"path": { "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", "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": [ "oneOf": [
{ "type": "string" }, {
"type": "string"
},
{ {
"type": "array", "type": "array",
"items": { "items": {
@ -388,7 +453,9 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "type": "string" } "items": {
"type": "string"
}
}, },
{ {
"type": "object", "type": "object",
@ -402,7 +469,9 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "type": "string" } "items": {
"type": "string"
}
} }
] ]
}, },
@ -414,7 +483,9 @@
{ {
"type": "array", "type": "array",
"minLength": 1, "minLength": 1,
"items": { "type": "string" } "items": {
"type": "string"
}
} }
] ]
} }
@ -440,10 +511,14 @@
"oneOf": [ "oneOf": [
{ {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"minLength": 1 "minLength": 1
}, },
{ "type": "string" } {
"type": "string"
}
] ]
}, },
"step_environment": { "step_environment": {
@ -451,7 +526,9 @@
"oneOf": [ "oneOf": [
{ {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"minLength": 1 "minLength": 1
}, },
{ {
@ -467,13 +544,19 @@
"type": "array", "type": "array",
"items": { "items": {
"oneOf": [ "oneOf": [
{ "type": "string" }, {
"type": "string"
},
{ {
"type": "object", "type": "object",
"required": ["source", "target"], "required": ["source", "target"],
"properties": { "properties": {
"source": { "type": "string" }, "source": {
"target": { "type": "string" } "type": "string"
},
"target": {
"type": "string"
}
} }
} }
] ]
@ -490,7 +573,9 @@
"step_volumes": { "step_volumes": {
"description": "Mount files or folders from the host machine into your step container. Read more: https://woodpecker-ci.org/docs/usage/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", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"minLength": 1 "minLength": 1
}, },
"step_directory": { "step_directory": {
@ -502,7 +587,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"kubernetes": { "kubernetes": {
"$ref": "#/definitions/step_backend_kubernetes_resources" "$ref": "#/definitions/step_backend_kubernetes"
} }
} }
}, },
@ -512,6 +597,9 @@
"properties": { "properties": {
"resources": { "resources": {
"$ref": "#/definitions/step_backend_kubernetes_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": { "step_kubernetes_resources_object": {
"description": "A list of kubernetes resource mappings", "description": "A list of kubernetes resource mappings",
"type": "object", "type": "object",
@ -556,7 +665,9 @@
"services": { "services": {
"description": "Read more: https://woodpecker-ci.org/docs/usage/services", "description": "Read more: https://woodpecker-ci.org/docs/usage/services",
"type": "object", "type": "object",
"additionalProperties": { "$ref": "#/definitions/service" }, "additionalProperties": {
"$ref": "#/definitions/service"
},
"minProperties": 1 "minProperties": 1
}, },
"service": { "service": {
@ -597,7 +708,14 @@
"description": "expose ports to which other steps can connect to", "description": "expose ports to which other steps can connect to",
"type": "array", "type": "array",
"items": { "items": {
"oneOf": [{ "type": "number" }, { "type": "string" }] "oneOf": [
{
"type": "number"
},
{
"type": "string"
}
]
}, },
"minLength": 1 "minLength": 1
} }

View file

@ -24,6 +24,7 @@ type KubernetesBackendOptions struct {
ServiceAccountName string `yaml:"serviceAccountName,omitempty"` ServiceAccountName string `yaml:"serviceAccountName,omitempty"`
NodeSelector map[string]string `yaml:"nodeSelector,omitempty"` NodeSelector map[string]string `yaml:"nodeSelector,omitempty"`
Tolerations []Toleration `yaml:"tolerations,omitempty"` Tolerations []Toleration `yaml:"tolerations,omitempty"`
SecurityContext *SecurityContext `yaml:"securityContext,omitempty"`
} }
type Resources struct { type Resources struct {
@ -53,3 +54,11 @@ const (
TolerationOpExists TolerationOperator = "Exists" TolerationOpExists TolerationOperator = "Exists"
TolerationOpEqual TolerationOperator = "Equal" 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"`
}