Security for engineers · Lesson 9 of 11 · about 35 minutes

Least privilege in Kubernetes

Lesson 1's findings, fixed: nginx running as root, two monitoring tools that could read every Secret, and a bookshop any pod could reach. Pod Security Standards say what's wrong with every namespace; one pull request makes staging's nginx non-root, read-only and without capabilities, cuts Alloy and Grafana down to what they use, and lets only monitoring connect. On the way: a binding Kubernetes won't change, a script that called the change done before it started, and a log collector left with no access at all.

By the end you'll know

  • What least privilege means for a pod, a service account and a network
  • How to check any namespace against the Pod Security Standards, without changing it
  • What securityContext settings take away from a container, and how to see the difference from inside
  • How to find out who can read Secrets, and why, and replace a chart's role with a smaller one
  • How a NetworkPolicy limits who can connect, and how to prove it does
  • Why taking access away needs the same care as giving it

Before you start

  • You need: the cluster and Argo CD from the earlier tracks, and Lesson 8 done (the bookshop now serves HTTPS on 443, a port below 1024).
  • This lesson changes the cluster through one pull request, for staging and for monitoring. Production's bookshop is unchanged, so it shows the difference.
  • Maps to Security+: domain 3 (secure configuration, segmentation) and domain 4 (identity and access management, least privilege).

The short version

  • Before: nginx ran as root with Docker's default capabilities, no system call filter, a writable file system and an API token it never used. Alloy and Grafana could read every Secret. Any pod could reach both bookshops, Alertmanager, Loki and Grafana.
  • After, in staging: nginx runs as user 101 with no capabilities, a system call filter, a read-only file system and no token; the namespace refuses any pod that doesn't meet the restricted standard; only monitoring can connect.
  • Monitoring: Alloy can read pod logs and nothing else; Grafana can read ConfigMaps, not Secrets. 12 accounts could read every Secret; now 10. Dashboards, logs and probes all still work.
  • On the way: "cannot change roleRef", and 22 forbidden errors from Alloy while it had no role at all.

What least privilege means

Least privilege is giving everything only the access it needs to do its job, and no more. It doesn't stop an attacker getting in; it limits what they get when they do. If the bookshop's nginx is ever compromised, the question is what that nginx could do next, and the answer should be "very little".

In Kubernetes, that's three layers:

  1. The container: which user it runs as, which Linux powers it has, what it can write.
  2. The service account: what it can ask the Kubernetes API to do.
  3. The network: who can connect to it, and who it can connect to.

Lesson 1 found a problem on each. This lesson fixes them for staging, checking each one from inside, from outside, and through the API.

1. Pod Security Standards: what each namespace would fail

Kubernetes has three built-in Pod Security Standards:

  • privileged: no limits.
  • baseline: blocks the known ways out of a container: host access, privileged mode, extra capabilities.
  • restricted: baseline plus current best practice: non-root, no privilege escalation, every capability dropped, a system call filter.

A namespace opts in with a label, and Pod Security Admission, built into the API server, enforces it. Before turning it on, a dry run asks the server what it would reject, without changing anything:

BASH
kubectl label --dry-run=server --overwrite ns NAMESPACE pod-security.kubernetes.io/enforce=restricted
OUTPUT
--- baseline
  argocd               passes
  bookshop             passes
  bookshop-staging     passes
  ...
  kube-system          4 pods fail
      etcd-gitops-control-plane (and 3 other pods): host namespaces, hostPath volumes, hostPort, probe or lifecycle host
      kindnet-bn9n5 (and 1 other pod): non-default capabilities, host namespaces, hostPath volumes
      kube-proxy-dcng6 (and 1 other pod): host namespaces, hostPath volumes, privileged
  monitoring           2 pods fail
      monitoring-prometheus-node-exporter-sdtsm (and 1 other pod): host namespaces, hostPath volumes, hostPort

--- restricted
  argocd               passes
  bookshop             2 pods fail
      bookshop-65d8d9ff66-2krfn (and 1 other pod): allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
  bookshop-staging     1 pods fail
  ...
  monitoring           3 pods fail
      alert-receiver-57b6c5c4b-w8hkl (and 1 other pod): allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile

How to read it:

  • kube-system and the node exporter fail even baseline, and that's correct. The control plane, the network plugin and a tool that reads each node's own statistics need the host. Those namespaces stay at privileged on purpose, with as little else in them as possible.
  • Argo CD passes restricted already. Its charts were built that way.
  • The bookshops fail restricted on the same four things: privilege escalation, capabilities, root, and no system call filter. That's what this lesson fixes in staging.

2. Inside the bookshop's nginx, before

OUTPUT
$ id
uid=0(root) gid=0(root) groups=0(root)

$ grep -E "^(CapEff|NoNewPrivs|Seccomp):" /proc/1/status
CapEff:	00000000a80425fb
NoNewPrivs:	0
Seccomp:	0

$ touch /etc/nginx/written-by-an-attacker
wrote to /etc/nginx

$ ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt
namespace
token
  • uid=0: root inside the container. Not root on the node, but one container escape bug away from it.
  • CapEff: the process's effective capabilities, the pieces root's power is split into (changing file ownership, binding low ports, sending raw network packets). a80425fb is the default set container runtimes give every container: fourteen capabilities nginx doesn't need once it has started.
  • NoNewPrivs: 0: the process could gain more privileges, through a setuid program for instance.
  • Seccomp: 0: no seccomp filter, so every one of the kernel's hundreds of system calls is available. Most container escapes go through a system call ordinary programs never use.
  • A writable file system: an attacker could change nginx's configuration or drop tools onto the disk.
  • A token for the Kubernetes API, which it never uses. (Its service account can't do anything today. A token nobody needs is still one more thing to steal, and one role change away from mattering.)

3. Who can read every Secret, and why

Every service account in the cluster, asked the same question:

BASH
kubectl auth can-i list secrets -A --as=system:serviceaccount:NAMESPACE:NAME
OUTPUT
  argocd:argocd-application-controller
  argocd:argocd-applicationset-controller
  cert-manager:cert-manager
  cert-manager:cert-manager-cainjector
  kube-system:generic-garbage-collector
  kube-system:namespace-controller
  kube-system:resourcequota-controller
  kube-system:sealed-secrets-controller
  monitoring:alloy
  monitoring:monitoring-grafana
  monitoring:monitoring-kube-prometheus-operator
  monitoring:monitoring-kube-state-metrics

Twelve. Most need it: Argo CD applies Secrets, cert-manager and Sealed Secrets make them, and the controllers in kube-system are Kubernetes itself. Two stand out, and the roles behind them say why:

OUTPUT
--- monitoring:alloy
ClusterRoleBinding alloy -> ClusterRole alloy
    core: pods,pods/log,namespaces [get,list,watch]
    ...
    core: configmaps,secrets [get,list,watch]
    ...

--- monitoring:monitoring-grafana
ClusterRoleBinding monitoring-grafana-clusterrolebinding -> ClusterRole monitoring-grafana-clusterrole
    core: configmaps,secrets [get,watch,list]

grafana-sc-dashboard: METHOD=WATCH  LABEL=grafana_dashboard  RESOURCE=both  NAMESPACE=ALL
grafana-sc-datasources: METHOD=WATCH  LABEL=grafana_datasource  RESOURCE=both
  • Alloy's chart grants thirteen rules, enough for every feature Alloy has. This configuration uses one of them: finding pods and reading their logs. It mentions Secrets zero times.
  • Grafana's role exists for its sidecars, the small containers that load dashboards and data sources from labelled objects. They're set to RESOURCE=both: ConfigMaps and Secrets, in every namespace. Every dashboard here is a ConfigMap.

That's the usual story: charts grant enough for every option, so the defaults work for everyone. Least privilege means narrowing them to what your setup uses. And since Lesson 8, "every Secret" includes the practice CA's private key.

4. What an ordinary pod can reach

OUTPUT
$ kubectl get networkpolicies -A
NAMESPACE   NAME                                              POD-SELECTOR
argocd      argocd-application-controller-network-policy      app.kubernetes.io/name=argocd-application-controller
...
argocd      argocd-server-network-policy                      app.kubernetes.io/name=argocd-server

  http://bookshop.bookshop-staging.svc/                                       200
  http://bookshop.bookshop.svc/                                               200
  http://monitoring-kube-prometheus-alertmanager.monitoring.svc:9093/api/v2/status 200
  http://loki.monitoring.svc:3100/ready                                       200
  http://monitoring-grafana.monitoring.svc/api/health                         200

From a pod in default, everything answers. Kubernetes networking is open by default: any pod can connect to any other, in any namespace. A NetworkPolicy changes that for the pods it selects. Argo CD ships its own; nothing else has one.

5. The change

One pull request, checked before it was opened.

First, do the charts support what this relies on? The script downloaded the exact chart versions the cluster runs and searched them:

OUTPUT
kube-prometheus-stack/charts/grafana/values.yaml:26:  # useExistingClusterRole: name-of-some-clusterRole
kube-prometheus-stack/charts/grafana/templates/clusterrolebinding.yaml:18:  {{- if .Values.rbac.useExistingClusterRole }}
kube-prometheus-stack/charts/grafana/templates/clusterrolebinding.yaml:19:  name: {{ .Values.rbac.useExistingClusterRole }}
177:rbac:
178-  # -- Whether to create RBAC resources for Alloy.

Both settings exist. Checking the chart's source beats trusting memory or a blog post: values change between versions.

The bookshop: a hardened option, on in staging

YAML
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 101     # the nginx user in the nginx image
        runAsGroup: 101
        seccompProfile:
          type: RuntimeDefault
        sysctls:
          # Lets a non-root process listen on ports below 1024 (80 and 443), in this pod only.
          - name: net.ipv4.ip_unprivileged_port_start
            value: "0"
  • automountServiceAccountToken: false: no API token in the pod.
  • runAsNonRoot and runAsUser: 101: run as the image's own nginx user, and refuse to start as root.
  • seccompProfile: RuntimeDefault: the container runtime's standard system call filter, which blocks the rarely used, risky calls.
  • The sysctl: ports below 1024 have traditionally needed root. This kernel setting, for this pod's network only, lets any user bind them, so nginx keeps listening on 80 and 443 without root or a capability. (The other way is moving nginx to 8080 and 8443 and changing the Service.)

Then, for each container:

YAML
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]

