Pods Should Be Managed By A Controller
More Info:
Verifies pods are owned by a controller (Deployment, StatefulSet, DaemonSet, Job). A naked pod is not rescheduled if its node dies.
Risk Level
Low
Address
Security
Compliance Standards
- Cloudanix Best Practice
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify naked pods (run on any machine with kubectl access):
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| select($own == null)| "\($m.namespace) \($m.name)"][]' -
For each naked pod, export its manifest (run on any machine with kubectl access):
NAMESPACE="<namespace-of-pod>"POD_NAME="<pod-name>"kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o yaml > "/tmp/${NAMESPACE}-${POD_NAME}-pod.yaml" -
Convert the pod manifest into a controller manifest (run on any machine with kubectl access; edit with your editor of choice):
- Open the exported file:
vi "/tmp/${NAMESPACE}-${POD_NAME}-pod.yaml"
- Change the top-level
kind: Podto an appropriate controller, e.g.:- For a stateless app:
kind: Deployment,apiVersion: apps/v1 - For a singleton:
kind: Deploymentwithspec.replicas: 1 - For node-wide:
kind: DaemonSet,apiVersion: apps/v1
- For a stateless app:
- Wrap the existing pod
specunder the controller spec, for example for a Deployment:apiVersion: apps/v1kind: Deploymentmetadata:name: <pod-name>namespace: <namespace-of-pod>labels:app: <app-label>spec:replicas: 1selector:matchLabels:app: <app-label>template:metadata:labels:app: <app-label>spec:# paste the original pod.spec content here - Remove fields that are not valid under a template (e.g.
status:,metadata.resourceVersion,metadata.uid,metadata.selfLink,metadata.creationTimestamp).
- Open the exported file:
-
Apply the new controller and delete the naked pod (run on any machine with kubectl access):
kubectl apply -f "/tmp/${NAMESPACE}-${POD_NAME}-pod.yaml"kubectl delete pod "$POD_NAME" -n "$NAMESPACE" -
Confirm the controller created replacement pods and workloads are healthy (run on any machine with kubectl access):
kubectl get deploy,ds,sts,job -n "$NAMESPACE" -o widekubectl get pods -n "$NAMESPACE" -o wide -
Re-run the audit to verify no remaining naked pods (run on any machine with kubectl access):
kubectl get pods --all-namespaces -o json | jq -r '[ .items[]| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)| .metadata as $m| (.spec.nodeName // "") as $node| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own| "kind=Pod ns=\($m.namespace) name=\($m.name) uid=\($m.uid) apiVersion=v1"+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)+ (if $node == "" then "" else " node=\($node)" end)+ (if $labels == "" then "" else " labels=\($labels)" end)+ (if $own == null then "" else " owner=\($own.kind)/\($m.namespace)/\($own.name)/\($own.uid)" end)+ " is_compliant=\(if $own == null then "false" else "true" end)"] as $rows| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end'
Using kubectl
On any machine with kubectl access:
- Identify naked pods (no controller ownerReference, excluding core namespaces):
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| select($own == null)
| "\($m.namespace) \($m.name)"
][]'
- For each reported pod, export its spec and remove fields that must not be in a controller template, then wrap it in a Deployment (example for
default myapp-pod):
kubectl get pod myapp-pod -n default -o json \
| jq '{
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: "myapp",
namespace: .metadata.namespace,
labels: (.metadata.labels // {})
},
spec: {
replicas: 1,
selector: { matchLabels: (.metadata.labels // { "app": "myapp" }) },
template: {
metadata: {
labels: (.metadata.labels // { "app": "myapp" })
},
spec: .spec
| del(.nodeName, .hostname, .subdomain, .affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[].matchFields)
}
}
}' > myapp-deployment.yaml
Adjust the generated myapp-deployment.yaml as needed (labels, replicas, affinity, etc.), then create the Deployment:
kubectl apply -f myapp-deployment.yaml
- Once the new controller-created pod is Running and Ready, delete the original naked pod:
kubectl delete pod myapp-pod -n default
-
Repeat steps 2–3 for every naked pod, choosing the appropriate controller type (Deployment/StatefulSet/DaemonSet/Job) and adapting the manifest kind and spec accordingly.
-
Verification (same logic as the audit):
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| "kind=Pod ns=\($m.namespace) name=\($m.name) is_compliant=\(if $own == null then "false" else "true" end)"
] as $rows
| if ($rows | map(select(. | contains("is_compliant=false"))) | length) == 0
then "is_compliant=true"
else $rows[]
end'
Automation
#!/usr/bin/env bash
#
# Remediate naked Pods by exporting them as manifests and wrapping them
# in a Deployment per namespace. Skips control-plane/system namespaces.
#
# Requirements:
# - Run on any machine with kubectl, jq, and yq installed and configured.
# - You must have permissions to list/get/create/delete Pods and Deployments.
#
# Operational notes:
# - Each naked Pod will be replaced by a Deployment with 1 replica.
# - The original Pod will be deleted after the Deployment is created.
# - This is NOT fully lossless (e.g., ephemeral container state, IPs),
# so review before running in production.
set -euo pipefail
# Namespaces to exclude from processing (system namespaces)
EXCLUDED_NAMESPACES=("kube-system" "kube-public" "kube-node-lease")
# Utility: check if namespace is excluded
is_excluded_ns() {
local ns="$1"
for e in "${EXCLUDED_NAMESPACES[@]}"; do
if [[ "$ns" == "$e" ]]; then
return 0
fi
done
return 1
}
# Ensure required tools are present
for bin in kubectl jq yq; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: $bin is required but not installed or not in PATH" >&2
exit 1
fi
done
# Get all naked Pods (no controller ownerReference) outside excluded namespaces
mapfile -t NAKED_PODS < <(
kubectl get pods --all-namespaces -o json \
| jq -r '
.items[]
| select(.metadata.namespace as $n
| ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| . as $pod
| (($pod.metadata.ownerReferences // []) | map(select(.controller == true)) | length) as $owns
| select($owns == 0)
| "\($pod.metadata.namespace) \($pod.metadata.name)"
'
)
if [[ "${#NAKED_PODS[@]}" -eq 0 ]]; then
echo "No naked Pods found outside excluded namespaces. Nothing to do."
else
echo "Found ${#NAKED_PODS[@]} naked Pod(s) to process:"
printf ' %s\n' "${NAKED_PODS[@]}"
fi
for line in "${NAKED_PODS[@]}"; do
ns="$(awk '{print $1}' <<<"$line")"
pod="$(awk '{print $2}' <<<"$line")"
if is_excluded_ns "$ns"; then
echo "Skipping Pod $ns/$pod (excluded namespace)"
continue
fi
echo "Processing naked Pod $ns/$pod"
# Export Pod manifest
tmp_pod_yaml="$(mktemp)"
kubectl get pod "$pod" -n "$ns" -o yaml > "$tmp_pod_yaml"
# Basic safety: ensure this Pod still has no controller owner
if kubectl get pod "$pod" -n "$ns" -o json \
| jq -e '((.metadata.ownerReferences // []) | map(select(.controller == true)) | length) != 0' \
>/dev/null 2>&1; then
echo " Pod $ns/$pod now has a controller. Skipping."
rm -f "$tmp_pod_yaml"
continue
fi
# Construct Deployment name (same as Pod name by default)
deploy_name="$pod"
# Build Deployment manifest using yq
tmp_deploy_yaml="$(mktemp)"
yq eval '
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": .metadata.name,
"namespace": .metadata.namespace,
"labels": (.metadata.labels // {})
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": (
if (.metadata.labels // {} | length) > 0 then
.metadata.labels
else
{"app": .metadata.name}
end
)
},
"template": {
"metadata": {
"labels": (
if (.metadata.labels // {} | length) > 0 then
.metadata.labels
else
{"app": .metadata.name}
end
)
},
"spec": .spec
}
}
}
# Clean up pod-specific fields from the template spec
| del(.spec.template.spec["nodeName","hostIP","phase","podIP","podIPs","status"])
' "$tmp_pod_yaml" > "$tmp_deploy_yaml"
echo " Applying Deployment $ns/$deploy_name"
kubectl apply -n "$ns" -f "$tmp_deploy_yaml"
# Wait briefly for Deployment to create a Pod
echo " Waiting for a Pod from Deployment $ns/$deploy_name"
kubectl rollout status deployment/"$deploy_name" -n "$ns" --timeout=120s || {
echo " WARNING: Deployment $ns/$deploy_name did not become ready in time. Skipping Pod deletion."
rm -f "$tmp_pod_yaml" "$tmp_deploy_yaml"
continue
}
# Delete original naked Pod
echo " Deleting original naked Pod $ns/$pod"
kubectl delete pod "$pod" -n "$ns" --ignore-not-found=true
rm -f "$tmp_pod_yaml" "$tmp_deploy_yaml"
done
echo
echo "Verification: re-running compliance check for naked Pods"
kubectl get pods --all-namespaces -o json | jq -r '
[ .items[]
| select(.metadata.namespace as $n | ["kube-system","kube-public","kube-node-lease"] | index($n) | not)
| .metadata as $m
| ([ ($m.ownerReferences // [])[] | select(.controller) ] | first) as $own
| select($own == null)
| "kind=Pod ns=\($m.namespace) name=\($m.name) is_compliant=false"
] as $rows
| if ($rows | length) == 0 then "is_compliant=true" else $rows[] end
'