Skip to content

Commit 1b80b58

Browse files
Merge pull request #22746 from sallyom/sccs-to-crd-final
add SCC validation plugin
2 parents 741891d + 11120eb commit 1b80b58

4 files changed

Lines changed: 701 additions & 1 deletion

File tree

pkg/admission/customresourcevalidation/customresourcevalidationregistration/cr_validation_registration.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package customresourcevalidationregistration
22

33
import (
4-
"github.com/openshift/origin/pkg/admission/customresourcevalidation/securitycontextconstraints"
54
"k8s.io/apiserver/pkg/admission"
65

76
"github.com/openshift/origin/pkg/admission/customresourcevalidation/authentication"
@@ -13,6 +12,7 @@ import (
1312
"github.com/openshift/origin/pkg/admission/customresourcevalidation/oauth"
1413
"github.com/openshift/origin/pkg/admission/customresourcevalidation/project"
1514
"github.com/openshift/origin/pkg/admission/customresourcevalidation/scheduler"
15+
"github.com/openshift/origin/pkg/admission/customresourcevalidation/securitycontextconstraints"
1616
)
1717

1818
// AllCustomResourceValidators are the names of all custom resource validators that should be registered
@@ -26,6 +26,7 @@ var AllCustomResourceValidators = []string{
2626
config.PluginName,
2727
scheduler.PluginName,
2828
clusterresourcequota.PluginName,
29+
securitycontextconstraints.PluginName,
2930

3031
// this one is special because we don't work without it.
3132
securitycontextconstraints.DefaultingPluginName,
@@ -44,6 +45,8 @@ func RegisterCustomResourceValidation(plugins *admission.Plugins) {
4445
// This plugin validates the quota.openshift.io/v1 ClusterResourceQuota resources.
4546
// NOTE: This is only allowed because it is required to get a running control plane operator.
4647
clusterresourcequota.Register(plugins)
48+
// This plugin validates the security.openshift.io/v1 SecurityContextConstraints resources.
49+
securitycontextconstraints.Register(plugins)
4750

4851
// this one is special because we don't work without it.
4952
securitycontextconstraints.RegisterDefaulting(plugins)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package securitycontextconstraints
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"k8s.io/apimachinery/pkg/runtime"
8+
"k8s.io/apimachinery/pkg/runtime/schema"
9+
"k8s.io/apimachinery/pkg/util/validation/field"
10+
"k8s.io/apiserver/pkg/admission"
11+
12+
securityv1 "github.com/openshift/api/security/v1"
13+
14+
"github.com/openshift/origin/pkg/admission/customresourcevalidation"
15+
sccvalidation "github.com/openshift/origin/pkg/admission/customresourcevalidation/securitycontextconstraints/validation"
16+
)
17+
18+
const PluginName = "security.openshift.io/ValidateSecurityContextConstraints"
19+
20+
func Register(plugins *admission.Plugins) {
21+
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
22+
return customresourcevalidation.NewValidator(
23+
map[schema.GroupResource]bool{
24+
{Group: securityv1.GroupName, Resource: "securitycontextconstraints"}: true,
25+
},
26+
map[schema.GroupVersionKind]customresourcevalidation.ObjectValidator{
27+
securityv1.GroupVersion.WithKind("SecurityContextConstraints"): securityContextConstraintsV1{},
28+
})
29+
})
30+
}
31+
32+
func toSecurityContextConstraints(uncastObj runtime.Object) (*securityv1.SecurityContextConstraints, field.ErrorList) {
33+
if uncastObj == nil {
34+
return nil, nil
35+
}
36+
37+
obj, ok := uncastObj.(*securityv1.SecurityContextConstraints)
38+
if !ok {
39+
return nil, field.ErrorList{
40+
field.NotSupported(field.NewPath("kind"), fmt.Sprintf("%T", uncastObj), []string{"SecurityContextConstraints"}),
41+
field.NotSupported(field.NewPath("apiVersion"), fmt.Sprintf("%T", uncastObj), []string{securityv1.GroupVersion.String()}),
42+
}
43+
}
44+
45+
return obj, nil
46+
}
47+
48+
type securityContextConstraintsV1 struct {
49+
}
50+
51+
func (securityContextConstraintsV1) ValidateCreate(obj runtime.Object) field.ErrorList {
52+
securityContextConstraintsObj, errs := toSecurityContextConstraints(obj)
53+
if len(errs) > 0 {
54+
return errs
55+
}
56+
57+
errs = append(errs, sccvalidation.ValidateSecurityContextConstraints(securityContextConstraintsObj)...)
58+
59+
return errs
60+
}
61+
62+
func (securityContextConstraintsV1) ValidateUpdate(obj runtime.Object, oldObj runtime.Object) field.ErrorList {
63+
securityContextConstraintsObj, errs := toSecurityContextConstraints(obj)
64+
if len(errs) > 0 {
65+
return errs
66+
}
67+
securityContextConstraintsOldObj, errs := toSecurityContextConstraints(oldObj)
68+
if len(errs) > 0 {
69+
return errs
70+
}
71+
72+
errs = append(errs, sccvalidation.ValidateSecurityContextConstraintsUpdate(securityContextConstraintsObj, securityContextConstraintsOldObj)...)
73+
74+
return errs
75+
}
76+
77+
func (c securityContextConstraintsV1) ValidateStatusUpdate(obj runtime.Object, oldObj runtime.Object) field.ErrorList {
78+
return c.ValidateUpdate(obj, oldObj)
79+
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package validation
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
corev1 "k8s.io/api/core/v1"
9+
"k8s.io/apimachinery/pkg/api/validation"
10+
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
11+
"k8s.io/apimachinery/pkg/util/validation/field"
12+
kapivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
13+
14+
securityv1 "github.com/openshift/api/security/v1"
15+
)
16+
17+
// ValidateSecurityContextConstraintsName can be used to check whether the given
18+
// security context constraint name is valid.
19+
// Prefix indicates this name will be used as part of generation, in which case
20+
// trailing dashes are allowed.
21+
var ValidateSecurityContextConstraintsName = apimachineryvalidation.NameIsDNSSubdomain
22+
23+
func ValidateSecurityContextConstraints(scc *securityv1.SecurityContextConstraints) field.ErrorList {
24+
allErrs := validation.ValidateObjectMeta(&scc.ObjectMeta, false, ValidateSecurityContextConstraintsName, field.NewPath("metadata"))
25+
26+
if scc.Priority != nil {
27+
if *scc.Priority < 0 {
28+
allErrs = append(allErrs, field.Invalid(field.NewPath("priority"), *scc.Priority, "priority cannot be negative"))
29+
}
30+
}
31+
32+
// ensure the user strat has a valid type
33+
runAsUserPath := field.NewPath("runAsUser")
34+
switch scc.RunAsUser.Type {
35+
case securityv1.RunAsUserStrategyMustRunAs, securityv1.RunAsUserStrategyMustRunAsNonRoot, securityv1.RunAsUserStrategyRunAsAny, securityv1.RunAsUserStrategyMustRunAsRange:
36+
//good types
37+
default:
38+
msg := fmt.Sprintf("invalid strategy type. Valid values are %s, %s, %s, %s", securityv1.RunAsUserStrategyMustRunAs, securityv1.RunAsUserStrategyMustRunAsNonRoot, securityv1.RunAsUserStrategyMustRunAsRange, securityv1.RunAsUserStrategyRunAsAny)
39+
allErrs = append(allErrs, field.Invalid(runAsUserPath.Child("type"), scc.RunAsUser.Type, msg))
40+
}
41+
42+
// if specified, uid cannot be negative
43+
if scc.RunAsUser.UID != nil {
44+
if *scc.RunAsUser.UID < 0 {
45+
allErrs = append(allErrs, field.Invalid(runAsUserPath.Child("uid"), *scc.RunAsUser.UID, "uid cannot be negative"))
46+
}
47+
}
48+
49+
// ensure the selinux strat has a valid type
50+
seLinuxContextPath := field.NewPath("seLinuxContext")
51+
switch scc.SELinuxContext.Type {
52+
case securityv1.SELinuxStrategyMustRunAs, securityv1.SELinuxStrategyRunAsAny:
53+
//good types
54+
default:
55+
msg := fmt.Sprintf("invalid strategy type. Valid values are %s, %s", securityv1.SELinuxStrategyMustRunAs, securityv1.SELinuxStrategyRunAsAny)
56+
allErrs = append(allErrs, field.Invalid(seLinuxContextPath.Child("type"), scc.SELinuxContext.Type, msg))
57+
}
58+
59+
// ensure the fsgroup strat has a valid type
60+
if scc.FSGroup.Type != securityv1.FSGroupStrategyMustRunAs && scc.FSGroup.Type != securityv1.FSGroupStrategyRunAsAny {
61+
allErrs = append(allErrs, field.NotSupported(field.NewPath("fsGroup", "type"), scc.FSGroup.Type,
62+
[]string{string(securityv1.FSGroupStrategyMustRunAs), string(securityv1.FSGroupStrategyRunAsAny)}))
63+
}
64+
allErrs = append(allErrs, validateIDRanges(scc.FSGroup.Ranges, field.NewPath("fsGroup"))...)
65+
66+
if scc.SupplementalGroups.Type != securityv1.SupplementalGroupsStrategyMustRunAs &&
67+
scc.SupplementalGroups.Type != securityv1.SupplementalGroupsStrategyRunAsAny {
68+
allErrs = append(allErrs, field.NotSupported(field.NewPath("supplementalGroups", "type"), scc.SupplementalGroups.Type,
69+
[]string{string(securityv1.SupplementalGroupsStrategyMustRunAs), string(securityv1.SupplementalGroupsStrategyRunAsAny)}))
70+
}
71+
allErrs = append(allErrs, validateIDRanges(scc.SupplementalGroups.Ranges, field.NewPath("supplementalGroups"))...)
72+
73+
// validate capabilities
74+
allErrs = append(allErrs, validateSCCCapsAgainstDrops(scc.RequiredDropCapabilities, scc.DefaultAddCapabilities, field.NewPath("defaultAddCapabilities"))...)
75+
allErrs = append(allErrs, validateSCCCapsAgainstDrops(scc.RequiredDropCapabilities, scc.AllowedCapabilities, field.NewPath("allowedCapabilities"))...)
76+
77+
if hasCap(securityv1.AllowAllCapabilities, scc.AllowedCapabilities) && len(scc.RequiredDropCapabilities) > 0 {
78+
allErrs = append(allErrs, field.Invalid(field.NewPath("requiredDropCapabilities"), scc.RequiredDropCapabilities,
79+
"required capabilities must be empty when all capabilities are allowed by a wildcard"))
80+
}
81+
82+
allErrs = append(allErrs, validateSCCDefaultAllowPrivilegeEscalation(field.NewPath("defaultAllowPrivilegeEscalation"), scc.DefaultAllowPrivilegeEscalation, scc.AllowPrivilegeEscalation)...)
83+
84+
allowsFlexVolumes := false
85+
hasNoneVolume := false
86+
87+
if len(scc.Volumes) > 0 {
88+
for _, fsType := range scc.Volumes {
89+
if fsType == securityv1.FSTypeNone {
90+
hasNoneVolume = true
91+
92+
} else if fsType == securityv1.FSTypeFlexVolume || fsType == securityv1.FSTypeAll {
93+
allowsFlexVolumes = true
94+
}
95+
}
96+
}
97+
98+
if hasNoneVolume && len(scc.Volumes) > 1 {
99+
allErrs = append(allErrs, field.Invalid(field.NewPath("volumes"), scc.Volumes,
100+
"if 'none' is specified, no other values are allowed"))
101+
}
102+
103+
if len(scc.AllowedFlexVolumes) > 0 {
104+
if allowsFlexVolumes {
105+
for idx, allowedFlexVolume := range scc.AllowedFlexVolumes {
106+
if len(allowedFlexVolume.Driver) == 0 {
107+
allErrs = append(allErrs, field.Required(field.NewPath("allowedFlexVolumes").Index(idx).Child("driver"),
108+
"must specify a driver"))
109+
}
110+
}
111+
} else {
112+
allErrs = append(allErrs, field.Invalid(field.NewPath("allowedFlexVolumes"), scc.AllowedFlexVolumes,
113+
"volumes does not include 'flexVolume' or '*', so no flex volumes are allowed"))
114+
}
115+
}
116+
117+
allowedUnsafeSysctlsPath := field.NewPath("allowedUnsafeSysctls")
118+
forbiddenSysctlsPath := field.NewPath("forbiddenSysctls")
119+
allErrs = append(allErrs, validateSCCSysctls(allowedUnsafeSysctlsPath, scc.AllowedUnsafeSysctls)...)
120+
allErrs = append(allErrs, validateSCCSysctls(forbiddenSysctlsPath, scc.ForbiddenSysctls)...)
121+
allErrs = append(allErrs, validatePodSecurityPolicySysctlListsDoNotOverlap(allowedUnsafeSysctlsPath, forbiddenSysctlsPath, scc.AllowedUnsafeSysctls, scc.ForbiddenSysctls)...)
122+
123+
return allErrs
124+
}
125+
126+
const sysctlPatternSegmentFmt string = "([a-z0-9][-_a-z0-9]*)?[a-z0-9*]"
127+
const sysctlPatternFmt string = "(" + kapivalidation.SysctlSegmentFmt + "\\.)*" + sysctlPatternSegmentFmt
128+
129+
var sysctlPatternRegexp = regexp.MustCompile("^" + sysctlPatternFmt + "$")
130+
131+
func IsValidSysctlPattern(name string) bool {
132+
if len(name) > kapivalidation.SysctlMaxLength {
133+
return false
134+
}
135+
return sysctlPatternRegexp.MatchString(name)
136+
}
137+
138+
// validatePodSecurityPolicySysctlListsDoNotOverlap validates the values in forbiddenSysctls and allowedSysctls fields do not overlap.
139+
func validatePodSecurityPolicySysctlListsDoNotOverlap(allowedSysctlsFldPath, forbiddenSysctlsFldPath *field.Path, allowedUnsafeSysctls, forbiddenSysctls []string) field.ErrorList {
140+
allErrs := field.ErrorList{}
141+
for i, allowedSysctl := range allowedUnsafeSysctls {
142+
isAllowedSysctlPattern := false
143+
allowedSysctlPrefix := ""
144+
if strings.HasSuffix(allowedSysctl, "*") {
145+
isAllowedSysctlPattern = true
146+
allowedSysctlPrefix = strings.TrimSuffix(allowedSysctl, "*")
147+
}
148+
for j, forbiddenSysctl := range forbiddenSysctls {
149+
isForbiddenSysctlPattern := false
150+
forbiddenSysctlPrefix := ""
151+
if strings.HasSuffix(forbiddenSysctl, "*") {
152+
isForbiddenSysctlPattern = true
153+
forbiddenSysctlPrefix = strings.TrimSuffix(forbiddenSysctl, "*")
154+
}
155+
switch {
156+
case isAllowedSysctlPattern && isForbiddenSysctlPattern:
157+
if strings.HasPrefix(allowedSysctlPrefix, forbiddenSysctlPrefix) {
158+
allErrs = append(allErrs, field.Invalid(allowedSysctlsFldPath.Index(i), allowedUnsafeSysctls[i], fmt.Sprintf("sysctl overlaps with %v", forbiddenSysctl)))
159+
} else if strings.HasPrefix(forbiddenSysctlPrefix, allowedSysctlPrefix) {
160+
allErrs = append(allErrs, field.Invalid(forbiddenSysctlsFldPath.Index(j), forbiddenSysctls[j], fmt.Sprintf("sysctl overlaps with %v", allowedSysctl)))
161+
}
162+
case isAllowedSysctlPattern:
163+
if strings.HasPrefix(forbiddenSysctl, allowedSysctlPrefix) {
164+
allErrs = append(allErrs, field.Invalid(forbiddenSysctlsFldPath.Index(j), forbiddenSysctls[j], fmt.Sprintf("sysctl overlaps with %v", allowedSysctl)))
165+
}
166+
case isForbiddenSysctlPattern:
167+
if strings.HasPrefix(allowedSysctl, forbiddenSysctlPrefix) {
168+
allErrs = append(allErrs, field.Invalid(allowedSysctlsFldPath.Index(i), allowedUnsafeSysctls[i], fmt.Sprintf("sysctl overlaps with %v", forbiddenSysctl)))
169+
}
170+
default:
171+
if allowedSysctl == forbiddenSysctl {
172+
allErrs = append(allErrs, field.Invalid(allowedSysctlsFldPath.Index(i), allowedUnsafeSysctls[i], fmt.Sprintf("sysctl overlaps with %v", forbiddenSysctl)))
173+
}
174+
}
175+
}
176+
}
177+
return allErrs
178+
}
179+
180+
// validatePodSecurityPolicySysctls validates the sysctls fields of PodSecurityPolicy.
181+
func validateSCCSysctls(fldPath *field.Path, sysctls []string) field.ErrorList {
182+
allErrs := field.ErrorList{}
183+
184+
if len(sysctls) == 0 {
185+
return allErrs
186+
}
187+
188+
coversAll := false
189+
for i, s := range sysctls {
190+
if len(s) == 0 {
191+
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), sysctls[i], fmt.Sprintf("empty sysctl not allowed")))
192+
} else if !IsValidSysctlPattern(string(s)) {
193+
allErrs = append(
194+
allErrs,
195+
field.Invalid(fldPath.Index(i), sysctls[i], fmt.Sprintf("must have at most %d characters and match regex %s",
196+
kapivalidation.SysctlMaxLength,
197+
sysctlPatternFmt,
198+
)),
199+
)
200+
} else if s[0] == '*' {
201+
coversAll = true
202+
}
203+
}
204+
205+
if coversAll && len(sysctls) > 1 {
206+
allErrs = append(allErrs, field.Forbidden(fldPath.Child("items"), fmt.Sprintf("if '*' is present, must not specify other sysctls")))
207+
}
208+
209+
return allErrs
210+
}
211+
212+
// validateSCCCapsAgainstDrops ensures an allowed cap is not listed in the required drops.
213+
func validateSCCCapsAgainstDrops(requiredDrops []corev1.Capability, capsToCheck []corev1.Capability, fldPath *field.Path) field.ErrorList {
214+
allErrs := field.ErrorList{}
215+
if requiredDrops == nil {
216+
return allErrs
217+
}
218+
for _, cap := range capsToCheck {
219+
if hasCap(cap, requiredDrops) {
220+
allErrs = append(allErrs, field.Invalid(fldPath, cap,
221+
fmt.Sprintf("capability is listed in %s and requiredDropCapabilities", fldPath.String())))
222+
}
223+
}
224+
return allErrs
225+
}
226+
227+
// validateSCCDefaultAllowPrivilegeEscalation validates the DefaultAllowPrivilegeEscalation field against the AllowPrivilegeEscalation field of a SecurityContextConstraints.
228+
func validateSCCDefaultAllowPrivilegeEscalation(fldPath *field.Path, defaultAllowPrivilegeEscalation, allowPrivilegeEscalation *bool) field.ErrorList {
229+
allErrs := field.ErrorList{}
230+
if defaultAllowPrivilegeEscalation != nil && allowPrivilegeEscalation != nil && *defaultAllowPrivilegeEscalation && !*allowPrivilegeEscalation {
231+
allErrs = append(allErrs, field.Invalid(fldPath, defaultAllowPrivilegeEscalation, "Cannot set DefaultAllowPrivilegeEscalation to true without also setting AllowPrivilegeEscalation to true"))
232+
}
233+
234+
return allErrs
235+
}
236+
237+
// hasCap checks for needle in haystack.
238+
func hasCap(needle corev1.Capability, haystack []corev1.Capability) bool {
239+
for _, c := range haystack {
240+
if needle == c {
241+
return true
242+
}
243+
}
244+
return false
245+
}
246+
247+
// validateIDRanges ensures the range is valid.
248+
func validateIDRanges(rng []securityv1.IDRange, fldPath *field.Path) field.ErrorList {
249+
allErrs := field.ErrorList{}
250+
251+
for i, r := range rng {
252+
// if 0 <= Min <= Max then we do not need to validate max. It is always greater than or
253+
// equal to 0 and Min.
254+
minPath := fldPath.Child("ranges").Index(i).Child("min")
255+
maxPath := fldPath.Child("ranges").Index(i).Child("max")
256+
257+
if r.Min < 0 {
258+
allErrs = append(allErrs, field.Invalid(minPath, r.Min, "min cannot be negative"))
259+
}
260+
if r.Max < 0 {
261+
allErrs = append(allErrs, field.Invalid(maxPath, r.Max, "max cannot be negative"))
262+
}
263+
if r.Min > r.Max {
264+
allErrs = append(allErrs, field.Invalid(minPath, r, "min cannot be greater than max"))
265+
}
266+
}
267+
268+
return allErrs
269+
}
270+
271+
func ValidateSecurityContextConstraintsUpdate(newScc, oldScc *securityv1.SecurityContextConstraints) field.ErrorList {
272+
allErrs := validation.ValidateObjectMetaUpdate(&newScc.ObjectMeta, &oldScc.ObjectMeta, field.NewPath("metadata"))
273+
allErrs = append(allErrs, ValidateSecurityContextConstraints(newScc)...)
274+
return allErrs
275+
}

0 commit comments

Comments
 (0)