Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

Container Security

Runtime hardening for containerised workloads: Pod Security Admission, security contexts, Falco runtime detection, network isolation, and secret encryption at rest.

Container Security

Hardening containers at deploy and run time — the controls that apply after an image has been built and scanned.

Overview

Container security splits cleanly into two halves. The build-time half asks "what is in this image, and who made it": scanning, SBOMs, signing, provenance. The run-time half asks "what can this container do once it starts": what it runs as, which capabilities it holds, which syscalls it may issue, what it can reach on the network, and what it can read out of the cluster.

This sheet covers the second half. The first half has its own sheets, and the split matters — a clean scan says nothing about a container running as root with hostPID: true.

Run time - this sheetFalco detectionSecret encryption atrestDeploy time - this sheetPod SecurityAdmissionSecurity contextsNetwork policyBuild time - covered elsewhereImage scanningSBOM generationSigning andprovenanceRun time - this sheetFalco detectionSecret encryption atrestDeploy time - this sheetPod SecurityAdmissionSecurity contextsNetwork policyBuild time - covered elsewhereImage scanningSBOM generationSigning andprovenance

Where each control lives

This sheet deliberately does not restate what other sheets own. Use the table to jump.

Control Sheet
Image vulnerability scanning Trivy, Grype and Syft, Clair
SBOM generation and formats Grype and Syft, Trivy
Image signing, provenance, SLSA Container Image Optimisation, Container Image Building
Registry-side policy and mirroring Container Registries, zot Registry
RBAC, service accounts, multi-tenancy Kubernetes RBAC and Multi-Tenancy
Rego policy, Gatekeeper, Conftest Policy as Code (OPA/Conftest)
Secret storage and injection HashiCorp Vault, SOPS
NetworkPolicy in depth, CNI behaviour Kubernetes Networking, Flannel and Calico
MAC labels, container contexts SELinux/AppArmor
Rootless containers, user namespaces Podman, Container Image Building
systemd-level sandboxing systemd Hardening

Pod Security Admission

Pod Security Admission (PSA) is the in-tree admission controller that enforces the three Pod Security Standards. It replaced PodSecurityPolicy, which was removed in Kubernetes 1.25.

The three standards

Level Blocks Use for
privileged Nothing System components, CNI and CSI daemonsets
baseline Known privilege escalations — hostPID, hostNetwork, privileged, and all hostPath volumes General workloads being migrated
restricted The above plus running as root, unset seccomp, undropped capabilities, and non-API volume types Everything you write yourself

restricted requires all of: runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL], and a seccompProfile of RuntimeDefault or Localhost. Volumes are limited to the eight API- and CSI-mediated types — configMap, csi, downwardAPI, emptyDir, ephemeral, persistentVolumeClaim, projected, secret — which is a good deal broader than it is usually described: PVCs and Secrets are fine.

restricted does not require readOnlyRootFilesystem. It is worth setting, and it appears in the compliant example below, but PSA will not reject a pod for a writable root filesystem at any level. Do not rely on the standard to enforce it — that needs Gatekeeper, Kyverno, or a ValidatingAdmissionPolicy.

Namespace labels

PSA is configured per namespace, with three independent modes. Each takes an optional version pin.

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    # Reject violating pods
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.37
    # Return a warning to the client, but admit
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.37
    # Record a violation in the audit log, but admit
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.37

Pinning -version to a release rather than latest is the safer default: latest means the standard's definition can tighten under you at the next cluster upgrade and start rejecting workloads that were admitted yesterday.

Rolling it out without breaking production

Set warn and audit first, leave enforce unset, and read the warnings for a release cycle. To find out what a label change would reject before making it, use a server-side dry run — this evaluates every pod already running in the namespace:

# Reports each existing pod that would violate the new level
kubectl label --dry-run=server --overwrite ns production \
  pod-security.kubernetes.io/enforce=restricted

Cluster-wide defaults

Namespace labels are opt-in, so an unlabelled namespace is unrestricted. To set a floor for the whole cluster, configure the admission plugin on the API server.

