From f111d9662d12a0fb5155415185258b98036a0e81 Mon Sep 17 00:00:00 2001 From: shimritproj Date: Mon, 8 Apr 2024 23:09:51 +0300 Subject: [PATCH] Check for the recommended security requirements of the container-native operators checks: USER id should not be 0 readOnlyRootFilesystem = true runAsNonRoot = true automount service account token = false{} --- CATALOG.md | 72 ++++++++++++- cnf-certification-test/accesscontrol/suite.go | 2 +- .../rbac/automount.go | 0 .../rbac/automount_test.go | 0 .../{accesscontrol => common}/rbac/roles.go | 0 .../rbac/roles_test.go | 0 .../identifiers/doclinks.go | 4 + .../identifiers/identifiers.go | 68 ++++++++++++ .../identifiers/remediation.go | 8 ++ cnf-certification-test/operator/suite.go | 101 ++++++++++++++++++ expected_results.yaml | 4 + pkg/provider/containers.go | 5 + pkg/provider/containers_test.go | 40 +++++++ pkg/provider/pods.go | 19 ++++ pkg/provider/pods_test.go | 49 +++++++++ pkg/provider/provider.go | 7 +- 16 files changed, 371 insertions(+), 8 deletions(-) rename cnf-certification-test/{accesscontrol => common}/rbac/automount.go (100%) rename cnf-certification-test/{accesscontrol => common}/rbac/automount_test.go (100%) rename cnf-certification-test/{accesscontrol => common}/rbac/roles.go (100%) rename cnf-certification-test/{accesscontrol => common}/rbac/roles_test.go (100%) diff --git a/CATALOG.md b/CATALOG.md index 281cd67318..c628a4f67b 100644 --- a/CATALOG.md +++ b/CATALOG.md @@ -7,7 +7,7 @@ Depending on the workload type, not all tests are required to pass to satisfy be ## Test cases summary -### Total test cases: 109 +### Total test cases: 113 ### Total suites: 10 @@ -19,7 +19,7 @@ Depending on the workload type, not all tests are required to pass to satisfy be |manageability|2| |networking|11| |observability|4| -|operator|7| +|operator|11| |performance|6| |platform-alteration|13| |preflight|17| @@ -36,11 +36,11 @@ Depending on the workload type, not all tests are required to pass to satisfy be |---|---| |7|1| -### Non-Telco specific tests only: 62 +### Non-Telco specific tests only: 66 |Mandatory|Optional| |---|---| -|42|20| +|46|20| ### Telco specific tests only: 27 @@ -1122,6 +1122,22 @@ Tags|telco,observability ### operator +#### operator-automount-tokens + +Property|Description +---|--- +Unique ID|operator-automount-tokens +Description|Tests that checks the automount service account token is disabled. +Suggested Remediation|Ensure that the automount service account token is disabled. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + #### operator-crd-openapi-schema Property|Description @@ -1202,6 +1218,54 @@ Tags|common,operator |Non-Telco|Mandatory| |Telco|Mandatory| +#### operator-read-only-file-system + +Property|Description +---|--- +Unique ID|operator-read-only-file-system +Description|Tests that checks the read-only root filesystem setting is enabled. +Suggested Remediation|Ensure that the read-only root filesystem setting is enabled. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + +#### operator-run-as-non-root + +Property|Description +---|--- +Unique ID|operator-run-as-non-root +Description|Tests that checks run as non root. +Suggested Remediation|Ensure that run as non root. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + +#### operator-run-as-user-id + +Property|Description +---|--- +Unique ID|operator-run-as-user-id +Description|Tests that checks user id should not be 0. +Suggested Remediation|Ensure that user id should not be 0. +Best Practice Reference|https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-cnf-operator-requirements +Exception Process|No exceptions +Tags|common,operator +|**Scenario**|**Optional/Mandatory**| +|Extended|Mandatory| +|Far-Edge|Mandatory| +|Non-Telco|Mandatory| +|Telco|Mandatory| + #### operator-semantic-versioning Property|Description diff --git a/cnf-certification-test/accesscontrol/suite.go b/cnf-certification-test/accesscontrol/suite.go index 567157ca22..5c3bb3d385 100644 --- a/cnf-certification-test/accesscontrol/suite.go +++ b/cnf-certification-test/accesscontrol/suite.go @@ -23,10 +23,10 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/namespace" - "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/rbac" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/resources" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/accesscontrol/securitycontextcontainer" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common" + "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common/rbac" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/identifiers" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/networking/netutil" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/networking/services" diff --git a/cnf-certification-test/accesscontrol/rbac/automount.go b/cnf-certification-test/common/rbac/automount.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/automount.go rename to cnf-certification-test/common/rbac/automount.go diff --git a/cnf-certification-test/accesscontrol/rbac/automount_test.go b/cnf-certification-test/common/rbac/automount_test.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/automount_test.go rename to cnf-certification-test/common/rbac/automount_test.go diff --git a/cnf-certification-test/accesscontrol/rbac/roles.go b/cnf-certification-test/common/rbac/roles.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/roles.go rename to cnf-certification-test/common/rbac/roles.go diff --git a/cnf-certification-test/accesscontrol/rbac/roles_test.go b/cnf-certification-test/common/rbac/roles_test.go similarity index 100% rename from cnf-certification-test/accesscontrol/rbac/roles_test.go rename to cnf-certification-test/common/rbac/roles_test.go diff --git a/cnf-certification-test/identifiers/doclinks.go b/cnf-certification-test/identifiers/doclinks.go index 650a9c4188..4fc070a477 100644 --- a/cnf-certification-test/identifiers/doclinks.go +++ b/cnf-certification-test/identifiers/doclinks.go @@ -110,6 +110,10 @@ const ( TestOperatorCrdSchemaIdentifierDocLink = DocOperatorRequirement TestOperatorCrdVersioningIdentifierDocLink = DocOperatorRequirement TestOperatorSingleCrdOwnerIdentifierDocLink = DocOperatorRequirement + TestOperatorRunAsUserIDDocLink = DocOperatorRequirement + TestOperatorRunAsNonRootDocLink = DocOperatorRequirement + TestOperatorAutomountTokensDocLink = DocOperatorRequirement + TestOperatorReadOnlyFilesystemDocLink = DocOperatorRequirement // Observability Test Suite TestLoggingIdentifierDocLink = "https://test-network-function.github.io/cnf-best-practices-guide/#cnf-best-practices-logging" diff --git a/cnf-certification-test/identifiers/identifiers.go b/cnf-certification-test/identifiers/identifiers.go index 8675ab52fb..4876055465 100644 --- a/cnf-certification-test/identifiers/identifiers.go +++ b/cnf-certification-test/identifiers/identifiers.go @@ -122,6 +122,10 @@ var ( TestHelmIsCertifiedIdentifier claim.Identifier TestOperatorIsInstalledViaOLMIdentifier claim.Identifier TestOperatorHasSemanticVersioningIdentifier claim.Identifier + TestOperatorReadOnlyFilesystem claim.Identifier + TestOperatorAutomountTokens claim.Identifier + TestOperatorRunAsNonRoot claim.Identifier + TestOperatorRunAsUserID claim.Identifier TestOperatorCrdVersioningIdentifier claim.Identifier TestOperatorCrdSchemaIdentifier claim.Identifier TestOperatorSingleCrdOwnerIdentifier claim.Identifier @@ -930,6 +934,70 @@ that Node's kernel may not have the same hacks.'`, }, TagCommon) + TestOperatorRunAsUserID = AddCatalogEntry( + "run-as-user-id", + common.OperatorTestKey, + `Tests that checks the user id of the pods created by the operator is not 0`, + OperatorRunAsUserID, + NoExceptions, + TestOperatorRunAsUserIDDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + + TestOperatorRunAsNonRoot = AddCatalogEntry( + "run-as-non-root", + common.OperatorTestKey, + `Tests that checks the pods created by the operator is run as non root.`, + OperatorRunAsNonRoot, + NoExceptions, + TestOperatorRunAsNonRootDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + + TestOperatorAutomountTokens = AddCatalogEntry( + "automount-tokens", + common.OperatorTestKey, + `Tests that check the pods created by the operator ensure that the automount service account token is disabled.`, + OperatorAutomountTokens, + NoExceptions, + TestOperatorAutomountTokensDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + + TestOperatorReadOnlyFilesystem = AddCatalogEntry( + "read-only-file-system", + common.OperatorTestKey, + `Tests that check the pods created by the operator ensure that the read-only root filesystem setting is enabled.`, + OperatorReadOnlyFilesystem, + NoExceptions, + TestOperatorReadOnlyFilesystemDocLink, + true, + map[string]string{ + FarEdge: Mandatory, + Telco: Mandatory, + NonTelco: Mandatory, + Extended: Mandatory, + }, + TagCommon) + TestOperatorCrdVersioningIdentifier = AddCatalogEntry( "crd-versioning", common.OperatorTestKey, diff --git a/cnf-certification-test/identifiers/remediation.go b/cnf-certification-test/identifiers/remediation.go index cdf57be9c7..88ae2df3df 100644 --- a/cnf-certification-test/identifiers/remediation.go +++ b/cnf-certification-test/identifiers/remediation.go @@ -83,6 +83,14 @@ const ( OperatorCrdSchemaIdentifierRemediation = `Ensure that the Operator CRD is defined with OpenAPI spec.` + OperatorRunAsUserID = `Ensure that the user ID of the pods created by the operator is not 0.` + + OperatorRunAsNonRoot = `Ensure that the pods created by the operator are run as non-root.` + + OperatorAutomountTokens = `Ensure that the pods created by the operator have the automount service account token disabled.` + + OperatorReadOnlyFilesystem = `Ensure that the pods created by the operator have the read-only root filesystem setting enabled.` + OperatorCrdVersioningRemediation = `Ensure that the Operator CRD has a valid version.` OperatorSingleCrdOwnerRemediation = `Ensure that a CRD is owned by only one Operator` diff --git a/cnf-certification-test/operator/suite.go b/cnf-certification-test/operator/suite.go index 5dfa3ed898..85bfca5faf 100644 --- a/cnf-certification-test/operator/suite.go +++ b/cnf-certification-test/operator/suite.go @@ -20,8 +20,10 @@ import ( "strings" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common" + "github.com/test-network-function/cnf-certification-test/cnf-certification-test/common/rbac" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/identifiers" "github.com/test-network-function/cnf-certification-test/cnf-certification-test/operator/phasecheck" + "github.com/test-network-function/cnf-certification-test/internal/clientsholder" "github.com/test-network-function/cnf-certification-test/internal/log" "github.com/test-network-function/cnf-certification-test/pkg/checksdb" @@ -39,6 +41,7 @@ var ( } ) +//nolint:funlen func LoadChecks() { log.Debug("Loading %s suite checks", common.OperatorTestKey) @@ -93,6 +96,33 @@ func LoadChecks() { testOperatorSingleCrdOwner(c, &env) return nil })) + + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorRunAsUserID)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorRunAsUserID(c, &env) + return nil + })) + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorRunAsNonRoot)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorRunAsNonRoot(c, &env) + return nil + })) + + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorAutomountTokens)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorAutomountTokens(c, &env) + return nil + })) + + checksGroup.Add(checksdb.NewCheck(identifiers.GetTestIDAndLabels(identifiers.TestOperatorReadOnlyFilesystem)). + WithSkipCheckFn(testhelper.GetNoOperatorsSkipFn(&env)). + WithCheckFn(func(c *checksdb.Check) error { + testOperatorReadOnlyFilesystem(c, &env) + return nil + })) } // This function check if the Operator CRD version follows K8s versioning @@ -317,3 +347,74 @@ func testOperatorSingleCrdOwner(check *checksdb.Check, env *provider.TestEnviron check.SetResult(compliantObjects, nonCompliantObjects) } + +func testOperatorRunAsUserID(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + + for _, put := range env.AllOperatorPods { + check.LogInfo("Testing Pod %q", put) + if put.IsRunAsUserID(0) { + check.LogError("Pod %q UserID of the pods created by the operator is 0", put.Name) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, "Pod has been found with UserID is 0", false)) + } else { + check.LogInfo("Pod %q UserID of the pods created by the operator is not 0", put.Name) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, "Pod has been found with UserID is not 0", true)) + } + } + check.SetResult(compliantObjects, nonCompliantObjects) +} + +func testOperatorRunAsNonRoot(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + for _, put := range env.AllOperatorPods { + check.LogInfo("Testing Pod %q", put) + if put.IsRunAsNonRoot() { + check.LogInfo("Pod %q created by the operator is run as not root", put.Name) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, "Pod has been found is run as not root", true)) + } else { + check.LogError("Pod %q created by the operator is run as root", put.Name) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, "Pod has been found is run as root", false)) + } + } + check.SetResult(compliantObjects, nonCompliantObjects) +} + +func testOperatorAutomountTokens(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + for _, put := range env.AllOperatorPods { + check.LogInfo("Testing Pod %q", put) + // Evaluate the pod's automount service tokens and any attached service accounts + client := clientsholder.GetClientsHolder() + podPassed, newMsg := rbac.EvaluateAutomountTokens(client.K8sClient.CoreV1(), put.Pod) + if !podPassed { + check.LogInfo("Pod %q have automount service tokens set to false", put) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, "Pod %q created by the operator that the automount service account token set to false", true)) + } else { + check.LogError(newMsg) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, newMsg, false)) + } + } + check.SetResult(compliantObjects, nonCompliantObjects) +} + +func testOperatorReadOnlyFilesystem(check *checksdb.Check, env *provider.TestEnvironment) { + var compliantObjects []*testhelper.ReportObject + var nonCompliantObjects []*testhelper.ReportObject + for _, put := range env.AllOperatorPods { + check.LogInfo("Testing Pod %q", put) + for _, cut := range put.Containers { + check.LogInfo("Testing Container %q", cut.Name) + if cut.IsReadOnlyRootFilesystem(check.GetLoggger()) { + check.LogInfo("Pod %q container %q created by the operator is read only root file system.", put.Name, cut.Name) + compliantObjects = append(compliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, "Pod has been found read only root file system", true)) + } else { + check.LogError("Pod %q container %q created by the operator is read not only root file system.", put.Name, cut.Name) + nonCompliantObjects = append(nonCompliantObjects, testhelper.NewPodReportObject(put.Namespace, put.Name, "Pod has been found read not only root file system", false)) + } + } + } + check.SetResult(compliantObjects, nonCompliantObjects) +} diff --git a/expected_results.yaml b/expected_results.yaml index 0c56047ea9..c1f1353b36 100644 --- a/expected_results.yaml +++ b/expected_results.yaml @@ -54,11 +54,15 @@ testCases: - observability-crd-status - observability-pod-disruption-budget - observability-termination-policy + - operator-automount-tokens - operator-crd-openapi-schema - operator-crd-versioning - operator-install-source - operator-install-status-no-privileges - operator-install-status-succeeded + - operator-read-only-file-system + - operator-run-as-non-root + - operator-run-as-user-id - operator-semantic-versioning - operator-single-crd-owner - performance-exclusive-cpu-pool diff --git a/pkg/provider/containers.go b/pkg/provider/containers.go index d53e21a047..6901590776 100644 --- a/pkg/provider/containers.go +++ b/pkg/provider/containers.go @@ -185,3 +185,8 @@ func (c *Container) HasExecProbes() bool { func (c *Container) IsTagEmpty() bool { return c.ContainerImageIdentifier.Tag == "" } + +func (c *Container) IsReadOnlyRootFilesystem(logger *log.Logger) bool { + logger.Info("Testing Container %q", c.Name) + return *(c.SecurityContext.ReadOnlyRootFilesystem) +} diff --git a/pkg/provider/containers_test.go b/pkg/provider/containers_test.go index af4779c461..5402042598 100644 --- a/pkg/provider/containers_test.go +++ b/pkg/provider/containers_test.go @@ -17,9 +17,11 @@ package provider import ( + "os" "testing" "github.com/stretchr/testify/assert" + "github.com/test-network-function/cnf-certification-test/internal/log" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -204,3 +206,41 @@ func TestIsTagEmpty(t *testing.T) { assert.Equal(t, tc.expectedOutput, tc.testContainer.IsTagEmpty()) } } + +func TestIsreadOnlyRootFilessystem(t *testing.T) { + trueVal := true + falseVal := false + testCases := []struct { + testContainer Container + expectedOutput bool + }{ + { + testContainer: Container{ + Container: &corev1.Container{ + Name: "TestContainer1", + SecurityContext: &corev1.SecurityContext{ + ReadOnlyRootFilesystem: &trueVal, + }, + }, + }, + expectedOutput: true, + }, + { + testContainer: Container{ + Container: &corev1.Container{ + Name: "TestContainer2", + SecurityContext: &corev1.SecurityContext{ + ReadOnlyRootFilesystem: &falseVal, + }, + }, + }, + expectedOutput: false, + }, + } + + for _, tc := range testCases { + log.SetupLogger(os.Stdout, "INFO") + actualOutput := tc.testContainer.IsReadOnlyRootFilesystem(log.GetLogger()) + assert.Equal(t, tc.expectedOutput, actualOutput) + } +} diff --git a/pkg/provider/pods.go b/pkg/provider/pods.go index dfef2fa484..ed14df0c49 100644 --- a/pkg/provider/pods.go +++ b/pkg/provider/pods.go @@ -415,6 +415,25 @@ func (p *Pod) GetTopOwner() (topOwners map[string]TopOwner, err error) { return topOwners, nil } +func (p *Pod) IsRunAsNonRoot() bool { + if p == nil || p.Pod == nil { + return false + } + + // Check if SecurityContext is nil + if p.Pod.Spec.SecurityContext == nil { + return false + } + + // Check if RunAsNonRoot is nil + if p.Pod.Spec.SecurityContext.RunAsNonRoot == nil { + return false + } + + // Return the value of RunAsNonRoot + return *p.Pod.Spec.SecurityContext.RunAsNonRoot +} + // Structure to describe a top owner of a pod type TopOwner struct { Kind string diff --git a/pkg/provider/pods_test.go b/pkg/provider/pods_test.go index 533887a578..b5d174c3de 100644 --- a/pkg/provider/pods_test.go +++ b/pkg/provider/pods_test.go @@ -599,3 +599,52 @@ func Test_followOwnerReferences(t *testing.T) { }) } } + +func TestIsRunAsNonRoot(t *testing.T) { + tests := []struct { + Pod *Pod + ExpectedResult bool + }{ + { + Pod: nil, + ExpectedResult: false, + }, + { + Pod: &Pod{ + Pod: &corev1.Pod{}, + }, + ExpectedResult: false, + }, + { + Pod: &Pod{ + Pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: boolPointer(true), + }, + }, + }, + }, + ExpectedResult: true, + }, + { + Pod: &Pod{ + Pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: boolPointer(false), + }, + }, + }, + }, + ExpectedResult: false, + }, + } + + for _, tc := range tests { + assert.Equal(t, tc.ExpectedResult, tc.Pod.IsRunAsNonRoot()) + } +} +func boolPointer(b bool) *bool { + return &b +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index a879b8e7f1..3eed01f74a 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -72,9 +72,10 @@ type TestEnvironment struct { // rename this with testTarget AbnormalEvents []*Event // Pod Groupings - Pods []*Pod `json:"testPods"` - DebugPods map[string]*corev1.Pod // map from nodename to debugPod - AllPods []*Pod `json:"AllPods"` + Pods []*Pod `json:"testPods"` + DebugPods map[string]*corev1.Pod // map from nodename to debugPod + AllPods []*Pod `json:"AllPods"` + AllOperatorPods []*Pod `json:"AllOperatorPods"` // Deployment Groupings Deployments []*Deployment `json:"testDeployments"`