Overview
- Chapter 10 of the Kubernetes Cookbook (ServiceAccounts, RBAC, security contexts), plus Recipe 14.6 (encrypted secrets in Git) and the modern pieces the book predates: Pod Security Admission, NetworkPolicy and admission policies.
- Related: Volumes & Configuration (Secrets), Best Practices (RBAC, policy, multi-tenancy). Back to the hub: Kubernetes Cookbook
Who Can Do What: the Request Path
- Authentication: who are you? A user (via OIDC, cloud IAM or client certs; Kubernetes has no User object) or a ServiceAccount (an app identity, namespaced).
- Authorization: are you allowed? RBAC.
- Admission: is this object acceptable, and should it be tweaked? Pod Security Admission, ValidatingAdmissionPolicy, webhooks (Kyverno, Gatekeeper).
Recipes
Recipe 10.1 — Give an App Its Own Identity (ServiceAccount)
- Problem: An app talks to the Kubernetes API and needs a fine-grained identity.
- Solution:
kubectl create serviceaccount myappsa
kubectl create token myappsa --duration=1h # mint a short-lived token by handapiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
serviceAccountName: myappsa
automountServiceAccountToken: true # set false for apps that never call the API
containers:
- name: main
image: busybox:1.36
command: ["sleep", "3600"]- Discussion: The token is mounted at
/var/run/secrets/kubernetes.io/serviceaccount/token. The identity string issystem:serviceaccount:<ns>:<name>. Every namespace has adefaultSA, which Pods get if you don’t specify one. - Gotchas: Most apps never call the Kubernetes API. Turn the token off for them (
automountServiceAccountToken: false), because a leaked token is a foothold.
Changed since the book:
Tokens are now bound, projected, short-lived (auto-rotated, about 1h, tied to the Pod). Since 1.24 no long-lived token Secret is created per ServiceAccount, so the book’s
kubectl describe secret myappsa-token-…step no longer exists. Usekubectl create token. Cloud workload identity (GKE Workload Identity, EKS Pod Identity/IRSA, AKS Workload Identity) maps SAs to cloud IAM.
Source: K8s Cookbook, Recipe 10.1
Recipe 10.2 — See What Access You Have
- Problem: Can I (or this ServiceAccount) do X?
- Solution:
kubectl auth can-i create deployments -n team-a
kubectl auth can-i list secrets --as=system:serviceaccount:sec:myappsa -n sec
kubectl auth can-i --list -n sec
kubectl auth whoami # who does the API server think I am?
kubectl get roles,rolebindings -n sec
kubectl get clusterroles | grep -v system: # admin, edit, view, cluster-admin
kubectl describe clusterrole view- Discussion: RBAC’s moving parts:
- Subject: a user, group or ServiceAccount.
- Role / ClusterRole: a list of verbs on resources (
get list watch create update patch delete). - RoleBinding / ClusterRoleBinding: glue a role to subjects.
- Gotchas: The book’s tip of binding
cluster-admintokube-system:defaultto make the dashboard work is a terrible idea. It gives every Pod using that SA full control of the cluster. Never do it.
Changed since the book:
RBAC is on by default everywhere (minikube included, no
--extra-configneeded).kubectl auth whoamiis new (GA 1.28).
Source: K8s Cookbook, Recipe 10.2
Recipe 10.3 — Control Access to Resources (Role + RoleBinding)
- Problem: Let an app read Pods in its namespace and nothing else.
- Solution:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: podreader
namespace: sec
rules:
- apiGroups: [""] # "" = core group
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: podreaderbinding
namespace: sec
subjects:
- kind: ServiceAccount
name: myappsa
namespace: sec
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: podreader# imperative equivalent
kubectl create role podreader --verb=get,list,watch --resource=pods -n sec
kubectl create rolebinding podreaderbinding --role=podreader --serviceaccount=sec:myappsa -n sec- Which combo?
| Want | Use |
|---|---|
| Namespaced resources in one namespace | Role + RoleBinding |
| Same permissions reused in many namespaces | ClusterRole + RoleBinding (per namespace) |
| Cluster-scoped things (nodes, PVs) or all namespaces | ClusterRole + ClusterRoleBinding |
- Gotchas:
list/watchon Secrets = read all secrets in that namespace.create podsis effectively “run as any SA in the namespace”.roleRefis immutable, so delete and recreate the binding to change it.
Changed since the book:
rbac.authorization.k8s.io/v1beta1→v1. The book’s--role=sec:podreadershould just be--role=podreader.
Source: K8s Cookbook, Recipe 10.3 · Best Practices book, Ch. 4 & 17
Recipe 10.4 — Secure Pods (securityContext + Pod Security Admission)
- Problem: Run as non-root, drop privileges, and enforce that cluster-wide.
- Solution, part 1: harden the Pod (this passes the
restrictedprofile):
apiVersion: v1
kind: Pod
metadata:
name: secpod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 5000
fsGroup: 5000
seccompProfile:
type: RuntimeDefault
containers:
- name: shell
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]- Solution, part 2: enforce it per namespace with Pod Security Admission (built in, label-driven):
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
pod-security.kubernetes.io/enforce: restricted # reject violators
pod-security.kubernetes.io/warn: restricted # warn on kubectl apply
pod-security.kubernetes.io/audit: restricted # log to audit- Discussion: Three profiles: privileged (anything goes, for system namespaces), baseline (blocks the known escalations: hostPath, privileged, hostNetwork…) and restricted (the hardened list above). Try a label in dry-run first:
kubectl label --dry-run=server --overwrite ns payments pod-security.kubernetes.io/enforce=restrictedprints which existing Pods would violate it. - Gotchas: Many public images run as root and break under
restricted. Use non-root images (distroless,-nonroottags) or setrunAsUser. PSA only covers Pods. For custom rules (“images must come from our registry”, “every Deployment needs an owner label”) use ValidatingAdmissionPolicy (CEL, built in, GA 1.30), Kyverno or OPA Gatekeeper.
Changed since the book:
PodSecurityPolicy (PSP) was removed in 1.25. The book’s PSP pointer (and the Best Practices book’s whole PSP section) is obsolete, and Pod Security Admission is the replacement.
seccompProfilebecame a proper field (it used to be an annotation).
Source: K8s Cookbook, Recipe 10.4 · Best Practices book, Ch. 10 & 11
Modern Extra — NetworkPolicy (Pod Firewall)
- Problem: By default every Pod can talk to every Pod. You want default-deny plus explicit allows.
- Solution:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: payments
spec:
podSelector: {} # all Pods in the namespace
policyTypes: ["Ingress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: payments
spec:
podSelector:
matchLabels:
app: api
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress # auto-label on every namespace
ports:
- protocol: TCP
port: 8080- Gotchas: Policies are only enforced if your CNI supports them (Calico, Cilium yes, plain Flannel no). Rules are additive, with no “deny” rules. If you add egress default-deny, remember to allow DNS (UDP/TCP 53 to kube-system) or everything breaks mysteriously. Start with ingress rules only (Best Practices book, Ch. 9).
Recipe 14.6 — Encrypted Secrets in Git
- Problem: You want everything in Git (GitOps), including Secrets, without leaking them.
- Solution options:
-
Sealed Secrets: encrypt with the controller’s public key. Only the in-cluster controller can decrypt.
kubectl create secret generic db --from-literal=password=s3cr3t \ --dry-run=client -o yaml | kubeseal --format yaml > db-sealed.yaml kubectl apply -f db-sealed.yaml # controller creates the real Secret -
External Secrets Operator: Secrets live in AWS Secrets Manager, GCP Secret Manager, Azure Key Vault or HashiCorp Vault. An
ExternalSecretCR syncs them in, so nothing secret is in Git at all. -
SOPS (with age or KMS keys): encrypt values in YAML files. Flux and Argo CD plugins decrypt at deploy time.
-
Secrets Store CSI Driver: mount secrets from the vault as files without creating a Secret object.
-
- Gotchas: A SealedSecret is bound to its namespace and name by default. Back up the controller’s private key, or you can’t decrypt after a cluster rebuild.
Changed since the book:
--dry-run→--dry-run=client,kubeseal --format yaml, and install via Helm. Thev1alpha1sealed-secrets API is current but the project is much more mature. ESO didn’t exist in 2018.
Source: K8s Cookbook, Recipe 14.6 · Best Practices book, Ch. 4
Summary
- Identity: ServiceAccounts with short-lived projected tokens (
kubectl create token). Disable automount when an app doesn’t need the API. - Authorization: RBAC
v1, least privilege, and check withkubectl auth can-i. Never hand outcluster-admincasually. - Pod hardening:
runAsNonRoot, drop ALL capabilities, no privilege escalation,RuntimeDefaultseccomp. Enforce with Pod Security Admission labels (PSP is gone). - Network: default-deny NetworkPolicies + explicit allows (needs a policy-capable CNI).
- Secrets in Git: Sealed Secrets / External Secrets / SOPS, never plain base64.