# /etc/kubernetes/admission/pod-security.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
  configuration:
    apiVersion: pod-security.admission.config.k8s.io/v1
    kind: PodSecurityConfiguration
    defaults:
      enforce: "baseline"
      enforce-version: "latest"
      audit: "restricted"
      audit-version: "latest"
      warn: "restricted"
      warn-version: "latest"
    exemptions:
      usernames: []
      runtimeClasses: []
      namespaces: [kube-system]

Point the API server at it with --admission-control-config-file=/etc/kubernetes/admission/pod-security.yaml.

These are fallbacks for unlabelled namespaces, not a floor. A namespace label replaces the configured default for that mode, and privileged is a legal value — so any namespace can set pod-security.kubernetes.io/enforce: privileged and opt straight out of a baseline cluster default. PSA has no mechanism for a minimum. If you need one that cannot be weakened, the label itself has to be governed by a separate admission policy (Gatekeeper, Kyverno, or a ValidatingAdmissionPolicy over Namespace objects). Treating this block as a security guarantee is the mistake it invites.

Exemptions are evaluated before the standards, and namespaces: [kube-system] is close to mandatory — the control-plane components in it are legitimately privileged.

The failure mode that wastes an afternoon

PSA evaluates pods, not the workloads that create them. A Deployment whose pod template violates the enforced level is still admitted — it is the ReplicaSet controller that then fails, repeatedly, to create pods. kubectl get deployment shows a healthy object with zero ready replicas.

kubectl does print a warning when it submits the object:

Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false ...
deployment.apps/web created

That line is easy to lose in CI output, and it is not printed at all when the object is created by a controller, an operator, or a GitOps reconciler rather than by kubectl. So the warning is a courtesy, not a gate — the object is created either way, and the enforcement happens one level down.

# The Deployment looks fine. The error is one level down.
kubectl describe rs -n production -l app=myapp | grep -A5 Events

# Or read it from the events stream directly
kubectl get events -n production --field-selector reason=FailedCreate

Security Contexts

The security context is where the actual hardening is expressed. PSA only checks that you have set these fields sensibly.

Pod-level

Applies to every container in the pod, and owns anything involving volumes.

spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 2000                    # Group ownership applied to volumes
    fsGroupChangePolicy: OnRootMismatch
    supplementalGroups: [3000]
    seccompProfile:
      type: RuntimeDefault           # Or Localhost + localhostProfile
    seLinuxOptions:
      level: "s0:c123,c456"
    appArmorProfile:
      type: RuntimeDefault
    sysctls:
    - name: net.core.somaxconn
      value: "1024"

Container-level

Overrides the pod-level value for the same field, and owns the process-level controls.

containers:
- name: app
  securityContext:
    allowPrivilegeEscalation: false  # Blocks setuid binaries and file capabilities
    readOnlyRootFilesystem: true
    privileged: false
    procMount: Default
    capabilities:
      drop: [ALL]
      add: [NET_BIND_SERVICE]        # Only if the process genuinely binds < 1024

Field reference

Field Level Effect
runAsNonRoot Both Kubelet refuses to start the container if it would run as UID 0
runAsUser / runAsGroup Both Override the image's USER
fsGroup Pod Sets group ownership on mounted volumes
fsGroupChangePolicy Pod OnRootMismatch skips the recursive chown when the top-level permissions already match
supplementalGroups Pod Extra GIDs, for shared volume access
seccompProfile Both RuntimeDefault uses the runtime's profile; Unconfined disables filtering
appArmorProfile Both Field form, Kubernetes 1.30+; older clusters use the annotation
seLinuxOptions Both MAC label — see SELinux/AppArmor
allowPrivilegeEscalation Container false sets no_new_privs, neutralising setuid binaries
readOnlyRootFilesystem Container Requires an emptyDir for every path the process writes
capabilities Container drop is applied before add
privileged Container All capabilities, all devices — equivalent to no isolation
procMount Container Unmasked exposes host /proc paths; needs privileged on most runtimes

Footguns

