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 manifests to files so you can recreate them in a new namespace (replace WORKLOAD and NAME accordingly; run on any machine with kubectl access):

    # Example for a Deployment
    kubectl get deployment NAME -n default -o yaml > NAME-deploy.yaml

    # Example for a StatefulSet
    kubectl get statefulset NAME -n default -o yaml > NAME-sts.yaml

    # Example for a DaemonSet
    kubectl get daemonset NAME -n default -o yaml > NAME-ds.yaml

    # Example for a Job
    kubectl get job NAME -n default -o yaml > NAME-job.yaml

    # Example for a CronJob
    kubectl get cronjob NAME -n default -o yaml > NAME-cronjob.yaml

    # Example for a Service
    kubectl get service NAME -n default -o yaml > NAME-svc.yaml
  3. Edit each exported manifest file to set a purpose-specific namespace and remove default-assigned fields (run on any machine with kubectl access):

    • In each YAML file, under metadata, set:
      namespace: my-tenant-namespace
    • Remove the following fields if present to avoid conflicts when recreating:
      • metadata: { uid, resourceVersion, selfLink, creationTimestamp, managedFields, ownerReferences }
      • status sections
    • Save the edited files.
  4. Create the new namespace if it does not already exist (run on any machine with kubectl access):

    kubectl create namespace my-tenant-namespace
  5. Recreate workloads in the new namespace, then delete them from default (run on any machine with kubectl access):

    # Apply the edited manifests in the new namespace
    kubectl apply -f NAME-deploy.yaml
    kubectl apply -f NAME-sts.yaml
    kubectl apply -f NAME-ds.yaml
    kubectl apply -f NAME-job.yaml
    kubectl apply -f NAME-cronjob.yaml
    kubectl apply -f NAME-svc.yaml

    # After confirming the workloads are running correctly in the new namespace,
    # delete the old workloads from the default namespace
    kubectl delete deployment NAME -n default
    kubectl delete statefulset NAME -n default
    kubectl delete daemonset NAME -n default
    kubectl delete job NAME -n default
    kubectl delete cronjob NAME -n default
    kubectl delete service NAME -n default
  6. Verification (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 workloads in the default namespace
kubectl get all -n default
  1. For each workload type, export its manifest from default and save it to a file, then edit the namespace field.

Example for a deployment named my-app:

# Export existing manifest
kubectl get deployment my-app -n default -o yaml > my-app-deploy.yaml

# Edit the manifest: in metadata, set
# namespace: my-tenant-namespace
# If no namespace field exists under metadata, add:
# namespace: my-tenant-namespace
#
# Also review and update any other namespaced references (ConfigMaps, Services, RBAC, etc.)
  1. Create the target namespace if it does not already exist:
kubectl create namespace my-tenant-namespace
  1. Apply the updated manifest into the new namespace:
kubectl apply -f my-app-deploy.yaml
  1. Once you have recreated all needed workloads in their new, purpose-specific namespaces and confirmed they are running correctly, delete the originals from the default namespace.

Examples by resource type:

# Deployments
kubectl delete deployment my-app -n default

# StatefulSets
kubectl delete statefulset my-stateful-app -n default

# DaemonSets
kubectl delete daemonset my-daemon -n default

# CronJobs
kubectl delete cronjob my-cronjob -n default

# Jobs (if still present)
kubectl delete job my-job -n default

# Services, ConfigMaps, Secrets, etc., that were only for these workloads
kubectl delete service my-app -n default
kubectl delete configmap my-app-config -n default
kubectl delete secret my-app-secret -n default

Repeat this export–edit–apply–delete process for every workload that currently runs in the default namespace, moving each into an appropriate purpose-specific namespace.

  1. Verification (pod count in default should be zero):
kubectl get pods -n default
Automation
#!/usr/bin/env bash
set -euo pipefail

# This script:
# - Finds all workload resources (Pods, Deployments, ReplicaSets, StatefulSets,
# DaemonSets, Jobs, CronJobs) in the "default" namespace.
# - For each, creates a copy in a target namespace and deletes the original.
# - Verifies that the "default" namespace has no Pods remaining.
#
# REQUIREMENTS:
# - Run on any machine with kubectl access and permissions to list/get/create/delete
# resources cluster-wide.
# - kubectl must be configured to point at the target GKE cluster.
#
# USAGE:
# ./move-default-workloads.sh <target-namespace>
#
# The target namespace MUST exist beforehand and be prepared with appropriate
# RBAC, ResourceQuotas, and NetworkPolicies.

if [[ $# -ne 1 ]]; then
echo "Usage: $0 <target-namespace>" >&2
exit 1
fi

TARGET_NS="$1"

echo "==> Verifying target namespace '${TARGET_NS}' exists"
if ! kubectl get namespace "${TARGET_NS}" >/dev/null 2>&1; then
echo "ERROR: target namespace '${TARGET_NS}' does not exist." >&2
echo "Create it first, for example:" >&2
echo " kubectl create namespace ${TARGET_NS}" >&2
exit 1
fi

echo "==> Checking for workloads in the 'default' namespace"
# If there are no pods, we consider the check already compliant and exit early.
POD_COUNT="$(kubectl get pods -n default --no-headers 2>/dev/null | wc -l | tr -d ' ')"
if [[ "${POD_COUNT}" -eq 0 ]]; then
echo "No pods found in 'default' namespace. Nothing to move."
echo "Cluster is already compliant with respect to this control."
exit 0
fi

echo "Workloads found in 'default' namespace. Beginning migration to '${TARGET_NS}'."

# Resource types to migrate.
# These cover the common workload controllers; adjust if you use additional kinds.
RESOURCE_KINDS=(
"deployment.apps"
"replicaset.apps"
"statefulset.apps"
"daemonset.apps"
"job.batch"
"cronjob.batch"
"pod" # standalone pods not managed by controllers
)

for KIND in "${RESOURCE_KINDS[@]}"; do
echo "==> Processing kind: ${KIND}"
# List resource names; ignore "No resources found" errors.
MAPFILE -t RESOURCES < <(kubectl get "${KIND}" -n default -o name 2>/dev/null || true)
if [[ "${#RESOURCES[@]}" -eq 0 ]]; then
echo " No ${KIND} resources in 'default' namespace."
continue
fi

for RES in "${RESOURCES[@]}"; do
NAME="${RES#*/}" # strip the kind prefix, e.g. deployment.apps/my-deploy -> my-deploy

echo " -> Migrating ${KIND} '${NAME}' from 'default' to '${TARGET_NS}'"

# Check if it already exists in the target namespace (idempotency).
if kubectl get "${KIND}" -n "${TARGET_NS}" "${NAME}" >/dev/null 2>&1; then
echo " Target '${KIND}/${NAME}' already exists in '${TARGET_NS}'."
echo " Skipping creation; will only ensure original is removed."
else
# Export the object from 'default', adjust namespace, and recreate in target namespace.
# Strip fields that should not be migrated (status, resourceVersion, uid, etc.).
echo " Creating '${KIND}/${NAME}' in '${TARGET_NS}'"
kubectl get "${KIND}" -n default "${NAME}" -o json \
| jq '
del(
.metadata.uid,
.metadata.resourceVersion,
.metadata.selfLink,
.metadata.creationTimestamp,
.metadata.generation,
.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration",
.status
)
| .metadata.namespace = "'"${TARGET_NS}"'"
' \
| kubectl apply -f -
fi

# Delete original from default namespace (idempotent: delete succeeds even if already gone).
echo " Deleting original '${KIND}/${NAME}' from 'default'"
kubectl delete "${KIND}" -n default "${NAME}" --ignore-not-found=true
done
done

echo "==> Waiting for any terminating pods in 'default' to fully disappear"
# Wait loop: break when no pods remain.
for i in {1..30}; do
REMAINING="$(kubectl get pods -n default --no-headers 2>/dev/null | wc -l | tr -d ' ')"
if [[ "${REMAINING}" -eq 0 ]]; then
break
fi
echo " ${REMAINING} pod(s) still present in 'default'; rechecking in 10s..."
sleep 10
done

echo "==> Final verification (matches benchmark audit style)"
{ 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)"'

echo "==> Migration complete."