Docker is the most confident line on a lot of resumes and the least examined file in a lot of repositories. Containerizing a service became a way to signal that you know what you are doing, and for a certain kind of engineer the signal is the point. The docker-compose.yml works on the laptop, it works in the demo, it ships, and the confidence that carried it there quietly becomes the reason nobody asks the one question that decides whether it is safe: what does this do when it breaks.
That question is where I spend my time, because it is where the entrances are. A system is rarely equally secure in every state, and the state nobody tested is almost always the weaker one. The engineers most sure of their setup are often the ones who have never once watched it fail, and the distance between that confidence and that blind spot is a place an attacker can stand.
The blind spot survives because the failure that opens it does not look like a security bug. It looks like an operations problem, and calling it a security problem means admitting the confident version of the story was incomplete. So it stays filed under flaky, unexamined, shipped, running in production under a green checkmark. Using it against a system starts with seeing the two kinds of failure as genuinely different things, which most people do not.
Two different failures
A correctness bug is a wrong answer. Given input X, the system returns Y when it should have returned Z. It lives in the domain of what the system computes. We test for it with assertions, we catch it with fuzzing and property checks, and when a security person finds one they already know the vocabulary: injection, logic flaw, broken access control.
A reliability defect is the absence of a correct answer in time. The system hangs, crashes, restarts, runs out of memory, times out, or returns a degraded result because a dependency it needed was slow or gone. It lives in the domain of whether and when the system answers at all. It gets triaged to the operations column. It gets the word flaky attached to it, which is engineering slang for we have decided not to think about this.
The reason that triage is a mistake is simple and almost never stated out loud. A system is rarely equally secure in every state. It is one thing when it is healthy and a different thing when it is degraded, and the degraded version is usually the weaker one.
Degraded states are usually weaker
Walk through what systems actually do when they come under stress, and the pattern is hard to unsee.
When an authorization dependency becomes unreachable, a great many systems fail open. The policy engine times out, the call throws, the catch block logs a warning and returns allow, because the engineer who wrote it was optimizing for availability and did not want a slow sidecar to take down the whole product. That is a reliability decision with a security consequence, and it has a name: CWE-636, failure to fail securely. The steady state checks the policy. The degraded state does not.
When a process crashes and something restarts it, there is a window during boot where it is listening before it is ready. It binds the port, then loads the config, then fetches the TLS material, then wires up authentication. For a few hundred milliseconds it is a server that answers without the controls that make it safe to answer. Steady state is locked. The restart window is not.
When a log pipeline backs up or a disk fills, logging degrades or stops. The application keeps serving, but it stops writing the record of what it served. Detection goes quiet while the system stays up. The steady state is observed. The degraded state is blind.
When a healthcheck only proves that a port is open, a process can report healthy while it is anything but, and a load balancer will keep sending real traffic to an instance that has lost the half of itself that matters.
None of these are logic bugs. Every one of them is a reliability defect. And every one of them moves the system from a stronger security posture to a weaker one, on a trigger that is often nothing more exotic than load, a slow network, or a dependency going away.
The red-team move is to force it
Here is where it stops being an availability conversation and becomes an attack one.
A reliability defect on its own is latent. It fires when the stars align, when traffic spikes at 3 a.m. or a deploy races a cache warm. The defender experiences it as bad luck. The thing that turns it from bad luck into a vulnerability is a reliable trigger. If an attacker can force the degraded state on demand, they are no longer waiting for the system to be weak. They are choosing the moment.
Say it plainly. Forcing a reliability defect converts the weakest state of a system from an occasional accident into a condition the attacker selects. You do not need the fail-open path to be reachable by luck. You need to be able to knock out the dependency whose absence opens it, whenever you want, and then walk through while it is open.
The classic red-team instinct is to ask what the system computes incorrectly. The more productive instinct, and the rarer one, is to ask how the system fails, and whether its failure mode is more or less privileged than its normal one. The second question is where the entrances are.
Why Docker is the richest ground for this
I am picking on Docker on purpose, because it is everywhere and its defaults are generous in a way that manufactures reliability defects for free. It is also where the confidence runs highest. The same engineer who will defend their container setup in a design review has usually never run it out of memory on purpose, and the defaults reward that incuriosity by working anyway, right up until someone makes them fail. Every example below runs against containers you own, in a lab you control. The point is to understand the mechanism well enough to find it and close it, not to point it at anyone.
Resource limits are off until you turn them on
By default a Docker container has no memory limit and no process-count limit. It can allocate until the host has nothing left, and it can spawn processes until the host runs out of PIDs. When memory runs out, the kernel out-of-memory killer wakes up and picks a victim by its own heuristic, and that victim is not guaranteed to be the greedy container. It can be a different container on the same host, or a process the host needed.
# The quiet version. Nothing here says "unlimited," which is exactly the problem.
services:
app:
image: your-app:latest
sidecar-auth:
image: your-policy-engine:latest
That file has a reliability defect in it and the defect is what is missing. There is no mem_limit, no pids_limit, no cpus. If a request can make app balloon its memory, or if app can be driven to fork, the OOM killer may take out sidecar-auth instead, and now you are in a very interesting situation: the thing enforcing policy is dead and the thing being policed is alive. That is CWE-400, uncontrolled resource consumption, and CWE-770, allocation without limits or throttling, turned into a targeted removal of a control. In MITRE ATT&CK terms the pressure is T1499, endpoint denial of service, but the goal is not the outage. The outage is the crowbar. The prize is the state the outage leaves behind.
The fix is boring and that is the point: