GoLeash: Package-Level Runtime Policy Enforcement
GoLeash: Package-Level Runtime Policy Enforcement
go.sum and govulncheck catch known bad versions. They do not stop a compromised dependency that starts doing something new after install — dialing home, rewriting files, spawning processes — while the rest of the binary still looks legitimate. GoLeash is an eBPF-based research prototype (CCS 2025 artifact; arXiv:2505.11016) that applies least privilege per Go package: it profiles which packages exercise which syscalls (and along which stack paths), then enforces those allowlists at runtime. This chapter teaches the mental model, how it differs from process- or container-scoped sandboxes, and the analysis → enforce workflow from the project’s primary docs. It sits next to the supply-chain chapter as the runtime half of dependency hygiene.
Thesis
Container seccomp and gVisor policies are process-wide. If any package in the binary is allowed to connect(2), every package inherits that right. GoLeash’s claim is finer: attribute each monitored syscall to a Go package (via stack traces and Go symbols) and allow only the capabilities that package earned during profiling. A JSON parser that never opened a socket during analysis should not open one in production.
Mental model
compromised dep "helpfuljson"
│ sneaks net.Dial into init or a rare branch
▼
process / container sandbox GoLeash (package scope)
--------------------------- ------------------------
seccomp: allow connect package encoding/json : {read, …}
→ whole process may connect package helpfuljson : { } ← no connect
syscall connect from helpfuljson → deny/log
| Mode | What it does |
|---|---|
| Analysis | Observe syscalls + stack traces under representative load; build per-package capability allowlists |
| Enforcement | On each monitored syscall, check package + approved call paths; log or block violations |
Properties called out in the paper and README:
- Works without recompiling the target (attach to binaries; needs usable Go symbol / stack info — avoid stripping with
-ldflags="-w -s"while profiling). - Remains useful under obfuscation better than pure static analysis, because decisions are based on runtime stacks.
- Overhead is described as acceptable for the evaluated workloads; treat that as research-reported, not a production SLA.
Worked example — profile then enforce (from upstream docs)
GoLeash is not installed here (building it needs clang/libbpf and would require system packages — out of scope for this chapter). The workflow below is taken from the project’s README runnable example against FRP (Fast Reverse Proxy).
Case 1: Analysis against FRP
# Target app: keep Go symbols (do not strip -w -s); CGO_ENABLED=1 as upstream asks
git clone https://github.com/fatedier/frp.git
cd frp
# Edit Makefile: drop strip flags / set CGO_ENABLED=1, then:
make
# GoLeash config.toml — which binaries to watch
# [targets]
# binaries = ["frpc", "frps"]
# binary_paths = ["../frp/bin/frps", "../frp/bin/frpc"]
cd goleash
make # build GoLeash (needs llvm/clang/libbpf on the build host)
make build # analysis mode — profiles while the workload runsIn another shell, drive realistic behavior (upstream uses FRP e2e):
cd frp
make e2eStop GoLeash when finished. Policy lands as something like trace/store.json (path per README: trace\store.json on Windows-style docs; on Unix expect trace/store.json). Re-run e2e a few times to thicken coverage before trusting the allowlist.
Case 2: Enforcement and a simulated violation
# Manually remove a capability for some package from the saved policy JSON
# Then:
cd goleash
make cap-enforce
# Re-run the workload; unauthorized syscalls from that package are blocked/loggedCase 3: What “package capability” means in practice
Think in capabilities, not raw syscall numbers alone:
| Package role | Syscalls you expect in analysis | Red flag later |
|---|---|---|
| Pure codec / math | mostly none, or read/write on caller-owned fds |
connect, execve, ptrace |
| HTTP client helper | socket/connect/sendto… |
openat of /etc/shadow, chmod on unrelated paths |
| Main / wiring package | whatever the app truly needs | sudden new families after a dependency bump |
The paper’s threat model is exactly that second column appearing after a supply-chain compromise.
The trap
- Profiling only unit tests — allowlists miss production paths; either under-block (false sense of security) or over-block when you finally hit real traffic.
- Stripping binaries before analysis — GoLeash needs symbol/stack information for package attribution.
- Treating GoLeash as a replacement for
govulncheck, module pinning, and reproducible builds — it is a complementary runtime layer, still a research prototype. - Equating “eBPF on the host” with “safe multi-tenant SaaS” — you still need the usual privileges and blast-radius story for loading BPF programs.
The boring rule
Pin and scan dependencies as usual; for high-value Go services, profile package capabilities under real load, then enforce. If a dependency suddenly needs connect and never did, that is an incident — not a silent go get success.
Try this
- Read the abstract and §1 of arXiv:2505.11016.
- Skim chains-project/goleash README modes and the FRP example.
- On a lab host where you already have clang/libbpf (no new system installs from this book), build GoLeash and profile a small
net/httpdemo: confirm yourmainpackage mayconnectwhile a vendored pure-Go helper may not. - Diff
go.modbefore/after a dependency update and re-run analysis — see which packages gain capabilities.
Sources
- Cesarano, Monperrus, Natella. GoLeash: Mitigating Golang Software Supply Chain Attacks with Runtime Policy Enforcement. arXiv:2505.11016. https://arxiv.org/abs/2505.11016 / https://arxiv.org/pdf/2505.11016
- GitHub: https://github.com/chains-project/goleash
- CCS 2025 artifact (Figshare): https://doi.org/10.6084/m9.figshare.28793891