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"}`.
- `WOODPECKER_BACKEND_K8S_SECCTX_NONROOT` (default: `false`)
Determines if containers must be required to run as non-root users.
## Job specific configuration
### Resources

View file

@ -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",

View file

@ -61,6 +61,10 @@ type Config struct {
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
}

View file

@ -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,
}
}

View file

@ -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"`
}

View file

@ -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,
}
}

View file

@ -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
}

View file

@ -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"`
}