runAsNonRoot with a named USER. The kubelet has to decide whether the container would run as UID 0 before starting it, and it only has the image config to work from. If the image's USER is a name rather than a number, the kubelet cannot resolve it against the image's /etc/passwd and fails the pod with container has runAsNonRoot and image has non-numeric user (appuser), cannot verify user is non-root. It is not asserting the user is root — it is refusing to guess. Use a numeric UID in the Dockerfile, or set runAsUser explicitly.

fsGroup on a large volume. The default fsGroupChangePolicy: Always recursively chowns the entire volume at every mount. On a volume with millions of files this turns a pod restart into a multi-minute stall that looks like a stuck mount. OnRootMismatch checks the top-level directory first and skips the walk when it already matches.

readOnlyRootFilesystem and temp files. Most runtimes and language stacks write somewhere — /tmp, /var/run, a cache directory. Each needs an emptyDir, and the failure is usually an obscure library error rather than a clear EROFS.

Dropping ALL then adding back. drop is applied first, so drop: [ALL] with add: [NET_BIND_SERVICE] leaves exactly that one capability. Adding a capability back does not require privileged, and is far better than reaching for it.


Runtime Security with Falco

Falco watches kernel events — syscalls via eBPF or a kernel module, plus the Kubernetes audit log — and evaluates them against rules. It is a detection tool, not an enforcement one: it tells you a shell was spawned in a container, it does not stop it.

modern_ebpf probeYesNoContainer syscallsKernelFalco engineK8s audit logRule matchOutputsDropstdout / JSONFalcosidekickSlack, PagerDuty,webhookmodern_ebpf probeYesNoContainer syscallsKernelFalco engineK8s audit logRule matchOutputsDropstdout / JSONFalcosidekickSlack, PagerDuty,webhook

Installation

# Kubernetes, via Helm
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco \
  --namespace falco --create-namespace \
  --set driver.kind=modern_ebpf \
  --set falcosidekick.enabled=true
# Debian/Ubuntu. Note the keyring: apt-key was removed in Debian 12 and Ubuntu 22.04.
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc |
  gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" |
  tee /etc/apt/sources.list.d/falcosecurity.list
apt-get update && apt-get install -y falco

systemctl enable --now falco

The Debian package prompts during install about enabling automatic ruleset updates. Accepting it has falcoctl periodically pull and install new rule artefacts, which is usually what you want — but it means the rules on disk are not the ones you shipped, so pin or disable it if that matters.

Choosing a driver

There are two, and only two. The legacy eBPF probe was removed outright in Falco 0.44.0, completing a deprecation begun in 0.43.0 — libs no longer builds it and falcoctl driver no longer offers operations on it.

Driver Requires Notes
modern_ebpf Kernel 5.8+ with BTF CO-RE, so no per-kernel probe to compile or download. The one to pick.
kmod Kernel headers, module loading permitted The fallback when the kernel is too old for CO-RE. Highest privilege, and a kernel panic surface.

The Helm chart's driver.kind defaults to auto, which selects between the two for you. Set it explicitly only when you need to override that choice. On a host install the equivalent key is engine.kind in falco.yaml, which accepts kmod, modern_ebpf, nodriver and replay — the last two being for plugin-only and capture-file operation, not live kernel capture.

A great deal of material online — including the previous version of this sheet — still recommends driver.kind=ebpf. That value no longer resolves to anything, and unlike a stale config key it is fatal rather than ignored:

Error: Error reading config file (/etc/falco/falco.yaml): engine.kind 'ebpf' is not a valid kind.

If the kernel cannot run modern_ebpf, the fallback is kmod, not the legacy probe.

Rules

Rules live in /etc/falco/rules.d/. Never edit the shipped falco_rules.yaml — it is replaced on upgrade.

# /etc/falco/rules.d/custom-rules.yaml
- list: crypto_miners
  items: [minerd, cpuminer, xmrig, cgminer, bfgminer]

- macro: trusted_images
  condition: container.image.repository in (myregistry/sidecar, myregistry/agent)

