deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Jenkins near-miss shows build agents, not controllers, are the attack surface

A fork-PR build briefly reached an internal metadata endpoint, and the resulting audit found shared agent pools, docker.sock mounts and long-lived build VMs behind the real risk.

Jenkins near-miss shows build agents, not controllers, are the attack surface

What happened

According to a first-person account by Oleksandr Kuryzhev on dev.to, originally published on his personal blog, a routine security review escalated when a build triggered by an outside contributor's pull request briefly had an open network path to an internal metadata endpoint. Nothing was exfiltrated, the author says, but the margin was thin enough that the team stopped other work and audited the whole build setup.

Their previous mental model, as described in the post, put the security boundary at the Jenkins controller: role-based access control, matrix authorization and folder permissions were assumed to cover everything downstream, and agents were regarded as dumb executors. The audit overturned that assumption. Agents execute whatever the pipeline contains — internal jobs, but also code from strangers' forks, community plugins fetched mid-build, and install-time scripts from dependencies — so the execution environment itself has to be treated as hostile.

One label, two trust levels

The first finding was structural. A single agent pool labelled linux-docker served both internal, trusted repositories and untrusted fork-PR builds, with the same images and the same credential-binding scope. In Jenkins a label only tells the scheduler where a job may run; it creates no isolation. External code was therefore landing on machines that held deploy credentials for internal services — an exposure nobody had explicitly accepted, maintained simply because one pool was easier to run than two. Separately, unrestricted JNLP/Remoting meant Groovy code running on an agent had a plausible route back into the controller JVM, potentially reaching the credentials store depending on which plugins and shared libraries a pipeline loaded.

docker.sock as a root shortcut

The second finding came from the review itself rather than an exploit: agents that built container images had /var/run/docker.sock mounted, which the author describes as functionally equivalent to handing the container root on the host node. The situation was aggravated by agent containers running as root, with no capabilities dropped and no seccomp or AppArmor profile applied. The post notes this pattern is extremely common in Jenkins-on-Kubernetes tutorials, and argues any docker.sock mount on a build node should be treated as a critical finding rather than a convenience.

Agents that never died

Third, the fleet mixed in static VMs that had been running for months, sometimes years, patched only when someone remembered. They drifted far from their declared images, accumulating manually installed tools, stale SDK versions and credentials left over from debugging sessions. Workspace hygiene was equally uneven, and the audit found at least one case where a previous job's temporary credentials file survived in a workspace and surfaced, unmasked, in an unrelated job's build artifacts. Patching lagged because ownership of the agent image was nobody's formal responsibility.

The replacement architecture

The team rebuilt around ephemeral, pod-per-build agents using the Kubernetes plugin: every build gets a fresh pod, destroyed the moment the build ends. Their hardened pod template runs as a non-root user with a read-only root filesystem, all Linux capabilities dropped, a runtime-default seccomp profile, no service-account token mount and an emptyDir workspace — and deliberately no docker.sock or hostPath volumes.

Internal and fork-PR builds now run in entirely separate pools, enforced through Kubernetes namespaces under a restricted Pod Security Standard that forbids privileged containers, hostPath and host networking. PR builds get no long-lived cloud credentials at all; anything they need arrives as a short-lived OIDC or Vault token scoped to the specific job and expired within minutes. A NetworkPolicy restricts agent egress to package registries and the internal artifact store, agent images are rebuilt weekly instead of patched in place, and the controller's script console is limited to a two-person admin group. The acknowledged trade-off is added cold-start latency from image pulls.

Why it matters

Build agents occupy an awkward position: they hold privileged access to source code, secrets, artifacts and deployment targets while simultaneously executing arbitrary third-party code. Most hardening effort goes to the controller — its UI, its RBAC, its plugins — but the agent execution environment is what determines how far a compromise actually spreads. The three patterns exposed here are not exotic misconfigurations; shared labels across trust tiers, docker.sock mounts and long-lived build VMs are defaults in countless CI setups, which is why this near-miss generalises well beyond one team's Jenkins installation.

  • #jenkins
  • #ci-cd
  • #kubernetes
  • #devops
  • #security

Related posts