github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/verify-install/security/pod_security_test.go (about)

     1  // Copyright (c) 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package security
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"regexp"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    14  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    15  	"github.com/verrazzano/verrazzano/pkg/nginxutil"
    16  	"github.com/verrazzano/verrazzano/tests/e2e/pkg"
    17  	"github.com/verrazzano/verrazzano/tests/e2e/pkg/test/framework"
    18  
    19  	. "github.com/onsi/ginkgo/v2"
    20  	. "github.com/onsi/gomega"
    21  	"go.uber.org/zap"
    22  	corev1 "k8s.io/api/core/v1"
    23  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/client-go/kubernetes"
    25  )
    26  
    27  var t = framework.NewTestFramework("security")
    28  
    29  const (
    30  	waitTimeout     = 3 * time.Minute
    31  	pollingInterval = 5 * time.Second
    32  
    33  	// Only allowed capability in restricted mode
    34  	capNetBindService = "NET_BIND_SERVICE"
    35  	capDacOverride    = "DAC_OVERRIDE"
    36  	capFOwner         = "FOWNER"
    37  
    38  	// MySQL ignore pattern; skip mysql-# or mysql-xxxx-xxxx pod names, but not mysql-router-#
    39  	mysqlPattern = "^mysql-([\\d]+)$"
    40  	// MySQL ignore pattern for Grafana DB test case; installs a mysql instance in verrazzano-install namespace
    41  	grafanaMysqlPattern = "^mysql-.*$"
    42  )
    43  
    44  var skipPods = map[string][]string{
    45  	"keycloak": {
    46  		mysqlPattern,
    47  	},
    48  	"verrazzano-install": {
    49  		grafanaMysqlPattern,
    50  	},
    51  	"verrazzano-system": {
    52  		"^coherence-operator.*$",
    53  		"^weblogic-operator.*$",
    54  	},
    55  	"verrazzano-backup": {
    56  		"^restic.*$",
    57  	},
    58  	"verrazzano-monitoring": {
    59  		"^jaeger-operator-jaeger-es-index-cleaner.*$",
    60  	},
    61  }
    62  
    63  var skipContainers = []string{"jaeger-agent"}
    64  var skipInitContainers = []string{"istio-init", "elasticsearch-init"}
    65  
    66  type podExceptions struct {
    67  	allowHostPath    bool
    68  	allowHostNetwork bool
    69  	allowHostPID     bool
    70  	allowHostPort    bool
    71  	containers       map[string]containerException
    72  }
    73  
    74  type containerException struct {
    75  	allowedCapabilities map[string]bool
    76  }
    77  
    78  var exceptionPods = map[string]podExceptions{
    79  	"node-exporter": {
    80  		allowHostPath:    true,
    81  		allowHostNetwork: true,
    82  		allowHostPID:     true,
    83  		allowHostPort:    true,
    84  	},
    85  	"fluentd": {
    86  		allowHostPath: true,
    87  		containers: map[string]containerException{
    88  			"fluentd": {
    89  				allowedCapabilities: map[string]bool{
    90  					capDacOverride: true,
    91  				},
    92  			},
    93  		},
    94  	},
    95  }
    96  
    97  var (
    98  	clientset *kubernetes.Clientset
    99  )
   100  
   101  var isMinVersion150 bool
   102  
   103  var beforeSuite = t.BeforeSuiteFunc(func() {
   104  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   105  	if err != nil {
   106  		Fail(fmt.Sprintf("Failed to get default kubeconfig path: %s", err.Error()))
   107  	}
   108  	isMinVersion150, err = pkg.IsVerrazzanoMinVersion("1.5.0", kubeconfigPath)
   109  	if err != nil {
   110  		Fail(fmt.Sprintf("Error checking Verrazzano version: %s", err.Error()))
   111  	}
   112  	Eventually(func() (*kubernetes.Clientset, error) {
   113  		clientset, err = k8sutil.GetKubernetesClientset()
   114  		return clientset, err
   115  	}, waitTimeout, pollingInterval).ShouldNot(BeNil())
   116  })
   117  
   118  var _ = BeforeSuite(beforeSuite)
   119  
   120  var _ = t.AfterEach(func() {})
   121  
   122  var _ = t.Describe("Ensure pod security", Label("f:security.podsecurity"), func() {
   123  	testFunc := func(ns string) {
   124  		if !isMinVersion150 {
   125  			t.Logs.Infof("Skipping test, minimum version requirement of v1.5.0 not met")
   126  			return
   127  		}
   128  		var podList *corev1.PodList
   129  		Eventually(func() (*corev1.PodList, error) {
   130  			var err error
   131  			podList, err = clientset.CoreV1().Pods(ns).List(context.TODO(), v1.ListOptions{})
   132  			return podList, err
   133  		}, waitTimeout, pollingInterval).ShouldNot(BeNil())
   134  
   135  		t.Logs.Debugf("Podlist length: %v", len(podList.Items))
   136  
   137  		var errors []error
   138  		pods := podList.Items
   139  		for _, pod := range pods {
   140  			t.Logs.Debugf("Checking pod %s/%s", ns, pod.Name)
   141  			if shouldSkipPod(t.Logs, pod.Name, ns) {
   142  				t.Logs.Debugf("Pod %s/%s on skip list, continuing...", ns, pod.Name)
   143  				continue
   144  			}
   145  			errors = append(errors, expectPodSecurityForNamespace(pod)...)
   146  		}
   147  		Expect(errors).To(BeEmpty())
   148  	}
   149  	ingressNGINXNamespace, err := nginxutil.DetermineNamespaceForIngressNGINX(vzlog.DefaultLogger())
   150  	if err != nil {
   151  		Fail("Error determining ingress-nginx namespace")
   152  	}
   153  	t.DescribeTable("Check pod security in system namespaces", testFunc,
   154  		Entry("Checking pod security in verrazzano-install", "verrazzano-install"),
   155  		Entry("Checking pod security in verrazzano-system", "verrazzano-system"),
   156  		Entry("Checking pod security in verrazzano-monitoring", "verrazzano-monitoring"),
   157  		Entry("Checking pod security in verrazzano-backup", "verrazzano-backup"),
   158  		Entry("Checking pod security in "+ingressNGINXNamespace, ingressNGINXNamespace),
   159  		Entry("Checking pod security in mysql-operator", "mysql-operator"),
   160  		Entry("Checking pod security in cert-manager", "cert-manager"),
   161  		Entry("Checking pod security in keycloak", "keycloak"),
   162  		Entry("Checking pod security in argocd", "argocd"),
   163  		Entry("Checking pod security in istio-system", "istio-system"),
   164  		Entry("Checking pod security in verrazzano-auth", "verrazzano-auth"),
   165  	)
   166  })
   167  
   168  func expectPodSecurityForNamespace(pod corev1.Pod) []error {
   169  	var errors []error
   170  
   171  	// get pod exceptions
   172  	isException, exception := isExceptionPod(pod.Name)
   173  
   174  	// ensure hostpath is not set unless it is an exception
   175  	if !isException || !exception.allowHostPath {
   176  		for _, vol := range pod.Spec.Volumes {
   177  			if vol.HostPath != nil {
   178  				errors = append(errors, fmt.Errorf("Pod Security not configured for pod %s, HostPath is set, HostPath = %s  Type = %s",
   179  					pod.Name, vol.HostPath.Path, *vol.HostPath.Type))
   180  			}
   181  		}
   182  	}
   183  
   184  	// ensure hostnetwork is not set unless it is an exception
   185  	if pod.Spec.HostNetwork && (!isException || !exception.allowHostNetwork) {
   186  		errors = append(errors, fmt.Errorf("Pod Security not configured for pod %s, HostNetwork is set", pod.Name))
   187  	}
   188  
   189  	// ensure hostPID is not set unless it is an exception
   190  	if pod.Spec.HostPID && (!isException || !exception.allowHostPID) {
   191  		errors = append(errors, fmt.Errorf("Pod Security not configured for pod %s, HostPID is set", pod.Name))
   192  	}
   193  
   194  	// ensure host port is not set unless it is an exception
   195  	if !isException || !exception.allowHostPort {
   196  		for _, container := range pod.Spec.Containers {
   197  			for _, port := range container.Ports {
   198  				if port.HostPort != 0 {
   199  					errors = append(errors, fmt.Errorf("Pod Security not configured for pod %s, HostPort is set, HostPort = %v",
   200  						pod.Name, port.HostPort))
   201  				}
   202  			}
   203  		}
   204  	}
   205  
   206  	// ensure pod SecurityContext set correctly
   207  	if errs := ensurePodSecurityContext(pod.Spec.SecurityContext, pod.Name); len(errs) > 0 {
   208  		errors = append(errors, errs...)
   209  	}
   210  
   211  	// ensure container SecurityContext set correctly
   212  	for _, container := range pod.Spec.Containers {
   213  		if shouldSkipContainer(container.Name, skipContainers) {
   214  			continue
   215  		}
   216  		if errs := ensureContainerSecurityContext(container.SecurityContext, pod.Name, container.Name); len(errs) > 0 {
   217  			errors = append(errors, errs...)
   218  		}
   219  	}
   220  
   221  	// ensure init container SecurityContext set correctly
   222  	for _, initContainer := range pod.Spec.InitContainers {
   223  		if shouldSkipContainer(initContainer.Name, skipInitContainers) {
   224  			continue
   225  		}
   226  		if errs := ensureContainerSecurityContext(initContainer.SecurityContext, pod.Name, initContainer.Name); len(errs) > 0 {
   227  			errors = append(errors, errs...)
   228  		}
   229  	}
   230  	return errors
   231  }
   232  
   233  func ensurePodSecurityContext(sc *corev1.PodSecurityContext, podName string) []error {
   234  	if sc == nil {
   235  		return []error{fmt.Errorf("PodSecurityContext is nil for pod %s", podName)}
   236  	}
   237  	var errors []error
   238  	if sc.RunAsUser != nil && *sc.RunAsUser == 0 {
   239  		errors = append(errors, fmt.Errorf("PodSecurityContext not configured correctly for pod %s, RunAsUser is 0", podName))
   240  	}
   241  	if sc.RunAsNonRoot != nil && !*sc.RunAsNonRoot {
   242  		errors = append(errors, fmt.Errorf("PodSecurityContext not configured correctly for pod %s, RunAsNonRoot != true", podName))
   243  	}
   244  	if sc.SeccompProfile == nil {
   245  		errors = append(errors, fmt.Errorf("PodSecurityContext not configured correctly for pod %s, Missing seccompProfile", podName))
   246  	}
   247  	return errors
   248  }
   249  
   250  func ensureContainerSecurityContext(sc *corev1.SecurityContext, podName, containerName string) []error {
   251  	exceptionContainer := false
   252  	exceptionPod, exception := isExceptionPod(podName)
   253  	if exceptionPod && exception.containers != nil {
   254  		_, exceptionContainer = exception.containers[containerName]
   255  	}
   256  
   257  	if sc == nil {
   258  		return []error{fmt.Errorf("SecurityContext is nil for pod %s, container %s", podName, containerName)}
   259  	}
   260  	var errors []error
   261  	if sc.RunAsUser != nil && *sc.RunAsUser == 0 {
   262  		errors = append(errors, fmt.Errorf("SecurityContext not configured correctly for pod %s, container %s,  RunAsUser is 0", podName, containerName))
   263  	}
   264  	if sc.RunAsNonRoot != nil && !*sc.RunAsNonRoot {
   265  		errors = append(errors, fmt.Errorf("SecurityContext not configured correctly for pod %s, container %s, RunAsNonRoot != true", podName, containerName))
   266  	}
   267  	if sc.Privileged != nil && *sc.Privileged {
   268  		errors = append(errors, fmt.Errorf("SecurityContext not configured correctly for pod %s, container %s, Privileged != false", podName, containerName))
   269  	}
   270  	if sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation {
   271  		errors = append(errors, fmt.Errorf("SecurityContext not configured correctly for pod %s, container %s, AllowPrivilegeEscalation != false", podName, containerName))
   272  	}
   273  	errors = append(errors, checkContainerCapabilities(sc, podName, containerName, exceptionContainer, exception)...)
   274  	return errors
   275  }
   276  
   277  func checkContainerCapabilities(sc *corev1.SecurityContext, podName string, containerName string, exceptionContainer bool, exception podExceptions) []error {
   278  	if sc.Capabilities == nil {
   279  		return []error{fmt.Errorf("SecurityContext not configured correctly for pod %s, container %s, Capabilities is nil", podName, containerName)}
   280  	}
   281  	var errors []error
   282  	dropCapabilityFound := false
   283  	for _, c := range sc.Capabilities.Drop {
   284  		if string(c) == "ALL" {
   285  			dropCapabilityFound = true
   286  		}
   287  	}
   288  	if !dropCapabilityFound {
   289  		errors = append(errors, fmt.Errorf("SecurityContext not configured correctly for pod %s, container %s, Missing `Drop -ALL` capabilities", podName, containerName))
   290  	}
   291  	if len(sc.Capabilities.Add) > 0 && !exceptionContainer {
   292  		if len(sc.Capabilities.Add) > 1 || sc.Capabilities.Add[0] != capNetBindService {
   293  			errors = append(errors, fmt.Errorf("only %s capability allowed, found unexpected capabilities added to container %s in pod %s: %v", capNetBindService, containerName, podName, sc.Capabilities.Add))
   294  		}
   295  	}
   296  	if exceptionContainer && len(sc.Capabilities.Add) > 0 {
   297  		if !capExceptions(exception.containers[containerName].allowedCapabilities, sc.Capabilities.Add) {
   298  			errors = append(errors, fmt.Errorf("%v capabilities are allowed, found unexpected capabilities for pod %s container %s: %v", exception.containers[containerName].allowedCapabilities, podName, containerName, sc.Capabilities.Add))
   299  		}
   300  	}
   301  	return errors
   302  }
   303  
   304  func shouldSkipPod(log *zap.SugaredLogger, podName, ns string) bool {
   305  	for _, pattern := range skipPods[ns] {
   306  		podNamePattern := pattern
   307  		match, err := regexp.MatchString(podNamePattern, podName)
   308  		if err != nil {
   309  			log.Errorf("Error parsing regex %s: %s", podNamePattern, err.Error())
   310  		}
   311  		log.Debugf("Matching pod %s against regex %s, result: %v", podName, podNamePattern, match)
   312  		if match {
   313  			return true
   314  		}
   315  	}
   316  	return false
   317  }
   318  
   319  func shouldSkipContainer(containerName string, skip []string) bool {
   320  	for _, c := range skip {
   321  		if strings.Contains(containerName, c) {
   322  			return true
   323  		}
   324  	}
   325  	return false
   326  }
   327  
   328  func isExceptionPod(podName string) (bool, podExceptions) {
   329  	for exceptionPod := range exceptionPods {
   330  		if strings.Contains(podName, exceptionPod) {
   331  			return true, exceptionPods[exceptionPod]
   332  		}
   333  	}
   334  	return false, podExceptions{}
   335  }
   336  
   337  func capExceptions(allowedCaps map[string]bool, givenCaps []corev1.Capability) bool {
   338  	exceptionsOk := true
   339  	for _, givenCap := range givenCaps {
   340  		_, ok := allowedCaps[string(givenCap)]
   341  		exceptionsOk = exceptionsOk && ok
   342  	}
   343  	return exceptionsOk
   344  }