- rule: Shell Spawned in Container
  desc: A shell was executed inside a container
  condition: >
    spawned_process and container and shell_procs
    and not trusted_images
  output: >
    Shell spawned in container
    (user=%user.name container=%container.name shell=%proc.name
     parent=%proc.pname cmdline=%proc.cmdline image=%container.image.repository)
  priority: WARNING
  tags: [container, shell, mitre_execution]

- rule: Crypto Miner Executed
  desc: A known mining binary or pool argument was seen
  condition: >
    spawned_process and container
    and (proc.name in (crypto_miners) or proc.args contains "stratum+tcp")
  output: >
    Crypto miner detected
    (user=%user.name command=%proc.cmdline container=%container.name)
  priority: CRITICAL
  tags: [container, mitre_execution]

Priorities, highest first: emergency, alert, critical, error, warning, notice, info, debug. Rules conventionally spell them in upper case; the priority key in falco.yaml uses the lower-case names above.

Tuning a noisy built-in rule

The usual reason Falco gets switched off is a default rule firing constantly on a legitimate workload. Do not disable it — narrow it. Modern Falco uses override for this; the older append: true form is deprecated and is slated for removal in Falco 1.0.0.

# Extend the shipped rule's condition rather than replacing it
- rule: Terminal shell in container
  condition: and not container.image.repository = myregistry/debug-toolbox
  override:
    condition: append

Configuration

# /etc/falco/falco.yaml
rules_files:                    # Plural. The singular rules_file is deprecated.
  - /etc/falco/falco_rules.yaml
  - /etc/falco/falco_rules.local.yaml
  - /etc/falco/rules.d

json_output: true
json_include_output_property: true
priority: notice                # Drop everything below this before rule evaluation

outputs_queue:
  capacity: 0                   # 0 is unbounded, and is the default

stdout_output:
  enabled: true

http_output:
  enabled: true
  url: http://falcosidekick:2801

metrics:
  enabled: true
  interval: 1h
  output_rule: true

webserver:
  enabled: true
  prometheus_metrics_enabled: true

Alert routing and metrics

Falcosidekick fans a single Falco output out to Slack, PagerDuty, Loki, S3 and around fifty other sinks, with a per-sink priority threshold. It is far easier than configuring outputs in Falco itself.

falcosidekick:
  enabled: true
  config:
    slack:
      webhookurl: "https://hooks.slack.com/services/XXX"
      minimumpriority: "warning"
    loki:
      hostport: "http://loki.monitoring:3100"
      minimumpriority: "notice"
# ServiceMonitor for Falco's own metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: falco
  namespace: falco
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: falco
  endpoints:
  - port: metrics
    interval: 30s

The series worth alerting on is dropped events. When the kernel ring buffer overflows, Falco silently stops seeing syscalls — detection coverage degrades with no error anywhere.


Network Isolation

The full NetworkPolicy reference belongs to Kubernetes Networking and Flannel and Calico. What follows is the security posture and the ways it silently fails.

Default deny

A namespace without policy allows everything. Start by denying both directions, then add allows.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}          # Every pod in the namespace
  policyTypes:
  - Ingress
  - Egress

Egress-deny breaks DNS immediately, which presents as every hostname failing to resolve. Allow it back explicitly:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

Four ways this goes wrong

The CNI may not implement NetworkPolicy at all. Flannel on its own does not. The API accepts the object, kubectl get networkpolicy lists it, and nothing is enforced. This is the dangerous one, because it fails open and looks exactly like success. Verify with a deliberate negative test rather than by reading the manifest.

# Prove the deny works, from a pod the policy selects. This is the test that counts.
# -i, not -it: with a TTY the API server rejects the attach ("tty and stderr cannot
# both be true") and you get "terminated (Error)" instead of the actual wget message.
kubectl run probe --rm -i --restart=Never -n production \
  --image=busybox -- wget -qO- -T3 http://blocked-service:80

# Weak corroboration only - see the caveat below
kubectl get pods -n kube-system -o name | grep -E 'calico|cilium|weave|antrea'

Do not trust the pod grep on its own. Some distributions run the policy controller inside another process rather than as a labelled pod — k3s embeds kube-router in the server process, so that grep returns nothing on a cluster that enforces policy perfectly well. A negative result there means "unknown", not "unsupported"; only the connectivity test answers the question.

Policies are additive, and there is no deny rule. A pod's permitted traffic is the union of every policy selecting it. You cannot write an exception that subtracts from an allow — one over-broad policy anywhere in the namespace undoes a careful one. Audit by selector, not by policy count.

policyTypes is per-direction. A policy listing only Ingress restricts ingress and leaves egress completely open, even though the pod is now "covered by a policy". Omitting policyTypes infers it from which rule blocks are present, which is a surprising default worth avoiding by always stating it.

Selectors are namespace-scoped by default. A bare podSelector in a from block means pods in this namespace. Reaching another namespace needs namespaceSelector, and the reliable label to match is the automatic kubernetes.io/metadata.name.


Secret Encryption at Rest

Kubernetes Secrets are base64 in etcd, not encrypted. Anyone with an etcd backup has them. Storage and injection are covered in HashiCorp Vault and SOPS; this is the cluster-level control underneath both.

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
  providers:
  # First provider is used for WRITES. Order is the whole configuration.
  - kms:
      apiVersion: v2
      name: vault-kms
      endpoint: unix:///var/run/kmsplugin/socket.sock
      timeout: 3s
  - identity: {}    # Last, so existing plaintext values can still be READ

Pass it to the API server with --encryption-provider-config=/etc/kubernetes/encryption-config.yaml.

Provider Key lives Use
kms v2 External KMS or Vault The recommendation — GA since 1.29. Key rotation without re-encrypting, and the key never sits on the control plane. KMS v1 was deprecated in 1.28 and is off by default from 1.29.
aescbc / aesgcm The config file on disk Protects an etcd backup, not a compromised control-plane node.
secretbox The config file on disk As above.
identity — Plaintext. First in the list means no encryption at all.

Three things reliably catch people out.

Order is everything. The first provider encrypts, every listed provider can decrypt. So identity first silently disables encryption while the file still looks configured.

Enabling encryption does not encrypt what is already there. Existing Secrets stay in their old form until something rewrites them:

# Force a rewrite of every Secret so it is stored under the new provider
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

identity last is a migration state, not an end state. It is there so the API server can still read the plaintext values written before the change. Leave it in place after the rewrite above and the API server goes on honouring unencrypted data indefinitely — drop it from the list once migration is done.


Quick Reference

Task Command or field
Enforce a standard on a namespace pod-security.kubernetes.io/enforce: restricted
Preview what a level would reject kubectl label --dry-run=server --overwrite ns X pod-security.kubernetes.io/enforce=restricted
Find why pods are not being created kubectl describe rs -n X -l app=Y
Minimum for restricted runAsNonRoot, allowPrivilegeEscalation: false, drop: [ALL], seccompProfile: RuntimeDefault
Skip the volume chown stall fsGroupChangePolicy: OnRootMismatch
Install Falco helm install falco falcosecurity/falco (driver.kind defaults to auto)
Custom Falco rules /etc/falco/rules.d/*.yaml
Narrow a noisy built-in rule override: {condition: append}
Default-deny a namespace podSelector: {} with policyTypes: [Ingress, Egress]
Confirm policy is actually enforced kubectl run probe --rm -i --restart=Never -n <ns> --image=busybox -- wget -qO- -T3 http://<svc> — a connectivity test, not a pod grep
Encrypt Secrets at rest --encryption-provider-config=... with kms v2 first
Re-encrypt existing Secrets kubectl get secrets -A -o json | kubectl replace -f -

Common Issues and Solutions

Pod rejected by Pod Security Admission

Symptom: pods "x" is forbidden: violates PodSecurity "restricted:latest", listing the specific fields.

The message names every violation at once, so fix them together rather than iterating.

kubectl get ns production -o jsonpath='{.metadata.labels}' | tr ',' '\n'

Add runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL] and seccompProfile.type: RuntimeDefault. If the workload genuinely needs more, move it to a baseline namespace rather than weakening the whole one.

Deployment healthy, zero pods

Symptom: kubectl get deploy shows 0/3 ready, no events on the Deployment.

PSA rejects pods, not Deployments, so the error is on the ReplicaSet. See kubectl describe rs -l app=<name>.

container has runAsNonRoot and image has non-numeric user

Symptom: Pod stuck in CreateContainerConfigError.

The image's USER is a name the kubelet cannot resolve. Set runAsUser: <uid> in the security context, or rebuild with USER 1000.

Falco reports no events

Symptom: Falco is running, logs are clean, nothing ever fires.

# Which driver actually loaded
kubectl logs -n falco -l app.kubernetes.io/name=falco | grep -i 'driver\|probe\|scap'

# Prove the pipeline end to end - this trips a shipped rule
kubectl exec -n production deploy/myapp -- cat /etc/shadow

If modern_ebpf failed to load, the kernel is likely below 5.8 or built without BTF. Fall back to driver.kind=kmod — not to ebpf, which was removed in Falco 0.44.0.

Falco using excessive CPU, or dropping events

Symptom: High CPU on busy nodes, or a rising drop count in the metrics.

The largest lever is almost always a noisy rule rather than Falco itself — find it in the metrics snapshot before tuning anything global, and narrow it with override rather than disabling it.

After that: raise priority so low-severity events are discarded before rule evaluation, bound the output queue so a slow sink cannot back-pressure the engine, and prefer modern_ebpf over kmod.

priority: notice          # Drop everything below this before rules run

outputs_queue:
  capacity: 10000         # Bound it. The default, 0, is unbounded.

syscall_event_drops:      # What to do when the kernel buffer overflows
  threshold: 0.1
  actions: [log, alert]
  rate: 0.03333
  max_burst: 1

Dropped events are silent loss of detection coverage, so alert on the drop counter rather than only on CPU.

Do not reach for outputs.rate and outputs.max_burst. They were the output throttle in older Falco and were removed in 0.37.0; most tuning advice online still shows them. Falco does not refuse to start — it loads, reports a schema validation failure for the offending file, and ignores the key:

/etc/falco/config.d/zz-out.yaml | schema validation: failed for <root>: Object contains
a property that could not be validated using 'properties' or 'additionalProperties'
constraints: 'outputs'.

That line scrolls past at startup, so the throttle silently is not applied.

NetworkPolicy applied but traffic still flows

Symptom: Policies exist, kubectl describe looks right, traffic is unaffected.

In order of likelihood: the CNI does not implement NetworkPolicy; another policy in the namespace allows the traffic (they are additive); policyTypes omits the direction being tested; or the pod labels do not match the selector.

kubectl get pods -n production --show-labels
kubectl get networkpolicy -n production -o wide

Everything fails to resolve after applying default-deny

Symptom: Name or service not known across the namespace.

Egress-deny includes DNS. Add the allow-dns policy above.

Secrets still readable in etcd after enabling encryption

Symptom: etcdctl get shows plaintext for existing Secrets.

Encryption applies on write. Either identity is first in the provider list, or the Secrets predate the change and need rewriting with kubectl replace.


Related Topics

The following topics complement this cheatsheet and would be valuable additions:

  1. Trivy — image, filesystem, IaC and secret scanning, severity gating, and the CI ergonomics for the build-time half
  2. Kubernetes RBAC and Multi-Tenancy — roles, service accounts, namespace isolation, and audit; the identity layer these controls assume
  3. Policy as Code (OPA/Conftest) — Rego, Gatekeeper and Conftest, for the policies PSA's three fixed levels cannot express
  4. SELinux/AppArmor — the mandatory access control underneath seLinuxOptions and appArmorProfile
  5. HashiCorp Vault — dynamic secrets, the agent injector, and External Secrets, sitting above the encryption-at-rest layer here
  6. Kubernetes Networking — Services, DNS, ingress, and the full NetworkPolicy reference this sheet defers to