And three small writable folders, because nginx does need to write somewhere: its cache, its process ID file, and temporary files. Each is an emptyDir, an empty folder that exists only as long as the pod:

YAML
            - name: cache
              mountPath: /var/cache/nginx
            - name: run
              mountPath: /run
            - name: tmp
              mountPath: /tmp

Who may connect: a NetworkPolicy

YAML
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: bookshop
spec:
  podSelector:
    matchLabels:
      app: bookshop
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
  • podSelector: this policy applies to the bookshop's pods.
  • policyTypes: [Ingress]: it controls incoming connections. Once a pod is selected by any ingress policy, everything not allowed is refused.
  • from the monitoring namespace: Prometheus scrapes the bookshop, and the blackbox exporter probes it. Nothing else needs to. (A real shop would also allow its ingress controller.)

The namespace enforces the restricted standard

In staging's Argo CD Application, so Argo CD labels the namespace:

YAML
    managedNamespaceMetadata:
      labels:
        pod-security.kubernetes.io/enforce: restricted
        pod-security.kubernetes.io/warn: restricted

Alloy and Grafana: smaller roles

Alloy's chart is told not to make its own role (rbac.create: false), and a new file in apps/monitoring gives it one with just what reading logs takes:

YAML
kind: ClusterRole
metadata:
  name: alloy-read-logs
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "namespaces"]
    verbs: ["get", "list", "watch"]

Grafana's sidecars are told to load from ConfigMaps only (resource: configmap), and its chart is pointed at a new role that can read ConfigMaps and nothing else (rbac.useExistingClusterRole: grafana-read-configmaps).

Checked before committing

OUTPUT
Production (should print nothing):
(nothing: production unchanged)

