pkg/audit/traefik.go
Rama main · réplica de lectura del repositorio original.
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
}