Skip to content

always-further/nono-go

Repository files navigation

nono-go

CI Go Reference

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.

Platform support

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.

Prerequisites

  • Go 1.24+
  • A C toolchain (gcc or clang) for CGo

Installation

go get github.com/always-further/nono-go

Building native libraries

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/nono

Testing

go test -v ./...
go vet ./...
staticcheck ./...   # go install honnef.co/go/tools/cmd/staticcheck@latest

Usage

Apply a sandbox

caps := 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)
}

Query permissions without applying

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.QueryAllowed

Serialize and deserialize state

SandboxState 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()

Error handling

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().

macOS path canonicalization

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"))

About

Go bindings for nono.sh

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors