As organizations accelerate their adoption of cloud-native infrastructure, Kubernetes has become the de facto standard for container orchestration. But with that ubiquity comes a critical responsibility: securing workloads running at scale. While Kubernetes ships with a strong set of built-in security primitives, the real-world security posture of a cluster often depends on how well those primitives are applied — and automated.

Shell scripting is one of the most practical tools in a platform engineer’s security toolkit. In this post, we’ll walk through how to use Bash (and compare it with PowerShell) to enforce container security policies, automate compliance checks, and harden your Kubernetes environment.

Native

Why Shell Scripting for Kubernetes Security?

Kubernetes provides several native security mechanisms out of the box:

  • Network Policies – Restrict pod-to-pod and pod-to-external traffic
  • Pod Security Admission (PSA) – Enforce security standards at the namespace level
  • RBAC – Control who can do what within the cluster
  • Secrets Management – Store and inject sensitive data securely

However, these tools are building blocks — not a complete security solution. Organizations often need custom enforcement logic that reflects their specific threat models, compliance requirements (PCI-DSS, SOC 2, HIPAA), or internal platform standards.

Shell scripting bridges that gap by allowing you to:

  • Automate repetitive security audits across namespaces and clusters
  • Enforce policies declaratively without deploying additional tooling
  • Reduce human error through consistent, testable scripts
  • Integrate with CI/CD pipelines to catch misconfigurations before they reach production

Prerequisites

Before diving in, make sure you have the following:

  • A working Kubernetes cluster (local via minikube/kind or remote)
  • kubectl installed and configured with appropriate permissions
  • Basic familiarity with Bash scripting
  • jq installed for JSON parsing (apt install jq or brew install jq)
  • (Optional) PowerShell 7+ for cross-platform PowerShell scripts

Bash Scripts for Kubernetes Security

1. Enforce the Latest Container Image Version

Running outdated container images is one of the most common sources of known vulnerabilities. The script below checks all pods in a given namespace and updates any container that isn’t using the latest tagged version from Docker Hub.

#!/bin/bash
# enforce-latest-image.sh
# Ensures all pods in a namespace use the latest tag of a specified image.

set -euo pipefail

NAMESPACE="${1:-default}"
IMAGE="${2:-nginx}"

echo "[*] Fetching latest tag for image: $IMAGE"
LATEST_TAG=$(curl -s "https://registry.hub.docker.com/v1/repositories/${IMAGE}/tags" \
  | jq -r 'first(.[].name)')

if [[ -z "$LATEST_TAG" ]]; then
  echo "[!] Could not retrieve latest tag for $IMAGE. Exiting."
  exit 1
fi

echo "[*] Latest tag: $LATEST_TAG"

