Skip to content

Notes07-Security

Security

Who may call the API, how TLS and kubeconfig identify them, how RBAC limits verbs, and how securityContext and NetworkPolicy constrain a running Pod.

Updated Oct 1, 2026

Security Quick Notes





The request path

Every API call passes four gates, in order:

  1. Authentication — who is this? Failure is 401.
  2. Authorization — may this identity perform this verb? Failure is 403.
  3. Admission — is the object allowed to exist (ResourceQuota, Pod Security, mutating and validating webhooks)?
  4. etcd — the apiserver writes the object.

Kubernetes users are not objects in etcd the way Pods are. A user is a string the authenticator produced (alice, or a certificate CN). ServiceAccounts are objects.

Authentication

Method

Where you see it

X.509 client certificate

kubeconfig users.user.client-certificate-data. The CN is the username. The O fields are groups.

Bearer token

Authorization: Bearer. ServiceAccount tokens, bootstrap tokens, OIDC.

ServiceAccount

Every Pod can mount a token. The username is system:serviceaccount:<namespace>:<name>.

OIDC / webhook

External identity. The apiserver is configured with issuer flags or a webhook file.

  • Anonymous: If anonymous auth is enabled, unauthenticated calls arrive as system:anonymous in group system:unauthenticated. kubeadm locks this down. system:public-info-viewer can still read some non-sensitive discovery data.
  • system:masters: A group that the apiserver authorizes as cluster-admin before RBAC. kubeadm’s super-admin.conf certificate is in this group. Day-to-day admin work uses a kubeconfig bound through RBAC (kubeadm:cluster-admins / cluster-admin).


TLS material on a kubeadm control plane



bash
/etc/kubernetes/pki/ca.crt                      cluster CA
/etc/kubernetes/pki/apiserver.crt               API server certificate
/etc/kubernetes/pki/apiserver-kubelet-client.*  apiserver → kubelet
/etc/kubernetes/pki/etcd/ca.crt                 etcd CA (separate)
/etc/kubernetes/pki/front-proxy-ca.crt          aggregation proxy


  • The API server certificate must list every name clients use in Subject Alternative Names: the advertise IP, kubernetes, kubernetes.default, the ClusterIP of the kubernetes Service, and the load-balancer hostname if you set --control-plane-endpoint. A missing SAN shows up as x509: certificate is valid for ….
  • Client CA is --client-ca-file on the apiserver. A client cert signed by a different CA is not a user, even if the file looks right in kubeconfig.

Inspect a certificate:


bash
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -dates
kubeadm certs check-expiration


Look at Subject, Issuer, Not After, and X509v3 Subject Alternative Name.


Certificates API

A user generates a key and a CSR. A signer (a controller, or you with kubectl certificate approve) issues a certificate signed by the cluster CA.


bash
openssl genrsa -out alice.key 2048
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice/O=developers"

cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: alice
spec:
  request: $(base64 < alice.csr | tr -d '\n')
  signerName: kubernetes.io/kube-apiserver-client
  usages: ["client auth"]
EOF

kubectl certificate approve alice
kubectl get csr alice -o jsonpath='{.status.certificate}' | base64 -d > alice.crt



kubectl certificate approve alice
kubectl get csr alice -o jsonpath='{.status.certificate}' | base64 -d > alice.crt

  • signerName: kubernetes.io/kube-apiserver-client is the signer for client certificates a human will put in kubeconfig.
  • usages must include client auth. A cert without that usage will not authenticate.
  • Groups come from O= in the subject. CN= is the username. Encode them in the CSR. You cannot add groups later in the CertificateSigningRequest object.
  • Approve is not RBAC. Approval means “this certificate may exist.” The RoleBinding is what lets alice list Pods.

kubeconfig

A kubeconfig file has three lists: clusters (API URL + CA), users (cert or token), contexts (which user talks to which cluster, plus a default namespace).


bash
kubectl config view
kubectl config current-context
kubectl config get-contexts
kubectl config use-context prod-admin

kubectl config set-cluster lab \
  --server=https://10.0.0.10:6443 \
  --certificate-authority=/etc/kubernetes/pki/ca.crt \
  --embed-certs=true

