Kubernetes Security: Stopping Root Containers with PSS and Admission Controllers

Security tutorial - IT technology blog
Security tutorial - IT technology blog

The Risks of Running Containers as Root

After more than 6 months of operating Kubernetes (K8s) in production environments, I realized a common vulnerability. According to a RedHat report, up to 53% of K8s security incidents stem from misconfigurations. Most don’t come from complex attacks but rather from overly permissive container privileges.

When first deploying, many engineers often set privileged: true to avoid annoying Permission Denied errors. However, this is a loophole for Container Breakout techniques. From a compromised container, an attacker can escalate to the physical node and control the entire cluster. To solve this, I’ve applied a combination of Pod Security Standards (PSS) and Admission Controllers.

Choosing the Right Pod Security Tools

Since Pod Security Policies (PSP) were completely removed in version 1.25, we have two main alternatives. Each approach has its own pros and cons depending on the system scale.

1. Pod Security Admission (PSA) – The Built-in Solution

PSA has been a default feature in Kubernetes since version 1.23+. It works based on predefined PSS standards.

  • Pros: Extremely fast deployment, zero resource overhead, and only requires labeling the Namespace.
  • Cons: Only offers 3 fixed levels (Privileged, Baseline, Restricted). You cannot customize specific rules for unique needs.

2. Admission Controllers (Kyverno or OPA Gatekeeper)

These are webhooks that intercept requests to the API Server.

  • Pros: Absolute flexibility. You can enforce that all images come from internal registries or check resource limits before creation.
  • Cons: Increases API Server latency (about 10-20ms per request). You also need to manage an additional component within the cluster.

Practical Advice: Use PSA as a baseline layer, then add Kyverno to handle complex business logic.

Implementing Pod Security Standards (PSS) in Practice

PSS divides security into 3 levels (Profiles). In real-world projects, I usually apply Baseline for standard applications and Restricted for services handling sensitive data.

Activating PSA for a Namespace

Suppose you need to protect the production-apps namespace. Instead of modifying every application YAML file, label the namespace directly to enforce the policy:

# Apply Restricted level to the namespace
kubectl label --overwrite ns production-apps \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.28

Once this command is executed, the API Server will immediately reject any Pod creation requests that violate security standards.

Verification: Testing a Blocked Insecure Pod

Create a bad-pod.yaml file with a privileged configuration:

apiVersion: v1
kind: Pod
metadata:
  name: root-pod
  namespace: production-apps
spec:
  containers:
  - name: nginx
    image: nginx
    securityContext:
      privileged: true # Violates Restricted level

When running kubectl apply, the system will return a detailed error. This mechanism forces development teams to standardize security configurations and perform vulnerability scanning from the very first deployment step.

Optimizing Security with Kyverno

PSA can sometimes be too rigid. There are cases where an application needs specific permissions but still needs other risks blocked. This is where Kyverno shines.

I chose Kyverno because it uses familiar YAML formatting. This makes it easy for DevOps teams to manage Policy as Code without learning OPA’s complex Rego language.

Policy to Enforce Read-Only Root Filesystem

Forcing the Root Filesystem to read-only mode reduces the attack surface and prevents hackers from overwriting executable files. Here is a practical policy I often apply:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: enforce-read-only-root-fs
spec:
  validationFailureAction: Enforce
  background: true
  rules:
  - name: check-read-only-root-fs
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "Setting readOnlyRootFilesystem: true is required for security!"
      pattern:
        spec:
          containers:
          - securityContext:
              readOnlyRootFilesystem: true

Deployment Strategy: Avoiding System Disruption

The biggest mistake is enabling Enforce mode immediately on a live cluster. This can cause many Pods to be deleted and fail to restart, leading to downtime.

Safe 4-step process:

  1. Audit/Warn Mode: Use the warn=restricted label to log violations without blocking Pods.
  2. Log Analysis: Monitor logs from Kyverno or PSA to identify non-compliant applications.
  3. Standardize Configuration: Update securityContext in Helm Charts or deployments across teams.
  4. Enforcement: Only switch to full blocking mode once all applications are compliant.

Standard SecurityContext Template for Secure Pods

Here is the optimal Pod configuration I usually use for production projects:

spec:
  securityContext:
    runAsNonRoot: true # Block root user (UID 0)
    runAsUser: 1000    # Run with specific user ID
    fsGroup: 2000      
  containers:
  - name: my-app
    image: my-app:v1.0.0
    securityContext:
      allowPrivilegeEscalation: false # Prevent privilege escalation
      capabilities:
        drop: ["ALL"] # Drop all unnecessary Linux capabilities
      readOnlyRootFilesystem: true   # Read-only filesystem

Conclusion

Kubernetes security is an ongoing process. Combining Pod Security Standards to build a foundation and Admission Controllers for fine-tuning creates a solid armor.

Check your Namespaces today. If everything is still running with Privileged rights, it’s a ticking time bomb for internal network vulnerabilities. Good luck building a safe and reliable K8s system!

Share: