Skip to content

Commit de94214

Browse files
committed
Security Context Constraints: prefer non-mutating SCC on update.
1 parent f4d81e2 commit de94214

File tree

6 files changed

+154
-114
lines changed

6 files changed

+154
-114
lines changed

pkg/security/admission/admission.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/golang/glog"
1010

11+
apiequality "k8s.io/apimachinery/pkg/api/equality"
1112
"k8s.io/apimachinery/pkg/util/validation/field"
1213
kapihelper "k8s.io/kubernetes/pkg/apis/core/helper"
1314
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
@@ -115,20 +116,54 @@ func (c *constraint) Admit(a admission.Attributes) error {
115116
return admission.NewForbidden(a, fmt.Errorf("no providers available to validate pod request"))
116117
}
117118

119+
// TODO(liggitt): allow spec mutation during initializing updates?
120+
specMutationAllowed := a.GetOperation() == admission.Create
121+
118122
// all containers in a single pod must validate under a single provider or we will reject the request
119123
validationErrs := field.ErrorList{}
124+
var (
125+
allowedPod *kapi.Pod
126+
allowingProvider scc.SecurityContextConstraintsProvider
127+
)
128+
129+
loop:
120130
for _, provider := range providers {
121-
if errs := scc.AssignSecurityContext(provider, pod, field.NewPath(fmt.Sprintf("provider %s: ", provider.GetSCCName()))); len(errs) > 0 {
131+
podCopy := pod.DeepCopy()
132+
133+
if errs := scc.AssignSecurityContext(provider, podCopy, field.NewPath(fmt.Sprintf("provider %s: ", provider.GetSCCName()))); len(errs) > 0 {
122134
validationErrs = append(validationErrs, errs...)
123135
continue
124136
}
125137

126-
// the entire pod validated, annotate and accept the pod
127-
glog.V(4).Infof("pod %s (generate: %s) validated against provider %s", pod.Name, pod.GenerateName, provider.GetSCCName())
138+
// the entire pod validated
139+
switch {
140+
case specMutationAllowed:
141+
// if mutation is allowed, use the first found SCC that allows the pod.
142+
// This behavior is different from Kubernetes which tries to search a non-mutating provider
143+
// even on creating. We prefer most restrictive SCC in this case even if it mutates a pod.
144+
allowedPod = podCopy
145+
allowingProvider = provider
146+
glog.V(6).Infof("pod %s (generate: %s) validated against provider %s with mutation", pod.Name, pod.GenerateName, provider.GetSCCName())
147+
break loop
148+
case apiequality.Semantic.DeepEqual(pod, podCopy):
149+
// if we don't allow mutation, only use the validated pod if it didn't require any spec changes
150+
allowedPod = podCopy
151+
allowingProvider = provider
152+
glog.V(6).Infof("pod %s (generate: %s) validated against provider %s without mutation", pod.Name, pod.GenerateName, provider.GetSCCName())
153+
break loop
154+
default:
155+
glog.V(6).Infof("pod %s (generate: %s) validated against provider %s, but required mutation, skipping", pod.Name, pod.GenerateName, provider.GetSCCName())
156+
}
157+
}
158+
159+
if allowedPod != nil {
160+
*pod = *allowedPod
161+
// annotate and accept the pod
162+
glog.V(4).Infof("pod %s (generate: %s) validated against provider %s", pod.Name, pod.GenerateName, allowingProvider.GetSCCName())
128163
if pod.ObjectMeta.Annotations == nil {
129164
pod.ObjectMeta.Annotations = map[string]string{}
130165
}
131-
pod.ObjectMeta.Annotations[allocator.ValidatedSCCAnnotation] = provider.GetSCCName()
166+
pod.ObjectMeta.Annotations[allocator.ValidatedSCCAnnotation] = allowingProvider.GetSCCName()
132167
return nil
133168
}
134169

pkg/security/admission/admission_test.go

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,17 @@ func TestAdmitCaps(t *testing.T) {
140140
for i := 0; i < 2; i++ {
141141
for k, v := range tc {
142142
v.pod.Spec.Containers, v.pod.Spec.InitContainers = v.pod.Spec.InitContainers, v.pod.Spec.Containers
143+
144+
testSCCAdmit(k, v.sccs, v.pod, v.shouldPass, t)
145+
143146
containers := v.pod.Spec.Containers
144147
if i == 0 {
145148
containers = v.pod.Spec.InitContainers
146149
}
147150

148-
testSCCAdmit(k, v.sccs, v.pod, v.shouldPass, t)
149-
150151
if v.expectedCapabilities != nil {
151152
if !reflect.DeepEqual(v.expectedCapabilities, containers[0].SecurityContext.Capabilities) {
152-
t.Errorf("%s resulted in caps that were not expected - expected: %v, received: %v", k, v.expectedCapabilities, containers[0].SecurityContext.Capabilities)
153+
t.Errorf("%s resulted in caps that were not expected - expected: %#v, received: %#v", k, v.expectedCapabilities, containers[0].SecurityContext.Capabilities)
153154
}
154155
}
155156
}
@@ -239,43 +240,44 @@ func TestAdmitSuccess(t *testing.T) {
239240
"specifyUIDInRange": {
240241
pod: specifyUIDInRange,
241242
expectedPodSC: podSC(seLinuxLevelFromNamespace, defaultGroup, defaultGroup),
242-
expectedContainerSC: containerSC(seLinuxLevelFromNamespace, goodUID),
243+
expectedContainerSC: containerSC(nil, goodUID),
243244
},
244245
"specifyLabels": {
245246
pod: specifyLabels,
246247
expectedPodSC: podSC(seLinuxLevelFromNamespace, defaultGroup, defaultGroup),
247-
expectedContainerSC: containerSC(seLinuxLevelFromNamespace, 1),
248+
expectedContainerSC: containerSC(&seLinuxLevelFromNamespace, 1),
248249
},
249250
"specifyFSGroup": {
250251
pod: specifyFSGroupInRange,
251252
expectedPodSC: podSC(seLinuxLevelFromNamespace, goodFSGroup, defaultGroup),
252-
expectedContainerSC: containerSC(seLinuxLevelFromNamespace, 1),
253+
expectedContainerSC: containerSC(nil, 1),
253254
},
254255
"specifySupGroup": {
255256
pod: specifySupGroup,
256257
expectedPodSC: podSC(seLinuxLevelFromNamespace, defaultGroup, 3),
257-
expectedContainerSC: containerSC(seLinuxLevelFromNamespace, 1),
258+
expectedContainerSC: containerSC(nil, 1),
258259
},
259260
"specifyPodLevelSELinuxLevel": {
260261
pod: specifyPodLevelSELinux,
261262
expectedPodSC: podSC(seLinuxLevelFromNamespace, defaultGroup, defaultGroup),
262-
expectedContainerSC: containerSC(seLinuxLevelFromNamespace, 1),
263+
expectedContainerSC: containerSC(nil, 1),
263264
},
264265
}
265266

266267
for i := 0; i < 2; i++ {
267268
for k, v := range testCases {
268269
v.pod.Spec.Containers, v.pod.Spec.InitContainers = v.pod.Spec.InitContainers, v.pod.Spec.Containers
269-
containers := v.pod.Spec.Containers
270-
if i == 0 {
271-
containers = v.pod.Spec.InitContainers
272-
}
273270

274271
hasErrors := testSCCAdmission(v.pod, p, saSCC.Name, k, t)
275272
if hasErrors {
276273
continue
277274
}
278275

276+
containers := v.pod.Spec.Containers
277+
if i == 0 {
278+
containers = v.pod.Spec.InitContainers
279+
}
280+
279281
if !reflect.DeepEqual(v.expectedPodSC, v.pod.Spec.SecurityContext) {
280282
t.Errorf("%s unexpected pod SecurityContext diff:\n%s", k, diff.ObjectGoPrintSideBySide(v.expectedPodSC, v.pod.Spec.SecurityContext))
281283
}
@@ -934,6 +936,85 @@ func TestAdmitSeccomp(t *testing.T) {
934936

935937
}
936938

939+
func TestAdmitPreferNonmutatingWhenPossible(t *testing.T) {
940+
941+
mutatingSCC := restrictiveSCC()
942+
mutatingSCC.Name = "mutating-scc"
943+
944+
nonMutatingSCC := laxSCC()
945+
nonMutatingSCC.Name = "non-mutating-scc"
946+
947+
simplePod := goodPod()
948+
simplePod.Spec.Containers[0].Name = "simple-pod"
949+
simplePod.Spec.Containers[0].Image = "test-image:0.1"
950+
951+
modifiedPod := simplePod.DeepCopy()
952+
modifiedPod.Spec.Containers[0].Image = "test-image:0.2"
953+
954+
tests := map[string]struct {
955+
oldPod *kapi.Pod
956+
newPod *kapi.Pod
957+
operation kadmission.Operation
958+
sccs []*securityapi.SecurityContextConstraints
959+
shouldPass bool
960+
expectedSCC string
961+
}{
962+
"creation: the first SCC (even if it mutates) should be used": {
963+
newPod: simplePod.DeepCopy(),
964+
operation: kadmission.Create,
965+
sccs: []*securityapi.SecurityContextConstraints{mutatingSCC, nonMutatingSCC},
966+
shouldPass: true,
967+
expectedSCC: mutatingSCC.Name,
968+
},
969+
"updating: the first non-mutating SCC should be used": {
970+
oldPod: simplePod.DeepCopy(),
971+
newPod: modifiedPod.DeepCopy(),
972+
operation: kadmission.Update,
973+
sccs: []*securityapi.SecurityContextConstraints{mutatingSCC, nonMutatingSCC},
974+
shouldPass: true,
975+
expectedSCC: nonMutatingSCC.Name,
976+
},
977+
"updating: a pod should be rejected when there are only mutating SCCs": {
978+
oldPod: simplePod.DeepCopy(),
979+
newPod: modifiedPod.DeepCopy(),
980+
operation: kadmission.Update,
981+
sccs: []*securityapi.SecurityContextConstraints{mutatingSCC},
982+
shouldPass: false,
983+
},
984+
}
985+
986+
for testCaseName, testCase := range tests {
987+
// We can't use testSCCAdmission() here because it doesn't support Update operation.
988+
// We can't use testSCCAdmit() here because it doesn't support Update operation and doesn't check for the SCC annotation.
989+
990+
tc := setupClientSet()
991+
lister := createSCCLister(t, testCase.sccs)
992+
plugin := NewTestAdmission(lister, tc)
993+
994+
attrs := kadmission.NewAttributesRecord(testCase.newPod, testCase.oldPod, kapi.Kind("Pod").WithVersion("version"), testCase.newPod.Namespace, testCase.newPod.Name, kapi.Resource("pods").WithVersion("version"), "", testCase.operation, &user.DefaultInfo{})
995+
err := plugin.(kadmission.MutationInterface).Admit(attrs)
996+
997+
if testCase.shouldPass {
998+
if err != nil {
999+
t.Errorf("%s expected no errors but received %v", testCaseName, err)
1000+
} else {
1001+
validatedSCC, ok := testCase.newPod.Annotations[allocator.ValidatedSCCAnnotation]
1002+
if !ok {
1003+
t.Errorf("expected %q to find the validated annotation on the pod for the scc but found none", testCaseName)
1004+
1005+
} else if validatedSCC != testCase.expectedSCC {
1006+
t.Errorf("%q should have validated against %q but found %q", testCaseName, testCase.expectedSCC, validatedSCC)
1007+
}
1008+
}
1009+
} else {
1010+
if err == nil {
1011+
t.Errorf("%s expected errors but received none", testCaseName)
1012+
}
1013+
}
1014+
}
1015+
1016+
}
1017+
9371018
// testSCCAdmission is a helper to admit the pod and ensure it was validated against the expected
9381019
// SCC. Returns true when errors have been encountered.
9391020
func testSCCAdmission(pod *kapi.Pod, plugin kadmission.Interface, expectedSCC, testName string, t *testing.T) bool {
@@ -1087,15 +1168,16 @@ func goodPod() *kapi.Pod {
10871168
}
10881169
}
10891170

1090-
func containerSC(seLinuxLevel string, uid int64) *kapi.SecurityContext {
1091-
no := false
1092-
return &kapi.SecurityContext{
1093-
Privileged: &no,
1094-
RunAsUser: &uid,
1095-
SELinuxOptions: &kapi.SELinuxOptions{
1096-
Level: seLinuxLevel,
1097-
},
1171+
func containerSC(seLinuxLevel *string, uid int64) *kapi.SecurityContext {
1172+
sc := &kapi.SecurityContext{
1173+
RunAsUser: &uid,
1174+
}
1175+
if seLinuxLevel != nil {
1176+
sc.SELinuxOptions = &kapi.SELinuxOptions{
1177+
Level: *seLinuxLevel,
1178+
}
10981179
}
1180+
return sc
10991181
}
11001182

11011183
func podSC(seLinuxLevel string, fsGroup, supGroup int64) *kapi.PodSecurityContext {

pkg/security/securitycontextconstraints/matcher.go

Lines changed: 13 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"k8s.io/apiserver/pkg/authentication/user"
1414
kapi "k8s.io/kubernetes/pkg/apis/core"
1515
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
16-
sc "k8s.io/kubernetes/pkg/securitycontext"
1716

1817
allocator "github.com/openshift/origin/pkg/security"
1918
securityapi "github.com/openshift/origin/pkg/security/apis/security"
@@ -71,87 +70,44 @@ func ConstraintAppliesTo(constraint *securityapi.SecurityContextConstraints, use
7170
// and validates that the sc falls within the scc constraints. All containers must validate against
7271
// the same scc or is not considered valid.
7372
func AssignSecurityContext(provider SecurityContextConstraintsProvider, pod *kapi.Pod, fldPath *field.Path) field.ErrorList {
74-
generatedSCs := make([]*kapi.SecurityContext, len(pod.Spec.InitContainers)+len(pod.Spec.Containers))
75-
7673
errs := field.ErrorList{}
7774

7875
psc, generatedAnnotations, err := provider.CreatePodSecurityContext(pod)
7976
if err != nil {
8077
errs = append(errs, field.Invalid(fldPath.Child("spec", "securityContext"), pod.Spec.SecurityContext, err.Error()))
8178
}
8279

83-
// save the original PSC and validate the generated PSC. Leave the generated PSC
84-
// set for container generation/validation. We will reset to original post container
85-
// validation.
86-
originalPSC := pod.Spec.SecurityContext
87-
originalAnnotations := pod.Annotations
88-
8980
pod.Spec.SecurityContext = psc
9081
pod.Annotations = generatedAnnotations
9182
errs = append(errs, provider.ValidatePodSecurityContext(pod, fldPath.Child("spec", "securityContext"))...)
9283

93-
// Note: this is not changing the original container, we will set container SCs later so long
94-
// as all containers validated under the same SCC.
95-
containerPath := fldPath.Child("spec", "initContainers")
96-
for i, containerCopy := range pod.Spec.InitContainers {
97-
csc, resolutionErrs := resolveContainerSecurityContext(provider, pod, &containerCopy, containerPath.Index(i))
98-
errs = append(errs, resolutionErrs...)
99-
if len(resolutionErrs) > 0 {
84+
for i := range pod.Spec.InitContainers {
85+
sc, err := provider.CreateContainerSecurityContext(pod, &pod.Spec.InitContainers[i])
86+
if err != nil {
87+
errs = append(errs, field.Invalid(field.NewPath("spec", "initContainers").Index(i).Child("securityContext"), "", err.Error()))
10088
continue
10189
}
102-
generatedSCs[i] = csc
90+
pod.Spec.InitContainers[i].SecurityContext = sc
91+
errs = append(errs, provider.ValidateContainerSecurityContext(pod, &pod.Spec.InitContainers[i], field.NewPath("spec", "initContainers").Index(i).Child("securityContext"))...)
10392
}
10493

105-
base := len(pod.Spec.InitContainers)
106-
107-
// Note: this is not changing the original container, we will set container SCs later so long
108-
// as all containers validated under the same SCC.
109-
containerPath = fldPath.Child("spec", "containers")
110-
for i, containerCopy := range pod.Spec.Containers {
111-
csc, resolutionErrs := resolveContainerSecurityContext(provider, pod, &containerCopy, containerPath.Index(i))
112-
errs = append(errs, resolutionErrs...)
113-
if len(resolutionErrs) > 0 {
94+
for i := range pod.Spec.Containers {
95+
sc, err := provider.CreateContainerSecurityContext(pod, &pod.Spec.Containers[i])
96+
if err != nil {
97+
errs = append(errs, field.Invalid(field.NewPath("spec", "containers").Index(i).Child("securityContext"), "", err.Error()))
11498
continue
11599
}
116-
generatedSCs[i+base] = csc
100+
pod.Spec.Containers[i].SecurityContext = sc
101+
errs = append(errs, provider.ValidateContainerSecurityContext(pod, &pod.Spec.Containers[i], field.NewPath("spec", "containers").Index(i).Child("securityContext"))...)
117102
}
103+
118104
if len(errs) > 0 {
119-
// ensure psc is not mutated if there are errors
120-
pod.Spec.SecurityContext = originalPSC
121-
pod.Annotations = originalAnnotations
122105
return errs
123106
}
124107

125-
// if we've reached this code then we've generated and validated an SC for every container in the
126-
// pod so let's apply what we generated. Note: the psc is already applied.
127-
for i := range pod.Spec.InitContainers {
128-
pod.Spec.InitContainers[i].SecurityContext = generatedSCs[i]
129-
}
130-
for i := range pod.Spec.Containers {
131-
pod.Spec.Containers[i].SecurityContext = generatedSCs[i+base]
132-
}
133108
return nil
134109
}
135110

136-
// resolveContainerSecurityContext checks the provided container against the provider, returning any
137-
// validation errors encountered on the resulting security context, or the security context that was
138-
// resolved. The SecurityContext field of the container is updated, so ensure that a copy of the original
139-
// container is passed here if you wish to preserve the original input.
140-
func resolveContainerSecurityContext(provider SecurityContextConstraintsProvider, pod *kapi.Pod, container *kapi.Container, path *field.Path) (*kapi.SecurityContext, field.ErrorList) {
141-
// We will determine the effective security context for the container and validate against that
142-
// since that is how the sc provider will eventually apply settings in the runtime.
143-
// This results in an SC that is based on the Pod's PSC with the set fields from the container
144-
// overriding pod level settings.
145-
container.SecurityContext = sc.InternalDetermineEffectiveSecurityContext(pod, container)
146-
147-
csc, err := provider.CreateContainerSecurityContext(pod, container)
148-
if err != nil {
149-
return nil, field.ErrorList{field.Invalid(path.Child("securityContext"), "", err.Error())}
150-
}
151-
container.SecurityContext = csc
152-
return csc, provider.ValidateContainerSecurityContext(pod, container, path.Child("securityContext"))
153-
}
154-
155111
// constraintSupportsGroup checks that group is in constraintGroups.
156112
func constraintSupportsGroup(group string, constraintGroups []string) bool {
157113
for _, g := range constraintGroups {

0 commit comments

Comments
 (0)