Skip to content

Commit

Permalink
Policies
Browse files Browse the repository at this point in the history
Signed-off-by: Philippe Martin <phmartin@redhat.com>
  • Loading branch information
feloy committed Feb 21, 2023
1 parent 27ace5e commit a7af25b
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 1 deletion.
5 changes: 4 additions & 1 deletion pkg/devfile/generator/generators.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ func GetPodTemplateSpec(devfileObj parser.DevfileObj, podTemplateParams PodTempl
return nil, err
}

// TODO: apply here patches for Pod Security Admission
podTemplateSpec, err = patchForPolicy(podTemplateSpec, podTemplateParams.PodSecurityAdmissionPolicy)
if err != nil {
return nil, err
}

if needsPodOverrides(globalAttributes, components) {
patchedPodTemplateSpec, err := applyPodOverrides(globalAttributes, components, podTemplateSpec)
Expand Down
197 changes: 197 additions & 0 deletions pkg/devfile/generator/generators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/pod-security-admission/api"
"k8s.io/utils/pointer"
)

Expand Down Expand Up @@ -1606,6 +1607,202 @@ func TestGetPodTemplateSpec(t *testing.T) {
},
},
},
{
name: "Restricted policy",
args: args{
devfileObj: func(ctrl *gomock.Controller) parser.DevfileObj {
containers := []v1alpha2.Component{
{
Name: "main",
ComponentUnion: v1.ComponentUnion{
Container: &v1.ContainerComponent{
Container: v1.Container{
Image: "an-image",
},
},
},
},
}
events := v1alpha2.Events{}
mockDevfileData := data.NewMockDevfileData(ctrl)
mockDevfileData.EXPECT().GetComponents(gomock.Any()).Return(containers, nil).AnyTimes()
mockDevfileData.EXPECT().GetDevfileContainerComponents(gomock.Any()).Return(containers, nil).AnyTimes()
mockDevfileData.EXPECT().GetEvents().Return(events).AnyTimes()
mockDevfileData.EXPECT().GetProjects(gomock.Any()).Return(nil, nil).AnyTimes()
mockDevfileData.EXPECT().GetAttributes().Return(attributes.Attributes{
PodOverridesAttribute: apiext.JSON{Raw: []byte("{\"spec\": {\"securityContext\": {\"seccompProfile\": {\"type\": \"Localhost\"}}}}")},
}, nil)

mockDevfileData.EXPECT().GetSchemaVersion().Return("2.1.0").AnyTimes()
return parser.DevfileObj{
Data: mockDevfileData,
}
},
podTemplateParams: PodTemplateParams{
PodSecurityAdmissionPolicy: api.Policy{
Enforce: api.LevelVersion{
Level: api.LevelRestricted,
Version: api.MajorMinorVersion(1, 25),
},
},
},
},
want: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: pointer.Bool(true),
SeccompProfile: &corev1.SeccompProfile{
Type: "Localhost",
},
},
Containers: []corev1.Container{
{
Name: "main",
Image: "an-image",
Env: []corev1.EnvVar{
{Name: "PROJECTS_ROOT", Value: "/projects"},
{Name: "PROJECT_SOURCE", Value: "/projects"},
},
ImagePullPolicy: corev1.PullAlways,
Ports: []corev1.ContainerPort{},
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: pointer.Bool(false),
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{
"ALL",
},
},
},
},
},
InitContainers: []corev1.Container{},
},
},
},
{
name: "Restricted policy and pod override",
args: args{
devfileObj: func(ctrl *gomock.Controller) parser.DevfileObj {
containers := []v1alpha2.Component{
{
Name: "main",
ComponentUnion: v1.ComponentUnion{
Container: &v1.ContainerComponent{
Container: v1.Container{
Image: "an-image",
},
},
},
},
}
events := v1alpha2.Events{}
mockDevfileData := data.NewMockDevfileData(ctrl)
mockDevfileData.EXPECT().GetComponents(gomock.Any()).Return(containers, nil).AnyTimes()
mockDevfileData.EXPECT().GetDevfileContainerComponents(gomock.Any()).Return(containers, nil).AnyTimes()
mockDevfileData.EXPECT().GetEvents().Return(events).AnyTimes()
mockDevfileData.EXPECT().GetProjects(gomock.Any()).Return(nil, nil).AnyTimes()
mockDevfileData.EXPECT().GetAttributes().Return(attributes.Attributes{}, nil)
mockDevfileData.EXPECT().GetSchemaVersion().Return("2.1.0").AnyTimes()
return parser.DevfileObj{
Data: mockDevfileData,
}
},
podTemplateParams: PodTemplateParams{
PodSecurityAdmissionPolicy: api.Policy{
Enforce: api.LevelVersion{
Level: api.LevelRestricted,
Version: api.MajorMinorVersion(1, 25),
},
},
},
},
want: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: pointer.Bool(true),
SeccompProfile: &corev1.SeccompProfile{
Type: "RuntimeDefault",
},
},
Containers: []corev1.Container{
{
Name: "main",
Image: "an-image",
Env: []corev1.EnvVar{
{Name: "PROJECTS_ROOT", Value: "/projects"},
{Name: "PROJECT_SOURCE", Value: "/projects"},
},
ImagePullPolicy: corev1.PullAlways,
Ports: []corev1.ContainerPort{},
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: pointer.Bool(false),
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{
"ALL",
},
},
},
},
},
InitContainers: []corev1.Container{},
},
},
},
{
name: "Baseline policy",
args: args{
devfileObj: func(ctrl *gomock.Controller) parser.DevfileObj {
containers := []v1alpha2.Component{
{
Name: "main",
ComponentUnion: v1.ComponentUnion{
Container: &v1.ContainerComponent{
Container: v1.Container{
Image: "an-image",
},
},
},
},
}
events := v1alpha2.Events{}
mockDevfileData := data.NewMockDevfileData(ctrl)
mockDevfileData.EXPECT().GetComponents(gomock.Any()).Return(containers, nil).AnyTimes()
mockDevfileData.EXPECT().GetDevfileContainerComponents(gomock.Any()).Return(containers, nil).AnyTimes()
mockDevfileData.EXPECT().GetEvents().Return(events).AnyTimes()
mockDevfileData.EXPECT().GetProjects(gomock.Any()).Return(nil, nil).AnyTimes()
mockDevfileData.EXPECT().GetAttributes().Return(attributes.Attributes{}, nil)
mockDevfileData.EXPECT().GetSchemaVersion().Return("2.1.0").AnyTimes()
return parser.DevfileObj{
Data: mockDevfileData,
}
},
podTemplateParams: PodTemplateParams{
PodSecurityAdmissionPolicy: api.Policy{
Enforce: api.LevelVersion{
Level: api.LevelBaseline,
Version: api.MajorMinorVersion(1, 25),
},
},
},
},
want: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Image: "an-image",
Env: []corev1.EnvVar{
{Name: "PROJECTS_ROOT", Value: "/projects"},
{Name: "PROJECT_SOURCE", Value: "/projects"},
},
ImagePullPolicy: corev1.PullAlways,
Ports: []corev1.ContainerPort{},
},
},
InitContainers: []corev1.Container{},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
131 changes: 131 additions & 0 deletions pkg/devfile/generator/policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//
// Copyright 2023 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package generator

