mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 12:43:23 +00:00
Merge pull request #11669 from pweil-/sc-nonroot
add non-root directive to SC and kubelet checking
This commit is contained in:
commit
3f7b54cbdb
@ -12782,6 +12782,10 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"description": "the user id that runs the first process in the container; see http://releases.k8s.io/HEAD/docs/design/security_context.md#security-context"
|
"description": "the user id that runs the first process in the container; see http://releases.k8s.io/HEAD/docs/design/security_context.md#security-context"
|
||||||
|
},
|
||||||
|
"runAsNonRoot": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "indicates the container must be run as a non-root user either by specifying the runAsUser or in the image specification"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1803,6 +1803,7 @@ func deepCopy_api_SecurityContext(in SecurityContext, out *SecurityContext, c *c
|
|||||||
} else {
|
} else {
|
||||||
out.RunAsUser = nil
|
out.RunAsUser = nil
|
||||||
}
|
}
|
||||||
|
out.RunAsNonRoot = in.RunAsNonRoot
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2166,6 +2166,11 @@ type SecurityContext struct {
|
|||||||
|
|
||||||
// RunAsUser is the UID to run the entrypoint of the container process.
|
// RunAsUser is the UID to run the entrypoint of the container process.
|
||||||
RunAsUser *int64 `json:"runAsUser,omitempty"`
|
RunAsUser *int64 `json:"runAsUser,omitempty"`
|
||||||
|
|
||||||
|
// RunAsNonRoot indicates that the container should be run as a non-root user. If the RunAsUser
|
||||||
|
// field is not explicitly set then the kubelet may check the image for a specified user or
|
||||||
|
// perform defaulting to specify a user.
|
||||||
|
RunAsNonRoot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SELinuxOptions are the labels to be applied to the container.
|
// SELinuxOptions are the labels to be applied to the container.
|
||||||
|
@ -2002,6 +2002,7 @@ func convert_api_SecurityContext_To_v1_SecurityContext(in *api.SecurityContext,
|
|||||||
} else {
|
} else {
|
||||||
out.RunAsUser = nil
|
out.RunAsUser = nil
|
||||||
}
|
}
|
||||||
|
out.RunAsNonRoot = in.RunAsNonRoot
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4344,6 +4345,7 @@ func convert_v1_SecurityContext_To_api_SecurityContext(in *SecurityContext, out
|
|||||||
} else {
|
} else {
|
||||||
out.RunAsUser = nil
|
out.RunAsUser = nil
|
||||||
}
|
}
|
||||||
|
out.RunAsNonRoot = in.RunAsNonRoot
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1812,6 +1812,7 @@ func deepCopy_v1_SecurityContext(in SecurityContext, out *SecurityContext, c *co
|
|||||||
} else {
|
} else {
|
||||||
out.RunAsUser = nil
|
out.RunAsUser = nil
|
||||||
}
|
}
|
||||||
|
out.RunAsNonRoot = in.RunAsNonRoot
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2030,6 +2030,11 @@ type SecurityContext struct {
|
|||||||
|
|
||||||
// RunAsUser is the UID to run the entrypoint of the container process.
|
// RunAsUser is the UID to run the entrypoint of the container process.
|
||||||
RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container; see http://releases.k8s.io/HEAD/docs/design/security_context.md#security-context"`
|
RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container; see http://releases.k8s.io/HEAD/docs/design/security_context.md#security-context"`
|
||||||
|
|
||||||
|
// RunAsNonRoot indicates that the container should be run as a non-root user. If the RunAsUser
|
||||||
|
// field is not explicitly set then the kubelet may check the image for a specified user or
|
||||||
|
// perform defaulting to specify a user.
|
||||||
|
RunAsNonRoot bool `json:"runAsNonRoot,omitempty" description:"indicates the container must be run as a non-root user either by specifying the runAsUser or in the image specification"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SELinuxOptions are the labels to be applied to the container
|
// SELinuxOptions are the labels to be applied to the container
|
||||||
|
@ -1619,6 +1619,15 @@ func (dm *DockerManager) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, pod
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if container.SecurityContext != nil && container.SecurityContext.RunAsNonRoot {
|
||||||
|
err := dm.verifyNonRoot(container)
|
||||||
|
dm.updateReasonCache(pod, container, err)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("Error running pod %q container %q: %v", kubecontainer.GetPodFullName(pod), container.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(dawnchen): Check RestartPolicy.DelaySeconds before restart a container
|
// TODO(dawnchen): Check RestartPolicy.DelaySeconds before restart a container
|
||||||
namespaceMode := fmt.Sprintf("container:%v", podInfraContainerID)
|
namespaceMode := fmt.Sprintf("container:%v", podInfraContainerID)
|
||||||
_, err = dm.runContainerInPod(pod, container, namespaceMode, namespaceMode)
|
_, err = dm.runContainerInPod(pod, container, namespaceMode, namespaceMode)
|
||||||
@ -1635,3 +1644,62 @@ func (dm *DockerManager) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, pod
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verifyNonRoot returns an error if the container or image will run as the root user.
|
||||||
|
func (dm *DockerManager) verifyNonRoot(container *api.Container) error {
|
||||||
|
if securitycontext.HasRunAsUser(container) {
|
||||||
|
if securitycontext.HasRootRunAsUser(container) {
|
||||||
|
return fmt.Errorf("container's runAsUser breaks non-root policy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imgRoot, err := dm.isImageRoot(container.Image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if imgRoot {
|
||||||
|
return fmt.Errorf("container has no runAsUser and image will run as root")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isImageRoot returns true if the user directive is not set on the image, the user is set to 0
|
||||||
|
// or the user is set to root. If there is an error inspecting the image this method will return
|
||||||
|
// false and return the error.
|
||||||
|
func (dm *DockerManager) isImageRoot(image string) (bool, error) {
|
||||||
|
img, err := dm.client.InspectImage(image)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if img == nil || img.Config == nil {
|
||||||
|
return false, fmt.Errorf("unable to inspect image %s, nil Config", image)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := getUidFromUser(img.Config.User)
|
||||||
|
// if no user is defined container will run as root
|
||||||
|
if user == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
// do not allow non-numeric user directives
|
||||||
|
uid, err := strconv.Atoi(user)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("unable to validate image is non-root, non-numeric user (%s) is not allowed", user)
|
||||||
|
}
|
||||||
|
// user is numeric, check for 0
|
||||||
|
return uid == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUidFromUser splits the uid out of a uid:gid string.
|
||||||
|
func getUidFromUser(id string) string {
|
||||||
|
if id == "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
// split instances where the id may contain uid:gid
|
||||||
|
if strings.Contains(id, ":") {
|
||||||
|
return strings.Split(id, ":")[0]
|
||||||
|
}
|
||||||
|
// no gid, just return the id
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
@ -2085,3 +2085,129 @@ func TestGetPodStatusSortedContainers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerifyNonRoot(t *testing.T) {
|
||||||
|
dm, fakeDocker := newTestDockerManager()
|
||||||
|
|
||||||
|
// setup test cases.
|
||||||
|
var rootUid int64 = 0
|
||||||
|
var nonRootUid int64 = 1
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
container *api.Container
|
||||||
|
inspectImage *docker.Image
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
// success cases
|
||||||
|
"non-root runAsUser": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: &nonRootUid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"numeric non-root image user": {
|
||||||
|
container: &api.Container{},
|
||||||
|
inspectImage: &docker.Image{
|
||||||
|
Config: &docker.Config{
|
||||||
|
User: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"numeric non-root image user with gid": {
|
||||||
|
container: &api.Container{},
|
||||||
|
inspectImage: &docker.Image{
|
||||||
|
Config: &docker.Config{
|
||||||
|
User: "1:2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// failure cases
|
||||||
|
"root runAsUser": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: &rootUid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: "container's runAsUser breaks non-root policy",
|
||||||
|
},
|
||||||
|
"non-numeric image user": {
|
||||||
|
container: &api.Container{},
|
||||||
|
inspectImage: &docker.Image{
|
||||||
|
Config: &docker.Config{
|
||||||
|
User: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: "unable to validate image is non-root, non-numeric user",
|
||||||
|
},
|
||||||
|
"numeric root image user": {
|
||||||
|
container: &api.Container{},
|
||||||
|
inspectImage: &docker.Image{
|
||||||
|
Config: &docker.Config{
|
||||||
|
User: "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: "container has no runAsUser and image will run as root",
|
||||||
|
},
|
||||||
|
"numeric root image user with gid": {
|
||||||
|
container: &api.Container{},
|
||||||
|
inspectImage: &docker.Image{
|
||||||
|
Config: &docker.Config{
|
||||||
|
User: "0:1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: "container has no runAsUser and image will run as root",
|
||||||
|
},
|
||||||
|
"nil image in inspect": {
|
||||||
|
container: &api.Container{},
|
||||||
|
expectedError: "unable to inspect image",
|
||||||
|
},
|
||||||
|
"nil config in image inspect": {
|
||||||
|
container: &api.Container{},
|
||||||
|
inspectImage: &docker.Image{},
|
||||||
|
expectedError: "unable to inspect image",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tests {
|
||||||
|
fakeDocker.Image = v.inspectImage
|
||||||
|
err := dm.verifyNonRoot(v.container)
|
||||||
|
if v.expectedError == "" && err != nil {
|
||||||
|
t.Errorf("%s had unexpected error %v", k, err)
|
||||||
|
}
|
||||||
|
if v.expectedError != "" && !strings.Contains(err.Error(), v.expectedError) {
|
||||||
|
t.Errorf("%s expected error %s but received %s", k, v.expectedError, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUidFromUser(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
"no gid": {
|
||||||
|
input: "0",
|
||||||
|
expect: "0",
|
||||||
|
},
|
||||||
|
"uid/gid": {
|
||||||
|
input: "0:1",
|
||||||
|
expect: "0",
|
||||||
|
},
|
||||||
|
"empty input": {
|
||||||
|
input: "",
|
||||||
|
expect: "",
|
||||||
|
},
|
||||||
|
"multiple spearators": {
|
||||||
|
input: "1:2:3",
|
||||||
|
expect: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for k, v := range tests {
|
||||||
|
actual := getUidFromUser(v.input)
|
||||||
|
if actual != v.expect {
|
||||||
|
t.Errorf("%s failed. Expected %s but got %s", k, v.expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -856,6 +856,8 @@ func (r *runtime) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, podStatus
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: check for non-root image directives. See ../docker/manager.go#SyncPod
|
||||||
|
|
||||||
// TODO(yifan): Take care of host network change.
|
// TODO(yifan): Take care of host network change.
|
||||||
containerChanged := c.Hash != 0 && c.Hash != expectedHash
|
containerChanged := c.Hash != 0 && c.Hash != expectedHash
|
||||||
if containerChanged {
|
if containerChanged {
|
||||||
|
@ -66,3 +66,24 @@ func ParseSELinuxOptions(context string) (*api.SELinuxOptions, error) {
|
|||||||
Level: fields[3],
|
Level: fields[3],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasNonRootUID returns true if the runAsUser is set and is greater than 0.
|
||||||
|
func HasRootUID(container *api.Container) bool {
|
||||||
|
if container.SecurityContext == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if container.SecurityContext.RunAsUser == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *container.SecurityContext.RunAsUser == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasRunAsUser determines if the sc's runAsUser field is set.
|
||||||
|
func HasRunAsUser(container *api.Container) bool {
|
||||||
|
return container.SecurityContext != nil && container.SecurityContext.RunAsUser != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasRootRunAsUser returns true if the run as user is set and it is set to 0.
|
||||||
|
func HasRootRunAsUser(container *api.Container) bool {
|
||||||
|
return HasRunAsUser(container) && HasRootUID(container)
|
||||||
|
}
|
||||||
|
@ -83,3 +83,124 @@ func compareContexts(name string, ex, ac *api.SELinuxOptions, t *testing.T) {
|
|||||||
t.Errorf("%v: expected level: %v, got: %v", name, e, a)
|
t.Errorf("%v: expected level: %v, got: %v", name, e, a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHaRootUID(t *testing.T) {
|
||||||
|
var nonRoot int64 = 1
|
||||||
|
var root int64 = 0
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
container *api.Container
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
"nil sc": {
|
||||||
|
container: &api.Container{SecurityContext: nil},
|
||||||
|
},
|
||||||
|
"nil runAsuser": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"runAsUser non-root": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: &nonRoot,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"runAsUser root": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: &root,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tests {
|
||||||
|
actual := HasRootUID(v.container)
|
||||||
|
if actual != v.expect {
|
||||||
|
t.Errorf("%s failed, expected %t but received %t", k, v.expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasRunAsUser(t *testing.T) {
|
||||||
|
var runAsUser int64 = 0
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
container *api.Container
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
"nil sc": {
|
||||||
|
container: &api.Container{SecurityContext: nil},
|
||||||
|
},
|
||||||
|
"nil runAsUser": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"valid runAsUser": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: &runAsUser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tests {
|
||||||
|
actual := HasRunAsUser(v.container)
|
||||||
|
if actual != v.expect {
|
||||||
|
t.Errorf("%s failed, expected %t but received %t", k, v.expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasRootRunAsUser(t *testing.T) {
|
||||||
|
var nonRoot int64 = 1
|
||||||
|
var root int64 = 0
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
container *api.Container
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
"nil sc": {
|
||||||
|
container: &api.Container{SecurityContext: nil},
|
||||||
|
},
|
||||||
|
"nil runAsuser": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"runAsUser non-root": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: &nonRoot,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"runAsUser root": {
|
||||||
|
container: &api.Container{
|
||||||
|
SecurityContext: &api.SecurityContext{
|
||||||
|
RunAsUser: &root,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tests {
|
||||||
|
actual := HasRootRunAsUser(v.container)
|
||||||
|
if actual != v.expect {
|
||||||
|
t.Errorf("%s failed, expected %t but received %t", k, v.expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user