Stop supply-chain attacks at the kernel.
When a poisoned npm package or compromised CI action runs in your GitLab Runner, it can't phone home. Default-deny egress, kernel-enforced, live in two minutes.
Open source, MPL-2.0, no signup. Want SaaS or Managed Runners instead? Talk to us →
- ·
Early 2026
Four trojanized CI dependencies. Two months.
A scanner used by ten thousand pipelines. An HTTP client with seventy million weekly downloads. A vault CLI used to fetch CI secrets. A 317-package npm wave shipping during this site's launch window. Different actors, different vectors, identical exfiltration shape — a trusted binary inside a Runner job, phoning home over plain egress. Leitwacht blocks each of them at the kernel.
Trivy supply-chain compromise
CI scanner / GitHub Actions tag repointing
76 of 77 release tags retroactively poisoned with a multi-stage credential stealer.
After a misconfigured GitHub Actions workflow leaked a privileged token in February 2026, attackers force-pushed every release tag in aquasecurity/trivy-action and aquasecurity/setup-trivy. The malicious code runs silently before the real scanner — pipelines look green, secrets walk out the side door.
axios npm compromise
Maintainer account takeover via social-engineered RAT
70 M weekly downloads. Two malicious versions for ~3 hours. Cross-platform RAT delivered through a bundled dependency.
axios 1.14.1 and 0.30.4 were published from a compromised maintainer account. The source itself was unchanged — the payload arrived via a new dependency, [email protected], which dropped a remote-access trojan on macOS, Windows and Linux.
Bitwarden CLI compromise
Compromised checkmarx/ast-github-action upstream
@bitwarden/[email protected] malicious for ~90 minutes. 334 downloads. Targeted Azure / AWS / GitHub / GCP tokens, SSH keys, shell history — and AI / MCP configuration files.
The trojanized release ran three collectors against cloud and developer secrets stores, including AI-tool configuration and MCP-related files — a brand-new exfiltration class. Self-propagating: the worm could rewrite a victim's npm packages and republish them with the same payload.
Mini Shai-Hulud npm wave
Compromised "atool" npm publisher · preinstall hook
317 packages, 637 malicious versions in two 90-minute waves. preinstall hook scrapes every CI credential class plus AI / MCP configs.
A 498 KB obfuscated Bun preinstall payload — same scanner architecture and credential regex set as the April Shai-Hulud worm — targets npm tokens, GitHub PATs, AWS / GCP / Azure / Stripe / Slack / Vault / 1Password / Bitwarden, plus EC2 (169.254.169.254) and ECS (169.254.170.2) metadata endpoints. Exfiltration over HTTPS to a Cloudflare-fronted endpoint disguised as OpenTelemetry traces, plus GitHub dead-drops. The egress shape — unknown outbound HTTPS plus cloud-metadata calls — is exactly what the agent default-blocks at the kernel.
Sources cited inline within the longer writeups on the blog.
How it works
Three layers, kernel-deep, no proxy in the path.
Egress enforcement runs inside the kernel, attached to the Runner job's own cgroup. Nothing is rerouted through a sidecar. Each layer is independent — the kernel filter keeps blocking even if the DNS proxy crashes, the agent dies, or the control plane is unreachable.
DNS verdict — only sanctioned names resolve
userspace · per query
Every Runner job's DNS traffic is redirected (nftables) to a per-job resolver. Allowed FQDNs return real IPs and seed the kernel allow-map for the next layer. Everything else returns NXDOMAIN. DoT (853), DoH endpoints, IPv6, ICMP, and cloud-metadata destinations are unconditionally blocked here.
09:01:14 q=registry.npmjs.org A → ALLOW 104.16.27.34 09:01:14 q=registry.npmjs.org AAAA → DENY (IPv6 disabled) 09:02:11 q=sfrclak.com A → DENY no rule (NXDOMAIN)
Kernel verdict — default-deny per cgroup
kernel · every packet
A pinned eBPF program on the Runner pod's cgroup drops every outbound packet whose destination didn't come from a sanctioned DNS answer. Attached before the build container starts via containerd events + an init-container DNS-poll handshake — no NRI, no node-wide blast radius. Independent of the DNS proxy and the control plane: if either crashes, the kernel filter keeps blocking.
cgroup_id=23476843 dst=104.16.27.34:443 verdict=ALLOW src=registry.npmjs.org cgroup_id=23476843 dst=142.11.206.73:8000 verdict=DENY reason=lpm_miss
Forensic record + per-job baselines
control plane · post-hoc
Every blocked or allowed egress carries the process ancestry, the cgroup → pod → GitLab job mapping, and the rule that fired. That row is what an operator triages on a 3am alert. Per-(project, job-name) baselines learn the normal egress shape over multiple runs; departures raise anomalies before secrets walk out.
job_name=install comm=node parent_comm=npm action=block domain=sfrclak.com ip=142.11.206.73 port=8000 count=3
Editions
Same enforcement engine. Four ways to run it.
CE is the open-source agent (MPL-2.0), runs against a YAML bundle on disk. Cloud is the hosted control plane in our EU regions, talking to your agent. Managed Runners goes one step further — we operate the GitLab runner pool itself, so you point your project at it and the agent is already there. Self-hosted runs the whole stack in your cluster, for buyers who can't send CI telemetry off-prem. The control plane is BUSL: source-published, audit-able, fork-clean, auto-converts to MPL-2.0 on the Change Date.
| Feature | CE (open source) | Cloud (SaaS) | Managed Runners (hosted CI) | Self-hosted (EE) |
|---|---|---|---|---|
| License (agent) | MPL-2.0 | MPL-2.0 | MPL-2.0 | MPL-2.0 + BUSL extensions |
| License (server) | n/a — no server | BUSL | BUSL | BUSL |
| Hosting | you run the agent | leitwacht.eu (EU regions) | leitwacht.eu — runner + agent + server | your cluster |
| Who runs the GitLab runner | you | you | we do | you |
| Enforcement primitives (DNS proxy, eBPF egress, default-deny) | ||||
| Process attribution on every event | ||||
| Rule source | YAML on disk | central UI | central UI | central UI |
| Violation reporting | stdout / Prom / webhook | aggregated | aggregated | aggregated |
| Anomaly detection, baselines | ||||
| Slack / email alerting | ||||
| OIDC SSO, RBAC, audit log | ||||
| Fleet-wide policy management | ||||
| Data stays in your cluster | no — CI runs on our infra | |||
| Air-gapped / on-prem |
Invariant: all data-plane enforcement lives in the MPL-2.0 agent. Cloud and Self-hosted wrap a BUSL control plane around the same open-source agent — they only differ in where the control plane runs. Both repos are source-published; anyone can read them and verify every enforcement guarantee.
Managed runners
Don't want to run runners? We will.
Give us a GitLab runner registration token from your project, group, or instance. We register a runner against it, in our hardened pool — the agent attributes every packet to a process from the first job. Flip to block mode once your allowlist is tuned. You tag the jobs that should land there. This is the lowest-friction way to adopt Leitwacht. It is also the least sovereign way; read the right-hand panel before you decide.
Bring a token, not a cluster
You hand us a GitLab runner registration token from your project, group, or instance. We register a hardened, agent-protected runner against it and you tag the jobs you want to land there. No Helm, no DaemonSet, no kernel-version checklist.
We carry the runner-ops cost
Pool capacity, kernel patching, hardened images, autoscaling, isolation between tenants — our problem. You don't run a runner fleet just to get the security layer.
The sovereignty trade
CI workloads run on our infrastructure. That's a real change in trust model from self-hosted — here's what it does and doesn't mean.
What we see
- ▸ CI job source as it arrives from GitLab
- ▸ Dependency fetches, build artifacts, every blocked egress
- ▸ Process ancestry on each verdict
- ▸ Per-project egress baselines we use to flag anomalies
What we don't see
- ▸ Your prod cluster, your databases, your office network
- ▸ Anything you don't explicitly transmit through CI
- ▸ Decrypted contents of TLS sessions to allowed domains
- ▸ CI environments outside the runner job's lifetime
If your CI handles data you legally can't send off-prem, pick self-hosted instead. Same enforcement engine, your hardware.
Adjacent tools
Why not just use what you already have?
Honest one-liners on each adjacent option. We're a narrow tool — narrow on purpose.
| StepSecurity Harden-Runner | GitLab-thin, SaaS-only. We are GitLab-native and self-hosted. |
| Bullfrog | GitHub Actions only. No GitLab story. |
| kntrl | Agent only — no management plane, no UI, no fleet policy. |
| Cilium toFQDNs | Kubernetes-only, no Docker-executor support, no CI-specific UX. |
| GitLab network_policy | IP/CIDR only — no FQDN allowlists, no process attribution. |
Honest limits
What Leitwacht doesn't do.
Most security copy is a brochure. This is the version we'd want a peer to read before deploying.
- ▸ L3/L4 only — POST-to-allowed-domain exfiltration is not prevented. We do not do HTTP body inspection.
- ▸ Single-host scope. Cross-host traffic correlation is out of scope.
- ▸ IPv6 is unconditionally blocked.
- ▸ Egress enforcement requires Linux kernel 5.8+. Credwatch and proc_mem LSM hooks require 5.7+ with CONFIG_BPF_LSM=y.
- ▸ POST-to-allowed-domain exfiltration (e.g. uploading to a public Gist on gitlab.com) is not prevented — we do not do HTTP body inspection.
- ▸ GitLab Runner only. No GitHub Actions support, current or planned.
Frequently asked
The questions buyers actually ask.
If yours isn't here, the contact page goes straight to engineering.
What is the runtime overhead of the agent?
The eBPF egress filter adds roughly 50–200 ns per outbound packet, dominated by an LPM-trie lookup against the cgroup-scoped allowlist. DNS resolution adds ~1 ms per cold lookup (intercepted via nftables). For typical CI workloads — even ones doing tens of thousands of HTTP requests per job — wall-clock impact is sub-percent and undetectable.
What kernel version do I need?
Linux 5.8+ for egress enforcement (the cgroup_skb/egress program type plus the helpers we use). Credwatch (credential file access detection) and proc_mem (/proc/*/mem read blocking) require 5.7+ with CONFIG_BPF_LSM=y. We test against current Debian, Ubuntu LTS, and the EKS / GKE / AKS managed node images. Older RHEL / CentOS hosts may need a kernel update.
What happens if the agent crashes mid-job?
Fail-closed for enforcement. The kernel filter program is attached to the cgroup independently of the userspace agent — it keeps blocking even if the agent dies. The agent loads and updates policy; if it stops, telemetry stops and policy can no longer be updated, but the existing block list remains in force until the runner pod terminates.
Where does my data live?
Two answers depending on edition. Cloud SaaS: EU regions only, no US transfer, standard DPA, controller is the operating entity disclosed on the imprint. Self-hosted EE: nowhere outside your cluster — the agent emits events to a control plane you operate on infrastructure you own. Either way we never capture HTTP bodies or request payloads — only DNS lookups, destination IPs, process attribution, and verdicts.
Where is the operating entity based?
LeitWacht (Pty) Ltd is incorporated in South Africa — disclosed on the imprint. That is a fact about the legal entity, not about where your data lives. The Cloud SaaS control plane runs in EU regions; the self-hosted EE deployment runs wherever you choose to run it. Data residency is a property of the deployment, not the vendor address — an EU-incorporated SaaS vendor running on us-east-1 is the worse outcome here, and not an uncommon one.
Why GitLab only? What about GitHub Actions?
GitHub Actions has a different threat model — different runner-reuse semantics, different action / cache mechanics, different harness assumptions. Doing one CI runtime correctly is more valuable than doing both poorly. StepSecurity already covers the GitHub Actions side; we are deliberately the GitLab-native option.
How long does it actually take to deploy?
For a single Kubernetes runner pool: a Helm chart, a per-runner ConfigMap, and a kernel-version check. Two minutes from `helm install` to first blocked egress event is the realistic path. Multi-cluster fleets with custom node images take longer — typically a few hours including review of the eBPF program before sign-off.