package main import ( "fmt" "iter" "k8s.io/api/core/v1" "maps" "os" "slices" "strconv" ) type ValidationLevel int const ( Info ValidationLevel = iota Warning Error ) type Resolver interface { ServiceAccounts(application *Application) []string PortNumbers(application *Application) []Port } func LogValidationMsg(level ValidationLevel, msg string, v ...any) { fmt.Fprintf(os.Stderr, "NOTICE: "+msg+"\n", v...) } func IterToSlice[K any](i iter.Seq[K]) []K { res := make([]K, 0) for v := range i { res = append(res, v) } return res } func MapKeys[K comparable, V any](m map[K]V) []K { return IterToSlice(maps.Keys(m)) } func MapValues[K comparable, V any](m map[K]V) []V { return IterToSlice(maps.Values(m)) } func validate(files []string, options *Options) error { clientset, _ := GetKubernetesConnection() config, err := readConfig(files) if err != nil { return err } cluster, err := NewCluster(clientset) if err != nil { return err } config.Infer(cluster) // map applname1 -> appname2 where appname1 is in an open namespace and app2 is in a closed namespace. // Exclusing when 'from' side is a CIDR. openToClosedAccess := make(map[string]string) applicationPods := make(map[string][]v1.Pod) for _, ns := range config.Namespaces { namespace := ns.Name if cluster.Namespace(namespace).Name != namespace { LogValidationMsg(Error, "ERROR: namespace not found: %s", namespace) continue } if !ns.Open { podsNotPartOfAnyApplication(cluster, namespace, ns) } // checking for service accounts shared by applications // map of namespace/sa -> []applicationname serviceAccountMap := make(map[string][]string) for _, application := range ns.Applications { pods := cluster.Pods(application) applicationPods[application.Name] = pods //log.Printf(namespace + "/" + application.Name) if len(pods) == 0 { LogValidationMsg(Error, "application %s: no running pods found", application.Name) } ownerReferences := cluster.OwnerReferences(application) if len(ownerReferences) > 1 { LogValidationMsg(Error, "Application %s: multiple owners found: %v. The application definition can possibly be made more fine-grain", application.Name, ownerReferences) } // check ports for _, port := range application.Ports { for _, pod := range pods { if !HasPort(pod, port) { LogValidationMsg(Error, "application %s: port %v not found in pod %s/%s", application.Name, port, pod.Namespace, pod.Name) } } } // Check service accounts applicationServiceAccounts := make(map[string]bool) for _, sa := range application.ServiceAccounts { applicationServiceAccounts[sa] = true } for _, pod := range pods { delete(applicationServiceAccounts, pod.Spec.ServiceAccountName) } if len(applicationServiceAccounts) > 0 { LogValidationMsg(Error, "application %s: service accounts %v configured but not used by running workloads", application.Name, MapKeys(applicationServiceAccounts)) } for _, pod := range pods { sa := pod.Namespace + "/" + pod.Spec.ServiceAccountName if !slices.Contains(serviceAccountMap[sa], application.Name) { serviceAccountMap[sa] = append(serviceAccountMap[sa], application.Name) } if pod.Spec.ServiceAccountName == "default" { LogValidationMsg(Warning, "Pod %s/%s: running with default service account", pod.Namespace, pod.Name) if application.ServiceAccounts != nil && !slices.Contains(application.ServiceAccounts, "default") { LogValidationMsg(Warning, "application %s: Pod %s/%s: running with 'default' service account but configured with %v", application.Name, pod.Namespace, pod.Name, application.ServiceAccounts) } } else { if !slices.Contains(application.ServiceAccounts, pod.Spec.ServiceAccountName) { LogValidationMsg(Warning, "application %s: Pod %s/%s: running with service account '%s' but configured with %v", application.Name, pod.Namespace, pod.Name, pod.Spec.ServiceAccountName, application.ServiceAccounts) } } } } // service accounts shared by multiple applications. for sa, applist := range serviceAccountMap { if len(applist) == 1 { continue } LogValidationMsg(Error, "service account %s: shared by multiple applications %v, the application definition can be made more fine-grain.", sa, applist) } } for _, communication := range config.Communications { if len(communication.Ports) == 0 { continue } for _, applicationName := range communication.To { application, _, _ := config.GetApplication(applicationName) if application == nil { continue } for _, port := range communication.Ports { pods := cluster.Pods(application) for _, pod := range pods { if !HasPort(pod, port) { LogValidationMsg(Error, "communication %v -> %v: port %v is not configured in pod %s/%s", communication.From, communication.To, port, pod.Namespace, pod.Name) } } } } for _, communication := range config.Communications { for _, applicationName := range communication.To { application, _, _ := config.GetApplication(applicationName) if application == nil { continue } // capability linkerd must exist on target namespace if !slices.Contains(application.Namespace.Capabilities, "linkerd") { continue } // annotation linkerd.io/inject must not be disabled pods := applicationPods[applicationName] for _, pod := range pods { if pod.Annotations["linkerd.io/inject"] == "disabled" { continue } } if !application.Namespace.Open { for _, applicationNameFrom := range communication.From { applicationFrom, networkFrom, _ := config.GetApplication(applicationNameFrom) if applicationFrom != nil && !applicationFrom.Namespace.Open { continue } if networkFrom == nil && cluster.IsLinkerdEnabled(application) { openToClosedAccess[applicationNameFrom] = applicationName } } } } } } for appFrom, appTo := range openToClosedAccess { LogValidationMsg(Error, "Access from 'open' application '%s' to 'closed' application '%s'. This will lead to generation of a network authentication for this workload.", appFrom, appTo) } return nil } func podsNotPartOfAnyApplication(cluster *Cluster, namespace string, ns *Namespace) { // Pods in the nemsapce that are not covered by any application namespacePods := cluster.PodList(namespace) namespacePods = slices.DeleteFunc(namespacePods, func(pod v1.Pod) bool { return pod.Spec.HostNetwork == true }) podNames := make(map[string]bool) for _, pod := range namespacePods { podNames[pod.Name] = true } for _, application := range ns.Applications { for _, pod := range cluster.Pods(application) { delete(podNames, pod.Name) } } for podName, _ := range podNames { LogValidationMsg(Error, "ERROR: pod %s/%s not part of any applications", namespace, podName) } } func HasPort(pod v1.Pod, port Port) bool { if port.Protocol == "" { port.Protocol = "TCP" } for _, container := range pod.Spec.Containers { for _, kport := range container.Ports { if kport.Protocol == "" { kport.Protocol = "TCP" } if port.Protocol == string(kport.Protocol) && (port.Port == kport.Name || port.Port == strconv.Itoa(int(kport.ContainerPort))) { return true } } } return false }