package audit import ( "fmt" "github.com/skyhook-io/radar/pkg/resourceid" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // checkTraefikDanglingRefs flags Traefik resources that reference a Service, // TraefikService, or Middleware that doesn't exist: routers (spec.routes → // Service/Middleware), chain middlewares (spec.chain.middlewares → Middleware), // and errors middlewares (spec.errors.service → Service). Traefik ships no // validation webhook or linter, and it does not load a router whose Service or // Middleware is missing (directly or through a chain or errors Middleware), so // the route stops serving and the error shows only in its log and dashboard. // Issues reports the routers a running Traefik loads; audit lists every // dangling reference, including middlewares no router uses yet. // // Matching is conservative to avoid false positives: // - Only genuinely-absent refs are flagged; an explicit cross-namespace ref // that resolves is accepted even though Traefik may reject it without // allowCrossNamespace (we can't see the provider config, so we don't guess). // - Refs are matched within the SAME Traefik group family — a traefik.io // router is not considered satisfied by a traefik.containo.us Middleware. // - Service / Middleware presence is only asserted when we actually have the // corresponding inventory (nil set = "couldn't list", so skip — never flag). // // Scope is still deliberately partial: nested TraefikService weighted/mirror // sub-services, ServersTransport refs, and TLS-secret refs can also dangle and // are not yet covered. func checkTraefikDanglingRefs(tr *evalTracker, input *CheckInput) []Finding { if len(input.IngressRoutes) == 0 && len(input.MiddlewareSubjects) == 0 { return nil } // Core Services for cross-namespace ref resolution (cluster-wide). Group- // agnostic (always core/v1). Trust level matches ingressNoMatchingService. coreServices := make(map[string]bool, len(input.AllServices)) for _, svc := range input.AllServices { coreServices[svc.Namespace+"/"+svc.Name] = true } servicesListed := input.AllServices != nil // Target inventories are gathered cluster-wide. Keys carry group + (for // middlewares) kind so a traefik.io router only resolves against traefik.io // targets, never the legacy group. traefikServices := make(map[string]bool, len(input.TraefikServices)) // group\x00ns/name for _, ts := range input.TraefikServices { traefikServices[resourceid.GroupFromAPIVersion(ts.GetAPIVersion())+"\x00"+ts.GetNamespace()+"/"+ts.GetName()] = true } middlewares := make(map[string]bool, len(input.Middlewares)) // group\x00kind\x00ns/name for _, mw := range input.Middlewares { middlewares[resourceid.GroupFromAPIVersion(mw.GetAPIVersion())+"\x00"+mw.GetKind()+"\x00"+mw.GetNamespace()+"/"+mw.GetName()] = true } // authoritative[group\x00Kind]: only assert a kind's absence when a synced // cluster-wide informer backs it (else the cache may know a subset of ns). authoritative := input.TraefikAuthoritativeKinds var findings []Finding seen := make(map[string]bool) add := func(subject *unstructured.Unstructured, checkID, msg string) { key := string(subject.GetUID()) + "\x00" + checkID + "\x00" + msg if seen[key] { return } seen[key] = true findings = append(findings, Finding{ Kind: subject.GetKind(), Group: resourceid.GroupFromAPIVersion(subject.GetAPIVersion()), Namespace: subject.GetNamespace(), Name: subject.GetName(), CheckID: checkID, Category: CategoryReliability, Severity: SeverityWarning, Message: msg, }) } // checkServiceRef resolves a Traefik Service reference (core Service or, when // kind=="TraefikService", a TraefikService) against the authoritative // inventory and reports it as missing via `add`. Shared by router // spec.routes[].services and errors-middleware spec.errors.service — both use // the same Service ref shape. `refDesc` labels the subject in the message. checkServiceRef := func(subject *unstructured.Unstructured, group, defaultNs, refDesc, checkID string, s map[string]any) { name, _ := s["name"].(string) if name == "" { return } ns, _ := s["namespace"].(string) if ns == "" { ns = defaultNs } if kind, _ := s["kind"].(string); kind == "TraefikService" { if authoritative[group+"\x00TraefikService"] && !traefikServices[group+"\x00"+ns+"/"+name] { add(subject, checkID, fmt.Sprintf("%s references TraefikService %q which is not found in the cluster", refDesc, traefikRefLabel(ns, name, defaultNs))) } } else if servicesListed && !coreServices[ns+"/"+name] { add(subject, checkID, fmt.Sprintf("%s references Service %q which is not found in the cluster", refDesc, traefikRefLabel(ns, name, defaultNs))) } } // checkMiddlewareRef resolves a Traefik MiddlewareRef (name + optional // namespace) against the authoritative inventory for mwKind. Shared by router // spec.routes[].middlewares and chain-middleware spec.chain.middlewares[]. checkMiddlewareRef := func(subject *unstructured.Unstructured, group, mwKind, defaultNs, refDesc, checkID string, m map[string]any) { if !authoritative[group+"\x00"+mwKind] { return // no synced cluster-wide inventory for this kind → can't assert absence } name, _ := m["name"].(string) if name == "" { return } ns, _ := m["namespace"].(string) if ns == "" { ns = defaultNs } if !middlewares[group+"\x00"+mwKind+"\x00"+ns+"/"+name] { add(subject, checkID, fmt.Sprintf("%s references %s %q which is not found in the cluster", refDesc, mwKind, traefikRefLabel(ns, name, defaultNs))) } } for _, route := range input.IngressRoutes { group := resourceid.GroupFromAPIVersion(route.GetAPIVersion()) routeKind := route.GetKind() routeNs := route.GetNamespace() // IngressRouteTCP chains MiddlewareTCP; the others chain Middleware. mwKind := "Middleware" if routeKind == "IngressRouteTCP" { mwKind = "MiddlewareTCP" } // A subject counts as evaluated only when (a) the ref inventory it // would be checked against is authoritative AND (b) it actually // carries at least one ref of that type — a route with no // middlewares isn't "passing" the dangling-middleware check, it's // out of scope. hasCoreSvcRef, hasTraefikSvcRef, hasMiddlewareRef := false, false, false routes, _, _ := unstructured.NestedSlice(route.Object, "spec", "routes") for _, r := range routes { rm, ok := r.(map[string]any) if !ok { continue } for _, svc := range nestedMaps(rm, "services") { if kind, _ := svc["kind"].(string); kind == "TraefikService" { hasTraefikSvcRef = true } else { hasCoreSvcRef = true } } if len(nestedMaps(rm, "middlewares")) > 0 { hasMiddlewareRef = true } } // Evaluated only when EVERY present ref kind is checkable — with a // core-Service ref but only the TraefikService inventory listed, the // core ref goes unchecked and "passed" would be a lie. svcRefsCheckable := (hasCoreSvcRef || hasTraefikSvcRef) && (!hasCoreSvcRef || servicesListed) && (!hasTraefikSvcRef || authoritative[group+"\x00TraefikService"]) if svcRefsCheckable { tr.record("traefikRouteMissingService", routeNs) } if hasMiddlewareRef && authoritative[group+"\x00"+mwKind] { tr.record("traefikRouteMissingMiddleware", routeNs) } for _, r := range routes { rm, ok := r.(map[string]any) if !ok { continue } for _, s := range nestedMaps(rm, "services") { checkServiceRef(route, group, routeNs, routeKind, "traefikRouteMissingService", s) } for _, m := range nestedMaps(rm, "middlewares") { checkMiddlewareRef(route, group, mwKind, routeNs, routeKind, "traefikRouteMissingMiddleware", m) } } } // Middleware subjects (audited namespaces): a `chain` middleware referencing a // missing Middleware, or an `errors` middleware referencing a missing Service. // Every router using such a middleware is not loaded. Only HTTP Middlewares // carry chain/errors; a MiddlewareTCP has neither key, so its loops are // no-ops. for _, mw := range input.MiddlewareSubjects { group := resourceid.GroupFromAPIVersion(mw.GetAPIVersion()) mwKind := mw.GetKind() mwNs := mw.GetNamespace() chain, _, _ := unstructured.NestedSlice(mw.Object, "spec", "chain", "middlewares") errSvc, hasErrSvc, _ := unstructured.NestedMap(mw.Object, "spec", "errors", "service") // Only HTTP Middlewares can carry chain/errors — and a Middleware // counts as evaluated only for the check whose ref it actually // carries (a middleware without spec.errors.service isn't "passing" // the errors check, it's out of scope). if mwKind == "Middleware" { if len(chain) > 0 && authoritative[group+"\x00"+mwKind] { tr.record("traefikChainMissingMiddleware", mwNs) } if hasErrSvc { errIsTraefik, _ := errSvc["kind"].(string) checkable := (errIsTraefik == "TraefikService" && authoritative[group+"\x00TraefikService"]) || (errIsTraefik != "TraefikService" && servicesListed) if checkable { tr.record("traefikErrorsMissingService", mwNs) } } } for _, c := range chain { cm, ok := c.(map[string]any) if !ok { continue } checkMiddlewareRef(mw, group, mwKind, mwNs, mwKind+" chain", "traefikChainMissingMiddleware", cm) } if hasErrSvc { checkServiceRef(mw, group, mwNs, mwKind+" errors", "traefikErrorsMissingService", errSvc) } } return findings } // traefikRefLabel shows the namespace only when it differs from the router's, // matching how operators write same-namespace refs (bare name). func traefikRefLabel(ns, name, routeNs string) string { if ns != "" && ns != routeNs { return ns + "/" + name } return name } func nestedMaps(m map[string]any, key string) []map[string]any { raw, _, _ := unstructured.NestedSlice(m, key) out := make([]map[string]any, 0, len(raw)) for _, item := range raw { if mm, ok := item.(map[string]any); ok { out = append(out, mm) } } return out }