Skip to main content

06 - Kubernetes Security: RBAC, Pod Security Standards and OPA Gatekeeper

Sicurezza Kubernetes: RBAC, Pod Security Standards e OPA Gatekeeper Un cluster Kubernetes di default e sorprendentemente insicuro: i container possono...

What do you think about this article?

At a Glance

Sicurezza Kubernetes: RBAC, Pod Security Standards e OPA Gatekeeper Un cluster Kubernetes di default e sorprendentemente insicuro: i container possono...

Kubernetes Security: RBAC, Pod Security Standards and OPA Gatekeeper

A default and surprisingly insecure Kubernetes cluster: containers can run as root, mount host node filesystem, access server API with token of ServiceAccount with broad permissions. In 2025, 68% of security incidents in environments containerized was attributable to configuration errors rather than vulnerabilities software (source: Sysdig Container Security Report 2025). The good news: Kubernetes offers powerful tools to eliminate these risky setups.

This article covers a complete cluster hardening: RBAC to check who can do what with the Kubernetes API, Pod Security Standards to prevent privileged containers and risky configurations, e OPA Gatekeeper for implement custom company policies such as “all images must come from internal registry" or "no deployment without resource limits".

What You Will Learn

  • Kubernetes authorization model: RBAC, verbs, resources, scope
  • Principle of Least Privilege: Role, ClusterRole, RoleBinding applied
  • ServiceAccount: How to limit auto-mounted tokens and use IRSA/Workload Identity
  • Pod Security Standards: Privileged, Baseline, Restricted, and enforcement by namespace
  • OPA Gatekeeper: Installation, ConstraintTemplate in Rego, Constraint
  • Common policies: approved registry images, mandatory resource limits, no root
  • Audit logging: Monitor who does what in the cluster
  • Complete hardening checklist

RBAC: Role Based Access Control

RBAC (Role-Based Access Control) is the core authorization mechanism of Kubernetes. Defines Who (Subject: User, Group, ServiceAccount) can execute what action (verb: get, list, watch, create, update, patch, delete) up what resource (Resource: pods, deployments, secrets) in which scope (namespace with Role/RoleBinding, or entire cluster with ClusterRole/ClusterRoleBinding).

Role and RoleBinding for a Development Team

