Kubernetes Security Baseline: From Default-Dangerous to Production-Hardened

15 min read
kubernetessecuritypod-security-standardsrbacnetwork-policiestrivysealed-secrets

Apply Pod Security Standards with namespace labels, write least-privilege RBAC roles, enforce NetworkPolicies, scan images with Trivy in CI, and manage secrets with Sealed Secrets. This guide takes a default Kubernetes cluster and hardens it to meet CIS Benchmark Level 2 requirements.

A default Kubernetes installation is designed for ease of adoption, not security. Pods run as root, there are no network restrictions between namespaces, and the default service account token is mounted into every container. The 2024 Red Hat State of Kubernetes Security report found that 89% of organizations experienced at least one container security incident in the previous 12 months, with misconfigurations being the leading cause. This guide provides the exact manifests and commands to close those gaps.

---

1. Enforce Pod Security Standards (PSS)

Pod Security Standards replaced the deprecated PodSecurityPolicy in Kubernetes 1.25. PSS uses namespace labels to enforce security profiles at three levels: Privileged (unrestricted), Baseline (prevents known privilege escalations), and Restricted (hardened best practices). According to the NSA/CISA Kubernetes Hardening Guide (updated 2024), all production namespaces should use at least the Baseline profile.

Apply PSS labels to namespaces

apiVersion: v1

kind: Namespace

metadata:

name: production

labels:

pod-security.kubernetes.io/enforce: restricted

pod-security.kubernetes.io/audit: restricted

pod-security.kubernetes.io/warn: restricted

Apply to all existing namespaces at once

<h1>Label all non-system namespaces with baseline enforcement</h1>

for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'

tr ' ' '\n'
grep -v "^kube-"); do

kubectl label ns "$ns" \

pod-security.kubernetes.io/enforce=baseline \

pod-security.kubernetes.io/warn=restricted \

--overwrite

done

This labels all non-system namespaces with baseline enforcement and restricted warnings. The warn mode shows developers what would fail under restricted without blocking deployments, giving teams time to migrate.

PSS level comparison

Security ControlPrivilegedBaselineRestricted

Run as rootAllowedAllowedBlocked
Privileged containersAllowedBlockedBlocked
Host networkingAllowedBlockedBlocked
Host PID/IPCAllowedBlockedBlocked
Writable root filesystemAllowedAllowedBlocked
Privilege escalationAllowedBlockedBlocked
Seccomp profileNoneNoneRequired
Volume typesAllAllLimited

---

2. Write Secure Pod Specifications

Even with PSS enforcement, writing secure pod specs from the start prevents debugging cycles. This template satisfies the Restricted PSS profile and passes CIS Benchmark checks.

Production-hardened pod template

apiVersion: apps/v1

kind: Deployment

metadata:

name: secure-api

namespace: production

spec:

replicas: 3

selector:

matchLabels:

app: secure-api

template:

metadata:

labels:

app: secure-api

spec:

automountServiceAccountToken: false

securityContext:

runAsNonRoot: true

runAsUser: 1000

runAsGroup: 1000

fsGroup: 1000

seccompProfile:

type: RuntimeDefault

containers:

- name: api

image: registry.example.com/api:v1.2.3

securityContext:

allowPrivilegeEscalation: false

readOnlyRootFilesystem: true

capabilities:

drop:

- ALL

resources:

requests:

cpu: 100m

memory: 128Mi

limits:

memory: 256Mi

ports:

- containerPort: 8080

protocol: TCP

livenessProbe:

httpGet:

path: /healthz

port: 8080

initialDelaySeconds: 10

periodSeconds: 15

readinessProbe:

httpGet:

path: /readyz

port: 8080

initialDelaySeconds: 5

periodSeconds: 10

volumeMounts:

- name: tmp

mountPath: /tmp

volumes:

- name: tmp

emptyDir: {}

Key security features in this template:

  • automountServiceAccountToken: false prevents the default SA token from being mounted. Most pods do not need Kubernetes API access.
  • runAsNonRoot: true with explicit UID/GID ensures the container never runs as root, even if the Dockerfile specifies USER root.
  • readOnlyRootFilesystem: true prevents attackers from writing malicious binaries to the container filesystem. The /tmp emptyDir volume provides writable space where needed.
  • capabilities drop ALL removes all Linux capabilities. If your app needs specific capabilities (like NET_BIND_SERVICE for port 80), add only those.
  • seccompProfile RuntimeDefault applies the container runtime's default seccomp filter, blocking about 44 dangerous syscalls.

Teams using Arxentra's managed Kubernetes get these security defaults applied automatically to all tenant workloads.

---

3. Configure Least-Privilege RBAC

