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 }