$ helm template ... values-staging.yaml --show-only templates/deployment.yaml | kubectl apply --dry-run=server -n l9-check -f -
deployment.apps/bookshop created (server dry run)
(and production's, for comparison)

$ helm template ... values-production.yaml --show-only templates/deployment.yaml | kubectl apply --dry-run=server -n l9-check -f -
Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (containers "nginx", "exporter" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (containers "nginx", "exporter" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or containers "nginx", "exporter" must set securityContext.runAsNonRoot=true), seccompProfile (pod or containers "nginx", "exporter" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

The API server judged both Deployments against the restricted standard, in a throwaway namespace labelled to warn (created for the check, deleted after). Staging's: no warnings. Production's: every one. The pull request went in as #76.

6. Applying it: three things that went wrong

"Synced" isn't "done"

The first check after the merge said everything was ready, instantly:

OUTPUT
  monitoring-config Synced Healthy       after 0s
  alloy             Synced Healthy       after 0s
  monitoring        Synced Healthy       after 0s
  bookshop-staging  Synced Healthy       after 0s

Argo CD hadn't noticed the merge yet, so every app was synced to the previous commit. Nginx was still root, the namespace had no labels. The script was fixed to wait until each app reported the merge commit as its revision, not just "Synced".

Kubernetes won't change a binding's role

OUTPUT
  monitoring        OutOfSync Progressing revision 92.18.0      after 381s

Argo CD couldn't change Grafana's binding:
  one or more synchronization tasks completed unsuccessfully, reason: ClusterRoleBinding.rbac.authorization.k8s.io "monitoring-grafana-clusterrolebinding" is invalid: roleRef: Invalid value: {"APIGroup":"rbac.authorization.k8s.io","Kind":"ClusterRole","Name":"grafana-read-configmaps"}: cannot change roleRef. Retrying attempt #5 at 10:27PM.

$ kubectl delete clusterrolebinding monitoring-grafana-clusterrolebinding
clusterrolebinding.rbac.authorization.k8s.io "monitoring-grafana-clusterrolebinding" deleted
  monitoring        Synced Progressing after 566s

A binding's roleRef can't be changed after it's created, on purpose: otherwise anyone allowed to edit bindings could quietly point one at cluster-admin. The only way is to delete it and make it again. Argo CD retried five times, then the binding was deleted, and Argo CD recreated it from Git, pointing at the new role. (Argo CD can do this itself, with the Replace=true sync option on that resource.)

A log collector with no access at all

During the first, too-early check, Alloy's log said:

OUTPUT
$ kubectl logs -n monitoring deploy/alloy -c alloy --since=5m | grep -ciE 'forbidden'
22

  alloy                list secrets                 no
  alloy                list configmaps              no
  alloy                get pods --subresource=log   no

Alloy couldn't read logs either. Two Argo CD apps were involved: alloy (which removed the chart's role) and monitoring-config (which adds the new one). The first synced before the second, so for a while Alloy had no role at all. In a real cluster, that's a gap in the logs, exactly when nobody's looking. The Spot the problem below is about this.

7. After

Once everything had applied:

OUTPUT
$ kubectl get ns bookshop-staging --show-labels
bookshop-staging   Active   2d23h   kubernetes.io/metadata.name=bookshop-staging,pod-security.kubernetes.io/enforce=restricted,pod-security.kubernetes.io/warn=restricted

Inside nginx

OUTPUT
$ id
uid=101(nginx) gid=101(nginx) groups=101(nginx)

$ grep -E "^(CapEff|NoNewPrivs|Seccomp):" /proc/1/status
CapEff:	0000000000000000
NoNewPrivs:	1
Seccomp:	2

$ touch /etc/nginx/written-by-an-attacker; touch /tmp/scratch && echo "/tmp is writable (an emptyDir)"
touch: cannot touch '/etc/nginx/written-by-an-attacker': Read-only file system
/tmp is writable (an emptyDir)

$ ls /var/run/secrets/kubernetes.io/serviceaccount/
ls: cannot access '/var/run/secrets/kubernetes.io/serviceaccount/': No such file or directory
  • User 101, not root.
  • CapEff: 0: no capabilities at all.
  • NoNewPrivs: 1: it can never gain more.
  • Seccomp: 2: a filter is active (2 means "filter"; 0 was "none").
  • Read-only, except the three emptyDir folders.
  • No token.

nginx's own log noticed two things, both harmless:

OUTPUT
10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)
...
nginx: [warn] the "user" directive makes sense only if the master process runs with super-user privileges, ignored in /etc/nginx/nginx.conf:2

The image's start-up script tries to edit its own configuration, can't, and carries on. And nginx's configuration says to switch to the nginx user, which it already is.

The namespace refuses what doesn't meet the standard

OUTPUT
$ kubectl run root-test -n bookshop-staging --image=nginx:1.29 --dry-run=server
Error from server (Forbidden): pods "root-test" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "root-test" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "root-test" must set securityContext.capabil...

A plain nginx pod, the way most people would start one, is now forbidden in staging. That's what keeps the next change from quietly undoing this one.

Who can reach the bookshop

OUTPUT
  from default     http://bookshop.bookshop-staging.svc/    000
  from default     https://bookshop.bookshop-staging.svc/   000
  from default     http://bookshop.bookshop.svc/            200
  from monitoring  http://bookshop.bookshop-staging.svc/    200
  from monitoring  https://bookshop.bookshop-staging.svc/   200
  from monitoring  http://bookshop.bookshop.svc/            200

From default, staging times out (000, curl's exit code 28); from monitoring, it answers. The connection isn't refused, it's dropped, so the caller waits. And the policy is enforced: kind's network plugin, kindnet, supports NetworkPolicies. Not every network plugin does, and a policy nothing enforces looks exactly like one that works until you test it. Production, untouched, still answers everyone.

Who can read every Secret

OUTPUT
  argocd:argocd-application-controller
  argocd:argocd-applicationset-controller
  cert-manager:cert-manager
  cert-manager:cert-manager-cainjector
  kube-system:generic-garbage-collector
  kube-system:namespace-controller
  kube-system:resourcequota-controller
  kube-system:sealed-secrets-controller
  monitoring:monitoring-kube-prometheus-operator
  monitoring:monitoring-kube-state-metrics

  alloy                list secrets                 no
  alloy                list configmaps              no
  alloy                get pods --subresource=log   yes
  monitoring-grafana   list secrets                 no
  monitoring-grafana   list configmaps              yes
  monitoring-grafana   get pods --subresource=log   no

Twelve down to ten, and each of the two can do exactly its job. The two left in monitoring are worth a look next: the Prometheus operator manages Alertmanager's configuration Secrets, so it needs some access; kube-state-metrics reports on Secrets (names and types, for dashboards) and can be told not to.

Does monitoring still work?

OUTPUT
$ kubectl exec -n monitoring deploy/monitoring-grafana -c grafana-sc-dashboard -- ls /tmp/dashboards | wc -l
      26

$ kubectl logs -n monitoring deploy/monitoring-grafana -c grafana-sc-dashboard --since=3m | grep -ciE 'forbidden|error'
2

$ kubectl logs -n monitoring deploy/alloy -c alloy --since=2m | grep -ciE 'forbidden'
0

$ curl ... loki ... 'sum(count_over_time({namespace="bookshop-staging"}[5m]))'
62

up bookshop 10.244.1.90:9113 = 1
probe_success bookshop-banner http://bookshop.bookshop-staging.svc/ = 1
probe_success bookshop-up http://bookshop.bookshop-staging.svc/ = 1
  • Grafana: 26 dashboards loaded from ConfigMaps. Two log lines matched "error", and they were worth reading rather than assuming:
OUTPUT
Traceback (most recent call last):
  File "/app/.venv/lib/python3.14/site-packages/healthz.py", line 179, in _create_health_http_server
    return ThreadingHTTPServer(("", health_port), HealthHandler)
  ...
OSError: [Errno 98] Address in use

Not a permission problem (that would say Forbidden). The dashboard sidecar starts a small health-check server, and so does the data-source sidecar next to it. Containers in a pod share one network, so the second to start finds the port taken. The data-source sidecar's log had none. A quirk of how the two sidecars are set up, not of this change, and nothing to do with loading dashboards.

  • Alloy: no forbidden errors, and Loki has staging's logs.
  • Prometheus: still scraping the bookshop's exporter, and both blackbox probes succeed, through the NetworkPolicy, from monitoring.

Taking access away is only finished when what depended on it is shown to still work.

In real systems

  • Enforce the standards everywhere you can, and warn everywhere else. restricted for applications, baseline as the floor, privileged only for system namespaces, with warn and audit labels set ahead of enforce so problems show up before they block anything.
  • Policy engines go further. Kyverno and OPA Gatekeeper enforce rules the built-in standards don't cover: allowed registries (Lesson 6's KSV-0125), required labels, no latest tags.
  • Start network policies with a default deny in every namespace, then allow what's needed: DNS, the ingress controller, monitoring. Egress policies limit what a compromised pod can reach, too.
  • Images built to run as non-root (such as nginxinc/nginx-unprivileged or Chainguard's, from Lesson 6) make this easier: no sysctl, no warnings.
  • Review RBAC regularly. kubectl auth can-i --list, and tools like rbac-tool or kubectl-who-can, show who can do what. Charts change their defaults between versions.
  • Expand, then contract. Grant the new access, check it works, then remove the old, in separate changes.

Spot the problem

Work out what's wrong before you read the explanation.

The tidy-up that blinded the alerts. A team reviews their log collector's permissions, sees it can read every Secret, and opens one pull request: turn off the chart's role, add a smaller one in their own config repository. It's reviewed, merged, and Argo CD applies it. Everything is green within ten minutes. A week later, during an incident investigation, they find twenty minutes of missing logs from the morning of the change, across every namespace. What happened? The old access was removed before the new access existed. Two Argo CD apps were involved, and they synced in whatever order Argo CD reached them. The collector's chart app removed its role first, and the config app with the new role followed later. In between, the collector could read nothing, and said so only in its own log, which it couldn't ship. This lesson had exactly that gap: 22 forbidden errors from Alloy, with "get pods/log: no". Three fixes: expand, then contract: add the new role in one change, check it works, and only then remove the old one; when one app has to go first, make it explicit (one app, with sync waves, or the order of merges); and alert on the collector's own health, such as Alloy's errors or a sudden drop in log volume, so a gap pages someone instead of waiting to be found.

Keep this

ToUse
Check a namespace against a standard, changing nothingkubectl label --dry-run=server --overwrite ns NS pod-security.kubernetes.io/enforce=restricted
Enforce itNamespace labels pod-security.kubernetes.io/enforce and warn
See a process's privilegesgrep -E '^(CapEff|NoNewPrivs|Seccomp):' /proc/1/status
Run as non-root, with no extra powersrunAsNonRoot, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], seccompProfile: RuntimeDefault
Read-only, with places to writereadOnlyRootFilesystem: true, plus emptyDir volumes
No API tokenautomountServiceAccountToken: false
Low ports without rootsysctl net.ipv4.ip_unprivileged_port_start: "0"
Can an account do something?kubectl auth can-i VERB RESOURCE -A --as=system:serviceaccount:NS:NAME
Everything an account can dokubectl auth can-i --list --as=...
Limit who can connectA NetworkPolicy with podSelector and ingress.from
Change a binding's roleDelete and recreate it (roleRef can't change)