The default RBAC configuration in many clusters is overly permissive. The 2024 Wiz Kubernetes Security report found that 40% of clusters have at least one service account with cluster-admin privileges that is never used. RBAC should follow the principle of least privilege: grant only the permissions each service actually needs.

Application-specific Role and RoleBinding

apiVersion: rbac.authorization.k8s.io/v1

kind: Role

metadata:

name: api-server-role

namespace: production

rules:

- apiGroups: [""]

resources: ["configmaps"]

verbs: ["get", "watch"]

resourceNames: ["api-config"]

- apiGroups: [""]

resources: ["secrets"]

verbs: ["get"]

resourceNames: ["api-credentials"]

---

apiVersion: rbac.authorization.k8s.io/v1

kind: RoleBinding

metadata:

name: api-server-binding

namespace: production

roleRef:

apiGroup: rbac.authorization.k8s.io

kind: Role

name: api-server-role

subjects:

- kind: ServiceAccount

name: api-server

namespace: production

RBAC audit commands

<h1>Find all ClusterRoleBindings granting cluster-admin</h1>

kubectl get clusterrolebindings -o json | jq -r '

.items[]

select(.roleRef.name == "cluster-admin")
.metadata.name + " -> " + (.subjects[]? | .kind + "/" + .name)

'

<h1>Find service accounts with broad permissions</h1>

kubectl auth can-i --list --as=system:serviceaccount:default:default

<h1>Check if any SA can create pods (potential privilege escalation)</h1>

kubectl auth can-i create pods --as=system:serviceaccount:default:default

Run these audit commands monthly and remove any bindings that are no longer needed. The Arxentra security documentation covers RBAC best practices for multi-tenant environments.

---

4. Enforce NetworkPolicies

By default, every pod in a Kubernetes cluster can communicate with every other pod across all namespaces. This means a compromised pod in your staging namespace can reach your production database. NetworkPolicies are namespace-scoped firewall rules that restrict this traffic. According to Cilium's 2024 survey, only 35% of production clusters have any NetworkPolicies applied, making lateral movement the most common post-compromise attack vector.

Default-deny ingress and egress

apiVersion: networking.k8s.io/v1

kind: NetworkPolicy

metadata:

name: default-deny-all

namespace: production

spec:

podSelector: {}

policyTypes:

- Ingress

- Egress

Allow specific traffic patterns

apiVersion: networking.k8s.io/v1

kind: NetworkPolicy

metadata:

name: allow-api-to-db

namespace: production

spec:

podSelector:

matchLabels:

app: postgres

policyTypes:

- Ingress

ingress:

- from:

- podSelector:

matchLabels:

app: api-server

ports:

- port: 5432

protocol: TCP

---

apiVersion: networking.k8s.io/v1

kind: NetworkPolicy

metadata:

name: allow-api-egress

namespace: production

spec:

podSelector:

matchLabels:

app: api-server

policyTypes:

- Egress

egress:

- to:

- podSelector:

matchLabels:

app: postgres

ports:

- port: 5432

- to:

- namespaceSelector: {}

podSelector:

matchLabels:

k8s-app: kube-dns

ports:

- port: 53

protocol: UDP

- port: 53

protocol: TCP

Always remember to allow DNS egress (port 53 to kube-dns) or your pods will fail to resolve any hostnames. This is the most common mistake when implementing default-deny policies.

---

5. Scan Images with Trivy in CI

Container images are a primary attack vector. The 2024 Sysdig Container Security report found that 87% of container images contain at least one high or critical vulnerability. Scanning in CI prevents vulnerable images from reaching production.

Trivy scanning in GitHub Actions

name: Container Security Scan

on:

push:

branches: [main]

pull_request:

branches: [main]

jobs:

scan:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Build image

run: docker build -t app:$GITHUB_SHA .

- name: Run Trivy vulnerability scanner

uses: aquasecurity/trivy-action@master

with:

image-ref: app:$GITHUB_SHA

format: table

exit-code: "1"

ignore-unfixed: true

vuln-type: os,library

severity: CRITICAL,HIGH

- name: Run Trivy config scanner

uses: aquasecurity/trivy-action@master

with:

scan-type: config

exit-code: "1"

severity: CRITICAL,HIGH

Local scanning during development

<h1>Scan a local image</h1>

trivy image app:latest

<h1>Scan a running cluster for vulnerabilities</h1>

trivy k8s --report summary cluster

<h1>Scan Kubernetes manifests for misconfigurations</h1>

trivy config ./k8s/manifests/

<h1>Generate SBOM (Software Bill of Materials)</h1>

trivy image --format spdx-json --output sbom.json app:latest

---

