The container boundary in 60 seconds
The process inside the container had UID 10001. The image scanner found no critical packages. The root filesystem was read-only, and the deployment passed its policy checks. Those are useful observations. They do not answer the question that matters after the process is compromised:
Which authority can this workload still reach outside its intended application boundary?
A non-root process can still write through a host bind mount, call a mounted runtime socket, use an over-permissioned service account, reach a cloud metadata endpoint, consume an exposed device, or exercise a dangerous kernel capability. None of those paths require the application process to display UID 0 inside the container.
This methodology treats container security as an attack-path problem. The container image, runtime configuration, orchestrator policy, node, and external identities are evaluated as one system. A control counts only when the application remains healthy and a named unauthorized action becomes unavailable.
This is a source-reviewed test design, not a hidden container-escape result. It does not claim that the example workload was compromised or that every control below was exercised against a production cluster. The commands are intended for an owned laboratory or an explicitly authorized environment, and the evidence model keeps observations separate from conclusions.
Non-root changes one coordinate
runAsNonRoot: true
answers a narrow and valuable question: the container entry process should not
start with UID 0. It can reduce the consequences of application compromise, prevent assumptions
made by poorly designed software, and support stronger admission policy. It is not an isolation
boundary by itself.
The effective boundary is assembled from several independent planes:
| Plane | Security question | Common surviving path |
|---|---|---|
| User identity | Which UID and GID does the process use? | Host file ownership or supplemental groups still authorize access |
| Kernel privilege | Which capabilities and syscalls remain? | A narrow-looking capability enables a host-relevant operation |
| Filesystem | Which host paths, volumes, sockets, and devices are visible? | Writable bind mount or runtime socket crosses the boundary |
| Process isolation | Which namespaces and LSM policies apply? | Host PID/network namespace or an unconfined profile removes separation |
| Workload identity | Which API credentials are injected or reachable? | Service account or cloud identity grants control-plane authority |
| Resource containment | What can one process exhaust? | Missing CPU, memory, PID, or storage limits turn compromise into node impact |
| Placement | What else shares the node and kernel? | A public workload sits beside a high-authority system component |
The important property is composition. A read-only root filesystem does not make a writable host mount read-only. Dropping most capabilities does not neutralize the Docker socket. Seccomp does not remove Kubernetes API permissions. A short-lived service-account token is still powerful during its valid lifetime.
Define the workload contract before inspecting it
Begin with a workload contract, not a generic benchmark. The contract describes what the container must do, what it may reach, and what must remain impossible after compromise.
Record at least:
- the approved image digest and build source;
- the application entry point, listening port, and health check;
- required writable paths and whether their contents must persist;
- required outbound destinations and protocols;
- expected Linux UID, GID, supplemental groups, and capabilities;
- required Kubernetes API verbs or external cloud permissions;
- expected runtime class, node pool, and sensitivity of neighboring workloads;
- the exact unauthorized effect the hardening change is meant to deny.
“The container should be secure” cannot be retested. “The application can write only to its named data volume, cannot access a host control socket, receives no Kubernetes credential, and cannot create a process with additional privileges” can.
The last sentence becomes the negative-control plan. It also exposes exceptions early. If the workload genuinely needs a device, host namespace, runtime socket, or broad control-plane identity, it is not a standard application container. It is a high-authority infrastructure component and should receive dedicated placement, a narrower interface, and a stronger isolation decision.
Preserve a read-only baseline
Inventory the deployed state before changing it. Do not rely on the manifest in Git: admission mutation, Helm values, platform defaults, and emergency patches can make the running object different from the reviewed source.
For Kubernetes, the following commands collect configuration without printing secret values:
kubectl version
kubectl get pod -n "$NAMESPACE" "$POD" -o yaml > pod-live.yaml
kubectl get pod -n "$NAMESPACE" "$POD" \
-o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,SA:.spec.serviceAccountName,HOSTPID:.spec.hostPID,HOSTNET:.spec.hostNetwork'
kubectl get pod -n "$NAMESPACE" "$POD" \
-o jsonpath='{range .spec.containers[*]}{.name}{"\t"}{.securityContext}{"\n"}{end}'
kubectl get pod -n "$NAMESPACE" "$POD" \
-o jsonpath='{range .spec.volumes[*]}{.name}{"\t"}{.hostPath.path}{"\t"}{.persistentVolumeClaim.claimName}{"\n"}{end}'
kubectl auth can-i --as="system:serviceaccount:$NAMESPACE:$SERVICE_ACCOUNT" --list -n "$NAMESPACE"
kubectl get namespace "$NAMESPACE" --show-labels
The --list output can be large and can reveal resource names, so store it with the assessment
evidence rather than pasting it into a public report. It describes allowed API operations; it does
not prove that a credential was mounted, reachable, or successfully used.
For a standalone Docker workload, preserve both the container configuration and daemon context:
docker version
docker info --format '{{json .SecurityOptions}}'
docker inspect "$CONTAINER" > container-inspect.json
docker inspect --format '{{json .HostConfig}}' "$CONTAINER" > host-config.json
docker inspect --format '{{json .Mounts}}' "$CONTAINER" > mounts.json
docker top "$CONTAINER" -eo pid,user,group,comm,args
These records may contain internal paths, image references, and environment metadata. Treat them as
sensitive evidence. Do not collect or publish environment-variable values merely because
docker inspect makes them available.
Triage authority before configuration hygiene
Container reviews often spend their first hour on image size, package counts, or whether the YAML
contains runAsNonRoot. Start instead with the edges that can directly change the host or control
plane.
1. Runtime and orchestrator sockets
A mounted Docker, containerd, CRI, or management socket is not ordinary application data. It is an interface to a control plane. Docker’s security documentation treats daemon access as a sensitive boundary because the daemon commonly carries host-level authority unless deliberately operated in rootless mode.
The right question is not “is the socket read-only?” Unix socket access is expressed through API requests rather than filesystem writes to stored data. Determine which operations the service accepts and whether the application needs any of them. For a normal web application, the correct answer is usually that the socket should not be mounted at all.
2. Host paths, devices, and propagation
A bind mount creates a direct relationship between a host path and the container. Docker documents that bind mounts are writable by default and can therefore modify host files. A non-root container may still be authorized by the host path’s UID, GID, ACL, supplemental group, or overly permissive mode.
Inventory:
- exact host source and container destination;
- read-only versus writable state;
- mount propagation;
- host ownership, group, ACL, and SELinux/AppArmor context;
- whether the path contains sockets, executable configuration, credentials, or files consumed by a privileged host service;
- whether the same purpose can be served by a named volume, projected file, or narrow API.
Read-only is a meaningful reduction, not a universal guarantee. Reading a deployment key, runtime credential, host process metadata, or privileged service configuration can be sufficient for a different attack path.
3. Privileged mode and Linux capabilities
privileged: true is a categorical exception. Kubernetes documents that
privileged containers override kernel constraints
override or neutralize several kernel constraints: seccomp becomes unconfined and AppArmor or
SELinux confinement is bypassed. Do not describe a privileged workload as hardened because it also
sets runAsNonRoot or a read-only root filesystem.
For ordinary applications, begin by dropping all capabilities and add back only a named,
functionally tested requirement. A capability is not harmless because its name is unfamiliar.
CAP_SYS_ADMIN in particular spans many unrelated administrative operations and also forces
allowPrivilegeEscalation to remain effectively true in Kubernetes.
4. Workload identity
Kubernetes assigns every Pod a
service account, using the
namespace’s default service account when
none is specified. Current clusters normally project short-lived, rotating tokens. Short-lived is
better than static. It does not mean low-authority.
For workloads that do not call the Kubernetes API, set automountServiceAccountToken: false and
verify that no token is present. For workloads that do call it, bind a dedicated service account to
the smallest namespaced role that supports the positive workload test. Review external workload
identity separately: a Pod with no Kubernetes permission can still receive cloud authority through
its node, metadata route, or identity federation configuration.
5. Shared kernel and workload placement
Containers on one node share a kernel unless a stronger sandboxed runtime introduces another boundary. Seccomp, AppArmor, SELinux, user namespaces, capability reduction, and current kernels reduce exposure in different ways; none retroactively makes high- and low-trust workloads good neighbors.
Kubernetes’ security checklist recommends separating workloads of different sensitivity and considering sandboxed runtimes for sensitive placement. A public parser, file converter, browser worker, or untrusted build job should not casually share a node with cluster administration or high-authority identity components.
A minimum application profile
The following security context is a defensible starting point for a conventional stateless Linux application. It is not a universal manifest and should first be applied to a development namespace with an image pinned by digest.
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-api
spec:
replicas: 1
selector:
matchLabels:
app: example-api
template:
metadata:
labels:
app: example-api
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: registry.example/example-api@sha256:<approved-digest>
ports:
- name: http
containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /health/ready
port: http
volumes:
- name: tmp
emptyDir:
sizeLimit: 64Mi
Each field has a claim attached:
runAsNonRootand explicit IDs prevent accidental root execution and unstable image defaults;allowPrivilegeEscalation: falserequests the kernel’sno_new_privsbehavior;- dropping all capabilities removes privilege fragments unless explicitly restored;
RuntimeDefaultretains the runtime’s maintained syscall filter;- a read-only root filesystem makes undeclared writes fail;
- a bounded
emptyDirprovides the application’s declared temporary write path; - resource requests and limits constrain some denial-of-service effects;
- disabling token automount removes an unused credential edge;
- the readiness probe provides a positive control during rollout.
There are deliberate omissions. This sample does not create network policy, admission policy, AppArmor or SELinux policy, a Pod Disruption Budget, image-signature enforcement, or cloud workload identity restrictions. Those belong to adjacent control planes and require their own testable claims.
Why the controls fail when treated as labels
runAsNonRoot without a fixed, compatible image user
An image can declare a symbolic user, depend on root-owned paths, or write into directories whose ownership changes between builds. Enforcing non-root without testing startup, upgrades, log rotation, and graceful shutdown turns security work into an availability incident. Conversely, leaving the UID implicit makes the same manifest behave differently across images.
Pin the numeric identity, build required ownership into the image, and prove the full lifecycle.
readOnlyRootFilesystem with writable authority mounted elsewhere
The container root can be immutable while /data, /config, /cache, or a host path remains
writable. Classify every mount by what consumes its output. A write-only export directory is not
equivalent to a directory watched by a privileged host automation service.
Seccomp without runtime evidence
Kubernetes and Docker both recommend using the runtime default seccomp profile as a practical baseline. A YAML field is not proof that the node supports or applied the intended profile. Record the runtime, node configuration, admission result, and a controlled denied operation where safe. Avoid disabling the default profile to fix an unexplained application error; identify the syscall, decide whether the application needs it, and retest after the smallest change.
Seccomp also does not make allowed syscalls safe. It reduces kernel attack surface; it does not validate application input or remove the shared-kernel trust decision.
AppArmor or SELinux without node coverage
Profiles must exist and be enforced on every eligible node. Kubernetes notes that an implicit runtime-default AppArmor choice can result in no restriction when AppArmor is disabled on the node, whereas explicitly requesting the profile can make admission fail. That failure may be preferable: it converts silent policy absence into visible deployment evidence.
Pod Security labels without enforcement and version pinning
Pod Security Admission supports warn, audit, and enforce. A namespace that only warns can
produce a clean-looking review while still admitting the workload. Record the mode, policy level,
and pinned version. Use warn and audit during migration, but do not report the path as closed
until enforce rejects the known-bad negative-control manifest.
Image scanning without runtime authority review
Package and vulnerability scanning answers what is present in the image and what public knowledge exists about it. It does not inventory a live Pod’s mounts, service account, namespace sharing, admission result, or node placement. Use image scanning as one evidence source, never as the verdict for workload isolation.
The three-state experiment
Keep the image digest, application input, node class, and expected workload behavior constant. Change only the control state.
State A — non-root baseline
The process runs as UID 10001, but one intentionally selected authority edge remains in the owned lab: for example, an unnecessary service-account token, a writable canary bind mount, or a broader capability set than the application requires. Do not expose a real runtime socket or production host path merely to make the test dramatic.
Record the positive application check and the safe observation showing that the chosen edge exists. This establishes reachability, not impact.
State B — label-driven hardening
Add common hardening fields while leaving the selected edge intact. The deployment may now pass a policy scanner. If the same negative control still succeeds, the result demonstrates the precise lesson: several valuable controls can coexist with the original attack path.
State C — threat-driven hardening
Remove or narrow the selected edge. Re-run the same positive workload check and the same negative control. The state passes only when the service remains healthy and the unauthorized behavior fails for the expected reason.
Positive and negative controls
Select controls before the change so that the result cannot be chosen after seeing the output.
Positive controls
Depending on the workload, record:
- readiness and liveness behavior;
- one representative authenticated request;
- required write to the declared data or temporary volume;
- graceful start, termination, and restart;
- expected outbound connection to an allowlisted dependency;
- one routine operational action such as log delivery or metrics scraping.
Negative controls
Use a harmless lab fixture for each security claim:
- the root filesystem refuses a write outside declared writable volumes;
- the canary host path is absent or mounted read-only;
- no Docker or CRI socket exists in the application filesystem;
- the process cannot gain a new privilege through a test helper designed for the lab;
- the service-account token path is absent when the workload does not use Kubernetes API access;
kubectl auth can-idenies a named API verb outside the workload contract;- admission rejects a known-bad copy of the manifest using host namespaces, privileged mode, or a disallowed capability;
- resource bounds produce a controlled container-level failure rather than node instability.
Do not use /etc/shadow, a real administrator key, a production runtime socket, or destructive
resource exhaustion as a canary. The purpose is to prove policy behavior without creating a new
incident.
Inside the authorized test container, low-impact checks can confirm declared state without printing credentials:
id
grep -E '^(CapInh|CapPrm|CapEff|CapBnd|NoNewPrivs|Seccomp):' /proc/1/status
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS
test ! -S /var/run/docker.sock
test ! -S /run/containerd/containerd.sock
test ! -e /var/run/secrets/kubernetes.io/serviceaccount/token
Interpret the last three commands against the workload contract. A failed test ! -e is not an
automatic vulnerability; it means the edge exists and requires an authority review.
Admission is the durable control
Fixing one manifest closes one instance. Admission policy prevents the same authority edge from returning through the next deployment, another namespace, or an emergency Helm value.
Kubernetes Pod Security Standards provide three useful profiles: Privileged, Baseline, and Restricted. Restricted is a strong general target for application workloads, while exceptions above Baseline are application-specific and deserve explicit ownership. Apply policies in stages:
- inventory current violations and their owners;
- enable version-pinned
warnandauditin a test namespace; - repair or isolate exceptions;
- switch the intended namespaces to
enforce; - submit a known-bad manifest and preserve the rejection as the negative control;
- monitor admission events and periodically retest after platform upgrades.
Admission does not inspect everything. Pod Security Standards intentionally do not solve all volume, image provenance, network, workload identity, or business-specific requirements. Add narrowly scoped policy for the gaps you can name and test. Avoid a sprawling rule set whose only proof is that the policy engine loaded it.
Rootless is a host-side reduction, not a workload verdict
Docker rootless mode runs both the daemon and containers inside a user namespace without a root-privileged daemon. This materially reduces the daemon and runtime attack surface compared with a conventional rootful deployment. It is especially valuable for developer systems and compatible single-user services.
Rootless mode does not answer the full workload contract. The process can still access data owned by the rootless user, use mounted credentials, call its rootless daemon socket, reach the network, or consume available resources. Record it as a host-side control and retain the same mount, identity, network, and negative-control review.
The same reasoning applies to user namespaces in orchestrated environments. Mapping container root to an unprivileged host ID can reduce consequences, but a deliberately exposed control socket or over-authorized API identity can bypass that benefit.
Evidence matrix
| Claim | Required observation | Positive control | Negative control | Conclusion limit |
|---|---|---|---|---|
| Application is non-root | Live PID 1 UID/GID and admitted security context | Service startup and normal request succeed | Known root-only startup is rejected | Does not prove host isolation |
| Privilege cannot increase | Capability sets, NoNewPrivs, privileged flag | Required operation still succeeds | Lab privilege-transition helper is denied | Does not cover kernel vulnerabilities |
| Syscall surface is constrained | Applied seccomp state and runtime profile | Full application lifecycle succeeds | Selected disallowed lab syscall returns expected denial | Does not make allowed syscalls safe |
| Host filesystem is not writable | Live mount table, source, options, host ownership | Declared data path remains writable | Canary host path is absent or read-only | Does not prove mounted data is non-sensitive |
| Runtime control plane is unreachable | Socket inventory and live mounts | Workload needs no runtime API | Known socket paths are absent | Does not cover remote management endpoints |
| Kubernetes credential is absent or narrow | Token mount state plus RBAC review | Required API verb succeeds, if any | Named out-of-contract verb is denied | Does not cover cloud workload identity |
| Admission prevents regression | Namespace labels, policy version, rejection event | Approved manifest is admitted | Known-bad manifest is rejected | Does not prove already-running Pods were remediated |
| Resource impact is bounded | Requests, limits, PID/storage policy, node telemetry | Expected peak workload succeeds | Controlled lab pressure stays within the container boundary | Does not prove node capacity planning |
| Placement matches trust | RuntimeClass, node, taints, affinity, neighbor classification | Scheduling and recovery succeed | High-risk workload cannot schedule onto protected pool | Does not prove the runtime has no escape flaws |
The matrix makes partial results publishable without turning them into findings they do not support. For example, an RBAC denial is evidence about a principal and verb. It is not proof that the application cannot affect the host through a mounted socket.
Roll out one boundary at a time
Container hardening changes can fail in non-obvious places: init containers, sidecars, debug containers, volume ownership, package caches, certificate refresh, log delivery, graceful shutdown, or autoscaling. Use a staged rollout with an explicit rollback object.
For each change:
- preserve the original live manifest, image digest, policy output, and workload result;
- change one authority plane or one coherent group of fields;
- deploy to an equivalent test namespace or canary workload;
- run the predefined positive and negative controls;
- observe at least one restart and reschedule;
- roll back once in the lab and confirm recovery;
- promote only after the denial and workload evidence agree.
Do not weaken a cluster-wide control to accommodate one exception. Isolate the exception, document the authority it retains, restrict who can deploy it, and place it on nodes that match its trust level.
Reporting language that survives review
Prefer bounded statements:
- “The live application process ran as UID 10001 and the admission policy rejected UID 0.”
- “No runtime socket or hostPath volume was present in the admitted Pod specification.”
- “The service account could read ConfigMaps in its namespace and was denied Secret reads.”
- “The positive request and restart tests passed after all capabilities were dropped.”
- “The result does not evaluate unknown kernel escape vulnerabilities or the cloud identity layer.”
Avoid:
- “The container is secure.”
- “Non-root prevents container escape.”
- “Restricted policy means the Pod cannot affect the node.”
- “The scanner passed, so the workload has least privilege.”
- “A short-lived token is harmless.”
Container security is strongest when every layer is allowed to make only its own claim. Image provenance supports trust in the artifact. A non-root UID reduces process privilege. Seccomp and LSM profiles constrain kernel interaction. Mount policy governs exposed files and sockets. RBAC governs the Kubernetes API. Placement governs which failures share a kernel. None should borrow certainty from the others.
The final retest
The final review should be understandable without the hardening tool that suggested the change. Start with the workload contract, show the original authority edge, record the admitted and live state, demonstrate that the application still works, and show that the same harmless negative control now fails.
That evidence supports a narrow but durable conclusion:
The container remained functional, and this specific path from application compromise to host or control-plane authority was removed under the tested configuration.
That is more useful than calling the workload “non-root.” It names the boundary, the behavior, the evidence, and what remains outside the test.
MITRE ATT&CK mapping
| Tactic | Technique ID | Technique name | Defender verification signal |
|---|---|---|---|
| Privilege Escalation | T1611 | Escape to Host | Host path mount audit + seccomp default profile enforcement |
| Defense Evasion | T1610 | Deploy Container | Admission controller webhook logs (Kyverno / Gatekeeper) |
| Discovery | T1613 | Container and Resource Discovery | Prohibit automountServiceAccountToken + namespace isolation |
| Execution | T1609 | Container Administration Command | Audit kubectl exec / Docker socket access with API audit logs |
Defender action checklist
Use this actionable checklist during container workload hardening:
- Drop all capabilities: Set
securityContext.capabilities.drop: ["ALL"]in container manifests. - Enforce read-only root filesystem: Set
securityContext.readOnlyRootFilesystem: truewith dedicated temporaryemptyDirmounts for/tmp. - Disable ambient API tokens: Set
automountServiceAccountToken: falseunless the workload explicitly calls the Kubernetes API. - Deny privilege escalation: Ensure
allowPrivilegeEscalation: falseis configured across all containers in the Pod. - Verify negative controls: Confirm non-root process cannot write to host mounts or invoke restricted syscalls.
How current is this note?
The latest source-review, content-update, or publication date is shown.
The author completed a technical review. This does not, by itself, claim lab reproduction.
- kubernetes.io · security-checklist ↗
- kubernetes.io · pod-security-standards ↗
- kubernetes.io · security-context ↗
- kubernetes.io · linux-kernel…ty-constraints ↗
- kubernetes.io · rbac-good-practices ↗
- kubernetes.io · service-accounts ↗
- docs.docker.com · security ↗
- docs.docker.com · rootless ↗
- docs.docker.com · seccomp ↗
- docs.docker.com · bind-mounts ↗
- csrc.nist.gov · final ↗
