package audit import ( "github.com/skyhook-io/radar/pkg/checks" "github.com/skyhook-io/radar/pkg/rolloutdiag" ) // CheckMeta is a check's static definition (human-readable context). Aliased // from pkg/checks so the k8s-free Checks rollup and the audit engine share one // shape — the Hub imports only pkg/checks; the engine + registry re-export it // here. Same fields/JSON, so radar's existing audit.CheckMeta references are // unchanged. type CheckMeta = checks.CheckMeta // Reference is an authoritative link shown in the expanded check card. type Reference = checks.Reference // Framework constants const ( FrameworkNSACISA = "NSA/CISA" FrameworkCIS = "CIS" ) // Authoritative references shared across checks. The inline copy stays brief; // these are the "go deeper" links (canonical Kubernetes docs + the hardening // guides the Frameworks tags name) rendered in the expanded card. var ( refSecurityContext = Reference{Label: "K8s: Security Context", URL: "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/"} refPodSecurityStd = Reference{Label: "K8s: Pod Security Standards", URL: "https://kubernetes.io/docs/concepts/security/pod-security-standards/"} refNSACISA = Reference{Label: "NSA/CISA Kubernetes Hardening Guide", URL: "https://media.defense.gov/2022/Aug/29/2003066362/-1/-1/0/CTR_KUBERNETES_HARDENING_GUIDANCE_1.2_20220829.PDF"} refResources = Reference{Label: "K8s: Resource Management", URL: "https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/"} refProbes = Reference{Label: "K8s: Liveness, Readiness & Startup Probes", URL: "https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/"} refImages = Reference{Label: "K8s: Container Images", URL: "https://kubernetes.io/docs/concepts/containers/images/"} refDeployments = Reference{Label: "K8s: Deployments", URL: "https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"} refDisruptions = Reference{Label: "K8s: Pod Disruption Budgets", URL: "https://kubernetes.io/docs/concepts/workloads/pods/disruptions/"} refTopologySpread = Reference{Label: "K8s: Topology Spread Constraints", URL: "https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/"} refAffinity = Reference{Label: "K8s: Affinity and Anti-affinity", URL: "https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity"} refGKEDiskTypes = Reference{Label: "Compute Engine: Machine series disk support", URL: "https://cloud.google.com/compute/docs/disks/persistent-disks"} refSAToken = Reference{Label: "K8s: Configure Service Accounts for Pods", URL: "https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/"} refSecrets = Reference{Label: "K8s: Secrets", URL: "https://kubernetes.io/docs/concepts/configuration/secret/"} refConfigMaps = Reference{Label: "K8s: ConfigMaps", URL: "https://kubernetes.io/docs/concepts/configuration/configmap/"} refHostPath = Reference{Label: "K8s: hostPath Volumes", URL: "https://kubernetes.io/docs/concepts/storage/volumes/#hostpath"} refDeprecatedAPI = Reference{Label: "K8s: Deprecated API Migration Guide", URL: "https://kubernetes.io/docs/reference/using-api/deprecation-guide/"} refService = Reference{Label: "K8s: Service", URL: "https://kubernetes.io/docs/concepts/services-networking/service/"} refIngress = Reference{Label: "K8s: Ingress", URL: "https://kubernetes.io/docs/concepts/services-networking/ingress/"} refTraefikCRD = Reference{Label: "Traefik: Kubernetes IngressRoute", URL: "https://doc.traefik.io/traefik/routing/providers/kubernetes-crd/"} refFinalizers = Reference{Label: "K8s: Finalizers", URL: "https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/"} refCrossplane = Reference{Label: "Crossplane: Managed Resources", URL: "https://docs.crossplane.io/latest/concepts/managed-resources/"} refOpenGitOps = Reference{Label: "OpenGitOps: Principles", URL: "https://opengitops.dev/"} refArgoCD = Reference{Label: "Argo CD: Declarative GitOps", URL: "https://argo-cd.readthedocs.io/en/stable/"} refHelm = Reference{Label: "Helm: Charts", URL: "https://helm.sh/docs/topics/charts/"} refCNPGBackup = Reference{Label: "CloudNativePG: Backup", URL: "https://cloudnative-pg.io/documentation/current/backup/"} refPV = Reference{Label: "K8s: Persistent Volumes", URL: "https://kubernetes.io/docs/concepts/storage/persistent-volumes/"} refStorageClass = Reference{Label: "K8s: Storage Classes", URL: "https://kubernetes.io/docs/concepts/storage/storage-classes/"} ) // CheckRegistry maps checkID → metadata for all built-in checks. // // Category MUST match the category the check emits at runtime (see pkg/audit/checks.go) // — the settings UI groups the catalog by it, so a mismatch would file a check // under a section its findings never appear in. TestRegistryCategoriesValid guards // the vocabulary; the cross-resource / lifecycle / deprecated-API checks all emit // as Reliability. var CheckRegistry = map[string]CheckMeta{ // ── Security ────────────────────────────────────────────────────── "runAsRoot": { ID: "runAsRoot", Title: "Run as non-root", Category: CategorySecurity, Description: "Containers running as root have full access to the host filesystem. If compromised, an attacker gains root-level access.", Remediation: "Set securityContext.runAsNonRoot: true and runAsUser: 1000 (or higher) on the container or pod spec.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refSecurityContext, refPodSecurityStd, refNSACISA}, }, "privileged": { ID: "privileged", Title: "Privileged container", Category: CategorySecurity, Description: "Privileged containers have all host capabilities, bypassing container isolation. An attacker in a privileged container can compromise the entire node.", Remediation: "Set securityContext.privileged: false on the container.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refSecurityContext, refPodSecurityStd, refNSACISA}, }, "privilegeEscalation": { ID: "privilegeEscalation", Title: "Privilege escalation", Category: CategorySecurity, Description: "Allows a process to gain more privileges than its parent. Attackers can exploit this to escalate from container to host.", Remediation: "Set securityContext.allowPrivilegeEscalation: false on the container.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refSecurityContext, refPodSecurityStd}, }, "readOnlyRootFs": { ID: "readOnlyRootFs", Title: "Read-only root filesystem", Category: CategorySecurity, Description: "A writable root filesystem allows attackers to modify binaries, install tools, or tamper with the application.", Remediation: "Set securityContext.readOnlyRootFilesystem: true. Use emptyDir volumes for writable paths.", Frameworks: []string{FrameworkNSACISA}, References: []Reference{refSecurityContext, refNSACISA}, }, "dangerousCapabilities": { ID: "dangerousCapabilities", Title: "Dangerous capabilities", Category: CategorySecurity, Description: "Capabilities like SYS_ADMIN, NET_ADMIN, and ALL grant near-root powers and can be used to escape the container.", Remediation: "Remove dangerous capabilities from securityContext.capabilities.add. Drop all and add only what is needed.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refSecurityContext, refPodSecurityStd}, }, "dockerSocketMount": { ID: "dockerSocketMount", Title: "Container runtime socket mounted", Category: CategorySecurity, Description: "Mounting the Docker/containerd socket gives the container full control over the host's container runtime — equivalent to root access. This is a documented attack vector used in cryptojacking campaigns.", Remediation: "Remove the hostPath volume mounting the container runtime socket. If CI/CD needs Docker access, use Docker-in-Docker (DinD) or Kaniko instead.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refHostPath, refNSACISA}, }, "sensitiveHostPath": { ID: "sensitiveHostPath", Title: "Sensitive host path mounted", Category: CategorySecurity, Description: "Mounting sensitive host directories (/etc, /proc, /sys, /var/run) exposes host configuration, credentials, and runtime state to the container. Attackers can use this to escalate privileges or access other containers.", Remediation: "Remove the hostPath volume or restrict it to a specific, non-sensitive directory. Use emptyDir or PVC for writable storage.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refHostPath, refPodSecurityStd}, }, "secretInConfigMap": { ID: "secretInConfigMap", Title: "Potential secret in ConfigMap", Category: CategorySecurity, Description: "ConfigMap keys with names suggesting sensitive data (passwords, tokens, keys) should use Kubernetes Secrets instead. ConfigMaps are not encrypted at rest and may appear in logs and kubectl output.", Remediation: "Move sensitive data to a Secret resource. ConfigMaps should only contain non-sensitive configuration.", Frameworks: []string{FrameworkNSACISA}, References: []Reference{refSecrets}, }, "insecureCapabilities": { ID: "insecureCapabilities", Title: "Insecure capabilities", Category: CategorySecurity, Description: "Capabilities like NET_RAW, SYS_PTRACE, and MKNOD can be exploited for packet spoofing, process injection, or device manipulation.", Remediation: "Remove insecure capabilities from securityContext.capabilities.add. Use capabilities.drop: [ALL] and add back only what is strictly needed.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refSecurityContext, refPodSecurityStd}, }, "hostNetwork": { ID: "hostNetwork", Title: "Host network", Category: CategorySecurity, Description: "Sharing the host network namespace lets the container see all network traffic and access host network services.", Remediation: "Remove hostNetwork: true unless the workload genuinely needs host network access.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refPodSecurityStd, refNSACISA}, }, "hostPID": { ID: "hostPID", Title: "Host PID namespace", Category: CategorySecurity, Description: "Sharing the host PID namespace lets the container see and signal all host processes, enabling process injection attacks.", Remediation: "Remove hostPID: true from the pod spec.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refPodSecurityStd, refNSACISA}, }, "hostIPC": { ID: "hostIPC", Title: "Host IPC namespace", Category: CategorySecurity, Description: "Sharing the host IPC namespace allows access to shared memory and inter-process communication with host processes.", Remediation: "Remove hostIPC: true from the pod spec.", Frameworks: []string{FrameworkNSACISA, FrameworkCIS}, References: []Reference{refPodSecurityStd, refNSACISA}, }, "automountServiceAccountToken": { ID: "automountServiceAccountToken", Title: "Service account token auto-mount", Category: CategorySecurity, Description: "Auto-mounted tokens can be used by attackers to authenticate to the Kubernetes API server and escalate privileges.", Remediation: "Set automountServiceAccountToken: false on the pod spec. Only enable for pods that need API access.", Frameworks: []string{FrameworkNSACISA}, References: []Reference{refSAToken, refNSACISA}, }, // ── Reliability ─────────────────────────────────────────────────── "readinessProbeMissing": { ID: "readinessProbeMissing", Title: "Missing readiness probe", Category: CategoryReliability, Description: "Without a readiness probe, Kubernetes sends traffic to pods before they are ready, causing errors for users.", Remediation: "Add a readinessProbe (HTTP GET, TCP, or exec) to each container.", References: []Reference{refProbes}, }, "livenessProbeMissing": { ID: "livenessProbeMissing", Title: "Missing liveness probe", Category: CategoryReliability, Description: "Without a liveness probe, Kubernetes cannot detect and restart containers that are stuck or deadlocked.", Remediation: "Add a livenessProbe (HTTP GET, TCP, or exec) to each container.", References: []Reference{refProbes}, }, "imageTagLatest": { ID: "imageTagLatest", Title: "Image tag latest or missing", Category: CategoryReliability, Description: "Using 'latest' or no tag means deployments are not reproducible — you cannot tell which version is running, and rollbacks may not work.", Remediation: "Pin images to a specific version tag (e.g., nginx:1.25.3).", References: []Reference{refImages}, }, "pullPolicyNotAlways": { ID: "pullPolicyNotAlways", Title: "Pull policy not Always", Category: CategoryReliability, Description: "With a mutable tag and non-Always pull policy, nodes may run stale cached images, causing inconsistency across replicas.", Remediation: "Set imagePullPolicy: Always when using mutable tags, or pin to immutable digests.", References: []Reference{refImages}, }, "tlsCertificateExpiry": { ID: "tlsCertificateExpiry", Title: "TLS certificate expiring", Category: CategoryReliability, Description: "A kubernetes.io/tls Secret contains a parseable leaf certificate with fewer than 30 days remaining. Fewer than 7 days or an expired certificate is High severity. Missing or malformed leaf certificates and sealed-secrets encryption keys are not evaluated. This checks the stored leaf certificate, not whether a service is using it or whether renewal is failing.", Remediation: "Renew or replace the certificate through its issuing system. For controller-managed Secrets, inspect the controller's renewal status; do not hand-edit the Secret. Verify the serving endpoint uses the renewed certificate.", References: []Reference{refSecrets}, }, "singleReplica": { ID: "singleReplica", Title: "Single replica", Category: CategoryReliability, Description: "A single-replica deployment has no redundancy — any pod disruption (node drain, OOM, crash) causes downtime.", Remediation: "Set replicas: 2 or higher, or configure an HPA for autoscaling.", References: []Reference{refDeployments, refDisruptions}, }, "rolloutAvailabilityRisk": { ID: "rolloutAvailabilityRisk", Title: "Rolling update permits zero availability", Category: CategoryReliability, Description: rolloutdiag.Description, Remediation: rolloutdiag.Remediation, References: []Reference{refDeployments}, }, checkPDVolumeHyperdiskOnlyNode: { ID: checkPDVolumeHyperdiskOnlyNode, Title: "Persistent Disk volume can land on a Hyperdisk-only node", Category: CategoryReliability, Description: "The workload mounts a GCE Persistent Disk volume (pd-standard, pd-balanced, pd-ssd or pd-extreme), and the cluster has nodes from a machine series that attaches only Hyperdisk (such as N4, N4D, C4 or C4A). The workload's node selector, required node affinity and tolerations don't keep its pods off those nodes. When a pod is rescheduled onto one — after a node pool resize, upgrade or drain — Compute Engine rejects the attach and the pod never starts.", Remediation: "Pin the workload to a node pool whose machine type supports its disk type: add a nodeSelector or required node affinity on node.kubernetes.io/instance-type or cloud.google.com/machine-family, or taint the Hyperdisk-only pool. Alternatively, move the data to a Hyperdisk StorageClass whose type every node pool the workload can run on supports — Hyperdisk support also varies by machine series.", References: []Reference{refGKEDiskTypes, refAffinity}, }, "missingPDB": { ID: "missingPDB", Title: "Missing PodDisruptionBudget", Category: CategoryReliability, Description: "Without a PDB, cluster operations (node drains, upgrades) can take down all replicas simultaneously.", Remediation: "Create a PodDisruptionBudget with minAvailable or maxUnavailable matching your availability requirements.", References: []Reference{refDisruptions}, }, "missingTopologySpread": { ID: "missingTopologySpread", Title: "Missing topology spread constraints", Category: CategoryReliability, Description: "Without topology spread constraints, all replicas can land on the same node or availability zone, giving an illusion of HA without real fault tolerance.", Remediation: "Add topologySpreadConstraints to the pod template with topologyKey: kubernetes.io/hostname or topology.kubernetes.io/zone.", References: []Reference{refTopologySpread}, }, "podHARisk": { ID: "podHARisk", Title: "Running replicas on same node", Category: CategoryReliability, Description: "All observed Running Pods (at least two) owned by this Deployment are on one node. Pending, terminal and terminating Pods are excluded. Both old and new ReplicaSets count during a rollout. This checks placement, not readiness or serving health. Incomplete ownership leaves placement unevaluated unless verified replicas already span multiple nodes.", Remediation: "Add pod anti-affinity or topology spread constraints to distribute replicas across nodes.", References: []Reference{refAffinity, refTopologySpread}, }, // ── Efficiency ──────────────────────────────────────────────────── "cpuRequestMissing": { ID: "cpuRequestMissing", Title: "Missing CPU request", Category: CategoryEfficiency, Description: "Without CPU requests, the scheduler cannot make informed placement decisions, leading to overcommitted nodes and throttling.", Remediation: "Set a non-zero resources.requests.cpu on each container, or spec.resources.requests.cpu for Linux Pods when pod-level resources are enabled. For Windows Pods, set requests on each container. Raise or remove a zero pod-level request before adding container requests.", Frameworks: []string{FrameworkCIS}, References: []Reference{refResources}, }, "memoryRequestMissing": { ID: "memoryRequestMissing", Title: "Missing memory request", Category: CategoryEfficiency, Description: "Without memory requests, pods may be scheduled on nodes without enough memory, causing OOM kills.", Remediation: "Set a non-zero resources.requests.memory on each container, or spec.resources.requests.memory for Linux Pods when pod-level resources are enabled. For Windows Pods, set requests on each container. Raise or remove a zero pod-level request before adding container requests.", Frameworks: []string{FrameworkCIS}, References: []Reference{refResources}, }, "cpuLimitMissing": { ID: "cpuLimitMissing", Title: "Missing CPU limit", Category: CategoryEfficiency, Description: "Without CPU limits, a single container can consume all CPU on a node, starving other workloads.", Remediation: "When using a shared pod request, set a non-zero spec.resources.limits.cpu for Linux Pods when pod-level resources are enabled. Otherwise set resources.limits.cpu on each container. Container limits also default missing container requests: set explicit requests that fit the shared pod request. Raise or remove a zero pod-level limit first, and check namespace constraints. For Windows Pods, set limits on each container.", Frameworks: []string{FrameworkCIS}, References: []Reference{refResources}, }, "memoryLimitMissing": { ID: "memoryLimitMissing", Title: "Missing memory limit", Category: CategoryEfficiency, Description: "Without memory limits, a container can consume unbounded memory, eventually triggering kernel OOM kills on the node.", Remediation: "When using a shared pod request, set a non-zero spec.resources.limits.memory for Linux Pods when pod-level resources are enabled. Otherwise set resources.limits.memory on each container. Container limits also default missing container requests: set explicit requests that fit the shared pod request. Raise or remove a zero pod-level limit first, and check namespace constraints. For Windows Pods, set limits on each container.", Frameworks: []string{FrameworkCIS}, References: []Reference{refResources}, }, "orphanConfigMapSecret": { ID: "orphanConfigMapSecret", Title: "Unused ConfigMap or Secret", Category: CategoryEfficiency, Description: "No workload, Ingress, or supported controller configuration references this ConfigMap or Secret. Radar checks the reference kinds it understands; a consumer that reads it by name from code or from an unsupported controller would not be seen.", Remediation: "Confirm nothing outside Radar's view still uses it, then either add the missing reference from the workload, Ingress, or controller that should consume it, or delete it. Keep a copy of a Secret before deleting it - its contents cannot be recovered.", References: []Reference{refConfigMaps, refSecrets}, }, "pvcNoConsumer": { ID: "pvcNoConsumer", Title: "PVC with no consumer observed", Category: CategoryEfficiency, Description: "A Bound claim without a controller owner has no reference from the Pods, built-in workload templates, or Pending clones Radar can see. Where Kubernetes reports the claim's Unused condition (1.37+), the 24 hours count from when it became unused, and a claim it reports in use is never flagged. StatefulSet generated claims count even at zero replicas. CRD consumers, snapshot operations, and intentional retained data may still need this storage.", Remediation: "Confirm whether an operator, virtual machine, snapshot, external consumer, or recovery plan still needs the data, and check the bound volume's reclaim policy and backups. If nothing needs it, delete the claim; if its PV is Retain, reclaim that too.", References: []Reference{refPV}, }, "pvcLongPending": { ID: "pvcLongPending", Title: "Long-Pending PVC", Category: CategoryEfficiency, Description: "A claim without a controller owner has been Pending since creation, more than 24 hours ago, and no Pod, built-in workload template, or Pending clone references it. WaitForFirstConsumer claims without a consumer are included. CRD consumers or snapshot operations may still reference this claim.", Remediation: "Check whether a workload still needs the claim. For WaitForFirstConsumer, provisioning waits for a consuming Pod to be scheduled; otherwise inspect provisioning events, the StorageClass, and storage quota.", References: []Reference{refPV, refStorageClass}, }, "releasedPV": { ID: "releasedPV", Title: "Released or Failed persistent volume", Category: CategoryEfficiency, Description: "A Released volume has been retained for more than 24 hours after claim deletion, deletion has not completed after one hour with reclaim policy Delete, or a recent VolumeFailedDelete warning shows deletion is failing. Without a phase-transition time, release duration is unknown: Retain volumes are reported with PV age, and Delete volumes without a recent warning are not evaluated. A Failed volume, whose deletion or recycling failed after claim deletion, is reported immediately. Capacity is provisioned storage, not a cost estimate.", Remediation: "For Retain, confirm the data retention and recovery plan first. Deleting a Retain PV keeps its disk: to reclaim a dynamically provisioned volume, set its reclaim policy to Delete; otherwise delete the PV, then the disk in your cloud provider. For Delete failures and Failed volumes, inspect the latest warning or status message, storage controller, and provider permissions. Do not remove finalizers without understanding the cleanup they protect.", References: []Reference{refPV}, }, // ── Cross-resource / lifecycle (emit under Reliability) ──────────── "deprecatedAPIVersion": { ID: "deprecatedAPIVersion", BadgeWorthy: true, Title: "Deprecated API version", Category: CategoryReliability, Description: "This cluster still serves a deprecated API version. Resources using this API will break after a Kubernetes upgrade that removes it.", Remediation: "Migrate resources to the replacement API version before upgrading the cluster.", References: []Reference{refDeprecatedAPI}, }, "serviceNoMatchingPods": { ID: "serviceNoMatchingPods", BadgeWorthy: true, Title: "Service has no matching pods", Category: CategoryReliability, Description: "The service selector does not match any running pods — traffic sent to this service will fail.", Remediation: "Verify the service selector labels match the pod template labels on the target workload.", References: []Reference{refService}, }, "ingressNoMatchingService": { ID: "ingressNoMatchingService", BadgeWorthy: true, Title: "Ingress references missing service", Category: CategoryReliability, Description: "The ingress backend references a service that does not exist, so incoming traffic will get 503 errors.", Remediation: "Check the ingress spec and correct the service name, or create the missing service.", References: []Reference{refIngress}, }, "traefikRouteMissingService": { ID: "traefikRouteMissingService", BadgeWorthy: true, Title: "Traefik router references missing service", Category: CategoryReliability, Description: "A Traefik IngressRoute references a Service or TraefikService that does not exist. Traefik ships no admission webhook, so the bad reference is accepted, and Traefik then does not load that route's router: requests matching it get a 404 unless another router matches, and the error shows only in Traefik's log and dashboard.", Remediation: "Correct the service name/namespace in the router's spec.routes[].services, or create the missing Service/TraefikService. Cross-namespace references also require allowCrossNamespace on the Traefik CRD provider.", References: []Reference{refTraefikCRD}, }, "traefikRouteMissingMiddleware": { ID: "traefikRouteMissingMiddleware", BadgeWorthy: true, Title: "Traefik router references missing middleware", Category: CategoryReliability, Description: "A Traefik IngressRoute references a Middleware that does not exist in the resolved namespace. Traefik does not load a router whose Middleware is missing, so the whole route stops serving (a 404 unless another router matches), not just the middleware; the error shows only in Traefik's log and dashboard.", Remediation: "Correct the middleware name/namespace in the router's spec.routes[].middlewares, or create the missing Middleware. A same-name Middleware in a different API group (traefik.io vs traefik.containo.us) does not satisfy the reference.", References: []Reference{refTraefikCRD}, }, "traefikChainMissingMiddleware": { ID: "traefikChainMissingMiddleware", BadgeWorthy: true, Title: "Traefik chain references missing middleware", Category: CategoryReliability, Description: "A Traefik chain Middleware lists a Middleware in spec.chain.middlewares that does not exist in the resolved namespace. Traefik can't build the chain, so every router that uses it is not loaded and its route stops serving. A chain no router uses has no effect until one does.", Remediation: "Correct the middleware name/namespace in spec.chain.middlewares, or create the missing Middleware. A same-name Middleware in a different API group (traefik.io vs traefik.containo.us) does not satisfy the reference.", References: []Reference{refTraefikCRD}, }, "traefikErrorsMissingService": { ID: "traefikErrorsMissingService", BadgeWorthy: true, Title: "Traefik errors middleware references missing service", Category: CategoryReliability, Description: "A Traefik errors Middleware references a Service in spec.errors.service that does not exist. Traefik doesn't create the middleware, so every router that uses it is not loaded and its route stops serving. A middleware no router uses has no effect until one does.", Remediation: "Correct the service name/namespace in spec.errors.service, or create the missing Service. Cross-namespace references also require allowCrossNamespace on the Traefik CRD provider.", References: []Reference{refTraefikCRD}, }, "stuckTerminating": { ID: "stuckTerminating", // Not BadgeWorthy: stuck-terminating is already surfaced by the live health // pipeline (pkg/health renders the node degraded) and the "Terminating" chip // in the resource-list name cell — a badge here would be a third redundant // signal. It stays in the Checks/Audit views with its deletion-age ramp. Title: "Stuck terminating resource", Category: CategoryReliability, Description: "This resource has metadata.deletionTimestamp set but is still alive past the cleanup window. Most controllers finish cleanup within seconds; minutes-long delays usually mean a finalizer's owning controller is unhealthy or unable to reach a dependent service. Common causes: the controller pod is CrashLoopBackOff, DNS resolution is broken, the finalizer logic depends on a webhook or external API that's unavailable.", Remediation: "Check the controller responsible for each finalizer key (kubectl describe will show finalizers under metadata). For Argo CD, look at argocd-application-controller in the argocd namespace. For Flux, look at the matching controller (kustomize-controller, helm-controller, source-controller) in flux-system. Once the controller is healthy, deletion will resume automatically. If you must remove a stuck resource and accept the orphaned cleanup, manually clear the finalizers field — but only as a last resort. Never clear storage finalizers by hand: kubernetes.io/pvc-protection and kubernetes.io/pv-protection hold deletion while data is in use, and CSI provisioner finalizers guard deletion of the backing disk.", References: []Reference{refFinalizers}, }, "crossplaneStuck": { ID: "crossplaneStuck", BadgeWorthy: true, Title: "Stuck Crossplane resource", Category: CategoryReliability, Description: "A Crossplane Managed Resource, Composite Resource, or Claim has been reporting Ready=False or Synced=False past the reconciliation window. Synced=False usually means a configuration error (bad ProviderConfig, malformed forProvider spec, missing IAM permissions, quota exceeded, schema mismatch). Ready=False usually means the provider accepted the spec but can't reach a desired state (target cloud API rejected, dependency missing, eventual-consistency lag past the threshold).", Remediation: "Open the resource and read the latest condition's message — Crossplane providers almost always include the upstream cloud error verbatim. Common fixes: (1) check the linked Provider's Healthy condition; the package controller crashlooping starves every MR it manages. (2) verify the ProviderConfig credentials Secret still exists and is current (rotated keys, expired tokens). (3) check IAM/RBAC at the target cloud — Synced=False with auth errors needs a credentials fix, not a Crossplane fix. (4) look at quota/limit errors in the cloud provider console. (5) for paused resources (crossplane.io/paused annotation), this finding is intentionally suppressed — remove the annotation to resume reconciliation.", References: []Reference{refCrossplane}, }, // ── CloudNativePG ───────────────────────────────────────────────── // Posture, not breakage: absence of a schedule doesn't prove a cluster is // unprotected, so this stays at the default Medium and is not BadgeWorthy. checkCNPGNoDeclarativeBackup: { ID: checkCNPGNoDeclarativeBackup, Title: "PostgreSQL cluster has no declarative backup schedule", Category: CategoryReliability, Description: "No ScheduledBackup targets this CloudNativePG cluster, so nothing in the cluster declares when its backups run. Backups may still be taken on demand or by an external scheduler — this reports what is declared, not whether backups exist. Without a declared schedule there is no in-cluster record of the intended backup cadence, and no signal when backups silently stop.", Remediation: "Create a ScheduledBackup in the cluster's namespace referencing it by spec.cluster.name, with the schedule and the method that matches how backups run (barmanObjectStore, volumeSnapshot, or plugin for barman-cloud). Note CNPG schedules are six-field cron expressions — they include a leading seconds field.", References: []Reference{refCNPGBackup}, }, // ── GitOps coverage (emit under Reliability) ─────────────────────── // Only evaluated when a GitOps controller is installed (see CheckInput. // GitOpsToolsPresent). Posture, not breakage — graded like the other // coverage checks (singleReplica, missingPDB): warning findings, Medium // default severity, not BadgeWorthy. checkGitOpsUnmanaged: { ID: checkGitOpsUnmanaged, Title: "Workload not managed by GitOps", Category: CategoryReliability, Description: "This cluster runs GitOps, but this workload carries no GitOps (Argo CD / Flux) or Helm management signal — it was likely applied imperatively (kubectl apply / edit). Its desired state has no Git source of truth, so there is no review trail, no drift detection, and no declarative rollback.", Remediation: "Bring the workload under a GitOps controller: commit its manifests to the repository Argo CD or Flux reconciles, or adopt it into an existing Application/Kustomization. If you deploy with Helm, drive the release through the GitOps controller rather than the Helm CLI.", References: []Reference{refOpenGitOps, refArgoCD}, }, checkGitOpsHelmOnly: { ID: checkGitOpsHelmOnly, Title: "Workload managed by Helm but not GitOps", Category: CategoryReliability, Description: "This workload was installed by Helm directly (helm install/upgrade) rather than through a GitOps controller. A Helm release is its source of truth, but helm-CLI upgrades run outside Git — they skip pull-request review and the cluster's declarative rollback path.", Remediation: "Reconcile the Helm release through a GitOps controller — a Flux HelmRelease or an Argo CD Application pointing at the chart — so upgrades go through Git review and rollback.", References: []Reference{refOpenGitOps, refHelm}, }, } // The two Checks-ladder levels a detector reaches with no override — // pkg/checks.MapSeverity emits only these (critical/low need an org override). var ( sevHigh = string(checks.SeverityHigh) sevMedium = string(checks.SeverityMedium) ) // nonMediumDefault lists the checks shown as High; everything else defaults to // Medium. Mirror pkg/audit/checks.go emission when adding or re-ranking a check // (TestRegistryDefaultSeverity guards the values). var nonMediumDefault = map[string]string{ // Always emit danger → High. "privileged": sevHigh, "privilegeEscalation": sevHigh, "dangerousCapabilities": sevHigh, "dockerSocketMount": sevHigh, "hostPID": sevHigh, "hostIPC": sevHigh, "imageTagLatest": sevHigh, "deprecatedAPIVersion": sevHigh, // Context-dependent — emit warning OR danger (sensitiveHostPath: a full // host-root mount is danger, sensitive prefixes warning; stuck*/crossplane // ramp warning→danger past ~30min). Surfaced as High (their worst case) so // the settings catalog doesn't UNDER-call them — under-calling severity is // the dangerous error on a posture surface. The queue still shows each // finding's exact severity. "sensitiveHostPath": sevHigh, "stuckTerminating": sevHigh, "crossplaneStuck": sevHigh, "tlsCertificateExpiry": sevHigh, } // Stamp DefaultSeverity onto the catalog so the Hub's Checks settings can show // the level a check resolves to (vs. just "Default") without each consumer // re-deriving it from raw findings. func init() { for id := range CheckRegistry { sev := sevMedium if s, ok := nonMediumDefault[id]; ok { sev = s } m := CheckRegistry[id] m.DefaultSeverity = sev CheckRegistry[id] = m } }