Skip to content

Commit

Permalink
[Security Hardened Kubernetes Cluster] Rule 2004 implementation (#376)
Browse files Browse the repository at this point in the history
* Add GetServices utils func

* Add service selector

* Add rule 2004

* Register rule

* Update docu

* Add sugested changes

* Use new object selector

* Add minor changes to NamespacedObjectSelector

* Add TODO

* Fix unit test
  • Loading branch information
AleksandarSavchev authored Nov 25, 2024
1 parent acaf802 commit f09debf
Show file tree
Hide file tree
Showing 14 changed files with 505 additions and 35 deletions.
12 changes: 10 additions & 2 deletions example/config/managedk8s.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,23 @@ providers: # contains information about known providers
# - ruleID: "2001"
# args:
# acceptedPods:
# - podMatchLabels:
# - matchLabels:
# foo: bar
# namespaceMatchLabels:
# foo: bar
# justification: "justification"
# - ruleID: "2004"
# args:
# acceptedServices:
# - matchLabels:
# foo: bar
# namespaceMatchLabels:
# foo: bar
# justification: "justification"
# - ruleID: "2008"
# args:
# acceptedPods:
# - podMatchLabels:
# - matchLabels:
# foo: bar
# namespaceMatchLabels:
# foo: bar
Expand Down
21 changes: 21 additions & 0 deletions pkg/kubernetes/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,27 @@ func GetPods(ctx context.Context, c client.Client, namespace string, selector la
}
}

// GetServices returns all services for a given namespace, or all namespaces if it's set to empty string "".
// It retrieves services by portions set by limit.
func GetServices(ctx context.Context, c client.Client, namespace string, selector labels.Selector, limit int64) ([]corev1.Service, error) {
var (
services []corev1.Service
serviceList = &corev1.ServiceList{}
)

for {
if err := c.List(ctx, serviceList, client.InNamespace(namespace), client.Limit(limit), client.MatchingLabelsSelector{Selector: selector}, client.Continue(serviceList.Continue)); err != nil {
return nil, err
}

services = append(services, serviceList.Items...)

if len(serviceList.Continue) == 0 {
return services, nil
}
}
}

// GetReplicaSets returns all replicaSets for a given namespace, or all namespaces if it's set to empty string "".
// It retrieves replicaSets by portions set by limit.
func GetReplicaSets(ctx context.Context, c client.Client, namespace string, selector labels.Selector, limit int64) ([]appsv1.ReplicaSet, error) {
Expand Down
72 changes: 72 additions & 0 deletions pkg/kubernetes/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,78 @@ var _ = Describe("utils", func() {

})

Describe("#GetServices", func() {
var (
fakeClient client.Client
ctx = context.TODO()
namespaceFoo = "foo"
namespaceDefault = "default"
)

BeforeEach(func() {
fakeClient = fakeclient.NewClientBuilder().Build()
for i := 0; i < 10; i++ {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: strconv.Itoa(i),
Namespace: namespaceDefault,
},
}
Expect(fakeClient.Create(ctx, service)).To(Succeed())
}
for i := 10; i < 12; i++ {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: strconv.Itoa(i),
Namespace: namespaceDefault,
Labels: map[string]string{
"foo": "bar",
},
},
}
Expect(fakeClient.Create(ctx, service)).To(Succeed())
}
for i := 0; i < 6; i++ {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: strconv.Itoa(i),
Namespace: namespaceFoo,
},
}
Expect(fakeClient.Create(ctx, service)).To(Succeed())
}
})

It("should return correct number of services in default namespace", func() {
services, err := utils.GetServices(ctx, fakeClient, namespaceDefault, labels.NewSelector(), 2)

Expect(len(services)).To(Equal(12))
Expect(err).To(BeNil())
})

It("should return correct number of services in foo namespace", func() {
services, err := utils.GetServices(ctx, fakeClient, namespaceFoo, labels.NewSelector(), 2)

Expect(len(services)).To(Equal(6))
Expect(err).To(BeNil())
})

It("should return correct number of services in all namespaces", func() {
services, err := utils.GetServices(ctx, fakeClient, "", labels.NewSelector(), 2)

Expect(len(services)).To(Equal(18))
Expect(err).To(BeNil())
})

