Skip to main content

The Gate That Wasn't There: What Nomad's CVE-2026-14891 Teaches About Scheduler Isolation

10 min readDora NodaDora Noda
Share
On this page

On July 7, 2026, HashiCorp shipped Nomad 2.0.4 with a security entry that should make every platform team pause before it makes them patch:

docker: Enforce allowed_modes or allow_privileged requirement to set host namespace modes in task. This is CVE-2026-14891.

Translated out of changelog-ese: until that release, a Nomad job could request the host's network, PID, or IPC namespace and the Docker task driver would simply allow it — even on a client whose operator had never opted into privileged workloads. No kernel exploit. No misconfiguration the operator chose. The gate that was supposed to stand between "run my container" and "join the host's namespaces" was not misconfigured; it was missing.

Here is the verdict up front: if you run multi-tenant workloads on Nomad, upgrade to 2.0.4 (or Enterprise 1.11.8 / 1.10.14), audit every job spec for host namespace modes, and treat this CVE as a lesson about where your scheduler enforces its trust boundary — because Nomad enforces it in the client-side task driver, while Kubernetes enforces it at the API server before scheduling ever happens. That architectural difference, more than the bug itself, is what should inform a self-hosted PaaS choosing between Nomad's operational simplicity and Cluster API's Kubernetes-native isolation stack. The rest of this post is the receipts.

The missing gate, concretely

Nomad's Docker driver lets a task request host namespaces through three familiar options:

hcl
task "web" {
  driver = "docker"
  config {
    image        = "my-tenant-image:latest"
    network_mode = "host"
    pid_mode     = "host"
    ipc_mode     = "host"
  }
}

Each of those lines dissolves a layer of container isolation. network_mode = "host" puts the container on the host's network stack — it can sniff node traffic and bind host ports. pid_mode = "host" exposes every process on the machine, including other tenants' workloads and the Nomad client itself. ipc_mode = "host" shares System V IPC and POSIX shared memory. Any one of them is a sandbox escape in practice; together they are root-on-the-node waiting for a motivated tenant.

Nomad's design anticipated exactly this: client agents carry a Docker plugin block where the operator declares which privilege escalations are acceptable on that machine:

hcl
plugin "docker" {
  config {
    allow_privileged = false
    volumes {
      enabled = false
    }
  }
}

The contract is straightforward — a task asking for more privilege than the client allows should be rejected. CVE-2026-14891 was the discovery that for host namespace modes, the driver never checked. The allowed_modes / allow_privileged requirement existed as configuration surface but was not enforced on this code path, so any authenticated user able to submit a Docker job — or any API caller at all on a cluster with ACLs disabled, which is Nomad's default — could land a container in the host's namespaces. Third-party trackers rate the CVE 8.7 (High), which fits: low attack complexity, no user interaction, and a full confidentiality/integrity/availability break of the node on success.

The fix versions tell you how seriously HashiCorp took it — a same-day patch across every supported line:

Release lineFixed inReleased
Nomad Community 2.0.x2.0.4July 7, 2026
Nomad Enterprise 2.0.x2.0.4July 7, 2026
Nomad Enterprise 1.11.x1.11.8July 7, 2026
Nomad Enterprise 1.10.x1.10.14July 7, 2026

Functionally, the fix inserts the check where it always belonged: when the Docker driver translates a task's requested namespace modes into a container-create call, it now validates the request against the client's allowed_modes / allow_privileged settings first and rejects the allocation when they do not permit it. The gate moved from "documented in the config reference" to "evaluated on the enforcement path" — which is the entire story of this CVE in one sentence.

It was not a one-off: the siblings in the same release

What elevates CVE-2026-14891 from "patch and move on" to "reconsider your trust model" is the company it kept. Nomad 2.0.4 shipped three security fixes, and all three are the same species — a driver- or client-side enforcement point that failed to hold:

CVE / fixWhat the gate was supposed to doWhat actually happened
CVE-2026-14891 (host namespace modes)Require allowed_modes / allow_privileged before granting host PID/net/IPCRequirement never evaluated; any job submitter got host namespaces
CVE-2026-14896 (volume symlink bypass)Honor volumes.enabled = false in the Docker plugin configA task using a symlink could bypass the disabled-volumes setting and mount host paths
Cross-namespace host-volume claim deletionScope host-volume-delete to the claim's own namespaceA token with delete permission in one namespace could delete another namespace's sticky volume claims

Three misses, one release, one pattern: Nomad's isolation guarantees were being enforced (or not) at distributed, per-client decision points rather than at a single admission boundary. Each fix is individually correct, but a platform team has to ask the uncomfortable question — how many more unenforced requirements are sitting in driver code paths that only get audited after someone reports them? HashiCorp's own July 8, 2026 security bulletins treated each as a distinct advisory, which is proper disclosure practice, but for an operator the correlated lesson matters more than any single CVE number.

Where the check lives: Nomad vs Kubernetes

This is the section the TODO item actually demands, so here it is as plainly as possible. The two schedulers put their trust boundaries in different places, and CVE-2026-14891 is what that difference costs when it fails.

