Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kubernetes AppArmor and seccomp #3123

Merged
merged 5 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
79 changes: 71 additions & 8 deletions pipeline/backend/kubernetes/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@

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)
Expand All @@ -81,6 +80,18 @@
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
}

Expand Down Expand Up @@ -297,6 +308,7 @@
user *int64
group *int64
fsGroup *int64
seccomp *v1.SeccompProfile
)

if sc != nil && sc.RunAsNonRoot != nil {
Expand All @@ -313,20 +325,41 @@
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
}

Check warning on line 350 in pipeline/backend/kubernetes/pod.go

View check run for this annotation

Codecov / codecov/patch

pipeline/backend/kubernetes/pod.go#L349-L350

Added lines #L349 - L350 were not covered by tests
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

Expand All @@ -347,6 +380,36 @@
return securityContext
}

func apparmorAnnotation(containerName string, scp *types.SecProfile) (*string, *string) {
if scp == nil {
return nil, nil
}

Check warning on line 386 in pipeline/backend/kubernetes/pod.go

View check run for this annotation

Codecov / codecov/patch

pipeline/backend/kubernetes/pod.go#L385-L386

Added lines #L385 - L386 were not covered by tests
log.Trace().Msgf("using AppArmor profile: %v", scp)

var (
profileType string
profilePath string
)

if scp.Type == types.SecProfileTypeRuntimeDefault {
profileType = "runtime"
profilePath = "default"
}

Check warning on line 397 in pipeline/backend/kubernetes/pod.go

View check run for this annotation

Codecov / codecov/patch

pipeline/backend/kubernetes/pod.go#L395-L397

Added lines #L395 - L397 were not covered by tests

if scp.Type == types.SecProfileTypeLocalhost {
profileType = "localhost"
profilePath = scp.LocalhostProfile
}

if len(profileType) == 0 {
return nil, nil
}

Check warning on line 406 in pipeline/backend/kubernetes/pod.go

View check run for this annotation

Codecov / codecov/patch

pipeline/backend/kubernetes/pod.go#L405-L406

Added lines #L405 - L406 were not covered by tests

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 {
Expand Down
34 changes: 24 additions & 10 deletions pipeline/backend/kubernetes/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand Down
24 changes: 19 additions & 5 deletions pipeline/backend/types/backend_kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
12 changes: 12 additions & 0 deletions pipeline/frontend/yaml/compiler/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,18 @@
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,
}
}

Check warning on line 250 in pipeline/frontend/yaml/compiler/convert.go

View check run for this annotation

Codecov / codecov/patch

pipeline/frontend/yaml/compiler/convert.go#L239-L250

Added lines #L239 - L250 were not covered by tests
}

return backend_types.KubernetesBackendOptions{
Expand Down
18 changes: 18 additions & 0 deletions pipeline/frontend/yaml/linter/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
17 changes: 12 additions & 5 deletions pipeline/frontend/yaml/types/backend_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}