import (
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/pod-security-admission/api"
psaapi "k8s.io/pod-security-admission/api"
psapolicy "k8s.io/pod-security-admission/policy"
"k8s.io/utils/pointer"
)

// ContainerVisitor is called with each container
type ContainerVisitor func(container *corev1.Container)

// visitContainers invokes the visitor function for every container in the given pod template spec
func visitContainers(podTemplateSpec *corev1.PodTemplateSpec, visitor ContainerVisitor) {
for i := range podTemplateSpec.Spec.InitContainers {
visitor(&podTemplateSpec.Spec.InitContainers[i])
}
for i := range podTemplateSpec.Spec.Containers {
visitor(&podTemplateSpec.Spec.Containers[i])
}
for i := range podTemplateSpec.Spec.EphemeralContainers {
visitor((*corev1.Container)(&podTemplateSpec.Spec.EphemeralContainers[i].EphemeralContainerCommon))
}
}

func patchForPolicy(podTemplateSpec *corev1.PodTemplateSpec, policy psaapi.Policy) (*corev1.PodTemplateSpec, error) {
evaluator, err := psapolicy.NewEvaluator(psapolicy.DefaultChecks())
if err != nil {
return nil, err
}
results := evaluator.EvaluatePod(policy.Enforce, &podTemplateSpec.ObjectMeta, &podTemplateSpec.Spec)
for _, result := range results {
if result.Allowed {
continue
}
switch result.ForbiddenReason {
case "allowPrivilegeEscalation != false":
podTemplateSpec = patchAllowPrivilegeEscalation(podTemplateSpec)
case "unrestricted capabilities":
podTemplateSpec = patchUnrestrictedCapabilities(podTemplateSpec)
case "runAsNonRoot != true":
podTemplateSpec = patchRunAsNonRoot(podTemplateSpec)
case "seccompProfile":
podTemplateSpec = patchSeccompProfile(podTemplateSpec, policy.Enforce.Level)
// Other policies are not implemented as they cannot be encountered with pods created by the library
}
}

newResults := evaluator.EvaluatePod(policy.Enforce, &podTemplateSpec.ObjectMeta, &podTemplateSpec.Spec)
for _, result := range newResults {
if !result.Allowed {
// This is an assertion for future developers, and should never happen in production
// This could happen during development if some unsecure fields are added from `getPodTemplateSpec` or `GetContainers`/`GetInitContainers`
return nil, fmt.Errorf("error patching pod for Pod Security Admission. The folowing policy is still not respected: %s (%s)", result.ForbiddenReason, result.ForbiddenDetail)
}
}

return podTemplateSpec, nil
}