Where you are now

You can check any namespace against the Pod Security Standards, take a container down to only what it needs, narrow a chart's role to what your setup uses, and limit who can connect, and prove each one from inside and outside. Staging is done; production is one values file away, and the same pull request pattern. The next lesson asks the question this one raised: when something does go wrong, would the logs show it? Logging for security, starting with the Kubernetes audit log Lesson 1 found missing.

Check yourself

  1. kube-system fails even the baseline standard. What should you do about it?

  2. What did CapEff: 0000000000000000 show after the change?

  3. How does non-root nginx still listen on ports 80 and 443?

  4. Why did Argo CD fail to update Grafana's ClusterRoleBinding?

  5. From default, the staging bookshop returned 000. What does that mean?

  6. Why did Alloy hit 'forbidden' errors during the change?

Key terms

  • Least privilege: giving everything only the access its job needs.
  • Pod Security Standards: Kubernetes's three built-in levels for pods: privileged, baseline and restricted.
  • Pod Security Admission: the part of the API server that enforces them, by namespace label.
  • securityContext: the settings that decide a pod's or container's user, capabilities and file system access.
  • Capabilities: the separate pieces root's power is divided into in Linux.
  • seccomp: a filter on which system calls a process may make.
  • emptyDir: an empty folder that lives as long as its pod.
  • RBAC: role-based access control: Roles and ClusterRoles say what's allowed, bindings say who gets them.
  • NetworkPolicy: rules for which pods may connect to which.
  • Expand and contract: add the new thing, move to it, then remove the old one.

Go deeper