# rbac-developer-role.yaml
# Un ruolo per i developer: possono vedere e modificare
# deployment/pod/service nel loro namespace, ma non secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: team-alpha
rules:
  # Deployment: lettura + scaling
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  # Pod: lettura + exec + logs
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create"]
  # Service e ConfigMap: accesso completo
  - apiGroups: [""]
    resources: ["services", "configmaps", "endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  # HPA
  - apiGroups: ["autoscaling"]
    resources: ["horizontalpodautoscalers"]
    verbs: ["get", "list", "watch"]
  # Ingress
  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-alpha-developers
  namespace: team-alpha
subjects:
  # Gruppo di utenti (gestito da OIDC/SSO)
  - kind: Group
    name: "team-alpha-devs"
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: developer
  apiGroup: rbac.authorization.k8s.io

ClusterRole for SRE/Admin with Limited Scope

# rbac-sre-clusterrole.yaml
# SRE: accesso in lettura a tutto il cluster, write solo su namespace specifici
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-reader
rules:
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["get", "list", "watch"]
  # Nega esplicita: non puo leggere i secrets (sovrascritta da DENY)
  # NOTA: in RBAC Kubernetes non esiste DENY esplicita - usa Gatekeeper per questo
---
# SRE: read su tutto + write limitato ai namespace di produzione
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sre-cluster-reader
subjects:
  - kind: Group
    name: "platform-sre"
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-reader
  apiGroup: rbac.authorization.k8s.io

# Verifica i permessi di un utente/serviceaccount
kubectl auth can-i create pods --as=system:serviceaccount:production:api-server-sa
kubectl auth can-i delete secrets --as=developer-user -n production
kubectl auth can-i '*' '*' --as=alice  # lista tutto quello che alice puo fare

ServiceAccount: Limit Automatic Tokens

By default, each Pod automatically mounts a valid ServiceAccount token. In the Pods that they don't call the Kubernetes API, this token is useless but increases the attack surface:

# serviceaccount-minimal.yaml
# ServiceAccount dedicata per ogni applicazione (mai usare default)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-server-sa
  namespace: production
  annotations:
    # Su AWS EKS: delega i permessi IAM tramite IRSA
    eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/ApiServerRole"
automountServiceAccountToken: false  # non montare il token automaticamente
---
# Deployment che usa la ServiceAccount
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
spec:
  template:
    spec:
      serviceAccountName: api-server-sa
      automountServiceAccountToken: false  # ridondante ma esplicito
      containers:
        - name: api-server
          image: my-registry/api-server:1.2.0
---
# Se il Pod deve chiamare l'API K8s, usa un token proiettato con scadenza
# invece del token auto-mounted (che non scade mai)
apiVersion: v1
kind: Pod
spec:
  serviceAccountName: api-server-sa
  volumes:
    - name: api-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 3600  # scade ogni ora
              audience: kubernetes.default.svc
  containers:
    - name: api-server
      volumeMounts:
        - name: api-token
          mountPath: /var/run/secrets/kubernetes.io/serviceaccount
          readOnly: true

Pod Security Standards (PSS)

Pod Security Standards replace the deprecated PodSecurityPolicies (removed in K8s 1.25). They define three levels of security applicable at the namespace level via labels:

  • Privileged: no restrictions (only for system namespaces like kube-system)
  • Baselines: prevents the most notorious and risky configurations (privileged containers, hostPath, hostNetwork)
  • Restricted: Current best practices for maximum security (non-root, seccomp, capabilities drop)

Each level has three modes: enforce (blocks the Pod), audit (logs but does not block), warn (shows a warning to the user).

Apply Pod Security Standards to Namespaces

# Applica PSS al namespace tramite label
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=latest

# Per namespace di sistema che richiedono pod privilegiati
kubectl label namespace kube-system \
  pod-security.kubernetes.io/enforce=privileged

# Testa cosa succederebbe se applicassi restricted a un namespace esistente
kubectl label --dry-run=server --overwrite namespace production \
  pod-security.kubernetes.io/enforce=restricted

Pod Compliant with Restricted Level

# pod-restricted-compliant.yaml
# Un Pod che rispetta tutte le regole del livello Restricted
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
  namespace: production
spec:
  securityContext:
    runAsNonRoot: true          # non girare come root
    runAsUser: 1000             # UID non-root
    runAsGroup: 3000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault      # profilo seccomp di default
    supplementalGroups: [1000]
  containers:
    - name: app
      image: my-registry/api-server:1.2.0
      securityContext:
        allowPrivilegeEscalation: false  # fondamentale: previene sudo
        readOnlyRootFilesystem: true     # filesystem immutabile
        capabilities:
          drop:
            - ALL                # rimuovi tutte le Linux capabilities
          # add: [] - non aggiungere nessuna capability
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      resources:
        requests:
          cpu: "100m"
          memory: "128Mi"
        limits:
          cpu: "500m"
          memory: "512Mi"
      volumeMounts:
        - name: tmp
          mountPath: /tmp      # se l'app scrive su /tmp, monta un volume
        - name: cache
          mountPath: /app/cache
  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}
  automountServiceAccountToken: false

OPA Gatekeeper: Policy Engine for Kubernetes

The Pod Security Standards cover a fixed set of controls. OPA Gatekeeper allows to define custom policies using Rego, the language of OPA. It is implemented as an Admission Webhook that intercepts every request to the API server and valid against defined policies.

OPA Gatekeeper installation

# Installa Gatekeeper con Helm
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update

helm install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system \
  --create-namespace \
  --version 3.17.0 \
  --set auditInterval=30 \
  --set constraintViolationsLimit=100 \
  --set logLevel=INFO

# Verifica installazione
kubectl get pods -n gatekeeper-system

ConstraintTemplate: Images Only from Approved Registry

# constraint-template-registry.yaml
# Il template definisce lo schema e la logica in Rego
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
  annotations:
    description: "Richiede che le immagini provengano solo da registry approvati"
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not starts_with_allowed(container.image)
          msg := sprintf("Il container '%v' usa l'immagine '%v' che non proviene da un registry approvato", [container.name, container.image])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.initContainers[_]
          not starts_with_allowed(container.image)
          msg := sprintf("L'initContainer '%v' usa l'immagine '%v' non approvata", [container.name, container.image])
        }

        starts_with_allowed(image) {
          repo := input.parameters.repos[_]
          startswith(image, repo)
        }
---
# Constraint: applica la policy con i parametri specifici
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: require-approved-registry
spec:
  enforcementAction: deny  # deny|warn|dryrun
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    excludedNamespaces:
      - kube-system
      - gatekeeper-system
      - monitoring
  parameters:
    repos:
      - "registry.company.internal/"
      - "gcr.io/company-project/"
      - "public.ecr.aws/company/"

ConstraintTemplate: Mandatory Resource Limits