for POD in $(kubectl get pods -n "$NAMESPACE" \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do

  CONTAINER_IMAGE=$(kubectl get pod "$POD" -n "$NAMESPACE" \
    -o jsonpath="{.spec.containers[?(@.name=='${IMAGE}')].image}")

  if [[ "$CONTAINER_IMAGE" != "${IMAGE}:${LATEST_TAG}" ]]; then
    echo "[~] Updating $POD: $CONTAINER_IMAGE -> ${IMAGE}:${LATEST_TAG}"
    kubectl set image "pod/$POD" "${IMAGE}=${IMAGE}:${LATEST_TAG}" -n "$NAMESPACE"
  else
    echo "[✓] $POD is already up to date"
  fi
done

Note: In production, prefer using an image digest (e.g., nginx@sha256:abc123...) instead of a mutable tag like latest. Tags can be overwritten; digests are immutable.


2. Detect Containers Running as Root

Containers running as the root user (UID 0) represent a significant security risk. If an attacker escapes the container, they may inherit root privileges on the host node. The following script audits all running pods and flags any container that lacks a runAsNonRoot: true policy.

#!/bin/bash
# detect-root-containers.sh
# Flags containers that may be running as root.

set -euo pipefail

NAMESPACE="${1:-default}"
FOUND=0

echo "[*] Scanning namespace: $NAMESPACE"

for POD in $(kubectl get pods -n "$NAMESPACE" \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do

  CONTAINERS=$(kubectl get pod "$POD" -n "$NAMESPACE" \
    -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{end}')

  for CONTAINER in $CONTAINERS; do
    RUN_AS_NON_ROOT=$(kubectl get pod "$POD" -n "$NAMESPACE" \
      -o jsonpath="{.spec.containers[?(@.name=='${CONTAINER}')].securityContext.runAsNonRoot}")
    RUN_AS_USER=$(kubectl get pod "$POD" -n "$NAMESPACE" \
      -o jsonpath="{.spec.containers[?(@.name=='${CONTAINER}')].securityContext.runAsUser}")

    if [[ "$RUN_AS_NON_ROOT" != "true" ]] || [[ "$RUN_AS_USER" == "0" ]]; then
      echo "[!] ALERT: Container '$CONTAINER' in pod '$POD' may be running as root"
      FOUND=$((FOUND + 1))
    fi
  done
done

echo ""
echo "[*] Scan complete. $FOUND potential root container(s) found."

To remediate flagged containers, add a securityContext block to your pod spec:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  allowPrivilegeEscalation: false

3. Enforce Pod Security Annotations

While Pod Security Admission (PSA) is the modern approach, many clusters still rely on annotations for compatibility with older tooling. This script ensures all pods in a namespace carry the required seccomp profile annotation.

#!/bin/bash
# enforce-pod-security-policy.sh
# Annotates pods missing a required seccomp security policy.

set -euo pipefail

NAMESPACE="${1:-default}"
POLICY="${2:-runtime/default}"
ANNOTATION_KEY="seccomp.security.alpha.kubernetes.io/pod"

echo "[*] Enforcing seccomp policy '$POLICY' in namespace '$NAMESPACE'"

for POD in $(kubectl get pods -n "$NAMESPACE" \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'); do

  CURRENT_POLICY=$(kubectl get pod "$POD" -n "$NAMESPACE" \
    -o jsonpath="{.metadata.annotations['${ANNOTATION_KEY}']}" 2>/dev/null || echo "")

  if [[ "$CURRENT_POLICY" != "$POLICY" ]]; then
    echo "[~] Annotating pod '$POD': $ANNOTATION_KEY=$POLICY"
    kubectl annotate pod "$POD" -n "$NAMESPACE" \
      "${ANNOTATION_KEY}=${POLICY}" --overwrite
  else
    echo "[✓] Pod '$POD' already has correct policy"
  fi
done

Heads up: The seccomp.security.alpha.kubernetes.io annotation is deprecated in Kubernetes v1.27+. For modern clusters, use the securityContext.seccompProfile field in your pod spec instead.


4. Audit Pods with Privileged Containers

Privileged containers can access all devices on the host and bypass most security mechanisms. This script lists any pod running a privileged container.

#!/bin/bash
# audit-privileged-containers.sh
# Lists any containers running in privileged mode.

set -euo pipefail

NAMESPACE="${1:---all-namespaces}"
NS_FLAG="-n $NAMESPACE"
[[ "$NAMESPACE" == "--all-namespaces" ]] && NS_FLAG="--all-namespaces"

echo "[*] Auditing privileged containers ($NAMESPACE)..."

kubectl get pods $NS_FLAG -o json | jq -r '
  .items[] |
  .metadata.namespace as $ns |
  .metadata.name as $pod |
  .spec.containers[] |
  select(.securityContext.privileged == true) |
  "[!] PRIVILEGED: \($ns)/\($pod) -> container: \(.name)"
'

Comparing Bash and PowerShell for Kubernetes Security

Bash is the default choice in Linux and macOS environments — and since most Kubernetes nodes run Linux, it’s usually the natural fit. But PowerShell has become a compelling option, especially for teams with a Windows-heavy background or mixed OS environments.

Key Differences at a Glance

Feature Bash PowerShell
Default OS Linux / macOS Windows (also available on Linux/macOS)
Output model Text streams Structured objects (.NET)
JSON handling Requires jq Native via ConvertFrom-Json
Kubernetes modules kubectl only kubectl + PSKubernetes module
Learning curve Unix-familiar Windows/OOP-familiar
CI/CD support Universal Good, growing

PowerShell Example: List All Pods in a Namespace

# list-pods.ps1
$Namespace = "default"
$AllPods = kubectl get pods -n $Namespace -o json | ConvertFrom-Json

foreach ($Pod in $AllPods.items) {
    Write-Host "$($Pod.metadata.namespace)/$($Pod.metadata.name)"
}

Because PowerShell works natively with objects, you skip the jq dependency entirely and can chain property access naturally.


PowerShell Example: Enforce Pod Security Policy via PSKubernetes

The PSKubernetes module wraps kubectl with idiomatic PowerShell cmdlets:

# enforce-security-policy.ps1
Import-Module PSKubernetes

$Namespace = "default"
$Policy    = "runtime/default"
$AnnotationKey = "seccomp.security.alpha.kubernetes.io/pod"

$PodList = Get-KubernetesPod -Namespace $Namespace

foreach ($Pod in $PodList) {
    $CurrentPolicy = $Pod.metadata.annotations.$AnnotationKey

    if ($CurrentPolicy -ne $Policy) {
        Write-Host "[~] Updating $($Pod.metadata.name)"
        Set-KubernetesPod -Namespace $Namespace -Name $Pod.metadata.name `
            -Annotation @{ $AnnotationKey = $Policy }
    } else {
        Write-Host "[✓] $($Pod.metadata.name) already compliant"
    }
}

When to Choose Which

Choose Bash when:

  • Your infrastructure runs on Linux-based nodes (most clusters)
  • You’re integrating with Unix-native CI tools (GitHub Actions, GitLab CI, Jenkins)
  • Your team has strong Unix/Linux fluency
  • You want minimal external dependencies

Choose PowerShell when:

  • Your team comes from a Windows/Azure background
  • You need to integrate Kubernetes security with Windows-based tooling
  • You’re managing hybrid Windows/Linux node pools
  • You want native object-oriented scripting without jq

Both languages are capable. The best choice is typically the one your team will actually maintain.


Security Best Practices When Writing These Scripts

Regardless of which shell you choose, keep the following practices in mind:

  1. Use set -euo pipefail in Bash — This ensures your script exits on any error and treats unset variables as failures.
  2. Avoid hardcoded credentials — Use Kubernetes ServiceAccounts with minimal RBAC permissions instead of embedding kubeconfig credentials in scripts.
  3. Parameterize your scripts — Accept namespace and policy values as arguments rather than hardcoding them. This makes scripts reusable and testable.
  4. Log all changes — Append script output to an audit log so you have a record of what was modified and when.
  5. Test in a non-production namespace first — Even simple kubectl set image or kubectl annotate commands can have unintended side effects on running workloads.
  6. Pin image versions by digest, not tag — Tags are mutable. Digests (image@sha256:...) guarantee you’re running exactly the image you audited.
  7. Integrate with your CI/CD pipeline — Run security audit scripts as a post-deploy gate to catch policy drift before it accumulates.

Going Further: Beyond Shell Scripts

Shell scripts are an excellent starting point, but as your security requirements grow, you may want to explore purpose-built Kubernetes security tools:

  • Kyverno — A Kubernetes-native policy engine that enforces policies as Kubernetes resources (no scripting required)
  • OPA/Gatekeeper — Open Policy Agent integration for fine-grained admission control
  • Falco — Runtime threat detection for containers and Kubernetes
  • Trivy — Vulnerability scanning for container images, which you can invoke directly from shell scripts
  • kube-bench — Checks your cluster configuration against CIS Kubernetes Benchmarks

Shell scripting and dedicated tools aren’t mutually exclusive — you can script the invocation of trivy or kube-bench as part of a broader security automation pipeline.


Conclusion

Shell scripting is a powerful and accessible way to extend Kubernetes security beyond its defaults. With a few well-crafted Bash or PowerShell scripts, you can detect misconfigurations, enforce organizational policies, and automate compliance checks across your entire cluster fleet.

The key is to treat your security scripts like any other code: version-control them, test them, review them, and iterate on them as your security posture matures. Start with the examples above, adapt them to your environment, and progressively move toward policy-as-code tools as your needs scale.


Photo by Brandon Jaramillo on Unsplash.