Kubernetes Guardrails
Enforce Kubernetes best practices on plain manifests and rendered Helm charts, including manifest validation, resource limits, probes, PodDisruptionBudgets, autoscaling, topology spread, graceful shutdown, and NetworkPolicy egress to the instance-metadata endpoint.
k8s to your lunar-config.yml:uses: github://earthly/lunar-lib/policies/k8s@v1.0.5
Included Guardrails
This policy includes 21 guardrails that enforce standards for your deployment and infrastructure.
valid
Validates that all Kubernetes manifests in the repository are syntactically correct and conform to K8s schema, and that every Helm chart renders. Invalid manifests will fail deployment.
requests-and-limits
Ensures all containers have CPU and memory requests and limits defined. Missing resource specs can cause scheduling failures and noisy neighbor issues.
probes
Requires liveness and readiness probes on all containers. Probes enable Kubernetes to detect unhealthy pods and route traffic correctly.
min-replicas
Enforces minimum replica counts on HorizontalPodAutoscalers. Ensures services maintain capacity during scaling events and failures.
pdb
Requires PodDisruptionBudgets for Deployments and StatefulSets. PDBs protect availability during voluntary disruptions like node drains.
non-root
Requires containers to run as non-root users via securityContext. Running as root inside containers violates least privilege principles.
host-users
Requires workload PodSpecs to set hostUsers: false. User Namespaces
(GA in Kubernetes v1.36) map container UIDs to unprivileged host UIDs
so that a container escape no longer hands the attacker root on the
node. Complements non-root — that check drops privilege inside the
container; this one isolates UIDs across the kernel boundary. Target
cluster must be on K8s >=1.36; workloads that legitimately need host
UIDs (e.g. log shippers reading host paths) can be opted out via
include/exclude in lunar-config.yml.
host-network
Fails when workload PodSpecs set hostNetwork: true. Sharing the host's
network namespace bypasses NetworkPolicy enforcement, exposes node-local
services to the pod, and gives a compromised container direct access to
the node's interfaces (and any cleartext traffic on them). Legitimate
hostNetwork users (CNI agents, node-local proxies, host-bound exporters)
can opt out via include/exclude in lunar-config.yml.
host-pid
Fails when workload PodSpecs set hostPID: true. Sharing the host's
PID namespace lets the container see, signal, and (with the right
capabilities) attach to every process on the node, undermining process
isolation and aiding lateral movement after a container compromise.
Workloads that need it (e.g. node-level monitoring agents) can opt out
via include/exclude in lunar-config.yml.
host-ipc
Fails when workload PodSpecs set hostIPC: true. Sharing the host's
IPC namespace exposes node-wide System V IPC and POSIX shared memory
to the container, which can be abused to read or tamper with other
workloads' shared-memory state. Workloads that genuinely need host IPC
can opt out via include/exclude in lunar-config.yml.
metadata-egress-blocked
Requires a NetworkPolicy to keep each workload's pods from reaching the cloud instance-metadata endpoint (169.254.169.254), where node credentials can be read.
min-kubectl-version
Ensures the kubectl version used in CI meets the minimum required version. Old kubectl clients may miss security fixes or lack support for current cluster APIs.
topology-spread
Requires Deployments and StatefulSets that can run more than one replica (their autoscaler's
maximum, else spec.replicas) to have a topologySpreadConstraint on topology_key (zone by
default) that selects their pods, with maxSkew within topology_max_skew.
graceful-shutdown
Requires Deployments and StatefulSets to drain before stopping: a preStop hook on a container
and a terminationGracePeriodSeconds of at least min_termination_grace_period_seconds.
pdb-budget
Fails a PodDisruptionBudget that allows no voluntary disruption, which blocks node drains: maxUnavailable 0, or a minAvailable (a percentage rounds up) that covers every pod of a workload at its smallest size, its autoscaler's minimum else spec.replicas. Also fails one with no selector, or with both minAvailable and maxUnavailable set.
no-static-replicas
Fails a Deployment or StatefulSet that sets spec.replicas while an HPA or KEDA ScaledObject scales it: every apply resets the replica count the autoscaler chose.
probes-distinct
Requires each container's liveness and readiness probes to check different endpoints, so a pod too busy to serve leaves rotation instead of being restarted.
probe-timeouts
Requires every probe's timeoutSeconds to be at least min_probe_timeout_seconds and below
its periodSeconds, so one slow check isn't still running when the next starts.
allowed-registries
Requires every container and init container image to come from a registry in
allowed_registries. Skipped until the list is set.
deprecated-api-versions
Fails manifests and rendered charts that use an apiVersion listed in
deprecated_api_versions. Skipped until the list is set.
pod-annotations
Requires the pod template annotations in required_pod_annotations and fails those in
forbidden_pod_annotations. Skipped until either list is set.
How Guardrails Fit into Lunar
Lunar guardrails define your engineering standards as code. They evaluate data collected by integrations and produce pass/fail checks with actionable feedback.
Policies support gradual enforcement—from silent scoring to blocking PRs or deployments—letting you roll out standards at your own pace without disrupting existing workflows.
Learn How Lunar Works →Required Integrations
This policy evaluates data gathered by one or more of the following integration(s).
Make sure to enable them in your lunar-config.yml.
Configuration
Configure this policy in your lunar-config.yml.
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
min_replicas
|
Optional |
3
|
Minimum replicas required for HPAs (default 3) |
max_limit_to_request_ratio
|
Optional |
4
|
Maximum ratio of limits to requests for CPU/memory (default 4) |
min_kubectl_version
|
Optional |
1.28
|
Minimum kubectl version required in CI (semver, e.g. "1.28") |
metadata_ips
|
Optional |
169.254.169.254
|
Comma-separated metadata endpoint IPs that metadata-egress-blocked keeps pods from reaching on TCP port 80 (add fd00:ec2::254 for IPv6 EKS clusters) |
topology_key
|
Optional |
topology.kubernetes.io/zone
|
Node label topology-spread requires a constraint on (default topology.kubernetes.io/zone) |
topology_max_skew
|
Optional |
1
|
Largest maxSkew topology-spread accepts on that constraint (default 1) |
topology_when_unsatisfiable
|
Required | — | whenUnsatisfiable value topology-spread requires on that constraint (DoNotSchedule or ScheduleAnyway). Empty accepts either. |
topology_require_match_label_keys
|
Optional |
false
|
Also require the constraint to set matchLabelKeys with pod-template-hash, so a rollout's skew counts only the new revision's pods (true/false) |
min_termination_grace_period_seconds
|
Optional |
30
|
Smallest terminationGracePeriodSeconds graceful-shutdown accepts. An unset value counts as the Kubernetes default, 30 (default 30) |
require_prestop
|
Optional |
true
|
Whether graceful-shutdown requires a preStop hook on at least one container (true/false) |
min_probe_timeout_seconds
|
Optional |
1
|
Smallest probe timeoutSeconds probe-timeouts accepts. An unset value counts as the Kubernetes default, 1 (default 1) |
allowed_registries
|
Required | — | Comma-separated registries, or registry/path prefixes, images may come from (e.g. "ghcr.io/acme,123456789012.dkr.ecr.us-east-1.amazonaws.com"). An image with no registry counts as docker.io. Empty skips allowed-registries. |
deprecated_api_versions
|
Required | — | Comma-separated apiVersions deprecated-api-versions fails, each optionally narrowed to one kind (e.g. "policy/v1beta1,networking.k8s.io/v1beta1:Ingress"). Empty skips the check. |
required_pod_annotations
|
Required | — | Comma-separated annotation keys every workload's pod template must set. Empty requires none. |
forbidden_pod_annotations
|
Required | — | Comma-separated annotation keys no workload's pod template may set (e.g. "karpenter.sh/do-not-disrupt"). Empty forbids none. |
Documentation
View on GitHubKubernetes Guardrails
Enforces Kubernetes best practices for production-ready workloads.
Overview
This policy validates Kubernetes manifests against industry best practices including resource limits, health probes, PodDisruptionBudgets, autoscaling, topology spread, graceful shutdown, and security configurations. It evaluates plain manifests and rendered Helm charts alike, so chart-based workloads are held to the same standard. It helps ensure your K8s workloads are production-ready and resilient.
Policies
This policy provides the following guardrails (use include to select a subset):
| Policy | Description | Failure Meaning |
|---|---|---|
valid |
Validates K8s manifest syntax, and that Helm charts render | Manifest has YAML or schema errors, or a chart fails to render |
requests-and-limits |
Checks CPU/memory requests and limits | Container missing resource specs |
probes |
Requires liveness and readiness probes | Container missing health probes |
min-replicas |
Enforces minimum HPA replicas | HPA minReplicas below threshold |
pdb |
Requires PodDisruptionBudgets | Deployment/StatefulSet missing PDB |
non-root |
Requires non-root security context | Container may run as root |
host-users |
Requires PodSpecs to set hostUsers: false (K8s ≥1.36 user namespaces) |
Container UIDs are not isolated from host UIDs — a container escape gives the attacker root on the node |
host-network |
Forbids hostNetwork: true on PodSpecs |
Workload shares the host network namespace — bypasses NetworkPolicy and exposes node interfaces |
host-pid |
Forbids hostPID: true on PodSpecs |
Workload shares the host PID namespace — can see, signal, and potentially attach to processes on the node |
host-ipc |
Forbids hostIPC: true on PodSpecs |
Workload shares the host IPC namespace — can read or tamper with node-wide shared memory |
metadata-egress-blocked |
Requires NetworkPolicy to block pod egress to the instance-metadata endpoint | Workload's pods can reach 169.254.169.254, where node credentials can be read |
min-kubectl-version |
Enforces minimum kubectl version in CI | kubectl client used in CI is below threshold |
topology-spread |
Requires a topologySpreadConstraint on topology_key (zone by default) for Deployments and StatefulSets that can run more than one replica (see Replica counts) |
Replicas can all land in one zone, so one zone failure takes the workload down |
graceful-shutdown |
Requires a preStop hook and a long enough termination grace period on Deployments and StatefulSets | Pods stop while requests are still in flight or still being routed to them |
pdb-budget |
Requires every PodDisruptionBudget to allow at least one voluntary disruption at the smallest size of the workloads it covers (see Replica counts) | The budget blocks node drains, so nodes can't be upgraded or patched |
no-static-replicas |
Forbids spec.replicas on a workload an HPA or KEDA ScaledObject scales |
Every deploy resets the replica count the autoscaler chose |
probes-distinct |
Requires liveness and readiness probes to check different endpoints | An overloaded pod is restarted instead of taken out of rotation |
probe-timeouts |
Requires probe timeouts of at least min_probe_timeout_seconds and below the probe period |
Slow responses fail probes, or a check is still running when the next one starts |
allowed-registries |
Restricts container and init container images to allowed_registries (opt-in) |
Workload pulls an image from an unapproved registry |
deprecated-api-versions |
Forbids the apiVersions in deprecated_api_versions (opt-in) |
Manifest uses an API that is deprecated or removed on the target clusters |
pod-annotations |
Enforces required_pod_annotations and forbidden_pod_annotations on pod templates (opt-in) |
Pod template is missing a required annotation or sets a forbidden one |
Helm charts
The k8s collector's helm sub-collector renders each chart and records the objects with a render key: the chart directory and the values files used. Every check covers those objects like plain manifests, with these differences:
- A failure names the chart template and the values files, e.g.
charts/api/templates/deployment.yaml [values-prod.yaml]: .... When the same finding shows up in several values sets of a chart, it's reported once and lists them. - A workload is never matched to a PodDisruptionBudget, autoscaler or NetworkPolicy from another values set of its own chart, since those are alternative deployments of the same release. Objects from the same render, from plain manifests and from other charts all match, so a plain-manifest workload can be covered by a chart's PodDisruptionBudget.
- A chart that fails to render fails
validwith helm's error, so it can't pass every other check by contributing nothing. - A chart that no
helm_valuesline orhelm_values_chainschain applies to is only checked byvalid: its default render contributes no objects, because chart defaults (resources: {}, a disabled PodDisruptionBudget) are often left for whoever deploys the chart to fill in. When such charts are a component's only Kubernetes content, the workload checks skip and say that no values set applies, pointing athelm_valuesandhelm_values_chains.
The checks added alongside chart rendering (topology-spread through pod-annotations) skip when the k8s collector is older than the fields they read, and when nothing in the repository is in their scope, such as no workload that can run more than one replica, rather than passing on nothing.
Replica counts
A workload that an HPA or KEDA ScaledObject scales gets its size from the autoscaler, matched by scaleTargetRef in the same namespace (and under the render rules above), since no-static-replicas asks for spec.replicas to be left out. pdb-budget uses its smallest size, the autoscaler's minimum (minReplicas / minReplicaCount, at least 1), and topology-spread its largest (maxReplicas / maxReplicaCount). A workload without one uses spec.replicas, or 1 when it's unset. Percentages round up against that count, as Kubernetes' disruption controller does: minAvailable: 50% of 3 pods leaves one free and passes, 67% of 3 covers all of them and fails, and only maxUnavailable: 0 or 0% blocks every drain. A budget that selects several workloads counts their pods together, as Kubernetes does.
Required Data
This policy reads from the following Component JSON paths:
| Path | Type | Provided By |
|---|---|---|
.k8s.manifests[] |
array | k8s collector (k8s and helm sub-collectors) |
.k8s.workloads[] |
array | k8s collector (k8s and helm sub-collectors) |
.k8s.hpas[] |
array | k8s collector (k8s and helm sub-collectors) |
.k8s.scaled_objects[] |
array | k8s collector (k8s and helm sub-collectors) |
.k8s.pdbs[] |
array | k8s collector (k8s and helm sub-collectors) |
.k8s.network_policies[] |
array | k8s collector (k8s and helm sub-collectors) |
.k8s.cicd.cmds[] |
array | k8s collector (cicd sub-collector) |
Note: Ensure the k8s collector is configured before enabling this policy.
Installation
Add to your lunar-config.yml:
collectors:
- uses: github://earthly/lunar-lib/collectors/k8s@v1.0.0
on: [kubernetes]
include:
- k8s # Plain manifests
- helm # Helm charts, rendered
- cicd # kubectl commands in CI
policies:
- uses: github://earthly/lunar-lib/policies/k8s@v1.0.0
on: [kubernetes]
enforcement: report-pr
include:
- valid
- requests-and-limits
- probes
- min-replicas
- pdb
- non-root
- host-users
- host-network
- host-pid
- host-ipc
- metadata-egress-blocked
- min-kubectl-version
- topology-spread
- graceful-shutdown
- pdb-budget
- no-static-replicas
- probes-distinct
- probe-timeouts
- allowed-registries
- deprecated-api-versions
- pod-annotations
# with:
# min_replicas: "3"
# max_limit_to_request_ratio: "4"
# min_kubectl_version: "1.28"
# metadata_ips: "169.254.169.254,fd00:ec2::254" # IPv6 EKS clusters
# topology_key: "topology.kubernetes.io/zone"
# topology_max_skew: "1"
# topology_when_unsatisfiable: "ScheduleAnyway"
# min_termination_grace_period_seconds: "60"
# min_probe_timeout_seconds: "2"
# allowed_registries: "ghcr.io/acme,123456789012.dkr.ecr.us-east-1.amazonaws.com"
# deprecated_api_versions: "policy/v1beta1,autoscaling/v2beta2"
# required_pod_annotations: "prometheus.io/scrape"
# forbidden_pod_annotations: "karpenter.sh/do-not-disrupt"
Examples
Passing Example
A compliant component with proper resource specs, probes, spread, shutdown handling, and security context:
{
"k8s": {
"manifests": [
{"path": "deploy/deployment.yaml", "valid": true, "resources": [
{"kind": "Deployment", "name": "payment-api", "namespace": "payments", "api_version": "apps/v1"}
]}
],
"workloads": [
{
"kind": "Deployment",
"name": "payment-api",
"namespace": "payments",
"path": "deploy/deployment.yaml",
"replicas": 3,
"replicas_set": true,
"pod_labels": {"app": "payment-api"},
"pod_annotations": {},
"host_users": false,
"host_network": false,
"host_pid": false,
"host_ipc": false,
"termination_grace_period_seconds": 60,
"topology_spread_constraints": [
{"topology_key": "topology.kubernetes.io/zone", "max_skew": 1, "when_unsatisfiable": "ScheduleAnyway", "label_selector": {"matchLabels": {"app": "payment-api"}}, "match_label_keys": ["pod-template-hash"]}
],
"containers": [
{
"name": "api",
"image": "gcr.io/acme/payment-api:v1.2.3",
"has_resources": true,
"has_requests": true,
"has_limits": true,
"has_liveness_probe": true,
"has_readiness_probe": true,
"liveness_probe": {"handler": "httpGet", "port": 8080, "path": "/livez", "period_seconds": 10, "timeout_seconds": 2},
"readiness_probe": {"handler": "httpGet", "port": 8080, "path": "/readyz", "period_seconds": 5, "timeout_seconds": 2},
"has_prestop": true,
"runs_as_non_root": true
}
],
"init_containers": []
}
],
"pdbs": [
{"name": "payment-api-pdb", "namespace": "payments", "selector": {"matchLabels": {"app": "payment-api"}}, "min_available": null, "max_unavailable": 1}
],
"network_policies": [
{
"name": "egress-no-metadata",
"namespace": "payments",
"pod_selector": {},
"policy_types": ["Egress"],
"egress": [{"to": [{"ipBlock": {"cidr": "0.0.0.0/0", "except": ["169.254.169.254/32"]}}]}]
}
]
}
}
Failing Example
A Helm chart rendered with values-prod.yaml, plus a second chart whose dependencies aren't vendored:
{
"k8s": {
"manifests": [
{"path": "charts/my-app", "render": {"chart": "charts/my-app", "values": ["values.yaml", "values-prod.yaml"], "validated_only": false}, "valid": true, "resources": [
{"kind": "Deployment", "name": "my-app", "namespace": "default", "api_version": "apps/v1"}
]},
{"path": "charts/reports", "render": {"chart": "charts/reports", "values": ["values.yaml"], "validated_only": true}, "valid": false,
"error": "helm template: An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: postgresql"}
],
"workloads": [
{
"kind": "Deployment",
"name": "my-app",
"namespace": "default",
"path": "charts/my-app/templates/deployment.yaml",
"render": {"chart": "charts/my-app", "values": ["values.yaml", "values-prod.yaml"]},
"replicas": 2,
"replicas_set": true,
"pod_labels": {"app": "my-app"},
"pod_annotations": {"karpenter.sh/do-not-disrupt": "true"},
"host_users": true,
"host_network": true,
"host_pid": true,
"host_ipc": true,
"termination_grace_period_seconds": 30,
"topology_spread_constraints": [],
"containers": [
{
"name": "app",
"image": "nginx:1.25",
"has_resources": false,
"has_requests": false,
"has_limits": false,
"has_liveness_probe": true,
"has_readiness_probe": true,
"liveness_probe": {"handler": "httpGet", "port": 8080, "path": "/healthz", "period_seconds": 10, "timeout_seconds": 10},
"readiness_probe": {"handler": "httpGet", "port": 8080, "path": "/healthz", "period_seconds": 10, "timeout_seconds": 1},
"has_prestop": false,
"runs_as_non_root": false
}
],
"init_containers": []
}
],
"pdbs": [
{"name": "my-app", "namespace": "default", "path": "charts/my-app/templates/pdb.yaml", "render": {"chart": "charts/my-app", "values": ["values.yaml", "values-prod.yaml"]},
"selector": {"matchLabels": {"app": "my-app"}}, "min_available": 2, "max_unavailable": null}
],
"hpas": [
{"name": "my-app", "namespace": "default", "path": "charts/my-app/templates/hpa.yaml", "render": {"chart": "charts/my-app", "values": ["values.yaml", "values-prod.yaml"]},
"target_workload": "my-app", "min_replicas": 2, "max_replicas": 6}
]
}
}
Failure messages (allowed-registries with allowed_registries: "ghcr.io/acme", pod-annotations with forbidden_pod_annotations: "karpenter.sh/do-not-disrupt"):
charts/reports [values.yaml]: helm template: An error occurred while checking for chart dependencies. ... missing in charts/ directory: postgresqlcharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app container 'app' missing resource requestscharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app container 'app' should set securityContext.runAsNonRoot: truecharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app should not set spec.hostNetwork: true (workload shares the host network namespace)charts/my-app/templates/hpa.yaml [values-prod.yaml]: HPA default/my-app has minReplicas=2, need at least 3charts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app has no topologySpreadConstraint on topology.kubernetes.io/zonecharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app has no container with a preStop hookcharts/my-app/templates/pdb.yaml [values-prod.yaml]: PodDisruptionBudget default/my-app allows no voluntary disruptions: minAvailable 2 covers Deployment my-app, which runs at least 2 replicascharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app sets spec.replicas: 2 while HorizontalPodAutoscaler my-app scales itcharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app container 'app' livenessProbe and readinessProbe both check httpGet :8080/healthzcharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app container 'app' livenessProbe timeoutSeconds 10 is not below periodSeconds 10charts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app container 'app' image nginx:1.25 (docker.io/library/nginx) is not from an allowed registrycharts/my-app/templates/deployment.yaml [values-prod.yaml]: Deployment default/my-app pod template sets forbidden annotation karpenter.sh/do-not-disrupt
Remediation
When this policy fails, resolve it by:
- For
validfailures: Fix YAML syntax errors or invalid K8s fields in the manifest. For a chart that fails to render, fix the error helm reports: vendor missing dependencies into the chart'scharts/directory (or set the collector'shelm_dependency_build: true), and pointhelm_valuesat the values files the chart needs - For
requests-and-limitsfailures: Addresources.requestsandresources.limitsfor CPU and memory - For
probesfailures: AddlivenessProbeandreadinessProbeto each container - For
min-replicasfailures: Increasespec.minReplicasin your HPA to meet the threshold - For
pdbfailures: Create a PodDisruptionBudget that selects your workload's pods - For
non-rootfailures: AddsecurityContext.runAsNonRoot: trueto the container or pod spec - For
host-usersfailures: Addspec.hostUsers: falseto the PodSpec (orspec.template.spec.hostUsers: falsefor Deployments/StatefulSets/DaemonSets/Jobs/CronJobs). Requires Kubernetes ≥1.36 in the target cluster. Privileged workloads that must share the host user namespace (e.g. log shippers reading host paths, kubelets, container-runtime sidecars) can useinclude/excludeinlunar-config.ymlto opt out. - For
host-networkfailures: Removespec.hostNetwork: truefrom the PodSpec. Workloads that legitimately need the host network (CNI agents, node-local proxies, host-bound metrics exporters) can opt out viainclude/excludeinlunar-config.yml. - For
host-pidfailures: Removespec.hostPID: truefrom the PodSpec. Node-level monitoring agents that need a host-wide process view can opt out viainclude/excludeinlunar-config.yml. - For
host-ipcfailures: Removespec.hostIPC: truefrom the PodSpec. Workloads that genuinely need host IPC (rare — usually legacy shared-memory consumers) can opt out viainclude/excludeinlunar-config.yml. - For
metadata-egress-blockedfailures: Select the workload with a NetworkPolicy that hasEgressinpolicyTypes, namespace-wide (podSelector: {}) or per workload, and allow0.0.0.0/0withexcept: [169.254.169.254/32], or no egress at all. Policies are additive: any selecting policy with a rule that has noto, or anipBlockwithout theexcept, reopens TCP 80 to the endpoint. Onlynetworking.k8s.ioNetworkPolicy in this repository is read, so exclude the check where the endpoint is blocked some other way (e.g. AdminNetworkPolicy), or where pods need it by design (GKE Workload Identity serves them through it).hostNetworkworkloads are left tohost-network. - For
min-kubectl-versionfailures: Upgrade the kubectl client in your CI pipeline (e.g., pinazure/setup-kubectl@v4orsetup-kubectlaction to a newer version, or update the installed kubectl on self-hosted runners) - For
topology-spreadfailures: Add atopologySpreadConstraintsentry withtopologyKeyset totopology_key, alabelSelectormatching the pod template labels, andmaxSkewwithintopology_max_skew - For
graceful-shutdownfailures: Add alifecycle.preStophook (for examplesleep: {seconds: 5}) so endpoints are removed before the process gets SIGTERM, make the application drain on SIGTERM, and raiseterminationGracePeriodSecondsto cover the preStop delay plus the longest request - For
pdb-budgetfailures: SetmaxUnavailableto 1 (or a percentage) instead of aminAvailablethat equals the replica count, set only one of the two fields, and give the budget aselector - For
no-static-replicasfailures: Stop renderingspec.replicaswhen an autoscaler manages the workload (in a chart, wrap it in{{- if not .Values.autoscaling.enabled }}), and set the floor on the autoscaler's minimum instead - For
probes-distinctfailures: Give liveness and readiness their own endpoints: liveness should only report whether the process is stuck, readiness whether the pod can take traffic right now - For
probe-timeoutsfailures: SettimeoutSecondsexplicitly, at or abovemin_probe_timeout_secondsand belowperiodSeconds - For
allowed-registriesfailures: Pull the image from an approved registry, mirroring it there first if needed - For
deprecated-api-versionsfailures: Move the resource to its current apiVersion (kubectl convertor the Kubernetes deprecation guide lists the replacement); in a chart, also check templates that pick the apiVersion from.Capabilities - For
pod-annotationsfailures: Add the missing annotation underspec.template.metadata.annotations, or remove the forbidden one
Consumers who want any of these surfaced without blocking can pin enforcement: report-pr at config time — but that's a consumer-side knob; the checks themselves just pass or fail.
Open Source
This policy is open source and available on GitHub. Contribute improvements, report issues, or fork it for your own use.
Common Use Cases
Explore how individual guardrails work with specific integrations.
Ready to Automate Your Standards?
See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 300+ built-in guardrails.