Skip to content

Commit a7af25b

Browse files
committed
Policies
Signed-off-by: Philippe Martin <[email protected]>
1 parent 27ace5e commit a7af25b

File tree

3 files changed

+332
-1
lines changed

3 files changed

+332
-1
lines changed

pkg/devfile/generator/generators.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,10 @@ func GetPodTemplateSpec(devfileObj parser.DevfileObj, podTemplateParams PodTempl
209209
return nil, err
210210
}
211211

212-
// TODO: apply here patches for Pod Security Admission
212+
podTemplateSpec, err = patchForPolicy(podTemplateSpec, podTemplateParams.PodSecurityAdmissionPolicy)
213+
if err != nil {
214+
return nil, err
215+
}
213216

214217
if needsPodOverrides(globalAttributes, components) {
215218
patchedPodTemplateSpec, err := applyPodOverrides(globalAttributes, components, podTemplateSpec)

pkg/devfile/generator/generators_test.go

+197
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
3838
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3939
"k8s.io/apimachinery/pkg/util/intstr"
40+
"k8s.io/pod-security-admission/api"
4041
"k8s.io/utils/pointer"
4142
)
4243

@@ -1606,6 +1607,202 @@ func TestGetPodTemplateSpec(t *testing.T) {
16061607
},
16071608
},
16081609
},
1610+
{
1611+
name: "Restricted policy",
1612+
args: args{
1613+
devfileObj: func(ctrl *gomock.Controller) parser.DevfileObj {
1614+
containers := []v1alpha2.Component{
1615+
{
1616+
Name: "main",
1617+
ComponentUnion: v1.ComponentUnion{
1618+
Container: &v1.ContainerComponent{
1619+
Container: v1.Container{
1620+
Image: "an-image",
1621+
},
1622+
},
1623+
},
1624+
},
1625+
}
1626+
events := v1alpha2.Events{}
1627+
mockDevfileData := data.NewMockDevfileData(ctrl)
1628+
mockDevfileData.EXPECT().GetComponents(gomock.Any()).Return(containers, nil).AnyTimes()
1629+
mockDevfileData.EXPECT().GetDevfileContainerComponents(gomock.Any()).Return(containers, nil).AnyTimes()
1630+
mockDevfileData.EXPECT().GetEvents().Return(events).AnyTimes()
1631+
mockDevfileData.EXPECT().GetProjects(gomock.Any()).Return(nil, nil).AnyTimes()
1632+
mockDevfileData.EXPECT().GetAttributes().Return(attributes.Attributes{
1633+
PodOverridesAttribute: apiext.JSON{Raw: []byte("{\"spec\": {\"securityContext\": {\"seccompProfile\": {\"type\": \"Localhost\"}}}}")},
1634+
}, nil)
1635+
1636+
mockDevfileData.EXPECT().GetSchemaVersion().Return("2.1.0").AnyTimes()
1637+
return parser.DevfileObj{
1638+
Data: mockDevfileData,
1639+
}
1640+
},
1641+
podTemplateParams: PodTemplateParams{
1642+
PodSecurityAdmissionPolicy: api.Policy{
1643+
Enforce: api.LevelVersion{
1644+
Level: api.LevelRestricted,
1645+
Version: api.MajorMinorVersion(1, 25),
1646+
},
1647+
},
1648+
},
1649+
},
1650+
want: &corev1.PodTemplateSpec{
1651+
Spec: corev1.PodSpec{
1652+
SecurityContext: &corev1.PodSecurityContext{
1653+
RunAsNonRoot: pointer.Bool(true),
1654+
SeccompProfile: &corev1.SeccompProfile{
1655+
Type: "Localhost",
1656+
},
1657+
},
1658+
Containers: []corev1.Container{
1659+
{
1660+
Name: "main",
1661+
Image: "an-image",
1662+
Env: []corev1.EnvVar{
1663+
{Name: "PROJECTS_ROOT", Value: "/projects"},
1664+
{Name: "PROJECT_SOURCE", Value: "/projects"},
1665+
},
1666+
ImagePullPolicy: corev1.PullAlways,
1667+
Ports: []corev1.ContainerPort{},
1668+
SecurityContext: &corev1.SecurityContext{
1669+
AllowPrivilegeEscalation: pointer.Bool(false),
1670+
Capabilities: &corev1.Capabilities{
1671+
Drop: []corev1.Capability{
1672+
"ALL",
1673+
},
1674+
},
1675+
},
1676+
},
1677+
},
1678+
InitContainers: []corev1.Container{},
1679+
},
1680+
},
1681+
},
1682+
{
1683+
name: "Restricted policy and pod override",
1684+
args: args{
1685+
devfileObj: func(ctrl *gomock.Controller) parser.DevfileObj {
1686+
containers := []v1alpha2.Component{
1687+
{
1688+
Name: "main",
1689+
ComponentUnion: v1.ComponentUnion{
1690+
Container: &v1.ContainerComponent{
1691+
Container: v1.Container{
1692+
Image: "an-image",
1693+
},
1694+
},
1695+
},
1696+
},
1697+
}
1698+
events := v1alpha2.Events{}
1699+
mockDevfileData := data.NewMockDevfileData(ctrl)
1700+
mockDevfileData.EXPECT().GetComponents(gomock.Any()).Return(containers, nil).AnyTimes()
1701+
mockDevfileData.EXPECT().GetDevfileContainerComponents(gomock.Any()).Return(containers, nil).AnyTimes()
1702+
mockDevfileData.EXPECT().GetEvents().Return(events).AnyTimes()
1703+
mockDevfileData.EXPECT().GetProjects(gomock.Any()).Return(nil, nil).AnyTimes()
1704+
mockDevfileData.EXPECT().GetAttributes().Return(attributes.Attributes{}, nil)
1705+
mockDevfileData.EXPECT().GetSchemaVersion().Return("2.1.0").AnyTimes()
1706+
return parser.DevfileObj{
1707+
Data: mockDevfileData,
1708+
}
1709+
},
1710+
podTemplateParams: PodTemplateParams{
1711+
PodSecurityAdmissionPolicy: api.Policy{
1712+
Enforce: api.LevelVersion{
1713+
Level: api.LevelRestricted,
1714+
Version: api.MajorMinorVersion(1, 25),
1715+
},
1716+
},
1717+
},
1718+
},
1719+
want: &corev1.PodTemplateSpec{
1720+
Spec: corev1.PodSpec{
1721+
SecurityContext: &corev1.PodSecurityContext{
1722+
RunAsNonRoot: pointer.Bool(true),
1723+
SeccompProfile: &corev1.SeccompProfile{
1724+
Type: "RuntimeDefault",
1725+
},
1726+
},
1727+
Containers: []corev1.Container{
1728+
{
1729+
Name: "main",
1730+
Image: "an-image",
1731+
Env: []corev1.EnvVar{
1732+
{Name: "PROJECTS_ROOT", Value: "/projects"},
1733+
{Name: "PROJECT_SOURCE", Value: "/projects"},
1734+
},
1735+
ImagePullPolicy: corev1.PullAlways,
1736+
Ports: []corev1.ContainerPort{},
1737+
SecurityContext: &corev1.SecurityContext{
1738+
AllowPrivilegeEscalation: pointer.Bool(false),
1739+
Capabilities: &corev1.Capabilities{
1740+
Drop: []corev1.Capability{
1741+
"ALL",
1742+
},
1743+
},
1744+
},
1745+
},
1746+
},
1747+
InitContainers: []corev1.Container{},
1748+
},
1749+
},
1750+
},
1751+
{
1752+
name: "Baseline policy",
1753+
args: args{
1754+
devfileObj: func(ctrl *gomock.Controller) parser.DevfileObj {
1755+
containers := []v1alpha2.Component{
1756+
{
1757+
Name: "main",
1758+
ComponentUnion: v1.ComponentUnion{
1759+
Container: &v1.ContainerComponent{
1760+
Container: v1.Container{
1761+
Image: "an-image",
1762+
},
1763+
},
1764+
},
1765+
},
1766+
}
1767+
events := v1alpha2.Events{}
1768+
mockDevfileData := data.NewMockDevfileData(ctrl)
1769+
mockDevfileData.EXPECT().GetComponents(gomock.Any()).Return(containers, nil).AnyTimes()
1770+
mockDevfileData.EXPECT().GetDevfileContainerComponents(gomock.Any()).Return(containers, nil).AnyTimes()
1771+
mockDevfileData.EXPECT().GetEvents().Return(events).AnyTimes()
1772+
mockDevfileData.EXPECT().GetProjects(gomock.Any()).Return(nil, nil).AnyTimes()
1773+
mockDevfileData.EXPECT().GetAttributes().Return(attributes.Attributes{}, nil)
1774+
mockDevfileData.EXPECT().GetSchemaVersion().Return("2.1.0").AnyTimes()
1775+
return parser.DevfileObj{
1776+
Data: mockDevfileData,
1777+
}
1778+
},
1779+
podTemplateParams: PodTemplateParams{
1780+
PodSecurityAdmissionPolicy: api.Policy{
1781+
Enforce: api.LevelVersion{
1782+
Level: api.LevelBaseline,
1783+
Version: api.MajorMinorVersion(1, 25),
1784+
},
1785+
},
1786+
},
1787+
},
1788+
want: &corev1.PodTemplateSpec{
1789+
Spec: corev1.PodSpec{
1790+
Containers: []corev1.Container{
1791+
{
1792+
Name: "main",
1793+
Image: "an-image",
1794+
Env: []corev1.EnvVar{
1795+
{Name: "PROJECTS_ROOT", Value: "/projects"},
1796+
{Name: "PROJECT_SOURCE", Value: "/projects"},
1797+
},
1798+
ImagePullPolicy: corev1.PullAlways,
1799+
Ports: []corev1.ContainerPort{},
1800+
},
1801+
},
1802+
InitContainers: []corev1.Container{},
1803+
},
1804+
},
1805+
},
16091806
}
16101807
for _, tt := range tests {
16111808
t.Run(tt.name, func(t *testing.T) {

pkg/devfile/generator/policy.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//
2+
// Copyright 2023 Red Hat, Inc.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package generator
17+
18+
import (
19+
"fmt"
20+
21+
corev1 "k8s.io/api/core/v1"
22+
"k8s.io/pod-security-admission/api"
23+
psaapi "k8s.io/pod-security-admission/api"
24+
psapolicy "k8s.io/pod-security-admission/policy"
25+
"k8s.io/utils/pointer"
26+
)
27+
28+
// ContainerVisitor is called with each container
29+
type ContainerVisitor func(container *corev1.Container)
30+
31+
// visitContainers invokes the visitor function for every container in the given pod template spec
32+
func visitContainers(podTemplateSpec *corev1.PodTemplateSpec, visitor ContainerVisitor) {
33+
for i := range podTemplateSpec.Spec.InitContainers {
34+
visitor(&podTemplateSpec.Spec.InitContainers[i])
35+
}
36+
for i := range podTemplateSpec.Spec.Containers {
37+
visitor(&podTemplateSpec.Spec.Containers[i])
38+
}
39+
for i := range podTemplateSpec.Spec.EphemeralContainers {
40+
visitor((*corev1.Container)(&podTemplateSpec.Spec.EphemeralContainers[i].EphemeralContainerCommon))
41+
}
42+
}
43+
44+
func patchForPolicy(podTemplateSpec *corev1.PodTemplateSpec, policy psaapi.Policy) (*corev1.PodTemplateSpec, error) {
45+
evaluator, err := psapolicy.NewEvaluator(psapolicy.DefaultChecks())
46+
if err != nil {
47+
return nil, err
48+
}
49+
results := evaluator.EvaluatePod(policy.Enforce, &podTemplateSpec.ObjectMeta, &podTemplateSpec.Spec)
50+
for _, result := range results {
51+
if result.Allowed {
52+
continue
53+
}
54+
switch result.ForbiddenReason {
55+
case "allowPrivilegeEscalation != false":
56+
podTemplateSpec = patchAllowPrivilegeEscalation(podTemplateSpec)
57+
case "unrestricted capabilities":
58+
podTemplateSpec = patchUnrestrictedCapabilities(podTemplateSpec)
59+
case "runAsNonRoot != true":
60+
podTemplateSpec = patchRunAsNonRoot(podTemplateSpec)
61+
case "seccompProfile":
62+
podTemplateSpec = patchSeccompProfile(podTemplateSpec, policy.Enforce.Level)
63+
// Other policies are not implemented as they cannot be encountered with pods created by the library
64+
}
65+
}
66+
67+
newResults := evaluator.EvaluatePod(policy.Enforce, &podTemplateSpec.ObjectMeta, &podTemplateSpec.Spec)
68+
for _, result := range newResults {
69+
if !result.Allowed {
70+
// This is an assertion for future developers, and should never happen in production
71+
// This could happen during development if some unsecure fields are added from `getPodTemplateSpec` or `GetContainers`/`GetInitContainers`
72+
return nil, fmt.Errorf("error patching pod for Pod Security Admission. The folowing policy is still not respected: %s (%s)", result.ForbiddenReason, result.ForbiddenDetail)
73+
}
74+
}
75+
76+
return podTemplateSpec, nil
77+
}
78+
79+
func patchAllowPrivilegeEscalation(podTemplateSpec *corev1.PodTemplateSpec) *corev1.PodTemplateSpec {
80+
visitContainers(podTemplateSpec, func(container *corev1.Container) {
81+
if container.SecurityContext == nil {
82+
container.SecurityContext = &corev1.SecurityContext{}
83+
}
84+
container.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(false)
85+
})
86+
return podTemplateSpec
87+
}
88+
89+
func patchUnrestrictedCapabilities(podTemplateSpec *corev1.PodTemplateSpec) *corev1.PodTemplateSpec {
90+
visitContainers(podTemplateSpec, func(container *corev1.Container) {
91+
if container.SecurityContext == nil {
92+
container.SecurityContext = &corev1.SecurityContext{}
93+
}
94+
if container.SecurityContext.Capabilities == nil {
95+
container.SecurityContext.Capabilities = &corev1.Capabilities{}
96+
}
97+
container.SecurityContext.Capabilities.Drop = append(container.SecurityContext.Capabilities.Drop, "ALL")
98+
})
99+
return podTemplateSpec
100+
}
101+
102+
func patchRunAsNonRoot(podTemplateSpec *corev1.PodTemplateSpec) *corev1.PodTemplateSpec {
103+
if podTemplateSpec.Spec.SecurityContext == nil {
104+
podTemplateSpec.Spec.SecurityContext = &corev1.PodSecurityContext{}
105+
}
106+
podTemplateSpec.Spec.SecurityContext.RunAsNonRoot = pointer.Bool(true)
107+
// No need to set the value as true for containers, as setting at the Pod level is sufficient
108+
return podTemplateSpec
109+
}
110+
111+
func patchSeccompProfile(podTemplateSpec *corev1.PodTemplateSpec, level psaapi.Level) *corev1.PodTemplateSpec {
112+
if level == api.LevelRestricted {
113+
if podTemplateSpec.Spec.SecurityContext == nil {
114+
podTemplateSpec.Spec.SecurityContext = &corev1.PodSecurityContext{}
115+
}
116+
if podTemplateSpec.Spec.SecurityContext.SeccompProfile == nil {
117+
podTemplateSpec.Spec.SecurityContext.SeccompProfile = &corev1.SeccompProfile{}
118+
}
119+
podTemplateSpec.Spec.SecurityContext.SeccompProfile.Type = "RuntimeDefault"
120+
} else if level == api.LevelBaseline {
121+
visitContainers(podTemplateSpec, func(container *corev1.Container) {
122+
if container.SecurityContext != nil && container.SecurityContext.SeccompProfile != nil && container.SecurityContext.SeccompProfile.Type == "Unconfined" {
123+
container.SecurityContext.SeccompProfile = nil
124+
}
125+
})
126+
if podTemplateSpec.Spec.SecurityContext != nil && podTemplateSpec.Spec.SecurityContext.SeccompProfile != nil && podTemplateSpec.Spec.SecurityContext.SeccompProfile.Type == "Unconfined" {
127+
podTemplateSpec.Spec.SecurityContext.SeccompProfile = nil
128+
}
129+
}
130+
return podTemplateSpec
131+
}

0 commit comments

Comments
 (0)