> ## Documentation Index
> Fetch the complete documentation index at: https://cloudanix.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

<Tabs>
  <Tab title="Remediation">
    ### Remediation

    <AccordionGroup>
      <Accordion title="Manual Steps" defaultOpen="true">
        1. Identify naked pods (run on any machine with kubectl access):
           ```bash theme={null}
           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)"
             ][]'
           ```

        2. For each naked pod, export its manifest (run on any machine with kubectl access):
           ```bash theme={null}
           NAMESPACE="<namespace-of-pod>"
           POD_NAME="<pod-name>"

           kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o yaml > "/tmp/${NAMESPACE}-${POD_NAME}-pod.yaml"
           ```

        3. 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:
             ```bash theme={null}
             vi "/tmp/${NAMESPACE}-${POD_NAME}-pod.yaml"
             ```
           * Change the top-level `kind: Pod` to an appropriate controller, e.g.:
             * For a stateless app: `kind: Deployment`, `apiVersion: apps/v1`
             * For a singleton: `kind: Deployment` with `spec.replicas: 1`
             * For node-wide: `kind: DaemonSet`, `apiVersion: apps/v1`
           * Wrap the existing pod `spec` under the controller spec, for example for a Deployment:
             ```yaml theme={null}
             apiVersion: apps/v1
             kind: Deployment
             metadata:
               name: <pod-name>
               namespace: <namespace-of-pod>
               labels:
                 app: <app-label>
             spec:
               replicas: 1
               selector:
                 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`).

        4. Apply the new controller and delete the naked pod (run on any machine with kubectl access):
           ```bash theme={null}
           kubectl apply -f "/tmp/${NAMESPACE}-${POD_NAME}-pod.yaml"

           kubectl delete pod "$POD_NAME" -n "$NAMESPACE"
           ```

        5. Confirm the controller created replacement pods and workloads are healthy (run on any machine with kubectl access):
           ```bash theme={null}
           kubectl get deploy,ds,sts,job -n "$NAMESPACE" -o wide
           kubectl get pods -n "$NAMESPACE" -o wide
           ```

        6. Re-run the audit to verify no remaining naked pods (run on any machine with kubectl access):
           ```bash theme={null}
           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'
           ```
      </Accordion>

      <Accordion title="Using kubectl">
        On any machine with `kubectl` access:

        1. Identify naked pods (no controller ownerReference, excluding core namespaces):

        ```bash theme={null}
        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)"
          ][]'
        ```

        2. 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`):

        ```bash theme={null}
        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:

        ```bash theme={null}
        kubectl apply -f myapp-deployment.yaml
        ```

        3. Once the new controller-created pod is Running and Ready, delete the original naked pod:

        ```bash theme={null}
        kubectl delete pod myapp-pod -n default
        ```

        4. 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.

        5. Verification (same logic as the audit):

        ```bash theme={null}
        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'
        ```
      </Accordion>

      <Accordion title="Automation">
        ```bash theme={null}
        #!/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
        '
        ```
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>
