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
Remediation​
Manual Steps
-
List all workloads in the
defaultnamespace (run on any machine with kubectl access):kubectl get all -n default -
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>.yamlStatefulSets:
kubectl get statefulset <statefulset-name> -n default -o yaml > /tmp/statefulset-<statefulset-name>.yamlDaemonSets:
kubectl get daemonset <daemonset-name> -n default -o yaml > /tmp/daemonset-<daemonset-name>.yamlJobs/CronJobs:
kubectl get job <job-name> -n default -o yaml > /tmp/job-<job-name>.yamlkubectl get cronjob <cronjob-name> -n default -o yaml > /tmp/cronjob-<cronjob-name>.yaml -
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>.yamlIf the
metadata.namespacefield is missing, add it undermetadata::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> -
Apply the modified manifests into the new namespace (run on any machine with kubectl access):
kubectl apply -f /tmp/deployment-<deployment-name>.yamlkubectl apply -f /tmp/statefulset-<statefulset-name>.yamlkubectl apply -f /tmp/daemonset-<daemonset-name>.yamlkubectl apply -f /tmp/job-<job-name>.yamlkubectl apply -f /tmp/cronjob-<cronjob-name>.yaml -
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 defaultkubectl delete statefulset <statefulset-name> -n defaultkubectl delete daemonset <daemonset-name> -n defaultkubectl delete job <job-name> -n defaultkubectl delete cronjob <cronjob-name> -n default -
Verification (derived from the audit command; run on any machine with kubectl access):
{ kubectl get pods -n default -o jsonkubectl 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=0andis_compliant=true.
Using kubectl
On any machine with kubectl access:
- Identify all workload types in the
defaultnamespace
kubectl get all -n default
kubectl get configmap,secret,serviceaccount,role,rolebinding -n default
- Choose or create a purpose-specific namespace (example:
team-a)
kubectl create namespace team-a
- Export existing workloads from
defaultand 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
metadatasuch as:creationTimestampresourceVersionuidannotationsthat are managed by controllers (e.g.deployment.kubernetes.io/revision)generation
- Remove status sections:
status: {}
# or delete the entire status: block
- Apply workloads into the new namespace
kubectl apply -f /tmp/default-workloads.yaml
- Delete old workloads from the
defaultnamespace
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
- Verify no pods remain in the
defaultnamespace (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