Isolation questionNomad (pre-2.0.4 behavior)Kubernetes (Pod Security + RBAC)
Where is "no host namespaces" decided?In the Docker task driver on each client agent, consulting local plugin configAt the API server by the Pod Security Admission controller, before the pod is persisted
What does a tenant's request hit first?The server accepts the job; the client driver decides at allocation timeAdmission rejects the pod object outright — it never exists, is never scheduled
Who can widen the boundary?Anyone who can edit a client agent's config file (usually the same team running jobs on small fleets)Only someone with RBAC rights to relabel the namespace or change cluster admission config
What can a tenant self-grant?Pre-fix: host namespaces with a three-line job spec, no operator actionNothing — a tenant cannot grant itself what admission denies; privilege requires a namespace-label or role change by an admin
Cross-namespace leakage surfaceNamespaces partition jobs and allocations, but nodes, ACL policies, quotas, and host volumes are cluster-global by designNamespaces plus NetworkPolicy, ResourceQuota, and per-namespace PSA levels compose a tenant boundary the API server holds

The Kubernetes parallel is worth making concrete, because it shows what "enforced at admission" feels like. A tenant pod requesting the host network:

yaml
spec:
  hostNetwork: true
  hostPID: true

submitted into a namespace labeled pod-security.kubernetes.io/enforce: restricted is rejected by the API server with a forbidden error naming the exact violated policy. The pod is never created. There is no client agent to misconfigure, no per-node config file whose absence silently permits the request — the decision happens once, centrally, on every write.

Honesty requires the mirror-image caveat: Kubernetes admission only bites where it is switched on. Pod Security Admission enforces nothing on namespaces that carry no pod-security.kubernetes.io/* labels — an unlabeled tenant namespace silently admits a hostNetwork pod, which is the exact same failure shape (an unenforced default) wearing different clothes. The difference is one of blast radius and auditability: on a Cluster-API-managed fleet you can list every namespace's labels from the management cluster in one command and put enforce: restricted on tenant namespaces as fleet policy, whereas Nomad's pre-fix gap lived in driver code no operator could have audited from config. Both systems need the operator to turn the locks; Kubernetes at least keeps all the locks in one building.

The enterprise-gating wrinkle

There is a second structural difference that matters for a self-hosted PaaS choosing a scheduler on isolation grounds, and it is about what it costs to write content-aware policy — rules like "tenants may only run pre-approved images" or "no job may request host namespaces, ever."

In Nomad, that layer is Sentinel, HashiCorp's policy-as-code framework — and Sentinel is Enterprise-only. An OSS Nomad operator gets ACLs (coarse capabilities per namespace: submit jobs, read logs, manage volumes) but no way to say "reject any job spec containing network_mode" short of an out-of-band admission webhook they build and operate themselves. The irony of CVE-2026-14891 is sharp here: the operators least able to compensate for the missing driver check with their own policy layer were the OSS users, because the policy layer is the paid tier.

In Kubernetes, the equivalents ship in the box: Pod Security Admission for the standard profiles, ValidatingAdmissionPolicy (CEL expressions evaluated by the API server, no webhook to operate) for custom rules like approved-registry-only, and RBAC for who-may-do-what — all OSS, all enforced at the same admission boundary. A Cluster-API fleet inherits this entire stack the moment the management cluster exists; there is no isolation feature that only unlocks with a license key.

None of this makes Nomad indefensible — its single-binary operational model, native non-container workloads, and simpler mental model are genuine advantages for small teams running trusted workloads. But "we run mutually untrusting tenants" is precisely the workload shape where the admission architecture matters more than the binary size, and CVE-2026-14891 is now the case study for why.

Monday-morning checklist

If you operate Nomad, in this order:

  1. Upgrade. Nomad 2.0.4, or Enterprise 1.11.8 / 1.10.14 on the 1.x lines. The fix is a hard enforcement change, so read the upgrade notes, but do not defer it — the exploit is a job spec, not a binary payload.
  2. Audit existing specs for host namespace modes. Search every job file for network_mode, pid_mode, and ipc_mode set to "host". Each hit is either a workload that needs an explicit privilege grant going forward or evidence of past exploitation to investigate.
  3. Confirm ACLs are actually enabled. If your cluster never set acl { enabled = true }, every CVE in this class is remotely triggerable by any API caller. Enabling ACLs after the fact is a migration, but this is the week to start it.
  4. Harden the client plugin blocks explicitly. Do not rely on defaults — set allow_privileged = false, enumerate an explicit allowed_modes allowlist instead of opening blanket privilege, and keep volumes { enabled = false } unless a client genuinely needs host volumes (remember the symlink sibling, CVE-2026-14896, targeted exactly this setting).
  5. If you run a CAPI fleet instead, take the transferable lesson: label every tenant namespace pod-security.kubernetes.io/enforce: restricted (with warn/audit first if you need a migration window), and verify from the management cluster that no tenant namespace is unlabeled — your version of the missing gate is the namespace someone forgot to label.

The question to ask about your scheduler

CVE-2026-14891 was not a kernel bug, not a zero-day in a dependency, and not a misconfiguration any operator opted into. It was a mainstream orchestrator's own privileged-mode gating failing to evaluate its own requirement — disclosed promptly, to HashiCorp's credit, and patched across every supported line within the week. But three sibling-class fixes in one release say the failure was structural: isolation decisions scattered across client-side drivers instead of concentrated at one admission boundary that every request must pass.

So the scheduler question for a self-hosted PaaS is not "which binary is simpler to operate" — it is "show me the single choke point every tenant request passes through, and show me it is on." Kubernetes' answer (admission at the API server, auditable from one place, with the OSS policy stack included) survived this particular comparison. Whoever you bet your tenants' isolation on next should have to give the same demonstration — with evidence, not architecture diagrams.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex