Go CGo bindings for the nono capability-based security sandbox.
nono applies an irreversible, least-privilege sandbox to the current process using Linux Landlock (Linux) or Seatbelt/sandbox_init (macOS). You declare the paths and network modes the process needs; nono enforces them at the kernel level.
| OS | Arch | Library bundled? |
|---|---|---|
| macOS | arm64 | yes |
| macOS | amd64 | yes |
| Linux | amd64 | yes |
| Linux | arm64 | yes |
All platforms work out of the box. The static libraries for all four targets are bundled in the repository.
- Go 1.24+
- A C toolchain (
gccorclang) for CGo
go get github.com/always-further/nono-go
The bundled libraries are built from the upstream nono repository using scripts/build-libs.sh. Run this script when you want to update the bundled libraries to a newer nono upstream commit.
Requirements: cargo (for Apple targets), Docker (for Linux targets — uses rust:latest via emulation)
# Clone nono automatically and build all targets
./scripts/build-libs.sh
# Use an existing nono checkout
./scripts/build-libs.sh --nono-src /path/to/nonogo test -v ./...
go vet ./...
staticcheck ./... # go install honnef.co/go/tools/cmd/staticcheck@latestcaps := nono.New()
defer caps.Close()
if err := caps.AllowPath("/home/user/data", nono.AccessRead); err != nil {
log.Fatal(err)
}
if err := caps.AllowPath("/tmp", nono.AccessReadWrite); err != nil {
log.Fatal(err)
}
if err := caps.SetNetworkMode(nono.NetworkBlocked); err != nil {
log.Fatal(err)
}
// Irreversible — applies to this process and all children.
if err := nono.Apply(caps); err != nil {
log.Fatal(err)
}QueryContext lets you check what a capability set would allow before (or instead of) applying it. The capability set is cloned internally, so later changes to caps don't affect the query context.
caps := nono.New()
if err := caps.AllowPath("/home/user/data", nono.AccessRead); err != nil {
log.Fatal(err)
}
if err := caps.SetNetworkMode(nono.NetworkAllowAll); err != nil {
log.Fatal(err)
}
qc, err := nono.NewQueryContext(caps)
if err != nil {
log.Fatal(err)
}
defer qc.Close()
result, err := qc.QueryPath("/home/user/data/file.txt", nono.AccessRead)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Status) // nono.QueryAllowed
netResult, err := qc.QueryNetwork()
if err != nil {
log.Fatal(err)
}
fmt.Println(netResult.Status) // nono.QueryAllowedSandboxState provides a JSON-serializable snapshot of a CapabilitySet, useful for persisting or transmitting sandbox configuration.
caps := nono.New()
if err := caps.AllowPath("/data", nono.AccessReadWrite); err != nil {
log.Fatal(err)
}
if err := caps.SetNetworkMode(nono.NetworkBlocked); err != nil {
log.Fatal(err)
}
state, err := nono.StateFromCaps(caps)
if err != nil {
log.Fatal(err)
}
defer state.Close()
jsonStr, err := state.ToJSON()
if err != nil {
log.Fatal(err)
}
// Later: restore from JSON
restored, err := nono.StateFromJSON(jsonStr)
if err != nil {
log.Fatal(err)
}
defer restored.Close()
caps2, err := restored.ToCaps()
if err != nil {
log.Fatal(err)
}
defer caps2.Close()All failing operations return *nono.Error. Use errors.Is with a sentinel accessor to test for specific failure kinds:
err := caps.AllowPath("/nonexistent", nono.AccessRead)
if errors.Is(err, nono.ErrPathNotFound()) {
// path does not exist
}Named sentinel accessors (each is a function — note the ()): ErrPathNotFound(), ErrExpectedDirectory(), ErrExpectedFile(), ErrPathCanonicalization(), ErrNoCapabilities(), ErrSandboxInit(), ErrUnsupportedPlatform(), ErrBlockedCommand(), ErrConfigParse(), ErrProfileParse(), ErrIO(), ErrInvalidArg(), ErrTrustVerification(), ErrUnknown().
On macOS, paths under /var (including those returned by os.TempDir and t.TempDir()) are symlinks to /private/var. nono canonicalizes paths, so the resolved capability will be under /private/var. When checking PathCovered, resolve symlinks first:
dir, _ := filepath.EvalSymlinks(t.TempDir())
covered, err := caps.PathCovered(filepath.Join(dir, "file.txt"))