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.
flowchart TB
subgraph Build["Build time - covered elsewhere"]
B1[Image scanning]
B2[SBOM generation]
B3[Signing and provenance]
end
subgraph Deploy["Deploy time - this sheet"]
D1[Pod Security Admission]
D2[Security contexts]
D3[Network policy]
end
subgraph Run["Run time - this sheet"]
R1[Falco detection]
R2[Secret encryption at rest]
end
Build --> Deploy
Deploy --> Run
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.
flowchart LR
A[Container syscalls] --> B[Kernel]
B -->|modern_ebpf probe| C[Falco engine]
D[K8s audit log] --> C
C --> E{Rule match}
E -->|Yes| F[Outputs]
E -->|No| G[Drop]
F --> H[stdout / JSON]
F --> I[Falcosidekick]
I --> J[Slack, 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:
- Trivy — image, filesystem, IaC and secret scanning, severity gating, and the CI ergonomics for the build-time half
- Kubernetes RBAC and Multi-Tenancy — roles, service accounts, namespace isolation, and audit; the identity layer these controls assume
- Policy as Code (OPA/Conftest) — Rego, Gatekeeper and Conftest, for the policies PSA's three fixed levels cannot express
- SELinux/AppArmor — the mandatory access control underneath
seLinuxOptionsandappArmorProfile - HashiCorp Vault — dynamic secrets, the agent injector, and External Secrets, sitting above the encryption-at-rest layer here
- Kubernetes Networking — Services, DNS, ingress, and the full NetworkPolicy reference this sheet defers to