func patchAllowPrivilegeEscalation(podTemplateSpec *corev1.PodTemplateSpec) *corev1.PodTemplateSpec {
visitContainers(podTemplateSpec, func(container *corev1.Container) {
if container.SecurityContext == nil {
container.SecurityContext = &corev1.SecurityContext{}
}
container.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(false)
})
return podTemplateSpec
}

func patchUnrestrictedCapabilities(podTemplateSpec *corev1.PodTemplateSpec) *corev1.PodTemplateSpec {
visitContainers(podTemplateSpec, func(container *corev1.Container) {
if container.SecurityContext == nil {
container.SecurityContext = &corev1.SecurityContext{}
}
if container.SecurityContext.Capabilities == nil {
container.SecurityContext.Capabilities = &corev1.Capabilities{}
}
container.SecurityContext.Capabilities.Drop = append(container.SecurityContext.Capabilities.Drop, "ALL")
})
return podTemplateSpec
}

func patchRunAsNonRoot(podTemplateSpec *corev1.PodTemplateSpec) *corev1.PodTemplateSpec {
if podTemplateSpec.Spec.SecurityContext == nil {
podTemplateSpec.Spec.SecurityContext = &corev1.PodSecurityContext{}
}
podTemplateSpec.Spec.SecurityContext.RunAsNonRoot = pointer.Bool(true)
// No need to set the value as true for containers, as setting at the Pod level is sufficient
return podTemplateSpec
}

func patchSeccompProfile(podTemplateSpec *corev1.PodTemplateSpec, level psaapi.Level) *corev1.PodTemplateSpec {
if level == api.LevelRestricted {
if podTemplateSpec.Spec.SecurityContext == nil {
podTemplateSpec.Spec.SecurityContext = &corev1.PodSecurityContext{}
}
if podTemplateSpec.Spec.SecurityContext.SeccompProfile == nil {
podTemplateSpec.Spec.SecurityContext.SeccompProfile = &corev1.SeccompProfile{}
}
podTemplateSpec.Spec.SecurityContext.SeccompProfile.Type = "RuntimeDefault"
} else if level == api.LevelBaseline {
visitContainers(podTemplateSpec, func(container *corev1.Container) {
if container.SecurityContext != nil && container.SecurityContext.SeccompProfile != nil && container.SecurityContext.SeccompProfile.Type == "Unconfined" {
container.SecurityContext.SeccompProfile = nil
}
})
if podTemplateSpec.Spec.SecurityContext != nil && podTemplateSpec.Spec.SecurityContext.SeccompProfile != nil && podTemplateSpec.Spec.SecurityContext.SeccompProfile.Type == "Unconfined" {
podTemplateSpec.Spec.SecurityContext.SeccompProfile = nil
}
}
return podTemplateSpec
}

0 comments on commit a7af25b

Please sign in to comment.