It("should return correct number of labeled services in default namespace", func() {
services, err := utils.GetServices(ctx, fakeClient, namespaceDefault, labels.SelectorFromSet(labels.Set{"foo": "bar"}), 2)

Expect(len(services)).To(Equal(2))
Expect(err).To(BeNil())
})

})

Describe("#GetReplicaSets", func() {
var (
fakeClient client.Client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import (
"github.com/gardener/diki/pkg/internal/utils"
kubeutils "github.com/gardener/diki/pkg/kubernetes/utils"
"github.com/gardener/diki/pkg/rule"
"github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option"
"github.com/gardener/diki/pkg/shared/kubernetes/option"
disaoptions "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option"
)

var (
_ rule.Rule = &Rule2001{}
_ rule.Severity = &Rule2001{}
_ option.Option = &Options2001{}
_ rule.Rule = &Rule2001{}
_ rule.Severity = &Rule2001{}
_ disaoptions.Option = &Options2001{}
)

type Rule2001 struct {
Expand All @@ -36,7 +37,7 @@ type Options2001 struct {
}

type AcceptedPods2001 struct {
option.PodSelector
option.NamespacedObjectSelector
Justification string `json:"justification" yaml:"justification"`
}

Expand Down Expand Up @@ -131,7 +132,7 @@ func (r *Rule2001) accepted(pod corev1.Pod, namespace corev1.Namespace) (bool, s
}

for _, acceptedPod := range r.Options.AcceptedPods {
if utils.MatchLabels(pod.Labels, acceptedPod.PodMatchLabels) &&
if utils.MatchLabels(pod.Labels, acceptedPod.MatchLabels) &&
utils.MatchLabels(namespace.Labels, acceptedPod.NamespaceMatchLabels) {
return true, acceptedPod.Justification
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (

"github.com/gardener/diki/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules"
"github.com/gardener/diki/pkg/rule"
"github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option"
"github.com/gardener/diki/pkg/shared/kubernetes/option"
)

var _ = Describe("#2001", func() {
Expand Down Expand Up @@ -105,8 +105,8 @@ var _ = Describe("#2001", func() {
rules.Options2001{
AcceptedPods: []rules.AcceptedPods2001{
{
PodSelector: option.PodSelector{
PodMatchLabels: map[string]string{"foo": "bar"},
NamespacedObjectSelector: option.NamespacedObjectSelector{
MatchLabels: map[string]string{"foo": "bar"},
NamespaceMatchLabels: map[string]string{"foo": "bar"},
},
Justification: "foo justify",
Expand Down
112 changes: 112 additions & 0 deletions pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package rules

import (
"context"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/gardener/diki/pkg/internal/utils"
kubeutils "github.com/gardener/diki/pkg/kubernetes/utils"
"github.com/gardener/diki/pkg/rule"
"github.com/gardener/diki/pkg/shared/kubernetes/option"
disaoptions "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option"
)

var (
_ rule.Rule = &Rule2004{}
_ rule.Severity = &Rule2004{}
_ disaoptions.Option = &Options2004{}
)

type Rule2004 struct {
Client client.Client
Options *Options2004
}

type Options2004 struct {
AcceptedServices []AcceptedServices2004 `json:"acceptedServices" yaml:"acceptedServices"`
}

type AcceptedServices2004 struct {
option.NamespacedObjectSelector
Justification string `json:"justification" yaml:"justification"`
}

// Validate validates that option configurations are correctly defined
func (o Options2004) Validate() field.ErrorList {
var allErrs field.ErrorList

for _, s := range o.AcceptedServices {
allErrs = append(allErrs, s.Validate()...)
}

return allErrs
}

func (r *Rule2004) ID() string {
return "2004"
}

func (r *Rule2004) Name() string {
return "Limit the Services of type NodePort."
}

func (r *Rule2004) Severity() rule.SeverityLevel {
return rule.SeverityMedium
}

func (r *Rule2004) Run(ctx context.Context) (rule.RuleResult, error) {
var checkResults []rule.CheckResult

services, err := kubeutils.GetServices(ctx, r.Client, "", labels.NewSelector(), 300)
if err != nil {
return rule.Result(r, rule.ErroredCheckResult(err.Error(), rule.NewTarget("kind", "serviceList"))), nil
}

namespaces, err := kubeutils.GetNamespaces(ctx, r.Client)
if err != nil {
return rule.Result(r, rule.ErroredCheckResult(err.Error(), rule.NewTarget("kind", "namespaceList"))), nil
}

for _, service := range services {
serviceTarget := rule.NewTarget("kind", "service", "name", service.Name, "namespace", service.Namespace)

if service.Spec.Type == corev1.ServiceTypeNodePort {
if accepted, justification := r.accepted(service, namespaces[service.Namespace]); accepted {
msg := "Service accepted to be of type NodePort."
if justification != "" {
msg = justification
}
checkResults = append(checkResults, rule.AcceptedCheckResult(msg, serviceTarget))
} else {
checkResults = append(checkResults, rule.FailedCheckResult("Service should not be of type NodePort.", serviceTarget))
}
} else {
checkResults = append(checkResults, rule.PassedCheckResult("Service is not of type NodePort.", serviceTarget))
}
}

return rule.Result(r, checkResults...), nil
}

func (r *Rule2004) accepted(service corev1.Service, namespace corev1.Namespace) (bool, string) {
if r.Options == nil {
return false, ""
}

for _, acceptedService := range r.Options.AcceptedServices {
if utils.MatchLabels(service.Labels, acceptedService.MatchLabels) &&
utils.MatchLabels(namespace.Labels, acceptedService.NamespaceMatchLabels) {
return true, acceptedService.Justification
}
}

return false, ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package rules_test

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"

"github.com/gardener/diki/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules"
"github.com/gardener/diki/pkg/rule"
"github.com/gardener/diki/pkg/shared/kubernetes/option"
)

var _ = Describe("#2004", func() {
var (
client client.Client
service *corev1.Service
ctx = context.TODO()
namespaceName = "foo"
namespace *corev1.Namespace
)

BeforeEach(func() {
client = fakeclient.NewClientBuilder().Build()
namespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName,
Labels: map[string]string{
"foo": "bar",
},
},
}
service = &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: namespaceName,
Labels: map[string]string{
"foo": "bar",
},
},
}
})

DescribeTable("Run cases",
func(serviceSpec corev1.ServiceSpec, ruleOptions rules.Options2004, expectedResult rule.CheckResult) {
r := &rules.Rule2004{Client: client, Options: &ruleOptions}
service.Spec = serviceSpec

Expect(client.Create(ctx, service)).To(Succeed())
Expect(client.Create(ctx, namespace)).To(Succeed())

ruleResult, err := r.Run(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(ruleResult.CheckResults).To(Equal([]rule.CheckResult{expectedResult}))
},

Entry("should pass when serviceSpec is not set",
corev1.ServiceSpec{}, rules.Options2004{},
rule.CheckResult{Status: rule.Passed, Message: "Service is not of type NodePort.", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")},
),
Entry("should fail when service is of type NodePort",
corev1.ServiceSpec{Type: "NodePort"}, rules.Options2004{},
rule.CheckResult{Status: rule.Failed, Message: "Service should not be of type NodePort.", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")},
),
Entry("should pass when service is not of type NodePort",
corev1.ServiceSpec{Type: "ClusterIP"}, rules.Options2004{},
rule.CheckResult{Status: rule.Passed, Message: "Service is not of type NodePort.", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")},
),
Entry("should pass when options are set",
corev1.ServiceSpec{Type: "NodePort"},
rules.Options2004{
AcceptedServices: []rules.AcceptedServices2004{
{
NamespacedObjectSelector: option.NamespacedObjectSelector{
MatchLabels: map[string]string{"foo": "bar"},
NamespaceMatchLabels: map[string]string{"foo": "bar"},
},
Justification: "foo justify",
},
},
},
rule.CheckResult{Status: rule.Accepted, Message: "foo justify", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")},
),
)
})
Loading

0 comments on commit f09debf

Please sign in to comment.