From 9bbc4460093eb403ff37ae349f3f6cc37c69a684 Mon Sep 17 00:00:00 2001 From: Thomas Anderson <127358482+zc-devs@users.noreply.github.com> Date: Sat, 13 Jan 2024 01:32:24 +0300 Subject: [PATCH] Kubernetes AppArmor and seccomp (#3123) Closes #2545 seccomp https://kubernetes.io/docs/tutorials/security/seccomp/ https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/135-seccomp/README.md AppArmor https://kubernetes.io/docs/tutorials/security/apparmor/ https://github.com/kubernetes/enhancements/blob/fddcbb9cbf3df39ded03bad71228265ac6e5215f/keps/sig-node/24-apparmor/README.md Went ahead and implemented API from KEP-24 above. --- pipeline/backend/kubernetes/pod.go | 79 +++++++++++++++++-- pipeline/backend/kubernetes/pod_test.go | 34 +++++--- pipeline/backend/types/backend_kubernetes.go | 24 ++++-- pipeline/frontend/yaml/compiler/convert.go | 12 +++ .../frontend/yaml/linter/schema/schema.json | 18 +++++ .../frontend/yaml/types/backend_options.go | 17 ++-- 6 files changed, 156 insertions(+), 28 deletions(-) diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index 5d44a3d960..bddc775750 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -70,9 +70,8 @@ func podName(step *types.Step) (string, error) { func podMeta(step *types.Step, config *config, podName string) metav1.ObjectMeta { meta := metav1.ObjectMeta{ - Name: podName, - Namespace: config.Namespace, - Annotations: config.PodAnnotations, + Name: podName, + Namespace: config.Namespace, } labels := make(map[string]string, len(config.PodLabels)+1) @@ -81,6 +80,18 @@ func podMeta(step *types.Step, config *config, podName string) metav1.ObjectMeta labels[StepLabel] = step.Name meta.Labels = labels + // copy to not alter the engine config + meta.Annotations = make(map[string]string, len(config.PodAnnotations)) + maps.Copy(meta.Annotations, config.PodAnnotations) + + securityContext := step.BackendOptions.Kubernetes.SecurityContext + if securityContext != nil { + key, value := apparmorAnnotation(podName, securityContext.ApparmorProfile) + if key != nil && value != nil { + meta.Annotations[*key] = *value + } + } + return meta } @@ -297,6 +308,7 @@ func podSecurityContext(sc *types.SecurityContext, secCtxConf SecurityContextCon user *int64 group *int64 fsGroup *int64 + seccomp *v1.SeccompProfile ) if sc != nil && sc.RunAsNonRoot != nil { @@ -313,20 +325,41 @@ func podSecurityContext(sc *types.SecurityContext, secCtxConf SecurityContextCon fsGroup = sc.FSGroup } - if nonRoot == nil && user == nil && group == nil && fsGroup == nil { + if sc != nil { + seccomp = seccompProfile(sc.SeccompProfile) + } + + if nonRoot == nil && user == nil && group == nil && fsGroup == nil && seccomp == nil { return nil } securityContext := &v1.PodSecurityContext{ - RunAsNonRoot: nonRoot, - RunAsUser: user, - RunAsGroup: group, - FSGroup: fsGroup, + RunAsNonRoot: nonRoot, + RunAsUser: user, + RunAsGroup: group, + FSGroup: fsGroup, + SeccompProfile: seccomp, } log.Trace().Msgf("pod security context that will be used: %v", securityContext) return securityContext } +func seccompProfile(scp *types.SecProfile) *v1.SeccompProfile { + if scp == nil || len(scp.Type) == 0 { + return nil + } + log.Trace().Msgf("using seccomp profile: %v", scp) + + seccompProfile := &v1.SeccompProfile{ + Type: v1.SeccompProfileType(scp.Type), + } + if len(scp.LocalhostProfile) > 0 { + seccompProfile.LocalhostProfile = &scp.LocalhostProfile + } + + return seccompProfile +} + func containerSecurityContext(sc *types.SecurityContext, stepPrivileged bool) *v1.SecurityContext { var privileged *bool @@ -347,6 +380,36 @@ func containerSecurityContext(sc *types.SecurityContext, stepPrivileged bool) *v return securityContext } +func apparmorAnnotation(containerName string, scp *types.SecProfile) (*string, *string) { + if scp == nil { + return nil, nil + } + log.Trace().Msgf("using AppArmor profile: %v", scp) + + var ( + profileType string + profilePath string + ) + + if scp.Type == types.SecProfileTypeRuntimeDefault { + profileType = "runtime" + profilePath = "default" + } + + if scp.Type == types.SecProfileTypeLocalhost { + profileType = "localhost" + profilePath = scp.LocalhostProfile + } + + if len(profileType) == 0 { + return nil, nil + } + + key := v1.AppArmorBetaContainerAnnotationKeyPrefix + containerName + value := profileType + "/" + profilePath + return &key, &value +} + func mapToEnvVars(m map[string]string) []v1.EnvVar { var ev []v1.EnvVar for k, v := range m { diff --git a/pipeline/backend/kubernetes/pod_test.go b/pipeline/backend/kubernetes/pod_test.go index b964652c28..5f8b68272a 100644 --- a/pipeline/backend/kubernetes/pod_test.go +++ b/pipeline/backend/kubernetes/pod_test.go @@ -151,7 +151,8 @@ func TestFullPod(t *testing.T) { "step": "go-test" }, "annotations": { - "apparmor.security": "runtime/default" + "apps.kubernetes.io/pod-index": "0", + "container.apparmor.security.beta.kubernetes.io/wp-01he8bebctabr3kgk0qj36d2me-0": "localhost/k8s-apparmor-example-deny-write" } }, "spec": { @@ -225,7 +226,11 @@ func TestFullPod(t *testing.T) { "runAsUser": 101, "runAsGroup": 101, "runAsNonRoot": true, - "fsGroup": 101 + "fsGroup": 101, + "seccompProfile": { + "type": "Localhost", + "localhostProfile": "profiles/audit.json" + } }, "imagePullSecrets": [ { @@ -264,6 +269,21 @@ func TestFullPod(t *testing.T) { {Name: "cloudflare", IP: "1.1.1.1"}, {Name: "cf.v6", IP: "2606:4700:4700::64"}, } + secCtx := types.SecurityContext{ + Privileged: newBool(true), + RunAsNonRoot: newBool(true), + RunAsUser: newInt64(101), + RunAsGroup: newInt64(101), + FSGroup: newInt64(101), + SeccompProfile: &types.SecProfile{ + Type: "Localhost", + LocalhostProfile: "profiles/audit.json", + }, + ApparmorProfile: &types.SecProfile{ + Type: "Localhost", + LocalhostProfile: "k8s-apparmor-example-deny-write", + }, + } pod, err := mkPod(&types.Step{ Name: "go-test", Image: "meltwater/drone-cache", @@ -283,20 +303,14 @@ func TestFullPod(t *testing.T) { Requests: map[string]string{"memory": "128Mi", "cpu": "1000m"}, Limits: map[string]string{"memory": "256Mi", "cpu": "2"}, }, - SecurityContext: &types.SecurityContext{ - Privileged: newBool(true), - RunAsNonRoot: newBool(true), - RunAsUser: newInt64(101), - RunAsGroup: newInt64(101), - FSGroup: newInt64(101), - }, + SecurityContext: &secCtx, }, }, }, &config{ Namespace: "woodpecker", ImagePullSecretNames: []string{"regcred", "another-pull-secret"}, PodLabels: map[string]string{"app": "test"}, - PodAnnotations: map[string]string{"apparmor.security": "runtime/default"}, + PodAnnotations: map[string]string{"apps.kubernetes.io/pod-index": "0"}, SecurityContext: SecurityContextConfig{RunAsNonRoot: false}, }, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64") assert.NoError(t, err) diff --git a/pipeline/backend/types/backend_kubernetes.go b/pipeline/backend/types/backend_kubernetes.go index 2077644350..0c1a85ec1b 100644 --- a/pipeline/backend/types/backend_kubernetes.go +++ b/pipeline/backend/types/backend_kubernetes.go @@ -54,9 +54,23 @@ const ( ) 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"` + 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"` + SeccompProfile *SecProfile `json:"seccompProfile,omitempty"` + ApparmorProfile *SecProfile `json:"apparmorProfile,omitempty"` } + +type SecProfile struct { + Type SecProfileType `json:"type,omitempty"` + LocalhostProfile string `json:"localhostProfile,omitempty"` +} + +type SecProfileType string + +const ( + SecProfileTypeRuntimeDefault SecProfileType = "RuntimeDefault" + SecProfileTypeLocalhost SecProfileType = "Localhost" +) diff --git a/pipeline/frontend/yaml/compiler/convert.go b/pipeline/frontend/yaml/compiler/convert.go index 4044eb9f16..b718fbf4e0 100644 --- a/pipeline/frontend/yaml/compiler/convert.go +++ b/pipeline/frontend/yaml/compiler/convert.go @@ -236,6 +236,18 @@ func convertKubernetesBackendOptions(kubeOpt *yaml_types.KubernetesBackendOption RunAsGroup: kubeOpt.SecurityContext.RunAsGroup, FSGroup: kubeOpt.SecurityContext.FSGroup, } + if kubeOpt.SecurityContext.SeccompProfile != nil { + securityContext.SeccompProfile = &backend_types.SecProfile{ + Type: backend_types.SecProfileType(kubeOpt.SecurityContext.SeccompProfile.Type), + LocalhostProfile: kubeOpt.SecurityContext.SeccompProfile.LocalhostProfile, + } + } + if kubeOpt.SecurityContext.ApparmorProfile != nil { + securityContext.ApparmorProfile = &backend_types.SecProfile{ + Type: backend_types.SecProfileType(kubeOpt.SecurityContext.ApparmorProfile.Type), + LocalhostProfile: kubeOpt.SecurityContext.ApparmorProfile.LocalhostProfile, + } + } } return backend_types.KubernetesBackendOptions{ diff --git a/pipeline/frontend/yaml/linter/schema/schema.json b/pipeline/frontend/yaml/linter/schema/schema.json index 37a99bdfc0..bdb4cccf79 100644 --- a/pipeline/frontend/yaml/linter/schema/schema.json +++ b/pipeline/frontend/yaml/linter/schema/schema.json @@ -729,6 +729,24 @@ }, "fsGroup": { "type": "number" + }, + "seccompProfile": { + "$ref": "#/definitions/step_backend_kubernetes_secprofile" + }, + "apparmorProfile": { + "$ref": "#/definitions/step_backend_kubernetes_secprofile" + } + } + }, + "step_backend_kubernetes_secprofile": { + "description": "Pods / containers security profile. Read more: https://woodpecker-ci.org/docs/administration/backends/kubernetes", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "localhostProfile": { + "type": "string" } } }, diff --git a/pipeline/frontend/yaml/types/backend_options.go b/pipeline/frontend/yaml/types/backend_options.go index 245f7e0d3c..5c1a1b7a9c 100644 --- a/pipeline/frontend/yaml/types/backend_options.go +++ b/pipeline/frontend/yaml/types/backend_options.go @@ -56,9 +56,16 @@ const ( ) 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"` + 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"` + SeccompProfile *SecProfile `yaml:"seccompProfile,omitempty"` + ApparmorProfile *SecProfile `yaml:"apparmorProfile,omitempty"` +} + +type SecProfile struct { + Type string `yaml:"type,omitempty"` + LocalhostProfile string `yaml:"localhostProfile,omitempty"` }