mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-04-26 13:34:45 +00:00
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/
fddcbb9cbf/keps/sig-node/24-apparmor/README.md
Went ahead and implemented API from KEP-24 above.
This commit is contained in:
parent
a5fa810bcc
commit
9bbc446009
6 changed files with 156 additions and 28 deletions
|
@ -70,9 +70,8 @@ func podName(step *types.Step) (string, error) {
|
||||||
|
|
||||||
func podMeta(step *types.Step, config *config, podName string) metav1.ObjectMeta {
|
func podMeta(step *types.Step, config *config, podName string) metav1.ObjectMeta {
|
||||||
meta := metav1.ObjectMeta{
|
meta := metav1.ObjectMeta{
|
||||||
Name: podName,
|
Name: podName,
|
||||||
Namespace: config.Namespace,
|
Namespace: config.Namespace,
|
||||||
Annotations: config.PodAnnotations,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
labels := make(map[string]string, len(config.PodLabels)+1)
|
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
|
labels[StepLabel] = step.Name
|
||||||
meta.Labels = labels
|
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
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +308,7 @@ func podSecurityContext(sc *types.SecurityContext, secCtxConf SecurityContextCon
|
||||||
user *int64
|
user *int64
|
||||||
group *int64
|
group *int64
|
||||||
fsGroup *int64
|
fsGroup *int64
|
||||||
|
seccomp *v1.SeccompProfile
|
||||||
)
|
)
|
||||||
|
|
||||||
if sc != nil && sc.RunAsNonRoot != nil {
|
if sc != nil && sc.RunAsNonRoot != nil {
|
||||||
|
@ -313,20 +325,41 @@ func podSecurityContext(sc *types.SecurityContext, secCtxConf SecurityContextCon
|
||||||
fsGroup = sc.FSGroup
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext := &v1.PodSecurityContext{
|
securityContext := &v1.PodSecurityContext{
|
||||||
RunAsNonRoot: nonRoot,
|
RunAsNonRoot: nonRoot,
|
||||||
RunAsUser: user,
|
RunAsUser: user,
|
||||||
RunAsGroup: group,
|
RunAsGroup: group,
|
||||||
FSGroup: fsGroup,
|
FSGroup: fsGroup,
|
||||||
|
SeccompProfile: seccomp,
|
||||||
}
|
}
|
||||||
log.Trace().Msgf("pod security context that will be used: %v", securityContext)
|
log.Trace().Msgf("pod security context that will be used: %v", securityContext)
|
||||||
return 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 {
|
func containerSecurityContext(sc *types.SecurityContext, stepPrivileged bool) *v1.SecurityContext {
|
||||||
var privileged *bool
|
var privileged *bool
|
||||||
|
|
||||||
|
@ -347,6 +380,36 @@ func containerSecurityContext(sc *types.SecurityContext, stepPrivileged bool) *v
|
||||||
return securityContext
|
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 {
|
func mapToEnvVars(m map[string]string) []v1.EnvVar {
|
||||||
var ev []v1.EnvVar
|
var ev []v1.EnvVar
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
|
|
|
@ -151,7 +151,8 @@ func TestFullPod(t *testing.T) {
|
||||||
"step": "go-test"
|
"step": "go-test"
|
||||||
},
|
},
|
||||||
"annotations": {
|
"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": {
|
"spec": {
|
||||||
|
@ -225,7 +226,11 @@ func TestFullPod(t *testing.T) {
|
||||||
"runAsUser": 101,
|
"runAsUser": 101,
|
||||||
"runAsGroup": 101,
|
"runAsGroup": 101,
|
||||||
"runAsNonRoot": true,
|
"runAsNonRoot": true,
|
||||||
"fsGroup": 101
|
"fsGroup": 101,
|
||||||
|
"seccompProfile": {
|
||||||
|
"type": "Localhost",
|
||||||
|
"localhostProfile": "profiles/audit.json"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"imagePullSecrets": [
|
"imagePullSecrets": [
|
||||||
{
|
{
|
||||||
|
@ -264,6 +269,21 @@ func TestFullPod(t *testing.T) {
|
||||||
{Name: "cloudflare", IP: "1.1.1.1"},
|
{Name: "cloudflare", IP: "1.1.1.1"},
|
||||||
{Name: "cf.v6", IP: "2606:4700:4700::64"},
|
{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{
|
pod, err := mkPod(&types.Step{
|
||||||
Name: "go-test",
|
Name: "go-test",
|
||||||
Image: "meltwater/drone-cache",
|
Image: "meltwater/drone-cache",
|
||||||
|
@ -283,20 +303,14 @@ func TestFullPod(t *testing.T) {
|
||||||
Requests: map[string]string{"memory": "128Mi", "cpu": "1000m"},
|
Requests: map[string]string{"memory": "128Mi", "cpu": "1000m"},
|
||||||
Limits: map[string]string{"memory": "256Mi", "cpu": "2"},
|
Limits: map[string]string{"memory": "256Mi", "cpu": "2"},
|
||||||
},
|
},
|
||||||
SecurityContext: &types.SecurityContext{
|
SecurityContext: &secCtx,
|
||||||
Privileged: newBool(true),
|
|
||||||
RunAsNonRoot: newBool(true),
|
|
||||||
RunAsUser: newInt64(101),
|
|
||||||
RunAsGroup: newInt64(101),
|
|
||||||
FSGroup: newInt64(101),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, &config{
|
}, &config{
|
||||||
Namespace: "woodpecker",
|
Namespace: "woodpecker",
|
||||||
ImagePullSecretNames: []string{"regcred", "another-pull-secret"},
|
ImagePullSecretNames: []string{"regcred", "another-pull-secret"},
|
||||||
PodLabels: map[string]string{"app": "test"},
|
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},
|
SecurityContext: SecurityContextConfig{RunAsNonRoot: false},
|
||||||
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64")
|
}, "wp-01he8bebctabr3kgk0qj36d2me-0", "linux/amd64")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -54,9 +54,23 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SecurityContext struct {
|
type SecurityContext struct {
|
||||||
Privileged *bool `json:"privileged,omitempty"`
|
Privileged *bool `json:"privileged,omitempty"`
|
||||||
RunAsNonRoot *bool `json:"runAsNonRoot,omitempty"`
|
RunAsNonRoot *bool `json:"runAsNonRoot,omitempty"`
|
||||||
RunAsUser *int64 `json:"runAsUser,omitempty"`
|
RunAsUser *int64 `json:"runAsUser,omitempty"`
|
||||||
RunAsGroup *int64 `json:"runAsGroup,omitempty"`
|
RunAsGroup *int64 `json:"runAsGroup,omitempty"`
|
||||||
FSGroup *int64 `json:"fsGroup,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"
|
||||||
|
)
|
||||||
|
|
|
@ -236,6 +236,18 @@ func convertKubernetesBackendOptions(kubeOpt *yaml_types.KubernetesBackendOption
|
||||||
RunAsGroup: kubeOpt.SecurityContext.RunAsGroup,
|
RunAsGroup: kubeOpt.SecurityContext.RunAsGroup,
|
||||||
FSGroup: kubeOpt.SecurityContext.FSGroup,
|
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{
|
return backend_types.KubernetesBackendOptions{
|
||||||
|
|
|
@ -729,6 +729,24 @@
|
||||||
},
|
},
|
||||||
"fsGroup": {
|
"fsGroup": {
|
||||||
"type": "number"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,9 +56,16 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SecurityContext struct {
|
type SecurityContext struct {
|
||||||
Privileged *bool `yaml:"privileged,omitempty"`
|
Privileged *bool `yaml:"privileged,omitempty"`
|
||||||
RunAsNonRoot *bool `yaml:"runAsNonRoot,omitempty"`
|
RunAsNonRoot *bool `yaml:"runAsNonRoot,omitempty"`
|
||||||
RunAsUser *int64 `yaml:"runAsUser,omitempty"`
|
RunAsUser *int64 `yaml:"runAsUser,omitempty"`
|
||||||
RunAsGroup *int64 `yaml:"runAsGroup,omitempty"`
|
RunAsGroup *int64 `yaml:"runAsGroup,omitempty"`
|
||||||
FSGroup *int64 `yaml:"fsGroup,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"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue