CL-0022: tmpfs exec — “Permission denied” running from tmpfs¶
Severity: LOW
Derivation (see severity model):
- Baseline: A — the attacker already has code execution in this container, as the workload uid
- Precondition: Removes a mitigation —
exec,suidanddevon a tmpfs grant no privilege. They remove three defaults Docker applies to every tmpfs - Impact: Single container
- Qualifier/modifier: none
- Derived: Removes a mitigation × Single container = LOW
- Shipped: LOW
- Evidence:
_cl0022and_t22_exec_tmpfs— a tmpfs isnoexecby default and:execremoves it, re-proven against a live container on every CI run
References: - OWASP Docker Security Rule #8 - Docker docs — tmpfs mounts
What it detects¶
A tmpfs: entry that explicitly passes exec, suid, or dev — each of which removes a security default Docker applies to every tmpfs:
tmpfs:
- /tmp:exec # flagged: re-enables execution (default is noexec)
- /tmp:suid # flagged: re-enables setuid (default nosuid)
- /scratch:exec,suid # flagged: names both
- /tmp # clean: gets noexec,nosuid,nodev by default
- /run:size=64m # clean: size doesn't remove the defaults
- /tmp:noexec # clean: that's the secure value
Matching is on whole comma-separated tokens, so the secure noexec is never mistaken for exec. The long volumes: [{type: tmpfs}] form is out of scope — it keeps the secure defaults and can't express these tokens.
Why it matters¶
Docker mounts every tmpfs with noexec,nosuid,nodev by default — verified across the short, list, and long mount forms, and the defaults survive even when other options like size= are set. The only way to lose them is to pass the opposite option explicitly:
| Option | Removes | Effect |
|---|---|---|
exec |
noexec |
binaries can be executed from the mount |
suid |
nosuid |
setuid/setgid bits are honored |
dev |
nodev |
device nodes on the mount are usable |
A tmpfs is always writable and in-memory. Re-enabling execution turns it into a place to stage and run a dropped payload — the classic foothold an attacker wants, and the more so under read_only: true (CL-0007), where tmpfs is often the only writable path. suid additionally opens a local privilege-escalation vector. Because the secure posture is the default, an explicit exec/suid/dev is a deliberate weakening that should be justified.
This is why the rule flags the presence of these options rather than the absence of noexec/nosuid/nodev: a plain tmpfs: [/tmp] is already hardened by Docker, so flagging it would be noise.
Fix¶
Remove the offending option to fall back to Docker's secure default:
# Before
tmpfs:
- /tmp:exec
# After
tmpfs:
- /tmp
There is no auto-fix: the option is set deliberately when present, so reverting it (which would break a workload that genuinely executes from the mount) is left to manual review.
Reading the failure¶
If you arrived here from an error, it is probably this one — a binary or script refusing to run from a tmpfs. The busybox wording is re-proven against a live container on every CI run (scripts/validate_rule_premises.py); the exact prefix varies by shell.
| Symptom in logs (verbatim) | What it means | Action — in this order |
|---|---|---|
sh: line 0: /scratch/busybox: Permission denied executing a file that is executable (x bit set) from a tmpfs |
the mount's default noexec blocked the exec — not a file-permission problem |
1. relocate the executable into the image (bake it in at build time) or onto a named volume; 2. only if the workload genuinely must write-then-execute on that path, add :exec — which is exactly what this rule flags, so pair it with a suppression carrying a documented reason |
The trap is that the obvious fix (:exec) is the finding: a writable, executable, in-memory mount is the classic staging ground for a dropped payload, and doubly so under read_only: true (CL-0007), where tmpfs is often the only writable path. Treat :exec as a justified exception, not a fix. A related red herring: chmod +x appears to succeed on the tmpfs file and changes nothing — noexec is a property of the mount, not the file.
ATT&CK coverage¶
No adversary technique maps to this rule. That is a finding about the rule, not an omission — see below.