# constraint-template-resource-limits.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredresources
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredResources
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredresources

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.cpu
          msg := sprintf("Container '%v': cpu limit obbligatorio ma mancante", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.memory
          msg := sprintf("Container '%v': memory limit obbligatorio ma mancante", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.requests.cpu
          msg := sprintf("Container '%v': cpu request obbligatoria ma mancante", [container.name])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
  name: require-resource-limits
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    excludedNamespaces:
      - kube-system
      - gatekeeper-system

ConstraintTemplate: No Container Root

# constraint-template-no-root.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8spsphostnamespace
spec:
  crd:
    spec:
      names:
        kind: K8sPSPHostNamespace
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8spsphostnamespace

        violation[{"msg": msg}] {
          input.review.object.spec.hostPID == true
          msg := "hostPID non e consentito"
        }

        violation[{"msg": msg}] {
          input.review.object.spec.hostIPC == true
          msg := "hostIPC non e consentito"
        }

        violation[{"msg": msg}] {
          input.review.object.spec.hostNetwork == true
          not input.review.object.metadata.annotations["policy.company.internal/exempt-hostnetwork"]
          msg := "hostNetwork non e consentito senza annotation di esenzione"
        }

# Verifica violazioni esistenti nel cluster
kubectl get constraints
kubectl describe k8srequiredresources require-resource-limits
# Nella sezione Status.Violations vedrai tutti i Pod non conformi

Audit Logging

Kubernetes can log every request to the API server in a structured audit log. It is essential for compliance and incident investigation.

# audit-policy.yaml - configurazione dell'audit log
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Registra tutto sui Secrets a livello RequestResponse (incluso il contenuto)
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["secrets"]

  # Registra le modifiche a RBAC
  - level: RequestResponse
    resources:
      - group: "rbac.authorization.k8s.io"
        resources: ["roles", "clusterroles", "rolebindings", "clusterrolebindings"]

  # Registra exec nei Pod (potenziale accesso malevolo)
  - level: Request
    resources:
      - group: ""
        resources: ["pods/exec", "pods/portforward", "pods/proxy"]

  # Per tutto il resto: registra solo i metadata (chi ha fatto cosa, quando)
  - level: Metadata
    omitStages:
      - RequestReceived

# Kube-apiserver config (aggiungi all'avvio di kube-apiserver):
# --audit-log-path=/var/log/kubernetes/audit.log
# --audit-log-maxage=30
# --audit-log-maxbackup=10
# --audit-log-maxsize=100
# --audit-policy-file=/etc/kubernetes/audit-policy.yaml

Hardening Kubernetes Checklist

Security Hardening Checklist

  • RBAC: Least privilege principle for each ServiceAccount and user group
  • Service Account: automountServiceAccountToken: false on all Pods that do not call the server API
  • Token expiration: use tokens projected with expirationSeconds instead of permanent tokens
  • Pod Security Standards: restricted across all production namespaces, baseline on staging
  • No root: runAsNonRoot: true e allowPrivilegeEscalation: false on all containers
  • Immutable filesystem: readOnlyRootFilesystem: true + volume emptyDir for /tmp and /cache
  • Resource limits: CPU and memory limits on all containers (Gatekeeper for enforcement)
  • Approved registries: Gatekeeper constraint that blocks images from unauthorized registries
  • Network Policy: default-deny on all production namespaces (see Article 1)
  • Secrets: use external secret manager (AWS Secrets Manager, Vault) instead of Secret Kubernetes plain
  • Audit logs: enabled and centralized in a SIEM or log aggregator
  • etcd: encrypted at-rest with encryption config
  • Server APIs: anonymous access disabled, admission controllers enabled

Common Security Mistakes

  • Default namespace for workloads: the default namespace has permissive RBAC by default; Always use dedicated namespaces for your workloads
  • ClusterAdmin for CI/CD pipelines: GitOps pipelines don't need cluster-admin; Create a ServiceAccount with the minimum permissions needed for the target namespace
  • Secret as environment variables: environment variables are visible in kubectl describe pod and in the logs; use files mounted by Secrets or external secret managers
  • Latest tags in images: the latest tag may change; Always use SHA256 digest or immutable tags to ensure reproducibility and security
  • Ignore base image CVEs: scan images regularly with Trivy, Snyk or Grype; an Ubuntu base image with critical CVEs cancels the cluster hardening work

Conclusions and Next Steps

The security of a Kubernetes cluster is a layered process: RBAC controls access to the API, Pod Security Standards prevents risky configurations at the Pod level, and OPA Gatekeeper allows you to encode company policies as versioned code and auditable. None of these tools alone is sufficient; together they form one defense-in-depth which drastically reduces the attack surface.

The next step to further strengthen security and implement a system runtime security tools like Falco, which monitors container system calls in real time and alert on anomalous behavior (shell opened in a container in production, reading of credential files, unexpected network connections).

Related Series

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects