Skip to content

Commit

Permalink
Extend UCPAuthorization for fine-grained pod parameters (kubernetes#8)
Browse files Browse the repository at this point in the history
* Extend UCPAuthorization for pod parameters

Signed-off-by: Alex Mavrogiannis <alex.mavrogiannis@docker.com>

* Only allow admins to use fine-grained parameters
  • Loading branch information
alexmavr authored and Wayne Song committed May 21, 2018
1 parent e859e57 commit 126cefd
Show file tree
Hide file tree
Showing 4 changed files with 425 additions and 24 deletions.
2 changes: 1 addition & 1 deletion cmd/kube-apiserver/app/options/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ go_library(
"//plugin/pkg/admission/serviceaccount:go_default_library",
"//plugin/pkg/admission/signingpolicy:go_default_library",
"//plugin/pkg/admission/storageclass/setdefault:go_default_library",
"//plugin/pkg/admission/ucpnodeselector:go_default_library",
"//plugin/pkg/admission/ucpauthz:go_default_library",
"//plugin/pkg/admission/ucpnodeselector:go_default_library",
"//plugin/pkg/admission/webhook:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library",
Expand Down
4 changes: 4 additions & 0 deletions plugin/pkg/admission/ucpauthz/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//pkg/api:go_default_library",
"//pkg/apis/apps:go_default_library",
"//pkg/apis/batch:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//vendor/github.com/docker/go-connections/tlsconfig:go_default_library",
"//vendor/github.com/sirupsen/logrus:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
Expand Down
163 changes: 155 additions & 8 deletions plugin/pkg/admission/ucpauthz/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ucpauthz

import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
Expand All @@ -28,11 +29,12 @@ import (
// admin.

const (
key = "key.pem"
cert = "cert.pem"
rootCA = "ca.pem"
apiPath = "/api/authz/isadmin"
queryKey = "user"
key = "key.pem"
cert = "cert.pem"
rootCA = "ca.pem"
isAdminPath = "/api/authz/isadmin"
parametersPath = "/api/authz/parameters"
queryKey = "user"
)

const (
Expand Down Expand Up @@ -77,6 +79,90 @@ type ucpAuthz struct {
systemPrefix string
}

type authzParameters struct {
hostBindMounts bool // true if the pod has a hostPath volume defined
privileged bool // true if any container's SecurityContext or the PodSecurityPolicy has `privileged` or has allowPrivilegedEscalation
extraCaps bool // true if any container's SecurityContext or the PodSecurityPolicy has `capabilities`
hostNetwork bool // true if the Pod has hostNetwork set
hostIPC bool // trie if the Pod has hostIPC set
hostPID bool // trie if the Pod has hostPID
}

// String serializes only the true parameters into a string.
func (p *authzParameters) String() string {
acc := []string{}
for _, v := range []struct {
param string
requested bool
}{
{"host bind mounts", p.hostBindMounts},
{"privileged mode", p.privileged},
{"extra kernel capabilities", p.extraCaps},
{"host networking", p.hostNetwork},
{"host IPC mode", p.hostIPC},
{"host PID mode", p.hostPID},
} {
if !v.requested {
continue
}
acc = append(acc, v.param)
}
return strings.Join(acc, ", ")
}

// HasRestrictedParameters returns true if any of the parameters are true
func (p *authzParameters) HasRestrictedParameters() bool {
return p.hostBindMounts ||
p.privileged ||
p.extraCaps ||
p.hostNetwork ||
p.hostIPC ||
p.hostPID
}

// ParamsFromPodSpec parses a PodSpec and calculates the requested authzParameters
func ParamsFromPodSpec(podSpec *api.PodSpec) *authzParameters {
if podSpec == nil {
return nil
}
resp := &authzParameters{}

// First parse the SecurityContext of the PodSpec
if podSpec.SecurityContext != nil {
resp.hostPID = podSpec.SecurityContext.HostPID
resp.hostIPC = podSpec.SecurityContext.HostIPC
resp.hostNetwork = podSpec.SecurityContext.HostNetwork
}

// If the PodSpec is defining any HostPath-source volumes, mark the
// hostBindMounts parameter.
for _, volume := range podSpec.Volumes {
if volume.HostPath != nil && volume.HostPath.Path != "" {
resp.hostBindMounts = true
break
}
}

// For each container, parse their individual SecurityContext
for _, container := range append(podSpec.Containers, podSpec.InitContainers...) {
if container.SecurityContext != nil {
if container.SecurityContext.Privileged != nil {
resp.privileged = resp.privileged || *container.SecurityContext.Privileged
}
if container.SecurityContext.AllowPrivilegeEscalation != nil {
resp.privileged = resp.privileged || *container.SecurityContext.AllowPrivilegeEscalation
}
if container.SecurityContext.Capabilities != nil {
if len(container.SecurityContext.Capabilities.Add) > 0 {
resp.extraCaps = true
}
}
}
}

return resp
}

func (a *ucpAuthz) Admit(attributes admission.Attributes) (err error) {
user := attributes.GetUserInfo().GetName()
log.Debugf("the user is: %s", user)
Expand All @@ -92,8 +178,10 @@ func (a *ucpAuthz) Admit(attributes admission.Attributes) (err error) {
return nil
}

// The `default` service account of each namespace is permitted because its
// permissions are blocked via authorization.
// Only UCP admins are allowed to use service accounts. However, the
// `default` service account of each namespace is permitted because it is
// automatically added to pods during the ServiceAccount admission
// controller and its actions will be blocked during authorization.
if podSpec.ServiceAccountName != "" && podSpec.ServiceAccountName != "default" {
isAdmin, err := a.isAdmin(user)
if err != nil {
Expand All @@ -104,15 +192,74 @@ func (a *ucpAuthz) Admit(attributes admission.Attributes) (err error) {
}
}

// Inspect the podSpec for low-level request parameters
params := ParamsFromPodSpec(podSpec)
if params.HasRestrictedParameters() {
allowed, err := a.isAdmin(user)
//allowed, err := a.userHasPermissions(user, params)
if err != nil {
return apierrors.NewInternalError(fmt.Errorf("unable to determine if user \"%s\" has fine-grained permissions \"%s\" for resource %s: %s", user, params.String(), attributes.GetName(), err))
}

if !allowed {
return admission.NewForbidden(attributes, fmt.Errorf("user \"%s\" is not an admin and does not have permissions to use %s for resource %s", user, params.String(), attributes.GetName()))
}
}

return nil
}

func (a *ucpAuthz) userHasPermissions(username string, params *authzParameters) (bool, error) {
u, err := url.Parse(a.ucpLocation)
if err != nil {
return false, fmt.Errorf("unable to parse UCP location \"%s\": %s", a.ucpLocation, err)
}
u.Path = parametersPath

// Serialize the parameters
m, err := json.Marshal(params)
if err != nil {
return false, fmt.Errorf("unable to marshal parameters request: %s", err)
}

q := u.Query()
q.Set("user", username)
q.Set("params", string(m))
u.RawQuery = q.Encode()

resp, err := a.httpClient.Get(u.String())
if err != nil {
return false, fmt.Errorf("request at %s failed: %s", u.String(), err)
}

defer resp.Body.Close()
msg, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("received status code %d from %s but unable to read response message: %s", resp.StatusCode, u.String(), err)
}
msgStr := strings.TrimSpace(string(msg))

if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("received status code %d from %s: %s", resp.StatusCode, u.String(), msgStr)
}

switch msgStr {
case "true":
return true, nil
case "false":
return false, nil
default:
return false, fmt.Errorf("unknown response \"%s\" while requesting parameter permissions for user %s", msgStr, username)
}

}

func (a *ucpAuthz) isAdmin(username string) (bool, error) {
u, err := url.Parse(a.ucpLocation)
if err != nil {
return false, fmt.Errorf("unable to parse UCP location \"%s\": %s", a.ucpLocation, err)
}
u.Path = apiPath
u.Path = isAdminPath
q := u.Query()
q.Set("user", username)
u.RawQuery = q.Encode()
Expand Down
Loading

0 comments on commit 126cefd

Please sign in to comment.