Skip to content

Commit

Permalink
SCC: recognize that SELinux levels can be logically equivalent.
Browse files Browse the repository at this point in the history
For example: s0:c0,c6 is the same as s0:c6,c0
Also add support for categories ranges which can be expressed with
shorthand of c0.c6 and sensitivity with identical levels.
  • Loading branch information
php-coder committed Oct 23, 2017
1 parent 3fddedc commit 7f13165
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 22 deletions.
134 changes: 133 additions & 1 deletion pkg/security/securitycontextconstraints/selinux/mustrunas.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package selinux

import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"

"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/api"
Expand Down Expand Up @@ -48,8 +52,11 @@ func (s *mustRunAs) Validate(pod *api.Pod, container *api.Container) field.Error
}
seLinuxOptionsPath := field.NewPath("seLinuxOptions")
seLinux := container.SecurityContext.SELinuxOptions
if seLinux.Level != s.opts.SELinuxOptions.Level {
if result, err := equalLevels(s.opts.SELinuxOptions.Level, seLinux.Level); err != nil || !result {
detail := fmt.Sprintf("seLinuxOptions.level on %s does not match required level. Found %s, wanted %s", container.Name, seLinux.Level, s.opts.SELinuxOptions.Level)
if err != nil {
detail = fmt.Sprintf("%s, error: %v", detail, err)
}
allErrs = append(allErrs, field.Invalid(seLinuxOptionsPath.Child("level"), seLinux.Level, detail))
}
if seLinux.Role != s.opts.SELinuxOptions.Role {
Expand All @@ -67,3 +74,128 @@ func (s *mustRunAs) Validate(pod *api.Pod, container *api.Container) field.Error

return allErrs
}

func equalLevels(expectedLevel, actualLevel string) (bool, error) {
if expectedLevel == actualLevel {
return true, nil
}

// Level format: https://selinuxproject.org/page/MLSStatements#level

// "s0:c6,c0" => [ "s0", "c6,c0" ]
expectedParts := strings.SplitN(expectedLevel, ":", 2)
actualParts := strings.SplitN(actualLevel, ":", 2)

// "s0-s0" => [ "s0" ]
expectedSensitivity, err := canonicalizeSensitivity(expectedParts[0])
if err != nil {
return false, err
}

actualSensitivity, err := canonicalizeSensitivity(actualParts[0])
if err != nil {
return false, err
}

if !reflect.DeepEqual(expectedSensitivity, actualSensitivity) {
return false, nil
}

expectedPartsLen := len(expectedParts)
actualPartsLen := len(actualParts)

// both levels don't have categories and equal ("s0" and "s0")
if expectedPartsLen == 1 && actualPartsLen == 1 {
return true, nil
}

// "s0" != "s0:c1"
if expectedPartsLen != actualPartsLen {
return false, nil
}

// "c6,c0" => [ "c0", "c6" ]
expectedCategories, err := canonicalizeCategories(expectedParts[1])
if err != nil {
return false, err
}
actualCategories, err := canonicalizeCategories(actualParts[1])
if err != nil {
return false, err
}

// TODO: add support for dominance
// See: https://selinuxproject.org/page/NB_MLS#Managing_Security_Levels_via_Dominance_Rules
return reflect.DeepEqual(expectedCategories, actualCategories), nil
}

// Parses and canonicalize a sensitivity. Performs expansion (s0-s1 => s0,s1)
// and simplification (s0-s0 => s0) if needed.
func canonicalizeSensitivity(sensitivity string) ([]string, error) {
return canonicalizeItems(sensitivity, ",", "-", "s")
}

// Parses and canonicalize a categories. Performs expansion (c0-c1 => c0,c1) if needed.
func canonicalizeCategories(categories string) ([]string, error) {
return canonicalizeItems(categories, ",", ".", "c")
}

func canonicalizeItems(str, itemsSeparator, rangeSeparator, boundaryPrefix string) ([]string, error) {
parts := strings.Split(str, itemsSeparator)
result := make([]string, 0, len(parts))

for _, value := range parts {
if strings.Index(value, rangeSeparator) == -1 {
// it's not a range, add it as-is
result = append(result, value)
continue
}

from, to, err := parseBoundaries(value, rangeSeparator, boundaryPrefix)
if err != nil {
return nil, fmt.Errorf("could not parse %q: %v", value, err)
}

for from <= to {
result = append(result, fmt.Sprintf("%s%d", boundaryPrefix, from))
from++
}
}

// although sorting digits as strings is leading to a wrong order,
// it doesn't matter because we only need to sort both parts in an indentical way
sort.Strings(result)

return result, nil
}

// Parses a string with a range in a format "$prefix$begin$separator$prefix$end" by extracting begin and end of a range.
func parseBoundaries(str, separator, prefix string) (int64, int64, error) {
strRange := strings.SplitN(str, separator, 2)

begin, err := parseBoundary(strRange[0], prefix)
if err != nil {
return -1, -1, fmt.Errorf("invalid initial of a boundary: %v", err)
}

end, err := parseBoundary(strRange[1], prefix)
if err != nil {
return -1, -1, fmt.Errorf("invalid end of a boundary: %v", err)
}

if begin > end {
return -1, -1, fmt.Errorf("initial of a boundary must be less than end of a boundary")
}

return begin, end, nil
}

// Parses a string with a range boundary in a format "$prefix$value" by extracting value and converting it to an integer.
func parseBoundary(str, prefix string) (int64, error) {
if !strings.HasPrefix(str, prefix) {
return -1, fmt.Errorf("must be prefixed with %q", prefix)
}

value := strings.TrimPrefix(str, prefix)
return strconv.ParseInt(value, 10, 16)
}
164 changes: 143 additions & 21 deletions pkg/security/securitycontextconstraints/selinux/mustrunas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,17 @@ func TestMustRunAsValidate(t *testing.T) {
return &api.SELinuxOptions{
User: "user",
Role: "role",
Level: "level",
Level: "s0:c6,c0",
Type: "type",
}
}

newValidOptsWithLevel := func(level string) *api.SELinuxOptions {
opts := newValidOpts()
opts.Level = level
return opts
}

role := newValidOpts()
role.Role = "invalid"

Expand All @@ -80,42 +86,158 @@ func TestMustRunAsValidate(t *testing.T) {
seType.Type = "invalid"

tests := map[string]struct {
seLinux *api.SELinuxOptions
expectedMsg string
seLinux *api.SELinuxOptions
expectedSeLinux *api.SELinuxOptions
expectedMsg string
}{
"invalid role": {
seLinux: role,
expectedMsg: "does not match required role",
seLinux: role,
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required role",
},
"invalid user": {
seLinux: user,
expectedMsg: "does not match required user",
seLinux: user,
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required user",
},
"invalid level": {
seLinux: level,
expectedMsg: "does not match required level",
seLinux: level,
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid type": {
seLinux: seType,
expectedMsg: "does not match required type",
seLinux: seType,
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required type",
},
"invalid level, expected sensitivity with a wrong prefix": {
seLinux: newValidOpts(),
expectedSeLinux: newValidOptsWithLevel("s0-w2"),
expectedMsg: "does not match required level",
},
"invalid level, actual sensitivity with a wrong prefix": {
seLinux: newValidOptsWithLevel("s0-w2"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid level, sensitivity only vs sensitivity with a category": {
seLinux: newValidOptsWithLevel("s1"),
expectedSeLinux: newValidOptsWithLevel("s1:c1"),
expectedMsg: "does not match required level",
},
"invalid level, expected category with a wrong prefix": {
seLinux: newValidOpts(),
expectedSeLinux: newValidOptsWithLevel("s0:c1.w2"),
expectedMsg: "does not match required level",
},
"invalid level, actual category with a wrong prefix": {
seLinux: newValidOptsWithLevel("s0:c1.w2"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid level, actual sensitivity with an invalid an initial boundary": {
seLinux: newValidOptsWithLevel("sS"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid level, actual sensitivity with an invalid end of a boundary": {
seLinux: newValidOptsWithLevel("s0-sS"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid level, actual sensitivity with an invalid boundaries": {
seLinux: newValidOptsWithLevel("s6-s0"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid level, actual category with an invalid an initial boundary": {
seLinux: newValidOptsWithLevel("s0:cC.c0"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid level, actual category with an invalid end of a boundary": {
seLinux: newValidOptsWithLevel("s0:c0.cC"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"invalid level, actual category with an invalid boundaries": {
seLinux: newValidOptsWithLevel("s0:c6.c0"),
expectedSeLinux: newValidOpts(),
expectedMsg: "does not match required level",
},
"valid": {
seLinux: newValidOpts(),
expectedMsg: "",
seLinux: newValidOpts(),
expectedSeLinux: newValidOpts(),
expectedMsg: "",
},
"valid level with human-readable definition (single value)": {
seLinux: newValidOptsWithLevel("SystemLow"),
expectedSeLinux: newValidOptsWithLevel("SystemLow"),
expectedMsg: "",
},
"valid level with human-readable definition (range)": {
seLinux: newValidOptsWithLevel("SystemLow-SystemHigh"),
expectedSeLinux: newValidOptsWithLevel("SystemLow-SystemHigh"),
expectedMsg: "",
},
"valid level with sensitivity only": {
seLinux: newValidOptsWithLevel("s0"),
expectedSeLinux: newValidOptsWithLevel("s0"),
expectedMsg: "",
},
"valid level with single sensitivity and category": {
seLinux: newValidOptsWithLevel("s0:c0"),
expectedSeLinux: newValidOptsWithLevel("s0:c0"),
expectedMsg: "",
},
"valid level with identical sensitivity": {
seLinux: newValidOptsWithLevel("s0-s0:c6,c0"),
expectedSeLinux: newValidOptsWithLevel("s0:c6,c0"),
expectedMsg: "",
},
"valid level with abbreviated sensitivity and categories": {
seLinux: newValidOptsWithLevel("s1-s3:c10.c12"),
expectedSeLinux: newValidOptsWithLevel("s1,s2,s3:c10,c11,c12"),
expectedMsg: "",
},
"valid level with a multiple sensitivity ranges": {
seLinux: newValidOptsWithLevel("s0-s0,s3-s4,s1-s2"),
expectedSeLinux: newValidOptsWithLevel("s0,s1,s2,s3,s4"),
expectedMsg: "",
},
"valid level with different order of categories": {
seLinux: newValidOptsWithLevel("s0:c0,c6"),
expectedSeLinux: newValidOptsWithLevel("s0:c6,c0"),
expectedMsg: "",
},
"valid level with abbreviated categories (that starts from 0)": {
seLinux: newValidOptsWithLevel("s0:c0.c3"),
expectedSeLinux: newValidOptsWithLevel("s0:c0,c1,c2,c3"),
expectedMsg: "",
},
"valid level with abbreviated categories (that starts from 1)": {
seLinux: newValidOptsWithLevel("s0:c1.c3"),
expectedSeLinux: newValidOptsWithLevel("s0:c1,c2,c3"),
expectedMsg: "",
},
"valid level with multiple abbreviated and non-abbreviated categories": {
seLinux: newValidOptsWithLevel("s0:c0,c2.c5,c7,c9.c10"),
expectedSeLinux: newValidOptsWithLevel("s0:c0,c2,c3,c4,c5,c7,c9,c10"),
expectedMsg: "",
},
}

opts := &securityapi.SELinuxContextStrategyOptions{
SELinuxOptions: newValidOpts(),
}

for name, tc := range tests {
opts := &securityapi.SELinuxContextStrategyOptions{
SELinuxOptions: tc.expectedSeLinux,
}
mustRunAs, err := NewMustRunAs(opts)
if err != nil {
t.Errorf("unexpected error initializing NewMustRunAs for testcase %s: %#v", name, err)
continue
}
container := &api.Container{
Name: "selinux-testing-container",
SecurityContext: &api.SecurityContext{
SELinuxOptions: tc.seLinux,
},
Expand All @@ -124,20 +246,20 @@ func TestMustRunAsValidate(t *testing.T) {
errs := mustRunAs.Validate(nil, container)
//should've passed but didn't
if len(tc.expectedMsg) == 0 && len(errs) > 0 {
t.Errorf("%s expected no errors but received %v", name, errs)
t.Errorf("%q expected no errors but received %v", name, errs)
}
//should've failed but didn't
if len(tc.expectedMsg) != 0 && len(errs) == 0 {
t.Errorf("%s expected error %s but received no errors", name, tc.expectedMsg)
t.Errorf("%q expected error %s but received no errors", name, tc.expectedMsg)
}
//failed with additional messages
if len(tc.expectedMsg) != 0 && len(errs) > 1 {
t.Errorf("%s expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs)
t.Errorf("%q expected error %s but received multiple errors: %v", name, tc.expectedMsg, errs)
}
//check that we got the right message
if len(tc.expectedMsg) != 0 && len(errs) == 1 {
if !strings.Contains(errs[0].Error(), tc.expectedMsg) {
t.Errorf("%s expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs)
t.Errorf("%q expected error to contain %s but it did not: %v", name, tc.expectedMsg, errs)
}
}
}
Expand Down

0 comments on commit 7f13165

Please sign in to comment.