Skip to content
back to the map
practice
stage: proteger
4 min read

Kubernetes - secure runtime

A Kubernetes cluster is a distributed operating system: RBAC, admission controllers and policies govern what each pod can do, mount and expose.

KubernetesOPA GatekeeperKyvernoKube-bench

What it is

Kubernetes is not just a container orchestrator, it is a distributed operating system with its own : API, etcd, pod network, secrets and volumes. Each layer brings controls: RBAC says who can ask what of the API, admission controllers say what can enter the cluster, NetworkPolicies say what each pod can talk to among the others, and the kubelet says what each container can do on the node.

Treating Kubernetes as 'just a deploy runner' is the mistake that leaks tokens in logs, opens ports to the internet and runs containers as root with writable filesystems. Defending the cluster requires all four controls combined, not just one.

RBAC: the first fence

RBAC (Role-Based Access Control) links a subject (ServiceAccount, User, Group) to a role, and the role to a set of verbs over resources. It works at two levels: Role + RoleBinding (namespace scope) and ClusterRole + ClusterRoleBinding (whole-cluster scope).

The classic mistake is granting cluster-admin to the CI pipeline or to an application service account. It works, the deploy passes, and any RCE in a pod becomes full control of the cluster, including reading all secrets in all namespaces.

rbac-insecure.yamlyaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ci-deployer-all
subjects:
  - kind: ServiceAccount
    name: ci-runner
    namespace: ci
roleRef:
  kind: ClusterRole
  name: cluster-admin          # total power, across the whole cluster
  apiGroup: rbac.authorization.k8s.io
rbac-fixed.yamlyaml
apiVersion: v1
kind: Namespace
metadata:
  name: payments
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployer
  namespace: payments
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch", "update", "patch"]
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-deployer
  namespace: payments
subjects:
  - kind: ServiceAccount
    name: ci-runner
    namespace: ci
roleRef:
  kind: Role
    name: deployer
  apiGroup: rbac.authorization.k8s.io

NetworkPolicies: the pods' firewall

By default, every pod talks to every pod in every namespace. NetworkPolicies fix this: they are pod selectors that say who can come in and go out, with allowlist semantics (what is not allowed is blocked).

The first policy is always deny-by-default in the namespace. Without it, opening a new port on a pod is opening a new port across the whole cluster. The second comes with explicit allows for legitimate traffic (ingress from the ingress controller, egress to external APIs, pod-to-pod within the same app).

netpol-default-deny.yamlyaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments
spec:
  podSelector: {}              # applies to all pods in the namespace
  policyTypes:
    - Ingress
    - Egress
  ingress: []                  # nothing gets in
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53             # DNS to resolve external services

Admission control: policy before the pod starts

Admission controllers sit between accepting the request and persisting it in etcd. They can validate (reject) or mutate (rewrite) the manifest. Kubernetes ships Pod Security Admission (PSA) built in since 1.23, with privileged, baseline and restricted profiles. For finer logic, policy engines come in:

  • Gatekeeper: uses Rego, policy-as-code syntax, ideal for teams with a policy engineering culture
  • Kyverno: pure YAML, reads like a normal Kubernetes manifest, ideal for teams that want to write policies without learning another language
  • Pod Security Admission: native, no extra dependency, baseline and restricted profiles cover 80% of cases
  • ValidatingAdmissionPolicy (KEP-3485): declarative admission in CEL, stabilizing in recent versions

Secrets in etcd: encryption at rest

By default, secrets in etcd are stored in plain text (only base64 in the YAML). Anyone with read access to etcd, or to a backup of it, reads all tokens, passwords and keys. Encryption at rest solves this: each etcd key is encrypted with a key wrapped by a .

The price is managing a key (rotation, revocation), and what you gain is : even an etcd dump becomes inert data without the KMS.

kube-apiserver --encryption-configyaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:
          name: aws-kms
          endpoint: unix:///var/run/kms-provider/socket
          cachesize: 100
          keyid: arn:aws:kms:us-east-1:111122223333:key/abcd-...
      - identity: {}                  # fallback for reading old data

Where it fits

Kubernetes is the border between what the pipeline produced and what will run in production. Every defense that failed earlier (unscanned image, manifest without policy) lands here: admission control rejects, NetworkPolicy isolates, RBAC limits the damage. On the other hand, it is the last chance to fix what slipped through: PSP/PSA catches a loose manifest, OPA catches wrong labels, secrets in KMS defeat an etcd dump.

Strengths

  • native Kubernetes controls already cover RBAC, NetworkPolicy, PSA, encryption at rest: nothing to buy
  • (Kyverno, Gatekeeper) becomes a PR matter, with review and testing, not an urgent ticket
  • namespaces become cheap isolation units, giving a limit without needing separate clusters

Limitations

  • RBAC, NetworkPolicy and admission are default-deny controls: they must be configured before anything runs, and the migration path for an existing cluster is manual
  • the whole cluster is only as secure as the node: an exposed kubelet or a privileged container breaks the guarantees of the layers above
  • operating policies at scale (X clusters, Y namespaces) becomes platform work, not app work, and without someone accountable it turns into a mess
Attack surfaceEverything exposed to untrusted input: endpoints, forms, dependencies, ports, pipelines. A minimal surface is a defense strategy.see in glossary →RegoOPADeclarative language of Open Policy Agent for writing policies evaluated against an input (a Terraform plan, a Kubernetes manifest, an API request).see in glossary →KMSkey management serviceA managed service that creates, stores and controls the use of cryptographic keys. The application rarely touches the master key: it asks the KMS to encrypt or decrypt, and the provider records every use in an auditable log. Without a KMS, the key ends up on disk and becomes a target.see in glossary →Defense in depthIndependent layers of protection, on the premise that any one of them can fail: validation + parameterization + WAF + monitoring.see in glossary →Policy as codeOPA / RegoThe security policy written as a versioned file that the pipeline consults (deny an image with a critical vulnerability, require a signature). Change the criterion = change the file, with review.see in glossary →Blast radiusblast radiusThe size of the damage when something breaks: one account, an entire region, a tenant. Good cloud design limits the radius through isolation (separate accounts, separate VPCs, narrow roles) so that a failure or an attack does not become a full-blown disaster.see in glossary →

connections on the map

updated on 14/10/2026 · sources to go deeper · report an error

Study before

test what you learned

Loading questions…