Skip to content

tempus.build

The CI dependency layer: caching for speed, isolation for security

Almost every CI job starts the same way: download dependencies. npm install, pip install, docker pull — packages from npm, PyPI, Docker Hub and a dozen other registries. This step is at once CI’s biggest speed lever and part of a build’s security model. Let’s look at both, and at how tempus.build builds the dependency layer.

On the left, ordinary CI: a job fetches dependencies straight from the open internet, every build; the boundary is a container. On the right, tempus.build: a job in a single-use Kata VM takes dependencies from a pull-through cache over a private network, and the cache goes to the internet only on a miss; npm, pip and cargo go direct. Paths to the cluster infrastructure and cloud metadata are blocked; Hubble records runner network flows, and Tetragon on the host records signs of an escape from the VM.
The same dependency install: straight from the internet versus a cache over a private network, with a hardware VM boundary around the job.

Side one: speed

Without a cache, every build downloads dependencies again — from public registries, often in another region. mvn, go mod download or docker pull makes hundreds of small requests to remote servers, adding network latency and unpacking on top. On dependency-heavy pipelines it’s a noticeable share of a job’s time — and it repeats on every run, even though the packages are the same.

On top of that, public registries are sometimes slow or down.

Side two: security

The same step is also a risk. CI runs untrusted code: steps from PRs, and above all the dependencies themselves and their install scripts (postinstall and the like). If that code is unconstrained, it can:

  • exfiltrate secrets — tokens and keys from the environment — to someone else’s server;
  • call an attacker’s server (C2) and pull a second stage;
  • fetch a malicious or typosquatted package, or exploit A supply-chain attack: an attacker publishes a package in a public registry under the name of your internal dependency, and the build pulls their code instead of yours. — a package in a public registry published under the name of your internal dependency.

Because of these risks, GitHub advises against self-hosted runners on public repositories (opens in new tab) (which is why tempus.build currently serves private repositories only). The point isn’t to deny the code everything — it’s to limit the blast radius if it turns out to be malicious.

The cache: dependencies arrive over a private network

The speedup rests on not downloading the same thing twice. In the same data-center region as the runners sits a A registry cache proxy: on the first request it pulls the package from the upstream registry and stores it; later builds take it locally, without going out to the internet. cache on fast NVMe. Docker Hub and other image registries, apt, Go modules, Maven/Gradle, NuGet, RubyGems and conda-forge go through it:

  • The first request for a package goes through the cache to the upstream registry and is stored there.
  • Later builds take it from the cache over the private network, not the internet.
  • The cache deduplicates: image layers are stored once by digest and served to every build and runner.

The cache holds only public content — by design. It pulls anonymously, without your credentials, so it can neither fetch nor store private packages or images. Your own registry settings always take precedence: a private index or registry wins over the cache, and private dependencies are fetched directly from it — A supply-chain attack: an attacker publishes a package in a public registry under the name of your internal dependency, and the build pulls their code instead of yours. gets no substitution point through a shared mirror. By the same logic npm, pip, poetry, uv and cargo go straight to their registries: these tools write the source URL into the lockfile, and a shared cache would end up in files you commit.

The runner points at the cache by default — no workflow changes. The estimate for a typical workflow is 1.8–2.5× (not a measurement): it comes from 4 vCPU on 4core instead of 2 on the standard ubuntu-latest for private repositories, bare-metal AMD Ryzen and the cache; on real pipelines with compilation and image builds we’ve seen up to 5×. Your own pipeline will show the real number for your case — more on speed.

Containment: the boundary is a VM

Speed is half the win. The other half is where the boundary around untrusted code sits. At tempus.build it’s a single-use Kata VM — hardware isolation per job, not a network perimeter (how the VM boundary works — in the Kata isolation deep-dive). The job is root inside its own VM, as on GitHub-hosted; the boundary is the VM. Around it:

  • Paths to the infrastructure are closed. A job can reach the open internet, but access to the cluster internals (control plane, neighbouring runners) and to cloud metadata (a common SSRF vector — stealing the node’s roles and tokens) is blocked. A compromised job can’t reach neighbouring runners or the control plane over the cluster network.
  • Traffic for the cached ecosystems goes to the cache over the private network — repeat downloads stay inside the perimeter.
  • Network and host events are recorded. An eBPF-based Kubernetes CNI: network policy and microsegmentation enforced in the kernel, plus flow visibility via Hubble. (an A Linux kernel technology: safely runs programs inside the kernel for networking, observability and security without a loadable module. Cilium and Tetragon are built on it. CNI) records runner network flows via Hubble; eBPF runtime detection: watches for suspicious behaviour on the node in real time — attempts to break out of isolation, infrastructure anomalies. (A Linux kernel technology: safely runs programs inside the kernel for networking, observability and security without a loadable module. Cilium and Tetragon are built on it. on the host) records signs of an escape from the VM.

What this adds up to

The dependency layer covers two jobs at once — by splitting the work:

  • Speed — the cache removes repeat downloads; it also keeps dependency traffic on the private network (less external contact).
  • A limited blast radius — untrusted code is locked in a Kata VM, the paths to infrastructure and metadata are closed, attempts to escape to the host are recorded.

Against typosquatting and A supply-chain attack: an attacker publishes a package in a public registry under the name of your internal dependency, and the build pulls their code instead of yours., your lockfile with hashes is the main defence. The cache doesn’t protect against them — it serves whatever is upstream.

Limitations

  • A cache miss still goes upstream. The first run of a new dependency isn’t instant — the cache fills up; the win comes from reuse.
  • Outbound network traffic from a runner — pulling dependencies and images. Clouds bill for it, and it has to be controlled for security. is open. The boundary around untrusted code is the Kata VM, not the network: paths to infrastructure and metadata are closed, but not the whole internet. It’s a deliberate trade-off: a strict list by default would stop the build on every dependency not yet on it.
  • Host eBPF can’t see inside the guest VM — it has its own kernel. Tetragon records attempts to reach the host, not events inside the guest; it complements VM isolation, it doesn’t replace it.
  • The cache isn’t backed up, and there’s no durability guarantee. The integrity of the packages themselves rests on their hashes and signatures and your lockfile, not on “trusting the mirror”.

Three sides of one model

It’s the same approach as the runner-image supply chain (what’s inside the image) and the Kata hardware boundary (what the image can do to the host) — here it’s the third side: where dependencies come from and how a job is contained. The full picture and links are on the Security & isolation page.

← back to the blog