6. Manage Secrets with Sealed Secrets

Kubernetes Secrets are base64-encoded, not encrypted. Anyone with read access to the namespace can decode them. Sealed Secrets solves this by encrypting secrets with a cluster-specific key, allowing you to safely store encrypted secrets in Git.

Install Sealed Secrets controller

<h1>Install the controller</h1>

helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets

helm install sealed-secrets sealed-secrets/sealed-secrets \

--namespace kube-system \

--set-string fullnameOverride=sealed-secrets-controller

<h1>Install kubeseal CLI</h1>

<h1>macOS</h1>

brew install kubeseal

<h1>Linux</h1>

KUBESEAL_VERSION=$(curl -s https://api.github.com/repos/bitnami-labs/sealed-secrets/releases/latest

jq -r .tag_name
cut -c 2-)

curl -OL "https://github.com/bitnami-labs/sealed-secrets/releases/download/v$KUBESEAL_VERSION/kubeseal-$KUBESEAL_VERSION-linux-amd64.tar.gz"

tar -xvzf kubeseal-*.tar.gz kubeseal

sudo install -m 755 kubeseal /usr/local/bin/kubeseal

Create and seal a secret

<h1>Create a regular secret (do not apply this to the cluster)</h1>

kubectl create secret generic db-credentials \

--from-literal=username=app_user \

--from-literal=password=supersecret \

--dry-run=client -o yaml > secret.yaml

<h1>Seal it (encrypted with cluster public key)</h1>

kubeseal --format yaml < secret.yaml > sealed-secret.yaml

<h1>Now safe to commit sealed-secret.yaml to Git</h1>

rm secret.yaml # Delete the unencrypted version

git add sealed-secret.yaml

git commit -m "Add encrypted database credentials"

The sealed secret can only be decrypted by the Sealed Secrets controller running in your specific cluster. Even if someone gains access to your Git repository, they cannot decrypt the values without the controller's private key.

---

Security Baseline Checklist Summary

ControlTool/FeatureStatus Check Command

Pod Security StandardsNamespace labels`kubectl get ns -L pod-security.kubernetes.io/enforce`
Non-root containersSecurityContext`kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.name}{" runAsNonRoot="}{.spec.securityContext.runAsNonRoot}{"\n"}{end}'`
RBAC auditkubectl auth`kubectl auth can-i --list --as=system:serviceaccount:default:default`
NetworkPoliciesCNI plugin`kubectl get networkpolicies -A`
Image scanningTrivy`trivy k8s --report summary cluster`
Secret encryptionSealed Secrets`kubectl get sealedsecrets -A`

---

Frequently Asked Questions

How long does it take to implement this baseline?

A dedicated engineer can implement all six controls in 2-3 days for a cluster with up to 10 namespaces. The biggest time investment is writing NetworkPolicies, because you need to map all legitimate traffic flows before applying default-deny. Start with PSS labels (30 minutes) and RBAC audit (1 hour) for immediate wins.

Do NetworkPolicies work with all CNI plugins?

No. The default kubenet CNI does not support NetworkPolicies. You need a CNI that implements the NetworkPolicy API: Calico, Cilium, Weave Net, or Antrea. Most managed Kubernetes services (EKS, GKE, AKS) support Calico or Cilium out of the box.

Should I use OPA/Gatekeeper instead of Pod Security Standards?

PSS and OPA/Gatekeeper serve different purposes. PSS handles pod-level security with zero additional software. OPA/Gatekeeper handles custom organizational policies (naming conventions, required labels, approved registries). Use PSS as your security baseline and add OPA/Gatekeeper for policy enforcement. The Arxentra security best practices guide covers both approaches.

How do I handle legacy applications that need to run as root?

Use a dedicated namespace with the Baseline PSS profile (which allows root) and apply compensating controls: mandatory NetworkPolicies, restricted RBAC, and image scanning. Create a migration plan to refactor the application to run as non-root. Most applications that "need" root actually just need write access to specific directories, which can be solved with init containers or volume mounts.

What about runtime security (detecting attacks in progress)?

This baseline covers preventive controls. For runtime detection, add Falco (open-source runtime security) to detect anomalous behavior like unexpected process execution, file access, or network connections. Falco integrates with the same Kubernetes audit infrastructure and can alert to Slack, PagerDuty, or your SIEM.

How do I rotate Sealed Secrets encryption keys?

The Sealed Secrets controller automatically rotates its encryption key every 30 days by default. Old keys are kept so existing SealedSecrets can still be decrypted. To manually rotate, delete the current key and restart the controller. All existing SealedSecrets will need to be re-encrypted with the new key using `kubeseal --re-encrypt`.