kubectl config set-credentials alice \
  --client-certificate=alice.crt \
  --client-key=alice.key \
  --embed-certs=true

kubectl config set-context alice@lab \
  --cluster=lab --user=alice --namespace=shop

kubectl config use-context alice@lab


  • --embed-certs=true writes the PEM into the file so you can copy one kubeconfig to another machine.
  • KUBECONFIG: A colon-separated (Linux) list of files. kubectl merges them. echo $KUBECONFIG before you edit ~/.kube/config. If the variable points somewhere else, edits to the default file never take effect.
  • Several clusters in one shell: kubectl config use-context <name> selects the cluster, then kubectl get nodes confirms you are looking at it. An explicit -n overrides the namespace stored on the context.


API groups

Authorization names a group, a resource, and a verb.

Group

Example resources

"" (core)

pods, services, configmaps, secrets, namespaces, nodes, persistentvolumes

apps

deployments, replicasets, daemonsets, statefulsets

rbac.authorization.k8s.io

roles, rolebindings, clusterroles, clusterrolebindings

networking.k8s.io

networkpolicies, ingresses

batch

jobs, cronjobs

storage.k8s.io

storageclasses, csidrivers

certificates.k8s.io

certificatesigningrequests


bash
kubectl api-resources
kubectl api-resources --namespaced=false
kubectl api-versions
kubectl explain networkpolicy.spec --api-version=networking.k8s.io/v1


  • api-resources prints the name you put in a Role (pods, not pod; deployments, not deployment). The short name (deploy, netpol, csr) works with kubectl and is the wrong string inside a Role rule.
  • Subresources are pods/log, pods/exec, deployments/scale. get on pods does not include pods/log.


Authorization modes

The apiserver flag --authorization-mode is a comma list. kubeadm enables Node and RBAC.

  • Node: kubelets may only touch objects for their own node (Pods bound here, that node’s leases).
  • RBAC: The mode you configure with Role and ClusterRole.
  • AlwaysAllow would skip the check. You will not enable that on purpose.


RBAC

Four objects:

Object

Scope

Binds

Role

One namespace

Rules only

RoleBinding

One namespace

A subject to a Role or ClusterRole, in this namespace

ClusterRole

Cluster

Rules that can be reused, or that name cluster-scoped resources

ClusterRoleBinding

Cluster

A subject to a ClusterRole, in every namespace

