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: falseon all Pods that do not call the server API - Token expiration: use tokens projected with
expirationSecondsinstead of permanent tokens - Pod Security Standards:
restrictedacross all production namespaces,baselineon staging - No root:
runAsNonRoot: trueeallowPrivilegeEscalation: falseon 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 podand 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).
Upcoming Articles in the Kubernetes at Scale Series
Previous Articles
Related Series
- DevSecOps — policy-as-code, security scanning in CI/CD pipelines
- Kubernetes Networking — Network Policy to isolate workloads
- Platform Engineering — security like automatic guardrails for teams







