diff --git a/pkg/util/util.go b/pkg/util/util.go index 3af0280e..94df3508 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -3,6 +3,11 @@ package util import ( "context" "fmt" + "os" + "runtime" + "strings" + "time" + secv1 "github.com/openshift/api/security/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -19,11 +24,7 @@ import ( aaqv1alpha1 "kubevirt.io/application-aware-quota/staging/src/kubevirt.io/application-aware-quota-api/pkg/apis/core/v1alpha1" sdkapi "kubevirt.io/controller-lifecycle-operator-sdk/api" utils "kubevirt.io/controller-lifecycle-operator-sdk/pkg/sdk/resources" - "os" - "runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "strings" - "time" ) const ( @@ -118,6 +119,7 @@ func CreateContainer(name, image, verbosity, pullPolicy string) corev1.Container }, AllowPrivilegeEscalation: pointer.Bool(false), RunAsNonRoot: pointer.Bool(true), + ReadOnlyRootFilesystem: pointer.Bool(true), } return *container } @@ -239,8 +241,8 @@ func SetRecommendedLabels(obj metav1.Object, installerLabels map[string]string, } func PrintVersion() { - klog.Infof(fmt.Sprintf("Go Version: %s", runtime.Version())) - klog.Infof(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) + klog.Infof("Go Version: %s", runtime.Version()) + klog.Infof("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH) } func getNamespace(path string) string { if data, err := os.ReadFile(path); err == nil { diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 00000000..b4c33745 --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,48 @@ +package util + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" +) + +func TestUtil(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Util Suite") +} + +var _ = Describe("CreateContainer SecurityContext", func() { + newContainer := func() corev1.Container { + return CreateContainer("test", "img:latest", "1", "Always") + } + + DescribeTable("should enforce restricted SecurityContext fields", + func(check func(corev1.Container)) { + check(newContainer()) + }, + Entry("ReadOnlyRootFilesystem=true", func(c corev1.Container) { + Expect(c.SecurityContext).NotTo(BeNil()) + Expect(c.SecurityContext.ReadOnlyRootFilesystem).NotTo(BeNil()) + Expect(*c.SecurityContext.ReadOnlyRootFilesystem).To(BeTrue()) + }), + Entry("AllowPrivilegeEscalation=false", func(c corev1.Container) { + Expect(c.SecurityContext.AllowPrivilegeEscalation).NotTo(BeNil()) + Expect(*c.SecurityContext.AllowPrivilegeEscalation).To(BeFalse()) + }), + Entry("RunAsNonRoot=true", func(c corev1.Container) { + Expect(c.SecurityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*c.SecurityContext.RunAsNonRoot).To(BeTrue()) + }), + Entry("drops ALL capabilities", func(c corev1.Container) { + Expect(c.SecurityContext.Capabilities).NotTo(BeNil()) + Expect(c.SecurityContext.Capabilities.Drop).To(ContainElement(corev1.Capability("ALL"))) + }), + Entry("SeccompProfile=RuntimeDefault", func(c corev1.Container) { + Expect(c.SecurityContext.SeccompProfile).NotTo(BeNil()) + Expect(c.SecurityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) + }), + ) +}) diff --git a/tests/aaq_operator_test.go b/tests/aaq_operator_test.go index 28cfd433..b1f8278a 100644 --- a/tests/aaq_operator_test.go +++ b/tests/aaq_operator_test.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" + "reflect" + "time" + schedulev1 "k8s.io/api/scheduling/v1" "kubevirt.io/application-aware-quota/pkg/aaq-operator/resources/cluster" resourcesutils "kubevirt.io/application-aware-quota/pkg/util" "kubevirt.io/application-aware-quota/tests/utils" - "reflect" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -690,6 +691,26 @@ var _ = Describe("ALL Operator tests", Serial, func() { }) }) +var _ = Describe("AAQ Security", func() { + f := framework.NewFramework("security-test") + + It("All AAQ deployments should have readOnlyRootFilesystem set to true", func() { + for _, deploymentName := range []string{"aaq-operator", "aaq-server", "aaq-controller"} { + deployment, err := f.K8sClient.AppsV1().Deployments(f.AAQInstallNs).Get(context.TODO(), deploymentName, metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred(), "failed to get deployment %s", deploymentName) + + for _, container := range deployment.Spec.Template.Spec.Containers { + Expect(container.SecurityContext).ToNot(BeNil(), + "container %s in deployment %s has no SecurityContext", container.Name, deploymentName) + Expect(container.SecurityContext.ReadOnlyRootFilesystem).ToNot(BeNil(), + "container %s in deployment %s has no ReadOnlyRootFilesystem", container.Name, deploymentName) + Expect(*container.SecurityContext.ReadOnlyRootFilesystem).To(BeTrue(), + "container %s in deployment %s should have ReadOnlyRootFilesystem=true", container.Name, deploymentName) + } + } + }) +}) + func getAAQPods(f *framework.Framework) *corev1.PodList { By("Getting AAQ pods") labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/component": "multi-tenant"}}