A ClusterRole referenced by a RoleBinding grants those rules only in the binding’s namespace. That is how the built-in edit and view ClusterRoles are applied per namespace.

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: shop
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: shop
subjects:
- kind: User
  name: alice
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io


  • roleRef is immutable. A typo means delete the binding and create it again.
  • subjects[].name is case-sensitive and must match the certificate CN or the ServiceAccount name.
  • ServiceAccount subject: kind: ServiceAccount, name: deployer, namespace: shop. Omit apiGroup (it is "" for ServiceAccounts).
  • Group subject: kind: Group, name: developers, apiGroup: rbac.authorization.k8s.io. This matches the O= from the client certificate.
  • resourceNames: Optional allow-list of object names. resourceNames: ["blue"] lets the verbs apply only to that object. list still returns an empty list for other names rather than a useful catalog; get on any other name is forbidden.
  • Verbs: get, list, watch, create, update, patch, delete, deletecollection. Wildcard * exists and is how cluster-admin is written.
  • Resources * and apiGroups *` together grant everything in RBAC. Prefer a built-in ClusterRole when that is really what you want.

Built-in ClusterRoles you can bind and should not reinvent:

ClusterRole

Rough scope

cluster-admin

All verbs on all resources, all namespaces

admin

All namespaced objects inside one namespace, including Roles

edit

Workloads, services, config in one namespace. No Role changes

view

Read namespaced objects. Secrets are excluded


bash
kubectl get role,rolebinding -n shop
kubectl describe role pod-reader -n shop
kubectl auth can-i create deployments -n shop
kubectl auth can-i delete nodes --as alice
kubectl auth can-i list pods -n shop --as system:serviceaccount:shop:deployer


auth can-i asks the API and prints yes or no. Run it as the user or ServiceAccount you just bound before you treat the Role as done.

Cluster-scoped example:


yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-reader
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: alice-nodes
subjects:
- kind: User
  name: alice
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: node-reader
  apiGroup: rbac.authorization.k8s.io


Nodes are cluster-scoped. A Role in shop cannot grant nodes.


Security context

Settings that change the process Linux sees. Pod-level fields are defaults. Container-level fields override them.


yaml
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: myapp:1.2
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"]


Field

Effect

runAsUser / runAsGroup

UID and GID of the process

runAsNonRoot

Kubelet refuses to start the container if the image would run as UID 0

fsGroup

Volume files are group-owned by this GID so the process can read a PVC

privileged

Essentially host root inside the container. Rare and broad

allowPrivilegeEscalation

Whether the process may gain privileges (no setuid climb when false)

readOnlyRootFilesystem

Root FS is read-only. Pair with an emptyDir mounted on /tmp

capabilities

Fine-grained root powers. Drop ALL, add back only what you need

hostNetwork / hostPID / hostIPC

Share the node’s namespaces. hostNetwork Pods use the node IP

Pod Security (the replacement for PodSecurityPolicy) is a built-in admission controller. Label a namespace:


bash
kubectl label namespace shop \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/enforce-version=latest


Profiles: privileged (no restrictions), baseline (blocks host namespaces, privileged containers, and common breakouts), restricted (adds non-root, dropped capabilities, seccomp). enforce rejects. warn and audit report.


ServiceAccounts and image pull

  • Default: Every namespace has a default ServiceAccount. Pods that omit spec.serviceAccountName use it.
  • Token: Projected into the Pod at /var/run/secrets/kubernetes.io/serviceaccount/. automountServiceAccountToken: false on the Pod or the ServiceAccount leaves it out. Set that on workloads that never call the API.
  • Dedicated accounts: One ServiceAccount per app, bound to a narrow Role. The default account should stay near-empty of bindings.


yaml
spec:
  serviceAccountName: deployer
  automountServiceAccountToken: true
  imagePullSecrets:
  - name: regcred


  • imagePullSecrets references a Secret of type kubernetes.io/dockerconfigjson. Create it with kubectl create secret docker-registry. You can also attach imagePullSecrets to the ServiceAccount so every Pod using that account pulls from the private registry.
  • imagePullPolicy: Always (what :latest does), IfNotPresent, Never (the image must already be on the node). Never is how you run a locally loaded image with no registry pull.


NetworkPolicy

NetworkPolicy is namespace-scoped and selects Pods with podSelector. It needs a CNI that implements it (Calico, Cilium, the reference plugins that advertise NetworkPolicy). Flannel alone does not enforce it. If the plugin ignores the object, the YAML applies and traffic still flows.

Default with zero policies: all ingress and all egress are allowed.

Once a Pod is selected by any policy:

  • If that policy lists policyTypes: ["Ingress"] (or has an ingress key), ingress is allowed only by those rules. An empty ingress: [] denies all ingress.
  • Egress stays open until a selected policy lists Egress.


yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-allow-app
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes: ["Ingress", "Egress"]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: edge
    ports:
    - protocol: TCP
      port: 80
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: db
    ports:
    - protocol: TCP
      port: 5432
  - ports:                 # DNS
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53


  • podSelector with no namespaceSelector: Pods in this namespace.
  • namespaceSelector alone: All Pods in namespaces that match.
  • Both in the same from item: Pods that match the labels and live in a matching namespace.
  • Separate list items are ORed.
  • ipBlock: CIDR, with except to carve out addresses. Mixing ipBlock with podSelector in one rule is legal and easy to get wider than you meant. Prefer pod and namespace selectors for east-west traffic.
  • A deny-all ingress is podSelector: {} (every Pod in the namespace), policyTypes: ["Ingress"], and ingress: [].
  • DNS: An egress deny that forgets UDP/TCP 53 makes every name lookup fail. Allow CoreDNS (kube-system) or port 53 before you allow application ports.
  • Services: Policy is enforced on Pod IPs. You still target the Service in the app. The policy selects the backend Pods, not the Service object.

Check from a throwaway Pod (nicolaka/netshoot or busybox) with wget/nc against the target. kubectl describe networkpolicy shows the selectors. It does not show a packet counter.


Official docs