Skip to content

Commit

Permalink
system:masters and bind,impersonate,escalate checks (#453)
Browse files Browse the repository at this point in the history
* add 4 of checks from CIS
  • Loading branch information
matas-cast authored Feb 11, 2025
1 parent 7f2c8f3 commit dd41cba
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 1 deletion.
20 changes: 20 additions & 0 deletions cmd/controller/state/kubelinter/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kubelinter

import (
"context"
rbacv1 "k8s.io/api/rbac/v1"
"testing"

castaipb "github.com/castai/kvisor/api/v1/runtime"
Expand Down Expand Up @@ -41,6 +42,25 @@ func TestSubscriber(t *testing.T) {
Name: "test_pod",
},
},
&rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "test_role_binding",
},
TypeMeta: metav1.TypeMeta{
Kind: "ClusterRoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1",
},
Subjects: []rbacv1.Subject{
{
Kind: "Group",
Name: "system:authenticated",
},
},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: "testrole",
},
},
}
ctx := context.Background()
r.NoError(ctrl.lintObjects(ctx, objects))
Expand Down
146 changes: 146 additions & 0 deletions cmd/controller/state/kubelinter/customchecks/bindings/bindings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package bindings

import (
"fmt"
"slices"

"golang.stackrox.io/kube-linter/pkg/check"
"golang.stackrox.io/kube-linter/pkg/config"
"golang.stackrox.io/kube-linter/pkg/diagnostic"
"golang.stackrox.io/kube-linter/pkg/lintcontext"
"golang.stackrox.io/kube-linter/pkg/objectkinds"
"golang.stackrox.io/kube-linter/pkg/templates"
"golang.stackrox.io/kube-linter/pkg/templates/util"
rbacv1 "k8s.io/api/rbac/v1"
)

const (
templateName = "bindings"
)

var (
ParamDescs = []check.ParameterDesc{}
)

func Checks() []*config.Check {
var checks []*config.Check
for k, v := range rules {
checks = append(checks, &config.Check{
Name: k,
Template: templateName,
Description: v.Name,
Params: map[string]interface{}{
"Values": v.Values,
"ExcludedValues": v.ExcludedValues,
},
})
}

return checks
}

func init() {
templates.Register(roleBindingsTemplate())
}

var (
rules = map[string]BindingsCheck{
"system-masters": {
Name: "Avoid use of system:masters group",
Values: []string{"system:masters"},
},
"system-anonymous": {
Name: "Avoid binding to system:anonymous",
Values: []string{"system:anonymous"},
},
"system-unauthenticated": {
Name: "Avoid non-default bindings to system:unauthenticated",
Values: []string{"system:unauthenticated"},
ExcludedValues: []string{"system:public-info-viewer"},
},
"system-authenticated": {
Name: "Avoid non-default bindings to system:authenticated",
Values: []string{"system:authenticated"},
ExcludedValues: []string{"system:public-info-viewer", "system:basic-user", "system:discovery"},
},
}
)

type BindingsCheck struct {
Name string
Values []string
ExcludedValues []string
}

func roleBindingsTemplate() check.Template {
template := check.Template{
HumanName: "Avoid bindings to certain groups",
Key: templateName,
SupportedObjectKinds: config.ObjectKindsDesc{
ObjectKinds: []string{
objectkinds.RoleBinding,
objectkinds.ClusterRoleBinding,
},
},
Parameters: ParamDescs,
ParseAndValidateParams: ParseAndValidate,
Instantiate: WrapInstantiateFunc(func(p Params) (check.Func, error) {
return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
var subjects []rbacv1.Subject
var roleRef rbacv1.RoleRef

rb, ok := object.K8sObject.(*rbacv1.RoleBinding)
if !ok {
crb, ok := object.K8sObject.(*rbacv1.ClusterRoleBinding)
if !ok {
return nil
}

subjects = crb.Subjects
roleRef = crb.RoleRef
} else {
subjects = rb.Subjects
roleRef = rb.RoleRef
}

for _, subject := range subjects {
if subject.Kind == "Group" && slices.Contains(p.Values, subject.Name) {
if p.ExcludedValues == nil || !slices.Contains(p.ExcludedValues, roleRef.Name) {
return []diagnostic.Diagnostic{{Message: fmt.Sprintf("Binding to %s", subject.Name)}}
}
}
}

return nil
}, nil
}),
}

