---
title: "The Container Was Non-Root. The Node Was Still One Mount Away."
description: "A container security methodology that measures mounts, runtime authority, kernel controls, and workload identity instead of treating a non-root UID or a passing policy check as proof of isolation."
date: 2026-08-25
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/container-non-root-host-boundary/
---

## 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`](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
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.

<figure class="diagram">
<svg viewBox="0 0 780 346" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A container attack path crosses five authority planes. An untrusted request reaches a non-root application, then a writable mount, runtime control socket, shared kernel, and workload identity provide separate paths to node or cluster impact. The UID boundary blocks only one path.">
<defs><marker id="cb-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker><marker id="cb-c" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="24" class="dg-accent">CONTAINER COMPROMISE IS THE STARTING CONDITION</text>
<rect x="0" y="60" width="144" height="76" rx="9" class="dg-box"/><text x="16" y="91" class="dg-label">UNTRUSTED</text><text x="16" y="115" class="dg-muted">request or data</text>
<line x1="144" y1="98" x2="190" y2="98" class="dg-line" marker-end="url(#cb-a)"/>
<rect x="194" y="52" width="184" height="92" rx="9" class="dg-box-accent"/><text x="210" y="84" class="dg-accent">APP · UID 10001</text><text x="210" y="108" class="dg-label">code execution assumed</text><text x="210" y="130" class="dg-muted">non-root is preserved</text>
<line x1="378" y1="98" x2="424" y2="98" class="dg-line-crit" marker-end="url(#cb-c)"/>
<rect x="428" y="52" width="154" height="92" rx="9" class="dg-box-crit"/><text x="444" y="82" class="dg-crit">AUTHORITY</text><text x="444" y="106" class="dg-label">mount · socket</text><text x="444" y="130" class="dg-label">kernel · token</text>
<line x1="582" y1="98" x2="628" y2="98" class="dg-line-crit" marker-end="url(#cb-c)"/>
<rect x="632" y="60" width="148" height="76" rx="9" class="dg-box-crit"/><text x="648" y="91" class="dg-crit">IMPACT</text><text x="648" y="115" class="dg-muted">node or cluster</text>
<line x1="286" y1="144" x2="286" y2="190" class="dg-line" marker-end="url(#cb-a)"/>
<rect x="194" y="194" width="184" height="56" rx="8" class="dg-box"/><text x="210" y="219" class="dg-label">UID BOUNDARY</text><text x="210" y="240" class="dg-muted">blocks UID 0 only</text>
<line x1="378" y1="222" x2="428" y2="222" class="dg-line dg-dash"/>
<rect x="428" y="178" width="352" height="92" rx="9" class="dg-box"/><text x="444" y="205" class="dg-accent">INDEPENDENT EDGES REMAIN</text><text x="444" y="230" class="dg-mono">host mount · daemon socket · CAP_* · API token</text><text x="444" y="252" class="dg-muted">each requires its own negative control</text>
<text x="0" y="314" class="dg-muted">CLAIM</text><text x="90" y="314" class="dg-mono">“runs as non-root” ≠ “cannot affect the host”</text>
</svg>
<figcaption>A non-root UID removes one privilege condition. It does not sever filesystem, runtime, kernel, identity, or placement edges that bypass that condition.</figcaption>
</figure>

## 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:

```bash
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:

```bash
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](https://docs.docker.com/engine/security/)
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](https://docs.docker.com/engine/storage/bind-mounts/)
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](https://kubernetes.io/docs/concepts/security/linux-kernel-security-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](https://kubernetes.io/docs/concepts/security/service-accounts/), 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](https://kubernetes.io/docs/concepts/security/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.

```yaml
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](https://docs.docker.com/engine/security/seccomp/) 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.

<figure class="diagram">
<svg viewBox="0 0 780 338" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A three-state container experiment. The first state runs as non-root with a red host authority edge. The second state adds checklist controls but keeps the edge. The third state removes the edge, retains a healthy service, and records both positive and negative controls.">
<defs><marker id="ct-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<text x="0" y="24" class="dg-accent">SAME IMAGE · SAME INPUT · SAME WORKLOAD CONTRACT</text>
<rect x="0" y="54" width="230" height="184" rx="10" class="dg-box-crit"/><text x="16" y="84" class="dg-crit">A · NON-ROOT</text><text x="16" y="112" class="dg-label">UID 10001</text><text x="16" y="138" class="dg-label">authority edge present</text><line x1="24" y1="172" x2="186" y2="172" class="dg-line-crit"/><text x="24" y="202" class="dg-crit">negative control succeeds</text>
<rect x="274" y="54" width="230" height="184" rx="10" class="dg-box"/><text x="290" y="84" class="dg-muted">B · LABEL-DRIVEN</text><text x="290" y="112" class="dg-label">read-only · seccomp</text><text x="290" y="138" class="dg-label">same edge survives</text><line x1="298" y1="172" x2="460" y2="172" class="dg-line-crit"/><text x="298" y="202" class="dg-crit">claim still fails</text>
<rect x="548" y="54" width="232" height="184" rx="10" class="dg-box-accent"/><text x="564" y="84" class="dg-accent">C · THREAT-DRIVEN</text><text x="564" y="112" class="dg-label">edge removed or narrowed</text><text x="564" y="138" class="dg-label">service remains healthy</text><line x1="572" y1="172" x2="632" y2="172" class="dg-line"/><rect x="640" y="154" width="36" height="36" rx="6" class="dg-box-crit"/><text x="648" y="178" class="dg-crit">CUT</text><line x1="684" y1="172" x2="748" y2="172" class="dg-line dg-dash"/><text x="572" y="202" class="dg-accent">negative control denied</text>
<line x1="230" y1="146" x2="270" y2="146" class="dg-line" marker-end="url(#ct-a)"/><line x1="504" y1="146" x2="544" y2="146" class="dg-line" marker-end="url(#ct-a)"/>
<text x="0" y="282" class="dg-muted">PASS</text><text x="90" y="282" class="dg-mono">health check succeeds · denied action fails · expected policy explains denial</text>
<text x="0" y="312" class="dg-accent">KEEP</text><text x="90" y="312" class="dg-mono">manifest · admission output · runtime state · timestamps · rollback result</text>
</svg>
<figcaption>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.</figcaption>
</figure>

## 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:

```bash
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](https://kubernetes.io/docs/concepts/security/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](https://docs.docker.com/engine/security/rootless/) 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:

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

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

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.