← Back to blog
KubernetesSecurityCompliance

Kubernetes Security Hardening: A Checklist for Production Clusters

· 8 min read

A default Kubernetes installation is not secure. It is functional — designed to get you started quickly — but production readiness requires deliberate hardening across multiple layers. We have audited dozens of Kubernetes clusters (as part of our infrastructure audit practice), and the same security gaps appear repeatedly.

This post is the checklist we wish every team had before going to production. Each section includes concrete YAML snippets and configuration you can apply today.

graph TD
    A[RBAC - Who can do what] --> B[Pod Security Standards - How pods run]
    B --> C[Network Policies - Who can talk to whom]
    C --> D[Image Scanning - What gets deployed]
    D --> E[Runtime Security - What happens at runtime]
    E --> F[Audit Logging - What happened]
    F --> G[CIS Benchmarks - Are we compliant]

1. RBAC: Principle of Least Privilege

Role-Based Access Control is Kubernetes’ authorization layer. The most common mistake is over-permissioning — granting cluster-admin to service accounts that only need read access to a single namespace.

Design principles:

  • No human user should have cluster-admin in production. Use time-limited escalation (e.g., through a tool like Teleport or a custom approval workflow).
  • Every application should have its own ServiceAccount with a Role scoped to exactly what it needs.
  • Audit who has what access regularly.

A properly scoped Role for an application that reads ConfigMaps and manages its own Deployments:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-deployer
  namespace: myapp
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "update", "patch"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-deployer-binding
  namespace: myapp
subjects:
  - kind: ServiceAccount
    name: myapp-deployer
    namespace: myapp
roleRef:
  kind: Role
  name: app-deployer
  apiGroup: rbac.authorization.k8s.io

Quick audit: Run kubectl auth can-i --list --as=system:serviceaccount:myapp:default for each namespace to see what the default ServiceAccount can do. If it can do anything beyond the basics, you have a problem.

2. Pod Security Standards

Pod Security Standards (PSS) replaced the deprecated PodSecurityPolicy. They define three profiles: Privileged, Baseline, and Restricted. Every production namespace should enforce at minimum Baseline, and ideally Restricted.

Apply enforcement at the namespace level:

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/audit: restricted

The Restricted profile requires pods to:

  • Run as non-root
  • Drop all capabilities
  • Use a read-only root filesystem
  • Set seccompProfile to RuntimeDefault

A compliant pod spec:

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: production
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myregistry.io/myapp:v1.2.3@sha256:abc123...
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
        limits:
          cpu: 500m
          memory: 512Mi
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

Note the image reference uses a digest (@sha256:...) rather than a mutable tag. This prevents tag-based supply chain attacks where an attacker pushes a malicious image to the same tag.

3. Network Policies

By default, every pod can communicate with every other pod in the cluster. This is the network equivalent of having no firewall. Network Policies are your in-cluster firewall rules.

Start with a default-deny policy for each namespace, then explicitly allow required traffic:

# Default deny all ingress and egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
# Allow DNS resolution (required for almost everything)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to: []
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
---
# Allow frontend to talk to backend on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

Network policies require a CNI plugin that supports them. Calico, Cilium, and Antrea all implement the full NetworkPolicy spec. The default kubenet does not. If you are running Kubernetes on Proxmox or bare metal, Cilium is our recommended CNI for its combined networking, security, and observability capabilities.

4. Image Scanning in CI/CD

Do not let vulnerable images reach your cluster. Integrate image scanning into your CI/CD pipeline so that builds fail if critical vulnerabilities are detected:

# GitLab CI example with Trivy
scan-image:
  stage: security
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity CRITICAL,HIGH
        --ignore-unfixed
        ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
  allow_failure: false

For GitHub Actions:

- name: Scan image with Trivy
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.IMAGE }}
    exit-code: '1'
    severity: 'CRITICAL,HIGH'
    ignore-unfixed: true

