Skip to main content

No Workloads Should Run In The default Namespace

More Info:​

Verifies the default namespace has no workloads so RBAC, quotas and NetworkPolicies can be scoped per tenant.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • Cloudanix Best Practice

Triage and Remediation​

Remediation​

Manual Steps
  1. List all workloads in the default namespace (run on any machine with kubectl access):

    kubectl get all -n default
  2. For each workload type in default, export its manifest to a file (replace placeholders with actual names from step 1; run on any machine with kubectl access):

    Deployments:

    kubectl get deployment <deployment-name> -n default -o yaml > /tmp/deployment-<deployment-name>.yaml

    StatefulSets:

    kubectl get statefulset <statefulset-name> -n default -o yaml > /tmp/statefulset-<statefulset-name>.yaml

    DaemonSets:

    kubectl get daemonset <daemonset-name> -n default -o yaml > /tmp/daemonset-<daemonset-name>.yaml

    Jobs/CronJobs:

    kubectl get job <job-name> -n default -o yaml > /tmp/job-<job-name>.yaml
    kubectl get cronjob <cronjob-name> -n default -o yaml > /tmp/cronjob-<cronjob-name>.yaml
  3. Edit each exported manifest to target a purpose-specific namespace (run on any machine with kubectl access):

    sed -i 's/namespace: default/namespace: <target-namespace>/' /tmp/deployment-<deployment-name>.yaml

    If the metadata.namespace field is missing, add it under metadata::

    metadata:
    name: <deployment-name>
    namespace: <target-namespace>

    Repeat for each manifest. Ensure the <target-namespace> already exists, or create it:

    kubectl create namespace <target-namespace>
  4. Apply the modified manifests into the new namespace (run on any machine with kubectl access):

    kubectl apply -f /tmp/deployment-<deployment-name>.yaml
    kubectl apply -f /tmp/statefulset-<statefulset-name>.yaml
    kubectl apply -f /tmp/daemonset-<daemonset-name>.yaml
    kubectl apply -f /tmp/job-<job-name>.yaml
    kubectl apply -f /tmp/cronjob-<cronjob-name>.yaml
  5. After confirming the workloads are running correctly in the new namespace, delete the originals from default (run on any machine with kubectl access):

    kubectl delete deployment <deployment-name> -n default
    kubectl delete statefulset <statefulset-name> -n default
    kubectl delete daemonset <daemonset-name> -n default
    kubectl delete job <job-name> -n default
    kubectl delete cronjob <cronjob-name> -n default
  6. Verification (derived from the audit command; run on any machine with kubectl access):

    { kubectl get pods -n default -o json
    kubectl get namespace default -o json
    } | jq -rs '
    .[0] as $pods | .[1] |
    .metadata as $m
    | (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
    | (($pods.items // []) | length) as $count
    | "kind=Namespace name=default uid=\($m.uid) apiVersion=v1"
    + (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
    + (if $labels == "" then "" else " labels=\($labels)" end)
    + " podCount=\($count)"
    + " is_compliant=\(if $count == 0 then "true" else "false" end)"'

    Confirm that podCount=0 and is_compliant=true.

Using kubectl

On any machine with kubectl access:

  1. Identify all workload types in the default namespace
kubectl get all -n default
kubectl get configmap,secret,serviceaccount,role,rolebinding -n default
  1. Choose or create a purpose-specific namespace (example: team-a)
kubectl create namespace team-a
  1. Export existing workloads from default and edit their namespace
kubectl get deploy,sts,ds,job,cronjob,svc,ingress \
-n default -o yaml > /tmp/default-workloads.yaml

Edit /tmp/default-workloads.yaml:

  • For every object, set:
metadata:
namespace: team-a
  • Remove runtime-only fields under metadata such as:
    • creationTimestamp
    • resourceVersion
    • uid
    • annotations that are managed by controllers (e.g. deployment.kubernetes.io/revision)
    • generation
  • Remove status sections:
status: {}
# or delete the entire status: block
  1. Apply workloads into the new namespace
kubectl apply -f /tmp/default-workloads.yaml
  1. Delete old workloads from the default namespace

Be careful to delete only what you intentionally moved; do not delete the kubernetes Service.

kubectl delete deploy,sts,ds,job,cronjob,ingress \
--all -n default

# Optionally, delete non-core Services (leave the "kubernetes" ClusterIP service)
kubectl get svc -n default
kubectl delete svc <service-name-1> <service-name-2> -n default
  1. Verify no pods remain in the default namespace (benchmark audit)
{ kubectl get pods -n default -o json
kubectl get namespace default -o json
} | jq -rs '
.[0] as $pods | .[1] |
.metadata as $m
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| (($pods.items // []) | length) as $count
| "kind=Namespace name=default uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ " podCount=\($count)"
+ " is_compliant=\(if $count == 0 then "true" else "false" end)"'
Automation
#!/usr/bin/env bash
set -euo pipefail

# This script:
# - Finds all workloads in the "default" namespace
# - Creates a dedicated namespace per workload owner (Deployment/StatefulSet/Job/CronJob/Pod)
# - Moves the workloads into those namespaces
# - Is idempotent and safe to re-run
#
# Run from any machine with kubectl access to the EKS cluster.

TARGET_NAMESPACE_PREFIX="team" # used only when no better name can be inferred

# Require kubectl
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl not found in PATH" >&2
exit 1
fi

# Check access
if ! kubectl get ns default >/dev/null 2>&1; then
echo "Cannot access the cluster or 'default' namespace" >&2
exit 1
fi

timestamp() { date -u +"%Y%m%d-%H%M%S"; }

# Create namespace if it does not exist
ensure_namespace() {
local ns="$1"
if ! kubectl get namespace "${ns}" >/dev/null 2>&1; then
echo "Creating namespace ${ns}"
kubectl create namespace "${ns}"
fi
}

# Derive a target namespace from labels/owner; fall back to ${TARGET_NAMESPACE_PREFIX}-<name>
derive_namespace() {
local kind="$1" name="$2"
local ns

# Try using a "team" label if present
ns="$(kubectl get "${kind}" "${name}" -n default -o jsonpath='{.metadata.labels.team}' 2>/dev/null || true)"
if [[ -n "${ns}" ]]; then
echo "${ns}"
return
fi

# Try using an "app" label
ns="$(kubectl get "${kind}" "${name}" -n default -o jsonpath='{.metadata.labels.app}' 2>/dev/null || true)"
if [[ -n "${ns}" ]]; then
echo "${ns}"
return
fi

# Fallback
echo "${TARGET_NAMESPACE_PREFIX}-${name}"
}

# Move a workload object to a namespace:
# - Export manifest
# - Change metadata.namespace
# - Apply to target namespace
# - Delete original object from default
move_object() {
local kind="$1" name="$2" src_ns="$3" dst_ns="$4"

if [[ "${src_ns}" == "${dst_ns}" ]]; then
echo "Skipping ${kind}/${name}; already in namespace ${src_ns}"
return
fi

echo "Moving ${kind}/${name} from ${src_ns} to ${dst_ns}"

ensure_namespace "${dst_ns}"

# Export object without status, then modify namespace and apply
tmpfile="$(mktemp)"
kubectl get "${kind}" "${name}" -n "${src_ns}" -o yaml \
| sed '/^status:/,$d' \
| sed "s/^ namespace: ${src_ns}$/ namespace: ${dst_ns}/" \
> "${tmpfile}"

# If no explicit namespace field, inject it
if ! grep -q '^ namespace:' "${tmpfile}"; then
sed -i "0,/^metadata:$/s//metadata:\n namespace: ${dst_ns}/" "${tmpfile}"
fi

kubectl apply -f "${tmpfile}"
kubectl delete "${kind}" "${name}" -n "${src_ns}" --cascade=orphan --wait=true
rm -f "${tmpfile}"
}

# 1. Handle controllers (Deployments, StatefulSets, DaemonSets, Jobs, CronJobs)
echo "Discovering controllers in 'default' namespace..."

declare -A SEEN_OWNERS=()

for kind in deployment statefulset daemonset job cronjob; do
items="$(kubectl get "${kind}" -n default -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)"
if [[ -z "${items}" ]]; then
continue
fi
while read -r name; do
[[ -z "${name}" ]] && continue
key="${kind}/${name}"
if [[ -n "${SEEN_OWNERS[${key}]:-}" ]]; then
continue
fi
SEEN_OWNERS[${key}]=1
dst_ns="$(derive_namespace "${kind}" "${name}")"
move_object "${kind}" "${name}" default "${dst_ns}"
done <<< "${items}"
done

# 2. Handle standalone Pods (no controller owner)
echo "Discovering standalone Pods in 'default' namespace..."

pods_json="$(kubectl get pods -n default -o json 2>/dev/null || echo '{}')"
pod_names="$(echo "${pods_json}" | jq -r '.items[] | select(.metadata.ownerReferences == null or .metadata.ownerReferences | length == 0) | .metadata.name' 2>/dev/null || true)"

if [[ -n "${pod_names}" ]]; then
while read -r pod; do
[[ -z "${pod}" ]] && continue
dst_ns="$(derive_namespace pod "${pod}")"

echo "Moving standalone Pod/${pod} from default to ${dst_ns}"
ensure_namespace "${dst_ns}"

tmpfile="$(mktemp)"
kubectl get pod "${pod}" -n default -o yaml \
| sed '/^status:/,$d' \
| sed "s/^ namespace: default$/ namespace: ${dst_ns}/" \
> "${tmpfile}"

if ! grep -q '^ namespace:' "${tmpfile}"; then
sed -i "0,/^metadata:$/s//metadata:\n namespace: ${dst_ns}/" "${tmpfile}"
fi

kubectl apply -f "${tmpfile}"
kubectl delete pod "${pod}" -n default --wait=true
rm -f "${tmpfile}"
done <<< "${pod_names}"
fi

# 3. Verification (same logic as the audit)
echo "Verifying that no Pods remain in 'default' namespace..."

verify_json="$(
{ kubectl get pods -n default -o json 2>/dev/null || echo '{"items":[]}';
kubectl get namespace default -o json;
} | jq -rs '
.[0] as $pods | .[1] |
.metadata as $m
| (($m.labels // {}) | to_entries | map("\(.key):\(.value)") | join(",")) as $labels
| (($pods.items // []) | length) as $count
| "kind=Namespace name=default uid=\($m.uid) apiVersion=v1"
+ (if ($m.creationTimestamp // "") == "" then "" else " created=\($m.creationTimestamp)" end)
+ (if $labels == "" then "" else " labels=\($labels)" end)
+ " podCount=\($count)"
+ " is_compliant=\(if $count == 0 then "true" else "false" end)"'
)"

echo "${verify_json}"

if echo "${verify_json}" | grep -q 'is_compliant=true'; then
echo "Success: no workloads are running in the 'default' namespace."
else
echo "Warning: some workloads still exist in the 'default' namespace; manual review required." >&2
exit 2
fi