return template
}

type Params struct {
Values []string
ExcludedValues []string
}

func (p *Params) Validate() error {
return nil
}

func ParseAndValidate(m map[string]interface{}) (interface{}, error) {
var p Params
if err := util.DecodeMapStructure(m, &p); err != nil {
return nil, err
}
if err := p.Validate(); err != nil {
return nil, err
}
return p, nil
}

func WrapInstantiateFunc(f func(p Params) (check.Func, error)) func(interface{}) (check.Func, error) {
return func(paramsInt interface{}) (check.Func, error) {
return f(paramsInt.(Params))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package privescverbs

import (
"fmt"
"slices"
"strings"

"golang.stackrox.io/kube-linter/pkg/check"
"golang.stackrox.io/kube-linter/pkg/config"
"golang.stackrox.io/kube-linter/pkg/diagnostic"
"golang.stackrox.io/kube-linter/pkg/lintcontext"
"golang.stackrox.io/kube-linter/pkg/objectkinds"
"golang.stackrox.io/kube-linter/pkg/templates"
"golang.stackrox.io/kube-linter/pkg/templates/util"
rbacv1 "k8s.io/api/rbac/v1"
)

func Check() *config.Check {
return &config.Check{
Name: "privesc-verbs",
Description: "Use of Bind, Impersonate and Escalate permissions",
Template: "privesc-verbs",
Params: map[string]interface{}{},
}
}

var (
privescVerbs = []string{"bind", "escalate", "impersonate"}
)

func init() {
templates.Register(check.Template{
HumanName: "Use of Bind, Impersonate and Escalate permissions",
Key: "privesc-verbs",
SupportedObjectKinds: config.ObjectKindsDesc{
ObjectKinds: []string{
objectkinds.Role,
objectkinds.ClusterRole,
},
},
Parameters: ParamDescs,
ParseAndValidateParams: ParseAndValidate,
Instantiate: WrapInstantiateFunc(func(_ Params) (check.Func, error) {
return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
var policyRules []rbacv1.PolicyRule
rb, ok := object.K8sObject.(*rbacv1.Role)
if !ok {
cr, ok := object.K8sObject.(*rbacv1.ClusterRole)
if !ok {
return nil
}
policyRules = cr.Rules
} else {
policyRules = rb.Rules
}
for _, rule := range policyRules {
for _, verb := range rule.Verbs {
if slices.Contains(privescVerbs, verb) {
return []diagnostic.Diagnostic{{Message: "Usage of 'bind, impersonate, escalate'"}}
}
}
}

return nil
}, nil
}),
})
}

type Params struct {
}

var (
// Use some imports in case they don't get used otherwise.
_ = util.MustParseParameterDesc
_ = fmt.Sprintf

ParamDescs = []check.ParameterDesc{}
)

func (p *Params) Validate() error {
var validationErrors []string
if len(validationErrors) > 0 {
return fmt.Errorf("invalid parameters: %s", strings.Join(validationErrors, ", "))
}
return nil
}

func ParseAndValidate(m map[string]interface{}) (interface{}, error) {
return Params{}, nil
}

// WrapInstantiateFunc is a convenience wrapper that wraps an untyped instantiate function
// into a typed one.
func WrapInstantiateFunc(f func(p Params) (check.Func, error)) func(interface{}) (check.Func, error) {
return func(paramsInt interface{}) (check.Func, error) {
return f(paramsInt.(Params))
}
}
9 changes: 8 additions & 1 deletion cmd/controller/state/kubelinter/kubelinter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (

"github.com/castai/kvisor/cmd/controller/state/kubelinter/customchecks/additionalcapabilities"
"github.com/castai/kvisor/cmd/controller/state/kubelinter/customchecks/automount"
"github.com/castai/kvisor/cmd/controller/state/kubelinter/customchecks/bindings"
"github.com/castai/kvisor/cmd/controller/state/kubelinter/customchecks/containerdsock"
"github.com/castai/kvisor/cmd/controller/state/kubelinter/customchecks/networkpolicypernamespace"
"github.com/castai/kvisor/cmd/controller/state/kubelinter/customchecks/privescverbs"
"github.com/castai/kvisor/cmd/controller/state/kubelinter/customchecks/securitycontext"
"github.com/castai/kvisor/cmd/controller/state/kubelinter/customobjectkinds"
"github.com/samber/lo"
Expand Down Expand Up @@ -54,6 +56,7 @@ import (
_ "golang.stackrox.io/kube-linter/pkg/templates/nonisolatedpod"
_ "golang.stackrox.io/kube-linter/pkg/templates/pdbmaxunavailable"
_ "golang.stackrox.io/kube-linter/pkg/templates/pdbminavailable"
_ "golang.stackrox.io/kube-linter/pkg/templates/pdbunhealthypodevictionpolicy"
_ "golang.stackrox.io/kube-linter/pkg/templates/ports"
_ "golang.stackrox.io/kube-linter/pkg/templates/privileged"
_ "golang.stackrox.io/kube-linter/pkg/templates/privilegedports"
Expand All @@ -76,7 +79,6 @@ import (
_ "golang.stackrox.io/kube-linter/pkg/templates/updateconfig"
_ "golang.stackrox.io/kube-linter/pkg/templates/wildcardinrules"
_ "golang.stackrox.io/kube-linter/pkg/templates/writablehostmount"
_ "golang.stackrox.io/kube-linter/pkg/templates/pdbunhealthypodevictionpolicy"
"k8s.io/apimachinery/pkg/types"
)

Expand Down Expand Up @@ -130,7 +132,12 @@ func registerCustomChecks(registry checkregistry.CheckRegistry) error {
securitycontext.Check(),
networkpolicypernamespace.Check(),
additionalcapabilities.Check(),
privescverbs.Check(),
}
for _, c := range bindings.Checks() {
checks = append(checks, c)
}

for _, check := range checks {
if err := registry.Register(check); err != nil {
return err
Expand Down
34 changes: 34 additions & 0 deletions cmd/controller/state/kubelinter/kubelinter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"golang.stackrox.io/kube-linter/pkg/lintcontext"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -91,4 +92,37 @@ func TestLinter(t *testing.T) {
r.NoError(err)
r.Contains(checks[0].Failed.Rules(), "additional-capabilities")
})

t.Run("checks for bindings", func(t *testing.T) {
r := require.New(t)

linter, err := New(lo.Keys(LinterRuleMap))
r.NoError(err)

checks, err := linter.Run([]lintcontext.Object{{
K8sObject: &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "test_role_binding",
},
TypeMeta: metav1.TypeMeta{
Kind: "ClusterRoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1",
},
Subjects: []rbacv1.Subject{
{
Kind: "Group",
Name: "system:authenticated",
},
},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: "testrole",
},
},
}})

r.NoError(err)
r.Len(checks, 1)
r.Contains(checks[0].Failed.Rules(), "system-authenticated")
})
}
10 changes: 10 additions & 0 deletions cmd/controller/state/kubelinter/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ const (
NetworkPolicyPerNamespace
ContainerdSock
AdditionalCapabilities
SystemMasters
SystemAnonymous
SystemAuthenticated
SystemUnauthenticated
PrivescVerbs
)

var LinterRuleMap = map[string]LinterRule{
Expand Down Expand Up @@ -93,6 +98,11 @@ var LinterRuleMap = map[string]LinterRule{
"network-policy-per-namespace": NetworkPolicyPerNamespace,
"containerd-sock": ContainerdSock,
"additional-capabilities": AdditionalCapabilities,
"system-masters": SystemMasters,
"system-anonymous": SystemAnonymous,
"system-authenticated": SystemAuthenticated,
"system-unauthenticated": SystemUnauthenticated,
"privesc-verbs": PrivescVerbs,
}

var HostIsolationBundle = map[string]LinterRule{
Expand Down

0 comments on commit dd41cba

Please sign in to comment.