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:

PlaneSecurity questionCommon surviving path
User identityWhich UID and GID does the process use?Host file ownership or supplemental groups still authorize access
Kernel privilegeWhich capabilities and syscalls remain?A narrow-looking capability enables a host-relevant operation
FilesystemWhich host paths, volumes, sockets, and devices are visible?Writable bind mount or runtime socket crosses the boundary
Process isolationWhich namespaces and LSM policies apply?Host PID/network namespace or an unconfined profile removes separation
Workload identityWhich API credentials are injected or reachable?Service account or cloud identity grants control-plane authority
Resource containmentWhat can one process exhaust?Missing CPU, memory, PID, or storage limits turn compromise into node impact
PlacementWhat 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.

CONTAINER COMPROMISE IS THE STARTING CONDITION UNTRUSTEDrequest or data APP · UID 10001code execution assumednon-root is preserved AUTHORITYmount · socketkernel · token IMPACTnode or cluster UID BOUNDARYblocks UID 0 only INDEPENDENT EDGES REMAINhost mount · daemon socket · CAP_* · API tokeneach requires its own negative control CLAIM“runs as non-root” ≠ “cannot affect the host”
A non-root UID removes one privilege condition. It does not sever filesystem, runtime, kernel, identity, or placement edges that bypass that condition.

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:

  • runAsNonRoot and explicit IDs prevent accidental root execution and unstable image defaults;
  • allowPrivilegeEscalation: false requests the kernel’s no_new_privs behavior;
  • dropping all capabilities removes privilege fragments unless explicitly restored;
  • RuntimeDefault retains the runtime’s maintained syscall filter;
  • a read-only root filesystem makes undeclared writes fail;
  • a bounded emptyDir provides 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.

SAME IMAGE · SAME INPUT · SAME WORKLOAD CONTRACT A · NON-ROOTUID 10001authority edge presentnegative control succeeds B · LABEL-DRIVENread-only · seccompsame edge survivesclaim still fails C · THREAT-DRIVENedge removed or narrowedservice remains healthyCUTnegative control denied PASShealth check succeeds · denied action fails · expected policy explains denial KEEPmanifest · admission output · runtime state · timestamps · rollback result
State B prevents a checklist from becoming the conclusion. State C requires the same workload to pass while the selected authority edge fails under a repeatable negative control.

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-i denies 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:

  1. inventory current violations and their owners;
  2. enable version-pinned warn and audit in a test namespace;
  3. repair or isolate exceptions;
  4. switch the intended namespaces to enforce;
  5. submit a known-bad manifest and preserve the rejection as the negative control;
  6. 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

ClaimRequired observationPositive controlNegative controlConclusion limit
Application is non-rootLive PID 1 UID/GID and admitted security contextService startup and normal request succeedKnown root-only startup is rejectedDoes not prove host isolation
Privilege cannot increaseCapability sets, NoNewPrivs, privileged flagRequired operation still succeedsLab privilege-transition helper is deniedDoes not cover kernel vulnerabilities
Syscall surface is constrainedApplied seccomp state and runtime profileFull application lifecycle succeedsSelected disallowed lab syscall returns expected denialDoes not make allowed syscalls safe
Host filesystem is not writableLive mount table, source, options, host ownershipDeclared data path remains writableCanary host path is absent or read-onlyDoes not prove mounted data is non-sensitive
Runtime control plane is unreachableSocket inventory and live mountsWorkload needs no runtime APIKnown socket paths are absentDoes not cover remote management endpoints
Kubernetes credential is absent or narrowToken mount state plus RBAC reviewRequired API verb succeeds, if anyNamed out-of-contract verb is deniedDoes not cover cloud workload identity
Admission prevents regressionNamespace labels, policy version, rejection eventApproved manifest is admittedKnown-bad manifest is rejectedDoes not prove already-running Pods were remediated
Resource impact is boundedRequests, limits, PID/storage policy, node telemetryExpected peak workload succeedsControlled lab pressure stays within the container boundaryDoes not prove node capacity planning
Placement matches trustRuntimeClass, node, taints, affinity, neighbor classificationScheduling and recovery succeedHigh-risk workload cannot schedule onto protected poolDoes 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:

  1. preserve the original live manifest, image digest, policy output, and workload result;
  2. change one authority plane or one coherent group of fields;
  3. deploy to an equivalent test namespace or canary workload;
  4. run the predefined positive and negative controls;
  5. observe at least one restart and reschedule;
  6. roll back once in the lab and confirm recovery;
  7. 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

TacticTechnique IDTechnique nameDefender verification signal
Privilege EscalationT1611Escape to HostHost path mount audit + seccomp default profile enforcement
Defense EvasionT1610Deploy ContainerAdmission controller webhook logs (Kyverno / Gatekeeper)
DiscoveryT1613Container and Resource DiscoveryProhibit automountServiceAccountToken + namespace isolation
ExecutionT1609Container Administration CommandAudit kubectl exec / Docker socket access with API audit logs

Defender action checklist

Use this actionable checklist during container workload hardening:

  1. Drop all capabilities: Set securityContext.capabilities.drop: ["ALL"] in container manifests.
  2. Enforce read-only root filesystem: Set securityContext.readOnlyRootFilesystem: true with dedicated temporary emptyDir mounts for /tmp.
  3. Disable ambient API tokens: Set automountServiceAccountToken: false unless the workload explicitly calls the Kubernetes API.
  4. Deny privilege escalation: Ensure allowPrivilegeEscalation: false is configured across all containers in the Pod.
  5. Verify negative controls: Confirm non-root process cannot write to host mounts or invoke restricted syscalls.
Sources & freshness

How current is this note?

Sources checkedAugust 25, 2026

The latest source-review, content-update, or publication date is shown.

ReviewAuthor review complete

The author completed a technical review. This does not, by itself, claim lab reproduction.