Skip to content

Commit

Permalink
Add validation for rancher rbac objects
Browse files Browse the repository at this point in the history
Problem:
Adding the role restricted-admin in rancher requires validation of
objects through diferent means to validate the user doing the action

Solution:
Add webhook validation to GRBs PRTBs and CRTBs.
GRBs: Validation is done to ensure the GRB being created by the user will
not cause escalated privileges so the user creating the GRB must have at
minimum the permissions they are attempting to grant through the GRB

PRTBs and CRTBs: The same check is done for both. First validation is
only done on the local cluster. Then a subject access review is done for
the requester checking that they have '**' access in the local cluster.
  • Loading branch information
dramich committed Sep 25, 2020
1 parent cb1e742 commit 3d719a0
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/dist
*.swp
.idea
/webhook
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:generate go run pkg/codegen/cleanup/main.go
//go:generate go run pkg/codegen/main.go
package main

import (
Expand Down
83 changes: 69 additions & 14 deletions pkg/admission/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,33 @@ import (
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

var (
namespace = "cattle-system"
tlsName = "rancher-webhook.cattle-system.svc"
certName = "cattle-webhook-tls"
caName = "cattle-webhook-ca"
port = int32(443)
path = "/v1/webhook/validation"
clusterScope = v1.ClusterScope
failPolicy = v1.Ignore
sideEffect = v1.SideEffectClassNone
namespace = "cattle-system"
tlsName = "rancher-webhook.cattle-system.svc"
certName = "cattle-webhook-tls"
caName = "cattle-webhook-ca"
port = int32(443)
path = "/v1/webhook/validation"
clusterScope = v1.ClusterScope
namespaceScope = v1.NamespacedScope
failPolicyFail = v1.Fail
failPolicyIgnore = v1.Ignore
sideEffect = v1.SideEffectClassNone
)

func ListenAndServe(ctx context.Context, cfg *rest.Config) error {
if err := schemes.Register(v1.AddToScheme); err != nil {
return err
}

k8s, err := kubernetes.NewForConfig(cfg)
handler, err := Validation(cfg)
if err != nil {
return err
}

handler := Validation(k8s.AuthorizationV1().SubjectAccessReviews())

return listenAndServe(ctx, cfg, handler)
}

Expand Down Expand Up @@ -91,7 +90,63 @@ func listenAndServe(ctx context.Context, cfg *rest.Config, handler http.Handler)
},
},
},
FailurePolicy: &failPolicy,
FailurePolicy: &failPolicyIgnore,
SideEffects: &sideEffect,
AdmissionReviewVersions: []string{"v1"},
},
{
Name: "rancherauth.cattle.io",
ClientConfig: v1.WebhookClientConfig{
Service: &v1.ServiceReference{
Namespace: namespace,
Name: "rancher-webhook",
Path: &path,
Port: &port,
},
CABundle: secret.Data[corev1.TLSCertKey],
},
Rules: []v1.RuleWithOperations{
{
Operations: []v1.OperationType{
v1.Create,
v1.Update,
v1.Delete,
},
Rule: v1.Rule{
APIGroups: []string{"management.cattle.io"},
APIVersions: []string{"v3"},
Resources: []string{"globalrolebindings"},
Scope: &clusterScope,
},
},
{
Operations: []v1.OperationType{
v1.Create,
v1.Update,
v1.Delete,
},
Rule: v1.Rule{
APIGroups: []string{"management.cattle.io"},
APIVersions: []string{"v3"},
Resources: []string{"projectroletemplatebindings"},
Scope: &namespaceScope,
},
},
{
Operations: []v1.OperationType{
v1.Create,
v1.Update,
v1.Delete,
},
Rule: v1.Rule{
APIGroups: []string{"management.cattle.io"},
APIVersions: []string{"v3"},
Resources: []string{"clusterroletemplatebindings"},
Scope: &namespaceScope,
},
},
},
FailurePolicy: &failPolicyFail,
SideEffects: &sideEffect,
AdmissionReviewVersions: []string{"v1"},
},
Expand Down
40 changes: 36 additions & 4 deletions pkg/admission/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,47 @@ import (

"github.com/rancher/rancher/pkg/apis/management.cattle.io"
v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"github.com/rancher/webhook/pkg/auth"
"github.com/rancher/webhook/pkg/cluster"
mgmtcontrollers "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io"
"github.com/rancher/wrangler-api/pkg/generated/controllers/rbac"
"github.com/rancher/wrangler/pkg/webhook"
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

func Validation(sar authorizationv1.SubjectAccessReviewInterface) http.Handler {
clusters := newClusterValidator(sar)
func Validation(cfg *rest.Config) (http.Handler, error) {
grb, err := mgmtcontrollers.NewFactoryFromConfig(cfg)
if err != nil {
return nil, err
}

r, err := rbac.NewFactoryFromConfig(cfg)
if err != nil {
return nil, err
}

globalRoleBindings, err := auth.NewGRBValidator(grb.Management().V3().GlobalRole(), r.Rbac())
if err != nil {
return nil, err
}

k8s, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}

sar := k8s.AuthorizationV1().SubjectAccessReviews()

clusters := cluster.NewClusterValidator(sar)
prtbs := auth.NewPRTBalidator(sar)
crtbs := auth.NewCRTBalidator(sar)

router := webhook.NewRouter()
router.Kind("Cluster").Group(management.GroupName).Type(&v3.Cluster{}).Handle(clusters)
router.Kind("GlobalRoleBinding").Group(management.GroupName).Type(&v3.GlobalRoleBinding{}).Handle(globalRoleBindings)
router.Kind("ProjectRoleTemplateBinding").Group(management.GroupName).Type(&v3.ProjectRoleTemplateBinding{}).Handle(prtbs)
router.Kind("ClusterRoleTemplateBinding").Group(management.GroupName).Type(&v3.ClusterRoleTemplateBinding{}).Handle(crtbs)

return router
return router, nil
}
96 changes: 96 additions & 0 deletions pkg/auth/clusterrtb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package auth

import (
"net/http"
"time"

rancherv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"github.com/rancher/wrangler/pkg/webhook"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/utils/trace"
)

const adminRole = "admin"

func NewCRTBalidator(sar authorizationv1.SubjectAccessReviewInterface) webhook.Handler {
return &clusterRoleTemplateBindingValidator{
sar: sar,
}
}

type clusterRoleTemplateBindingValidator struct {
sar authorizationv1.SubjectAccessReviewInterface
}

func (c *clusterRoleTemplateBindingValidator) Admit(response *webhook.Response, request *webhook.Request) error {
listTrace := trace.New("clusterRoleTemplateBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(1 * time.Second)

crtb, err := crtbObject(request)
if err != nil {
return err
}

if crtb.ClusterName != "local" {
response.Allowed = true
return nil
}

return adminAccessCheck(c.sar, response, request)
}

func crtbObject(request *webhook.Request) (*rancherv3.ClusterRoleTemplateBinding, error) {
var crtb runtime.Object
var err error
if request.Operation == admissionv1.Delete {
crtb, err = request.DecodeOldObject()
} else {
crtb, err = request.DecodeObject()
}
return crtb.(*rancherv3.ClusterRoleTemplateBinding), err
}

func toExtra(extra map[string]authenticationv1.ExtraValue) map[string]v1.ExtraValue {
result := map[string]v1.ExtraValue{}
for k, v := range extra {
result[k] = v1.ExtraValue(v)
}
return result
}

// adminAccessCheck checks that the user submitting the request has ** access in the local cluster
func adminAccessCheck(sar authorizationv1.SubjectAccessReviewInterface, response *webhook.Response, request *webhook.Request) error {
resp, err := sar.Create(request.Context, &v1.SubjectAccessReview{
Spec: v1.SubjectAccessReviewSpec{
ResourceAttributes: &v1.ResourceAttributes{
Group: "*",
Verb: "*",
Resource: "*",
},
User: request.UserInfo.Username,
Groups: request.UserInfo.Groups,
Extra: toExtra(request.UserInfo.Extra),
UID: request.UserInfo.UID,
},
}, metav1.CreateOptions{})
if err != nil {
return err
}

if resp.Status.Allowed {
response.Allowed = true
} else {
response.Result = &metav1.Status{
Status: "Failure",
Message: resp.Status.Reason,
Reason: metav1.StatusReasonUnauthorized,
Code: http.StatusUnauthorized,
}
}
return nil
}
107 changes: 107 additions & 0 deletions pkg/auth/globarolebinding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package auth

import (
"context"
"fmt"
"net/http"
"time"

rancherv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"github.com/rancher/webhook/pkg/authentication"
v3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3"
"github.com/rancher/wrangler-api/pkg/generated/controllers/rbac"
"github.com/rancher/wrangler/pkg/webhook"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/user"
k8srequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/registry/rbac/validation"
rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation"
"k8s.io/utils/trace"
)

func NewGRBValidator(grClient v3.GlobalRoleClient, r rbac.Interface) (webhook.Handler, error) {
rbacRestGetter := authentication.RBACRestGetter{
Interface: r,
}

ruleResolver := rbacregistryvalidation.NewDefaultRuleResolver(rbacRestGetter, rbacRestGetter, rbacRestGetter, rbacRestGetter)

return &globalRoleBindingValidator{
globalRoleClient: grClient,
ruleSolver: ruleResolver,
}, nil

}

type globalRoleBindingValidator struct {
globalRoleClient v3.GlobalRoleClient
ruleSolver validation.AuthorizationRuleResolver
}

func (grbv *globalRoleBindingValidator) Admit(response *webhook.Response, request *webhook.Request) error {
listTrace := trace.New("globalRoleBindingValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(1 * time.Second)

newGRB, err := grbObject(request)
if err != nil {
return err
}

// Pull the global role to get the rules
globalRole, err := grbv.globalRoleClient.Get(newGRB.GlobalRoleName, metav1.GetOptions{})
if err != nil {
return err
}

userInfo := &user.DefaultInfo{
Name: request.UserInfo.Username,
UID: request.UserInfo.UID,
Groups: request.UserInfo.Groups,
Extra: toExtraString(request.UserInfo.Extra),
}

if err := grbv.ConfirmNoEscalation(globalRole.Rules, userInfo); err != nil {
response.Result = &metav1.Status{
Status: "Failure",
Message: err.Error(),
Reason: metav1.StatusReasonUnauthorized,
Code: http.StatusUnauthorized,
}
return nil
}
response.Allowed = true
return nil
}

// ConfirmNoEscalation checks that the user attempting to create the GRB has all the permissions they are attempting
// to grant through the GRB
func (grbv *globalRoleBindingValidator) ConfirmNoEscalation(rules []rbacv1.PolicyRule, userInfo *user.DefaultInfo) error {
globaleCtx := k8srequest.WithNamespace(k8srequest.WithUser(context.Background(), userInfo), "")
if err := rbacregistryvalidation.ConfirmNoEscalation(globaleCtx, grbv.ruleSolver, rules); err != nil {
return fmt.Errorf("failed to validate user: %v", err)
}
return nil
}

func grbObject(request *webhook.Request) (*rancherv3.GlobalRoleBinding, error) {
var grb runtime.Object
var err error
if request.Operation == admissionv1.Delete {
grb, err = request.DecodeOldObject()
} else {
grb, err = request.DecodeObject()
}
return grb.(*rancherv3.GlobalRoleBinding), err
}

func toExtraString(extra map[string]authenticationv1.ExtraValue) map[string][]string {
result := make(map[string][]string)
for k, v := range extra {
result[k] = v
}
return result
}
Loading

0 comments on commit 3d719a0

Please sign in to comment.