Beyond scanning, enforce that only images from trusted registries can run in your cluster. Use an admission controller like Kyverno or OPA Gatekeeper:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: Enforce
  rules:
    - name: validate-registries
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Images must come from the trusted registry."
        pattern:
          spec:
            containers:
              - image: "myregistry.io/*"

5. Runtime Security with Falco

Image scanning catches known vulnerabilities before deployment. Runtime security catches anomalous behavior during execution — things like a container spawning a shell, reading /etc/shadow, or making unexpected network connections.

Falco is the de facto standard for Kubernetes runtime security. It monitors system calls and alerts on suspicious activity:

# Falco custom rule example
- rule: Terminal shell in container
  desc: Detect a shell spawned in a container
  condition: >
    spawned_process and container and
    proc.name in (bash, sh, zsh, dash) and
    not proc.pname in (cron, crond, supervisord)
  output: >
    Shell spawned in container
    (user=%user.name container=%container.name
    shell=%proc.name parent=%proc.pname
    image=%container.image.repository)
  priority: WARNING
  tags: [container, shell]

- rule: Unexpected outbound connection
  desc: Detect outbound connections to non-whitelisted IPs
  condition: >
    outbound and container and
    not fd.sip in (10.0.0.0/8, 172.16.0.0/12)
  output: >
    Unexpected outbound connection
    (container=%container.name image=%container.image.repository
    connection=%fd.name)
  priority: NOTICE
  tags: [container, network]

Deploy Falco using the Helm chart with falcosidekick to route alerts to Slack, PagerDuty, or your SIEM.

6. Audit Logging

Kubernetes API server audit logs record every request to the API. Without them, you cannot investigate security incidents. Enable a comprehensive audit policy:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Log all changes to secrets at Metadata level (no secret data)
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]

  # Log all changes to RBAC
  - level: RequestResponse
    resources:
      - group: "rbac.authorization.k8s.io"
        resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]

  # Log pod exec/attach (potential interactive access)
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["pods/exec", "pods/attach", "pods/portforward"]

  # Log all other write operations at Request level
  - level: Request
    verbs: ["create", "update", "patch", "delete"]

  # Ignore read-only requests to reduce volume
  - level: None
    verbs: ["get", "list", "watch"]

Ship these logs to a centralized system (Loki, Elasticsearch, or a cloud SIEM) where they cannot be tampered with. Log retention should be at least 90 days, longer if compliance requires it.

7. CIS Benchmark Compliance

The CIS Kubernetes Benchmark provides a comprehensive list of security configurations. Rather than checking them manually, automate it:

# Run kube-bench against your cluster
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml

# Check results
kubectl logs job/kube-bench

Common CIS findings we see in audits:

  • Anonymous authentication enabled on the API server
  • --kubelet-certificate-authority not set
  • etcd not encrypted at rest
  • Admission controllers not enabled (specifically NodeRestriction, PodSecurity)

Address these in your cluster provisioning automation so that every new cluster starts CIS-compliant.

The Hardening Checklist Summary

AreaActionPriority
RBACNo default cluster-admin, scoped Roles per appCritical
Pod SecurityRestricted PSS on all production namespacesCritical
Network PoliciesDefault deny, explicit allow per serviceCritical
Image SecurityScan in CI/CD, enforce trusted registriesHigh
Runtime SecurityDeploy Falco with custom rulesHigh
Audit LoggingEnable API audit logs, ship to central SIEMHigh
CIS BenchmarksAutomate checks, remediate findingsMedium
Secrets ManagementUse external secrets operator (Vault, cloud KMS)High
etcd EncryptionEnable encryption at restHigh
API ServerDisable anonymous auth, enable admission controllersCritical

Conclusion

Kubernetes security is not a one-time task. It is a continuous practice that evolves as your cluster grows and threats change. The checklist above covers the essentials, but security also depends on the surrounding infrastructure — your network design, your cloud architecture, and your team’s operational discipline.

If you are unsure where your clusters stand, a focused security audit can identify the gaps quickly. At robto, we run these assessments regularly and provide actionable remediation plans — not just a report, but a working pull request with the fixes.