diff --git a/cmd/policygen/config.go b/cmd/policygen/config.go index 6fe62ab..bf4a525 100644 --- a/cmd/policygen/config.go +++ b/cmd/policygen/config.go @@ -31,6 +31,7 @@ func (c *CIDR) UnmarshalYAML(value []byte) error { return err } *c = CIDR(s) + return nil } @@ -65,6 +66,7 @@ type Application struct { MatchLabels map[string]string `yaml:"matchLabels"` //MatchExpressions []MatchExpression `yaml:"matchExpressions" validate:"omitempty,dive"` MatchExpressions []metav1.LabelSelectorRequirement `yaml:"matchExpressions" validate:"omitempty,dive"` + ServiceAccounts []string `yaml:"serviceAccounts,omitempty"` Namespace *Namespace `yaml:"-" validate:"-"` } @@ -171,6 +173,16 @@ func (c Config) GetApplication(name string) (*Application, *Network, string) { return nil, nil, "" } +func (c *Config) Infer(resolver func(application *Application) []string) { + for _, ns := range c.Namespaces { + for _, app := range ns.Applications { + if len(app.ServiceAccounts) == 0 { + app.ServiceAccounts = resolver(app) + } + } + } +} + func LoadConfig(file string) (*Config, error) { fmt.Fprintf(os.Stderr, "Reading config %s\n", file) yamlFile, err := os.ReadFile(file) diff --git a/cmd/policygen/configvalidator.go b/cmd/policygen/configvalidator.go index 14d25ff..974c41b 100644 --- a/cmd/policygen/configvalidator.go +++ b/cmd/policygen/configvalidator.go @@ -10,6 +10,7 @@ import ( "log" "maps" "os" + "slices" "strconv" ) @@ -38,11 +39,28 @@ func MapKeys[K comparable, V any](m map[K]V) []K { } func validate(files []string, options *Options) error { + clientset, _ := GetKubernetesConnection() config, err := readConfig(files) if err != nil { return err } - clientset, _ := GetKubernetesConnection() + config.Infer(func(application *Application) []string { + pods := FindPods(application, clientset) + var res []string + for _, pod := range pods { + if !slices.Contains(res, pod.Spec.ServiceAccountName) { + res = append(res, pod.Spec.ServiceAccountName) + } + } + log.Printf("Inferred service accounts: %s/%s: %v", application.Namespace.Name, application.Name, + res) + return res + }) + + // map applname1 -> appname2 where appname1 is in an open namespace and app2 is in a closed namespace. + openToClosedAccess := make(map[string]string) + + applicationPods := make(map[string][]v1.Pod) for _, ns := range config.Namespaces { namespace := ns.Name _, err = clientset.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{}) @@ -50,8 +68,14 @@ func validate(files []string, options *Options) error { LogValidationMsg(Error, "ERROR: namespace not found: %s", namespace) continue } + + // checking for service accounts shared by applications + // map of namespace/sa -> []applicationname + serviceAccountMap := make(map[string][]string) + for _, application := range ns.Applications { pods := FindPods(application, clientset) + applicationPods[application.Name] = pods //log.Printf(namespace + "/" + application.Name) if len(pods) == 0 { LogValidationMsg(Error, "application %s: no running pods found", application.Name) @@ -76,9 +100,47 @@ func validate(files []string, options *Options) error { } } // 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 workloads", + application.Name, MapKeys(applicationServiceAccounts)) + } + for _, pod := range pods { + sa := pod.Namespace + "/" + pod.Spec.ServiceAccountName + 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", sa, applist) } } + for _, communication := range config.Communications { if len(communication.Ports) == 0 { continue @@ -99,7 +161,42 @@ func validate(files []string, options *Options) error { } } + 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, _, _ := config.GetApplication(applicationNameFrom) + if applicationFrom != nil && !applicationFrom.Namespace.Open { + continue + } + openToClosedAccess[applicationNameFrom] = applicationName + } + } + } + } } + + for appFrom, appTo := range openToClosedAccess { + LogValidationMsg(Error, "Access from 'open' application '%s' to 'closed' application '%s'", + appFrom, appTo) + } + return nil } diff --git a/example/config.yaml b/example/config.yaml index 6a1f24a..f1f9327 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -27,6 +27,8 @@ namespaces: - port: 8081 - port: 8082 protocol: UDP + serviceAccounts: + - jantje matchLabels: app: nexus-server #matchExpressions: