rename Fence to Greywall as GreyHaven sandboxing component

Rebrand the project from Fence to Greywall, the sandboxing layer of the
GreyHaven platform. This updates:

- Go module path to gitea.app.monadical.io/monadical/greywall
- Binary name, CLI help text, and all usage examples
- Config paths (~/.config/greywall/greywall.json), env vars (GREYWALL_*)
- Log prefixes ([greywall:*]), temp file prefixes (greywall-*)
- All documentation, scripts, CI workflows, and example files
- README rewritten with GreyHaven branding and Fence attribution

Directory/file renames: cmd/fence → cmd/greywall, pkg/fence → pkg/greywall,
docs/why-fence.md → docs/why-greywall.md, example JSON files, and banner.
This commit is contained in:
2026-02-10 16:00:24 -06:00
parent 481616455a
commit da3a2ac3a4
68 changed files with 586 additions and 586 deletions

View File

@@ -19,7 +19,7 @@ on:
# paths:
# - "internal/sandbox/**"
# - "internal/proxy/**"
# - "cmd/fence/**"
# - "cmd/greywall/**"
permissions:
contents: read
@@ -72,7 +72,7 @@ jobs:
- name: Install benchstat
run: go install golang.org/x/perf/cmd/benchstat@latest
- name: Build fence
- name: Build greywall
run: make build-ci
- name: Run Go microbenchmarks
@@ -146,7 +146,7 @@ jobs:
- name: Install benchstat
run: go install golang.org/x/perf/cmd/benchstat@latest
- name: Build fence
- name: Build greywall
run: make build-ci
- name: Run Go microbenchmarks

View File

@@ -115,7 +115,7 @@ jobs:
run: make build-ci
- name: Run smoke tests
run: FENCE_TEST_NETWORK=1 ./scripts/smoke_test.sh ./fence
run: GREYWALL_TEST_NETWORK=1 ./scripts/smoke_test.sh ./greywall
test-macos:
name: Test (macOS)
@@ -160,4 +160,4 @@ jobs:
run: make build-ci
- name: Run smoke tests
run: FENCE_TEST_NETWORK=1 ./scripts/smoke_test.sh ./fence
run: GREYWALL_TEST_NETWORK=1 ./scripts/smoke_test.sh ./greywall

View File

@@ -75,7 +75,7 @@ jobs:
{
"version": "${{ github.ref_name }}",
"published_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"url": "https://github.com/Use-Tusk/fence/releases/tag/${{ github.ref_name }}"
"url": "https://gitea.app.monadical.io/monadical/greywall/releases/tag/${{ github.ref_name }}"
}
EOF

8
.gitignore vendored
View File

@@ -1,7 +1,7 @@
# Binary (only at root, not cmd/fence or pkg/fence)
/fence
/fence_*
/fence-*
# Binary (only at root, not cmd/greywall or pkg/greywall)
/greywall
/greywall_*
/greywall-*
# Tar archives
*.tar.gz

View File

@@ -7,11 +7,11 @@ linters-settings:
sections:
- standard
- default
- prefix(github.com/Use-Tusk/fence)
- prefix(gitea.app.monadical.io/monadical/greywall)
gofmt:
simplify: true
goimports:
local-prefixes: github.com/Use-Tusk/fence
local-prefixes: gitea.app.monadical.io/monadical/greywall
gocritic:
disabled-checks:
- singleCaseSwitch

View File

@@ -18,8 +18,8 @@ builds:
- -X main.version={{.Version}}
- -X main.buildTime={{.Date}}
- -X main.gitCommit={{.Commit}}
binary: fence
main: ./cmd/fence
binary: greywall
main: ./cmd/greywall
archives:
- formats: [tar.gz]
@@ -77,7 +77,7 @@ changelog:
release:
github:
owner: Use-Tusk
name: fence
owner: monadical
name: greywall
draft: false
prerelease: auto

View File

@@ -1,6 +1,6 @@
# Architecture
Fence restricts network, filesystem, and command access for arbitrary commands. It works by:
Greywall restricts network, filesystem, and command access for arbitrary commands. It works by:
1. **Blocking commands** via configurable deny/allow lists before execution
2. **Intercepting network traffic** via HTTP/SOCKS5 proxies that filter by domain
@@ -9,7 +9,7 @@ Fence restricts network, filesystem, and command access for arbitrary commands.
```mermaid
flowchart TB
subgraph Fence
subgraph Greywall
Config["Config<br/>(JSON)"]
Manager
CmdCheck["Command<br/>Blocking"]
@@ -30,8 +30,8 @@ flowchart TB
## Project Structure
```text
fence/
├── cmd/fence/ # CLI entry point
greywall/
├── cmd/greywall/ # CLI entry point
│ └── main.go # Includes --landlock-apply wrapper mode
├── internal/ # Private implementation
│ ├── config/ # Configuration loading/validation
@@ -52,8 +52,8 @@ fence/
│ ├── dangerous.go # Protected file/directory lists
│ ├── shell.go # Shell quoting utilities
│ └── utils.go # Path normalization
└── pkg/fence/ # Public Go API
└── fence.go
└── pkg/greywall/ # Public Go API
└── greywall.go
```
## Core Components
@@ -71,7 +71,7 @@ type Config struct {
}
```
- Loads from XDG config dir (`~/.config/fence/fence.json` or `~/Library/Application Support/fence/fence.json`) or custom path
- Loads from XDG config dir (`~/.config/greywall/greywall.json` or `~/Library/Application Support/greywall/greywall.json`) or custom path
- Falls back to restrictive defaults (block all network, default command deny list)
- Validates paths and normalizes them
@@ -181,7 +181,7 @@ flowchart TB
SOCKS["SOCKS Proxy<br/>:random"]
HSOCAT["socat<br/>(HTTP bridge)"]
SSOCAT["socat<br/>(SOCKS bridge)"]
USOCK["Unix Sockets<br/>/tmp/fence-*.sock"]
USOCK["Unix Sockets<br/>/tmp/greywall-*.sock"]
end
subgraph Sandbox ["Sandbox (bwrap --unshare-net)"]
@@ -221,7 +221,7 @@ flowchart TB
subgraph Host
HSOCAT["socat<br/>TCP-LISTEN:8888"]
USOCK["Unix Socket<br/>/tmp/fence-rev-8888-*.sock"]
USOCK["Unix Socket<br/>/tmp/greywall-rev-8888-*.sock"]
end
subgraph Sandbox
@@ -286,7 +286,7 @@ flowchart TD
### Linux Security Layers
On Linux, fence uses multiple security layers with graceful fallback:
On Linux, greywall uses multiple security layers with graceful fallback:
1. bubblewrap (core isolation via Linux namespaces)
2. seccomp (syscall filtering)
@@ -306,15 +306,15 @@ The `-m` (monitor) flag enables real-time visibility into blocked operations. Th
| Prefix | Source | Description |
|--------|--------|-------------|
| `[fence:http]` | Both | HTTP/HTTPS proxy (blocked requests only in monitor mode) |
| `[fence:socks]` | Both | SOCKS5 proxy (blocked requests only in monitor mode) |
| `[fence:logstream]` | macOS only | Kernel-level sandbox violations from `log stream` |
| `[fence:ebpf]` | Linux only | Filesystem/syscall failures (requires CAP_BPF or root) |
| `[fence:filter]` | Both | Domain filter rule matches (debug mode only) |
| `[greywall:http]` | Both | HTTP/HTTPS proxy (blocked requests only in monitor mode) |
| `[greywall:socks]` | Both | SOCKS5 proxy (blocked requests only in monitor mode) |
| `[greywall:logstream]` | macOS only | Kernel-level sandbox violations from `log stream` |
| `[greywall:ebpf]` | Linux only | Filesystem/syscall failures (requires CAP_BPF or root) |
| `[greywall:filter]` | Both | Domain filter rule matches (debug mode only) |
### macOS Log Stream
On macOS, fence spawns `log stream` with a predicate to capture sandbox violations:
On macOS, greywall spawns `log stream` with a predicate to capture sandbox violations:
```bash
log stream --predicate 'eventMessage ENDSWITH "_SBX"' --style compact
@@ -344,4 +344,4 @@ Filtered out (too noisy):
## Security Model
See [`docs/security-model.md`](docs/security-model.md) for Fence's threat model, guarantees, and limitations.
See [`docs/security-model.md`](docs/security-model.md) for Greywall's threat model, guarantees, and limitations.

View File

@@ -1,6 +1,6 @@
# Contributing
Thanks for helping improve `fence`!
Thanks for helping improve `greywall`!
If you have any questions, feel free to open an issue.
@@ -12,11 +12,11 @@ If you have any questions, feel free to open an issue.
- Clone and prepare:
```bash
git clone https://github.com/Use-Tusk/fence
cd fence
git clone https://gitea.app.monadical.io/monadical/greywall
cd greywall
make setup # Install deps and lint tools
make build # Build the binary
./fence --help
./greywall --help
```
## Dev workflow
@@ -25,7 +25,7 @@ Common targets:
| Command | Description |
|---------|-------------|
| `make build` | Build the binary (`./fence`) |
| `make build` | Build the binary (`./greywall`) |
| `make run` | Build and run |
| `make test` | Run tests |
| `make test-ci` | Run tests with coverage |
@@ -63,14 +63,14 @@ make test-ci
```bash
# Test blocked network request
./fence curl https://example.com
./greywall curl https://example.com
# Test with allowed domain
echo '{"network":{"allowedDomains":["example.com"]}}' > /tmp/test.json
./fence -s /tmp/test.json curl https://example.com
./greywall -s /tmp/test.json curl https://example.com
# Test monitor mode
./fence -m -c "touch /etc/test"
./greywall -m -c "touch /etc/test"
```
### Testing on Linux
@@ -82,7 +82,7 @@ Requires `bubblewrap` and `socat`:
sudo apt install bubblewrap socat
# Test in Colima or VM
./fence curl https://example.com
./greywall curl https://example.com
```
## Troubleshooting

View File

@@ -3,7 +3,7 @@ GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOMOD=$(GOCMD) mod
BINARY_NAME=fence
BINARY_NAME=greywall
BINARY_UNIX=$(BINARY_NAME)_unix
TUN2SOCKS_VERSION=v2.5.2
TUN2SOCKS_BIN_DIR=internal/sandbox/bin
@@ -29,14 +29,14 @@ download-tun2socks:
build: download-tun2socks
@echo "Building $(BINARY_NAME)..."
$(GOBUILD) -o $(BINARY_NAME) -v ./cmd/fence
$(GOBUILD) -o $(BINARY_NAME) -v ./cmd/greywall
build-ci: download-tun2socks
@echo "CI: Building $(BINARY_NAME) with version info..."
$(eval VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev"))
$(eval BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ'))
$(eval GIT_COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown"))
$(GOBUILD) -ldflags "-s -w -X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.gitCommit=$(GIT_COMMIT)" -o $(BINARY_NAME) -v ./cmd/fence
$(GOBUILD) -ldflags "-s -w -X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.gitCommit=$(GIT_COMMIT)" -o $(BINARY_NAME) -v ./cmd/greywall
test:
@echo "Running tests..."
@@ -61,11 +61,11 @@ deps:
build-linux: download-tun2socks
@echo "Building for Linux..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) -v ./cmd/fence
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) -v ./cmd/greywall
build-darwin:
@echo "Building for macOS..."
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINARY_NAME)_darwin -v ./cmd/fence
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINARY_NAME)_darwin -v ./cmd/greywall
install-lint-tools:
@echo "Installing linting tools..."

View File

@@ -1,32 +1,30 @@
![Fence Banner](assets/fence-banner.png)
![Greywall Banner](assets/greywall-banner.png)
<div align="center">
# Greywall
![GitHub Release](https://img.shields.io/github/v/release/Use-Tusk/fence)
**The sandboxing layer of the GreyHaven platform.**
</div>
Fence wraps commands in a sandbox that blocks network access by default and restricts filesystem operations based on configurable rules. It's most useful for running semi-trusted code (package installs, build scripts, CI jobs, unfamiliar repos) with controlled side effects, and it can also complement AI coding agents as defense-in-depth.
Greywall wraps commands in a sandbox that blocks network access by default and restricts filesystem operations. It is the core sandboxing component of the GreyHaven platform, providing defense-in-depth for running untrusted code.
```bash
# Block all network access (default)
fence curl https://example.com # → 403 Forbidden
greywall curl https://example.com # → 403 Forbidden
# Allow specific domains
fence -t code npm install # → uses 'code' template with npm/pypi/etc allowed
greywall -t code npm install # → uses 'code' template with npm/pypi/etc allowed
# Block dangerous commands
fence -c "rm -rf /" # → blocked by command deny rules
greywall -c "rm -rf /" # → blocked by command deny rules
```
You can also think of Fence as a permission manager for your CLI agents. **Fence works with popular coding agents like Claude Code, Codex, Gemini CLI, Cursor Agent, OpenCode, Factory (Droid) CLI, etc.** See [agents.md](./docs/agents.md) for more details.
Greywall also works as a permission manager for CLI agents. **Greywall works with popular coding agents like Claude Code, Codex, Gemini CLI, Cursor Agent, OpenCode, Factory (Droid) CLI, etc.** See [agents.md](./docs/agents.md) for more details.
## Install
**macOS / Linux:**
```bash
curl -fsSL https://raw.githubusercontent.com/Use-Tusk/fence/main/install.sh | sh
curl -fsSL https://gitea.app.monadical.io/monadical/greywall/raw/branch/main/install.sh | sh
```
<details>
@@ -35,15 +33,15 @@ curl -fsSL https://raw.githubusercontent.com/Use-Tusk/fence/main/install.sh | sh
**Go install:**
```bash
go install github.com/Use-Tusk/fence/cmd/fence@latest
go install gitea.app.monadical.io/monadical/greywall/cmd/greywall@latest
```
**Build from source:**
```bash
git clone https://github.com/Use-Tusk/fence
cd fence
go build -o fence ./cmd/fence
git clone https://gitea.app.monadical.io/monadical/greywall
cd greywall
go build -o greywall ./cmd/greywall
```
</details>
@@ -60,27 +58,27 @@ go build -o fence ./cmd/fence
```bash
# Run command with all network blocked (no domains allowed by default)
fence curl https://example.com
greywall curl https://example.com
# Run with shell expansion
fence -c "echo hello && ls"
greywall -c "echo hello && ls"
# Enable debug logging
fence -d curl https://example.com
greywall -d curl https://example.com
# Use a template
fence -t code -- claude # Runs Claude Code using `code` template config
greywall -t code -- claude # Runs Claude Code using `code` template config
# Monitor mode (shows violations)
fence -m npm install
greywall -m npm install
# Show all commands and options
fence --help
greywall --help
```
### Configuration
Fence reads from `~/.config/fence/fence.json` by default (or `~/Library/Application Support/fence/fence.json` on macOS).
Greywall reads from `~/.config/greywall/greywall.json` by default (or `~/Library/Application Support/greywall/greywall.json` on macOS).
```json
{
@@ -91,12 +89,12 @@ Fence reads from `~/.config/fence/fence.json` by default (or `~/Library/Applicat
}
```
Use `fence --settings ./custom.json` to specify a different config.
Use `greywall --settings ./custom.json` to specify a different config.
### Import from Claude Code
```bash
fence import --claude --save
greywall import --claude --save
```
## Features
@@ -109,7 +107,7 @@ fence import --claude --save
- **Violation monitoring** - Real-time logging of blocked requests (`-m`)
- **Cross-platform** - macOS (sandbox-exec) + Linux (bubblewrap)
Fence can be used as a Go package or CLI tool.
Greywall can be used as a Go package or CLI tool.
## Documentation
@@ -123,4 +121,6 @@ Fence can be used as a Go package or CLI tool.
## Attribution
Greywall is based on [Fence](https://github.com/Use-Tusk/fence) by Use-Tusk.
Inspired by Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime).

View File

Before

Width:  |  Height:  |  Size: 407 KiB

After

Width:  |  Height:  |  Size: 407 KiB

View File

@@ -1,4 +1,4 @@
// Package main implements the fence CLI.
// Package main implements the greywall CLI.
package main
import (
@@ -10,9 +10,9 @@ import (
"strconv"
"syscall"
"github.com/Use-Tusk/fence/internal/config"
"github.com/Use-Tusk/fence/internal/platform"
"github.com/Use-Tusk/fence/internal/sandbox"
"gitea.app.monadical.io/monadical/greywall/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/platform"
"gitea.app.monadical.io/monadical/greywall/internal/sandbox"
"github.com/spf13/cobra"
)
@@ -45,29 +45,29 @@ func main() {
}
rootCmd := &cobra.Command{
Use: "fence [flags] -- [command...]",
Use: "greywall [flags] -- [command...]",
Short: "Run commands in a sandbox with network and filesystem restrictions",
Long: `fence is a command-line tool that runs commands in a sandboxed environment
Long: `greywall is a command-line tool that runs commands in a sandboxed environment
with network and filesystem restrictions.
By default, all network access is blocked. Use --proxy to route traffic through
an external SOCKS5 proxy, or configure a proxy URL in your settings file at
~/.config/fence/fence.json (or ~/Library/Application Support/fence/fence.json on macOS).
~/.config/greywall/greywall.json (or ~/Library/Application Support/greywall/greywall.json on macOS).
On Linux, fence uses tun2socks for truly transparent proxying: all TCP/UDP traffic
On Linux, greywall uses tun2socks for truly transparent proxying: all TCP/UDP traffic
from any binary is captured at the kernel level via a TUN device and forwarded
through the external SOCKS5 proxy. No application awareness needed.
On macOS, fence uses environment variables (best-effort) to direct traffic
On macOS, greywall uses environment variables (best-effort) to direct traffic
to the proxy.
Examples:
fence -- curl https://example.com # Blocked (no proxy)
fence --proxy socks5://localhost:1080 -- curl https://example.com # Via proxy
fence -- curl -s https://example.com # Use -- to separate flags
fence -c "echo hello && ls" # Run with shell expansion
fence --settings config.json npm install
fence -p 3000 -c "npm run dev" # Expose port 3000
greywall -- curl https://example.com # Blocked (no proxy)
greywall --proxy socks5://localhost:1080 -- curl https://example.com # Via proxy
greywall -- curl -s https://example.com # Use -- to separate flags
greywall -c "echo hello && ls" # Run with shell expansion
greywall --settings config.json npm install
greywall -p 3000 -c "npm run dev" # Expose port 3000
Configuration file format:
{
@@ -112,7 +112,7 @@ Configuration file format:
func runCommand(cmd *cobra.Command, args []string) error {
if showVersion {
fmt.Printf("fence - lightweight, container-free sandbox for running untrusted commands\n")
fmt.Printf("greywall - lightweight, container-free sandbox for running untrusted commands\n")
fmt.Printf(" Version: %s\n", version)
fmt.Printf(" Built: %s\n", buildTime)
fmt.Printf(" Commit: %s\n", gitCommit)
@@ -135,7 +135,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
}
if debug {
fmt.Fprintf(os.Stderr, "[fence] Command: %s\n", command)
fmt.Fprintf(os.Stderr, "[greywall] Command: %s\n", command)
}
var ports []int
@@ -148,7 +148,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
}
if debug && len(ports) > 0 {
fmt.Fprintf(os.Stderr, "[fence] Exposing ports: %v\n", ports)
fmt.Fprintf(os.Stderr, "[greywall] Exposing ports: %v\n", ports)
}
// Load config: settings file > default path > default config
@@ -169,7 +169,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
}
if cfg == nil {
if debug {
fmt.Fprintf(os.Stderr, "[fence] No config found at %s, using default (block all network)\n", configPath)
fmt.Fprintf(os.Stderr, "[greywall] No config found at %s, using default (block all network)\n", configPath)
}
cfg = config.Default()
}
@@ -196,7 +196,7 @@ func runCommand(cmd *cobra.Command, args []string) error {
logMonitor = sandbox.NewLogMonitor(sandbox.GetSessionSuffix())
if logMonitor != nil {
if err := logMonitor.Start(); err != nil {
fmt.Fprintf(os.Stderr, "[fence] Warning: failed to start log monitor: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall] Warning: failed to start log monitor: %v\n", err)
} else {
defer logMonitor.Stop()
}
@@ -209,13 +209,13 @@ func runCommand(cmd *cobra.Command, args []string) error {
}
if debug {
fmt.Fprintf(os.Stderr, "[fence] Sandboxed command: %s\n", sandboxedCommand)
fmt.Fprintf(os.Stderr, "[greywall] Sandboxed command: %s\n", sandboxedCommand)
}
hardenedEnv := sandbox.GetHardenedEnv()
if debug {
if stripped := sandbox.GetStrippedEnvVars(os.Environ()); len(stripped) > 0 {
fmt.Fprintf(os.Stderr, "[fence] Stripped dangerous env vars: %v\n", stripped)
fmt.Fprintf(os.Stderr, "[greywall] Stripped dangerous env vars: %v\n", stripped)
}
}
@@ -280,24 +280,24 @@ func newCompletionCmd(rootCmd *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion scripts",
Long: `Generate shell completion scripts for fence.
Long: `Generate shell completion scripts for greywall.
Examples:
# Bash (load in current session)
source <(fence completion bash)
source <(greywall completion bash)
# Zsh (load in current session)
source <(fence completion zsh)
source <(greywall completion zsh)
# Fish (load in current session)
fence completion fish | source
greywall completion fish | source
# PowerShell (load in current session)
fence completion powershell | Out-String | Invoke-Expression
greywall completion powershell | Out-String | Invoke-Expression
To persist completions, redirect output to the appropriate completions
directory for your shell (e.g., /etc/bash_completion.d/ for bash,
${fpath[1]}/_fence for zsh, ~/.config/fish/completions/fence.fish for fish).
${fpath[1]}/_greywall for zsh, ~/.config/fish/completions/greywall.fish for fish).
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
@@ -322,11 +322,11 @@ ${fpath[1]}/_fence for zsh, ~/.config/fish/completions/fence.fish for fish).
// runLandlockWrapper runs in "wrapper mode" inside the sandbox.
// It applies Landlock restrictions and then execs the user command.
// Usage: fence --landlock-apply [--debug] -- <command...>
// Config is passed via FENCE_CONFIG_JSON environment variable.
// Usage: greywall --landlock-apply [--debug] -- <command...>
// Config is passed via GREYWALL_CONFIG_JSON environment variable.
func runLandlockWrapper() {
// Parse arguments: --landlock-apply [--debug] -- <command...>
args := os.Args[2:] // Skip "fence" and "--landlock-apply"
args := os.Args[2:] // Skip "greywall" and "--landlock-apply"
var debugMode bool
var cmdStart int
@@ -347,25 +347,25 @@ func runLandlockWrapper() {
parseCommand:
if cmdStart >= len(args) {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Error: no command specified\n")
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Error: no command specified\n")
os.Exit(1)
}
command := args[cmdStart:]
if debugMode {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Applying Landlock restrictions\n")
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Applying Landlock restrictions\n")
}
// Only apply Landlock on Linux
if platform.Detect() == platform.Linux {
// Load config from environment variable (passed by parent fence process)
// Load config from environment variable (passed by parent greywall process)
var cfg *config.Config
if configJSON := os.Getenv("FENCE_CONFIG_JSON"); configJSON != "" {
if configJSON := os.Getenv("GREYWALL_CONFIG_JSON"); configJSON != "" {
cfg = &config.Config{}
if err := json.Unmarshal([]byte(configJSON), cfg); err != nil {
if debugMode {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Warning: failed to parse config: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Warning: failed to parse config: %v\n", err)
}
cfg = nil
}
@@ -381,23 +381,23 @@ parseCommand:
err := sandbox.ApplyLandlockFromConfig(cfg, cwd, nil, debugMode)
if err != nil {
if debugMode {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Warning: Landlock not applied: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Warning: Landlock not applied: %v\n", err)
}
// Continue without Landlock - bwrap still provides isolation
} else if debugMode {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Landlock restrictions applied\n")
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Landlock restrictions applied\n")
}
}
// Find the executable
execPath, err := exec.LookPath(command[0])
if err != nil {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Error: command not found: %s\n", command[0])
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Error: command not found: %s\n", command[0])
os.Exit(127)
}
if debugMode {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Exec: %s %v\n", execPath, command[1:])
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Exec: %s %v\n", execPath, command[1:])
}
// Sanitize environment (strips LD_PRELOAD, etc.)
@@ -406,7 +406,7 @@ parseCommand:
// Exec the command (replaces this process)
err = syscall.Exec(execPath, command, hardenedEnv) //nolint:gosec
if err != nil {
fmt.Fprintf(os.Stderr, "[fence:landlock-wrapper] Exec failed: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock-wrapper] Exec failed: %v\n", err)
os.Exit(1)
}
}

View File

@@ -1,26 +1,26 @@
# Fence Documentation
# Greywall Documentation
Fence is a sandboxing tool that restricts network and filesystem access for arbitrary commands. It's most useful for running semi-trusted code (package installs, build scripts, CI jobs, unfamiliar repos) with controlled side effects.
Greywall is a sandboxing tool that restricts network and filesystem access for arbitrary commands. It's most useful for running semi-trusted code (package installs, build scripts, CI jobs, unfamiliar repos) with controlled side effects.
## Getting Started
- [Quickstart](quickstart.md) - Install fence and run your first sandboxed command in 5 minutes
- [Why Fence](why-fence.md) - What problem it solves (and what it doesn't)
- [Quickstart](quickstart.md) - Install greywall and run your first sandboxed command in 5 minutes
- [Why Greywall](why-greywall.md) - What problem it solves (and what it doesn't)
## Guides
- [Concepts](concepts.md) - Mental model: OS sandbox + local proxies + config
- [Troubleshooting](troubleshooting.md) - Common failure modes and fixes
- [Using Fence with AI agents](agents.md) - Defense-in-depth and policy standardization
- [Using Greywall with AI agents](agents.md) - Defense-in-depth and policy standardization
- [Recipes](recipes/README.md) - Common workflows (npm/pip/git/CI)
- [Templates](./templates.md) - Copy/paste templates you can start from
## Reference
- [README](../README.md) - CLI usage
- [Library Usage (Go)](library.md) - Using Fence as a Go package
- [Configuration](./configuration.md) - How to configure Fence
- [Architecture](../ARCHITECTURE.md) - How fence works under the hood
- [Library Usage (Go)](library.md) - Using Greywall as a Go package
- [Configuration](./configuration.md) - How to configure Greywall
- [Architecture](../ARCHITECTURE.md) - How greywall works under the hood
- [Security model](security-model.md) - Threat model, guarantees, and limitations
- [Linux security features](linux-security-features.md) - Landlock, seccomp, eBPF details and fallback behavior
- [Testing](testing.md) - How to run tests and write new ones
@@ -36,20 +36,20 @@ See [`examples/`](../examples/README.md) for runnable demos.
```bash
# Block all network (default)
fence <command>
greywall <command>
# Use custom config
fence --settings ./fence.json <command>
greywall --settings ./greywall.json <command>
# Debug mode (verbose output)
fence -d <command>
greywall -d <command>
# Monitor mode (show blocked requests)
fence -m <command>
greywall -m <command>
# Expose port for servers
fence -p 3000 <command>
greywall -p 3000 <command>
# Run shell command
fence -c "echo hello && ls"
greywall -c "echo hello && ls"
```

View File

@@ -1,6 +1,6 @@
# Using Fence with AI Agents
# Using Greywall with AI Agents
Many popular coding agents already include sandboxing. Fence can still be useful when you want a tool-agnostic policy layer that works the same way across:
Many popular coding agents already include sandboxing. Greywall can still be useful when you want a tool-agnostic policy layer that works the same way across:
- local developer machines
- CI jobs
@@ -15,7 +15,7 @@ Treat an agent as "semi-trusted automation":
- Allowlist only the network destinations you actually need
- Use `-m` (monitor mode) to audit blocked attempts and tighten policy
Fence can also reduce the risk of running agents with fewer interactive permission prompts (e.g. "skip permissions"), as long as your Fence config tightly scopes writes and outbound destinations. It's defense-in-depth, not a substitute for the agent's own safeguards.
Greywall can also reduce the risk of running agents with fewer interactive permission prompts (e.g. "skip permissions"), as long as your Greywall config tightly scopes writes and outbound destinations. It's defense-in-depth, not a substitute for the agent's own safeguards.
## Example: API-only agent
@@ -33,7 +33,7 @@ Fence can also reduce the risk of running agents with fewer interactive permissi
Run:
```bash
fence --settings ./fence.json <agent-command>
greywall --settings ./greywall.json <agent-command>
```
## Popular CLI coding agents
@@ -43,7 +43,7 @@ We provide these template for guardrailing CLI coding agents:
- [`code`](/internal/templates/code.json) - Strict deny-by-default network filtering via proxy. Works with agents that respect `HTTP_PROXY`. Blocks cloud metadata APIs, protects secrets, restricts dangerous commands.
- [`code-relaxed`](/internal/templates/code-relaxed.json) - Allows direct network connections for agents that ignore `HTTP_PROXY`. Same filesystem/command protections as `code`, but `deniedDomains` only enforced for proxy-respecting apps.
You can use it like `fence -t code -- claude`.
You can use it like `greywall -t code -- claude`.
| Agent | Works with template | Notes |
|-------|--------| ----- |
@@ -60,7 +60,7 @@ Note: On Linux, if OpenCode or Gemini CLI is installed via Linuxbrew, Landlock c
## Protecting your environment
Fence includes additional "dangerous file protection" (writes blocked regardless of config) to reduce persistence and environment-tampering vectors like:
Greywall includes additional "dangerous file protection" (writes blocked regardless of config) to reduce persistence and environment-tampering vectors like:
- `.git/hooks/*`
- shell startup files (`.zshrc`, `.bashrc`, etc.)

View File

@@ -1,6 +1,6 @@
# Benchmarking
This document describes how to run, interpret, and compare sandbox performance benchmarks for Fence.
This document describes how to run, interpret, and compare sandbox performance benchmarks for Greywall.
## Quick Start
@@ -29,9 +29,9 @@ go test -run=^$ -bench=. -benchmem ./internal/sandbox/...
### Layer 1: CLI Benchmarks (`scripts/benchmark.sh`)
**What it measures**: Real-world agent cost - full `fence` invocation including proxy startup, socat bridges (Linux), and sandbox-exec/bwrap setup.
**What it measures**: Real-world agent cost - full `greywall` invocation including proxy startup, socat bridges (Linux), and sandbox-exec/bwrap setup.
This is the most realistic benchmark for understanding the cost of running agent commands through Fence.
This is the most realistic benchmark for understanding the cost of running agent commands through Greywall.
```bash
# Full benchmark suite
@@ -51,7 +51,7 @@ This is the most realistic benchmark for understanding the cost of running agent
| Option | Description |
|--------|-------------|
| `-b, --binary PATH` | Path to fence binary (default: ./fence) |
| `-b, --binary PATH` | Path to greywall binary (default: ./greywall) |
| `-o, --output DIR` | Output directory (default: ./benchmarks) |
| `-n, --runs N` | Minimum runs per benchmark (default: 30) |
| `-q, --quick` | Quick mode: fewer runs, skip slow benchmarks |
@@ -92,13 +92,13 @@ benchstat bench.txt
```bash
# Quick syscall cost breakdown
strace -f -c ./fence -- true
strace -f -c ./greywall -- true
# Context switches, page faults
perf stat -- ./fence -- true
perf stat -- ./greywall -- true
# Full profiling (flamegraph-ready)
perf record -F 99 -g -- ./fence -- git status
perf record -F 99 -g -- ./greywall -- git status
perf report
```
@@ -106,10 +106,10 @@ perf report
```bash
# Time Profiler via Instruments
xcrun xctrace record --template 'Time Profiler' --launch -- ./fence -- true
xcrun xctrace record --template 'Time Profiler' --launch -- ./greywall -- true
# Quick call-stack snapshot
./fence -- sleep 5 &
./greywall -- sleep 5 &
sample $! 5 -file sample.txt
```
@@ -150,7 +150,7 @@ The overhead factor decreases as the actual workload increases (because sandbox
1. Run benchmarks on each platform independently
2. Compare overhead factors, not absolute times
3. Use the same fence version and workloads
3. Use the same greywall version and workloads
```bash
# On macOS
@@ -256,7 +256,7 @@ Linux initialization is ~3,700x slower because it must:
macOS only generates a Seatbelt profile string (very cheap).
### Cold Start Overhead (one `fence` invocation per command)
### Cold Start Overhead (one `greywall` invocation per command)
| Workload | Linux | macOS |
|----------|-------|-------|
@@ -264,7 +264,7 @@ macOS only generates a Seatbelt profile string (very cheap).
| Python | 124 ms | 33 ms |
| Git status | 114 ms | 25 ms |
This is the realistic cost for scripts running `fence -c "command"` repeatedly.
This is the realistic cost for scripts running `greywall -c "command"` repeatedly.
### Warm Path Overhead (pre-initialized manager)
@@ -289,9 +289,9 @@ Overhead decreases as the actual workload increases (sandbox setup is fixed cost
## Impact on Agent Usage
### Long-Running Agents (`fence claude`, `fence codex`)
### Long-Running Agents (`greywall claude`, `greywall codex`)
For agents that run as a child process under fence:
For agents that run as a child process under greywall:
| Phase | Cost |
|-------|------|
@@ -306,11 +306,11 @@ Child processes inherit the sandbox - no re-initialization, no WrapCommand overh
| `git status` | 2.1 ms | 5.9 ms |
| Python script | 11 ms | 15 ms |
**Bottom line**: For `fence <agent>` usage, sandbox overhead is a one-time startup cost. Tool calls inside the agent run at native speed.
**Bottom line**: For `greywall <agent>` usage, sandbox overhead is a one-time startup cost. Tool calls inside the agent run at native speed.
### Per-Command Invocation (`fence -c "command"`)
### Per-Command Invocation (`greywall -c "command"`)
For scripts or CI running fence per command:
For scripts or CI running greywall per command:
| Session | Linux Cost | macOS Cost |
|---------|------------|------------|

View File

@@ -1,15 +1,15 @@
# Concepts
Fence combines two ideas:
Greywall combines two ideas:
1. **An OS sandbox** to enforce "no direct network" and restrict filesystem operations.
2. **Local filtering proxies** (HTTP + SOCKS5) to selectively allow outbound traffic by domain.
## Network model
By default, fence blocks all outbound network access.
By default, greywall blocks all outbound network access.
When you allow domains, fence:
When you allow domains, greywall:
- Starts local HTTP and SOCKS5 proxies
- Sets proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`)
@@ -29,13 +29,13 @@ These are separate on purpose. A typical safe default for dev servers is:
## Filesystem model
Fence is designed around "read mostly, write narrowly":
Greywall is designed around "read mostly, write narrowly":
- **Reads**: allowed by default (you can block specific paths via `denyRead`).
- **Writes**: denied by default (you must opt-in with `allowWrite`).
- **denyWrite**: overrides `allowWrite` (useful for protecting secrets and dangerous files).
Fence also protects some dangerous targets regardless of config (e.g. shell startup files and git hooks). See `ARCHITECTURE.md` for the full list.
Greywall also protects some dangerous targets regardless of config (e.g. shell startup files and git hooks). See `ARCHITECTURE.md` for the full list.
## Debug vs Monitor mode

View File

@@ -1,6 +1,6 @@
# Configuration
Fence reads settings from `~/.config/fence/fence.json` by default (or `~/Library/Application Support/fence/fence.json` on macOS). Legacy `~/.fence.json` is also supported. Pass `--settings ./fence.json` to use a custom path. Config files support JSONC.
Greywall reads settings from `~/.config/greywall/greywall.json` by default (or `~/Library/Application Support/greywall/greywall.json` on macOS). Legacy `~/.greywall.json` is also supported. Pass `--settings ./greywall.json` to use a custom path. Config files support JSONC.
Example config:
@@ -60,7 +60,7 @@ You can also extend other config files using absolute or relative paths:
```json
{
"extends": "/etc/fence/company-base.json",
"extends": "/etc/greywall/company-base.json",
"filesystem": {
"denyRead": ["~/company-secrets/**"]
}
@@ -143,7 +143,7 @@ Example:
### Default Denied Commands
When `useDefaults` is `true` (the default), fence blocks these dangerous commands:
When `useDefaults` is `true` (the default), greywall blocks these dangerous commands:
- System control: `shutdown`, `reboot`, `halt`, `poweroff`, `init 0/6`
- Kernel manipulation: `insmod`, `rmmod`, `modprobe`, `kexec`
@@ -155,7 +155,7 @@ To disable defaults: `"useDefaults": false`
### Command Detection
Fence detects blocked commands in:
Greywall detects blocked commands in:
- Direct commands: `git push origin main`
- Command chains: `ls && git push` or `ls; git push`
@@ -260,26 +260,26 @@ SSH host patterns support wildcards anywhere:
## Importing from Claude Code
If you've been using Claude Code and have already built up permission rules, you can import them into fence:
If you've been using Claude Code and have already built up permission rules, you can import them into greywall:
```bash
# Preview import (prints JSON to stdout)
fence import --claude
greywall import --claude
# Save to the default config path
fence import --claude --save
greywall import --claude --save
# Import from a specific file
fence import --claude -f ~/.claude/settings.json --save
greywall import --claude -f ~/.claude/settings.json --save
# Save to a specific output file
fence import --claude -o ./fence.json
greywall import --claude -o ./greywall.json
# Import without extending any template (minimal config)
fence import --claude --no-extend --save
greywall import --claude --no-extend --save
# Import and extend a different template
fence import --claude --extend local-dev-server --save
greywall import --claude --extend local-dev-server --save
```
### Default Template
@@ -294,7 +294,7 @@ Use `--no-extend` if you want a minimal config without these defaults, or `--ext
### Permission Mapping
| Claude Code | Fence |
| Claude Code | Greywall |
|-------------|-------|
| `Bash(xyz)` allow | `command.allow: ["xyz"]` |
| `Bash(xyz:*)` deny | `command.deny: ["xyz"]` |
@@ -302,9 +302,9 @@ Use `--no-extend` if you want a minimal config without these defaults, or `--ext
| `Write(path)` allow | `filesystem.allowWrite: [path]` |
| `Write(path)` deny | `filesystem.denyWrite: [path]` |
| `Edit(path)` | Same as `Write(path)` |
| `ask` rules | Converted to deny (fence doesn't support interactive prompts) |
| `ask` rules | Converted to deny (greywall doesn't support interactive prompts) |
Global tool permissions (e.g., bare `Read`, `Write`, `Grep`) are skipped since fence uses path/command-based rules.
Global tool permissions (e.g., bare `Read`, `Write`, `Grep`) are skipped since greywall uses path/command-based rules.
## See Also

View File

@@ -1,11 +1,11 @@
# Library Usage (Go)
Fence can be used as a Go library to sandbox commands programmatically.
Greywall can be used as a Go library to sandbox commands programmatically.
## Installation
```bash
go get github.com/Use-Tusk/fence
go get gitea.app.monadical.io/monadical/greywall
```
## Quick Start
@@ -17,25 +17,25 @@ import (
"fmt"
"os/exec"
"github.com/Use-Tusk/fence/pkg/fence"
"gitea.app.monadical.io/monadical/greywall/pkg/greywall"
)
func main() {
// Check platform support
if !fence.IsSupported() {
if !greywall.IsSupported() {
fmt.Println("Sandboxing not supported on this platform")
return
}
// Create config
cfg := &fence.Config{
Network: fence.NetworkConfig{
cfg := &greywall.Config{
Network: greywall.NetworkConfig{
AllowedDomains: []string{"api.example.com"},
},
}
// Create and initialize manager
manager := fence.NewManager(cfg, false, false)
manager := greywall.NewManager(cfg, false, false)
defer manager.Cleanup()
if err := manager.Initialize(); err != nil {
@@ -64,7 +64,7 @@ func main() {
Returns `true` if the current platform supports sandboxing (macOS or Linux).
```go
if !fence.IsSupported() {
if !greywall.IsSupported() {
log.Fatal("Platform not supported")
}
```
@@ -74,7 +74,7 @@ if !fence.IsSupported() {
Returns a default configuration with all network blocked.
```go
cfg := fence.DefaultConfig()
cfg := greywall.DefaultConfig()
cfg.Network.AllowedDomains = []string{"example.com"}
```
@@ -83,18 +83,18 @@ cfg.Network.AllowedDomains = []string{"example.com"}
Loads configuration from a JSON file. Supports JSONC (comments allowed).
```go
cfg, err := fence.LoadConfig(fence.DefaultConfigPath())
cfg, err := greywall.LoadConfig(greywall.DefaultConfigPath())
if err != nil {
log.Fatal(err)
}
if cfg == nil {
cfg = fence.DefaultConfig() // File doesn't exist
cfg = greywall.DefaultConfig() // File doesn't exist
}
```
#### `DefaultConfigPath() string`
Returns the default config file path (`~/.config/fence/fence.json` on Linux, `~/Library/Application Support/fence/fence.json` on macOS, with fallback to legacy `~/.fence.json`).
Returns the default config file path (`~/.config/greywall/greywall.json` on Linux, `~/Library/Application Support/greywall/greywall.json` on macOS, with fallback to legacy `~/.greywall.json`).
#### `NewManager(cfg *Config, debug, monitor bool) *Manager`
@@ -113,7 +113,7 @@ Creates a new sandbox manager.
Sets up sandbox infrastructure (starts HTTP and SOCKS proxies). Called automatically by `WrapCommand` if not already initialized.
```go
manager := fence.NewManager(cfg, false, false)
manager := greywall.NewManager(cfg, false, false)
defer manager.Cleanup()
if err := manager.Initialize(); err != nil {
@@ -222,8 +222,8 @@ type SSHConfig struct {
### Allow specific domains
```go
cfg := &fence.Config{
Network: fence.NetworkConfig{
cfg := &greywall.Config{
Network: greywall.NetworkConfig{
AllowedDomains: []string{
"registry.npmjs.org",
"*.github.com",
@@ -236,8 +236,8 @@ cfg := &fence.Config{
### Restrict filesystem access
```go
cfg := &fence.Config{
Filesystem: fence.FilesystemConfig{
cfg := &greywall.Config{
Filesystem: greywall.FilesystemConfig{
AllowWrite: []string{".", "/tmp"},
DenyRead: []string{"~/.ssh", "~/.aws"},
},
@@ -247,8 +247,8 @@ cfg := &fence.Config{
### Block dangerous commands
```go
cfg := &fence.Config{
Command: fence.CommandConfig{
cfg := &greywall.Config{
Command: greywall.CommandConfig{
Deny: []string{
"rm -rf /",
"git push",
@@ -261,7 +261,7 @@ cfg := &fence.Config{
### Expose dev server port
```go
manager := fence.NewManager(cfg, false, false)
manager := greywall.NewManager(cfg, false, false)
manager.SetExposedPorts([]int{3000})
defer manager.Cleanup()
@@ -271,12 +271,12 @@ wrapped, _ := manager.WrapCommand("npm run dev")
### Load and extend config
```go
cfg, err := fence.LoadConfig(fence.DefaultConfigPath())
cfg, err := greywall.LoadConfig(greywall.DefaultConfigPath())
if err != nil {
log.Fatal(err)
}
if cfg == nil {
cfg = fence.DefaultConfig()
cfg = greywall.DefaultConfig()
}
// Add additional restrictions

View File

@@ -1,6 +1,6 @@
# Linux Security Features
Fence uses multiple layers of security on Linux, with graceful fallback when features are unavailable.
Greywall uses multiple layers of security on Linux, with graceful fallback when features are unavailable.
## Security Layers
@@ -13,13 +13,13 @@ Fence uses multiple layers of security on Linux, with graceful fallback when fea
## Feature Detection
Fence automatically detects available features and uses the best available combination.
Greywall automatically detects available features and uses the best available combination.
To see what features are detected:
```bash
# Check what features are available on your system
fence --linux-features
greywall --linux-features
# Example output:
# Linux Sandbox Features:
@@ -41,7 +41,7 @@ fence --linux-features
Landlock is applied via an **embedded wrapper** approach:
1. bwrap spawns `fence --landlock-apply -- <user-command>`
1. bwrap spawns `greywall --landlock-apply -- <user-command>`
2. The wrapper applies Landlock kernel restrictions
3. The wrapper `exec()`s the user command
@@ -75,25 +75,25 @@ This provides **defense-in-depth**: both bwrap mounts AND Landlock kernel restri
- **Impact**: `--unshare-net` is skipped; network is not fully isolated
- **Cause**: Running in Docker, GitHub Actions, or other environments without `CAP_NET_ADMIN`
- **Fallback**: Proxy-based filtering still works; filesystem/PID/seccomp isolation still active
- **Check**: Run `fence --linux-features` and look for "Network namespace (--unshare-net): false"
- **Check**: Run `greywall --linux-features` and look for "Network namespace (--unshare-net): false"
- **Workaround**: Run with `sudo`, or in Docker use `--cap-add=NET_ADMIN`
> [!NOTE]
> This is the most common "reduced isolation" scenario. Fence automatically detects this at startup and adapts. See the troubleshooting guide for more details.
> This is the most common "reduced isolation" scenario. Greywall automatically detects this at startup and adapts. See the troubleshooting guide for more details.
### When bwrap is not available
- **Impact**: Cannot run fence on Linux
- **Impact**: Cannot run greywall on Linux
- **Solution**: Install bubblewrap: `apt install bubblewrap` or `dnf install bubblewrap`
### When socat is not available
- **Impact**: Cannot run fence on Linux
- **Impact**: Cannot run greywall on Linux
- **Solution**: Install socat: `apt install socat` or `dnf install socat`
## Blocked Syscalls (seccomp)
Fence blocks dangerous syscalls that could be used for sandbox escape or privilege escalation:
Greywall blocks dangerous syscalls that could be used for sandbox escape or privilege escalation:
| Syscall | Reason |
|---------|--------|
@@ -111,13 +111,13 @@ Fence blocks dangerous syscalls that could be used for sandbox escape or privile
## Violation Monitoring
On Linux, violation monitoring (`fence -m`) shows:
On Linux, violation monitoring (`greywall -m`) shows:
| Source | What it shows | Requirements |
|--------|---------------|--------------|
| `[fence:http]` | Blocked HTTP/HTTPS requests | None |
| `[fence:socks]` | Blocked SOCKS connections | None |
| `[fence:ebpf]` | Blocked filesystem access + syscalls | CAP_BPF or root |
| `[greywall:http]` | Blocked HTTP/HTTPS requests | None |
| `[greywall:socks]` | Blocked SOCKS connections | None |
| `[greywall:ebpf]` | Blocked filesystem access + syscalls | CAP_BPF or root |
**Notes**:
@@ -127,7 +127,7 @@ On Linux, violation monitoring (`fence -m`) shows:
## Comparison with macOS
| Feature | macOS (Seatbelt) | Linux (fence) |
| Feature | macOS (Seatbelt) | Linux (greywall) |
|---------|------------------|---------------|
| Filesystem control | Native | bwrap + Landlock |
| Glob patterns | Native regex | Expanded at startup |
@@ -181,12 +181,12 @@ sudo apk add bubblewrap socat
For full violation visibility without root:
```bash
# Grant CAP_BPF to the fence binary
sudo setcap cap_bpf+ep /usr/local/bin/fence
# Grant CAP_BPF to the greywall binary
sudo setcap cap_bpf+ep /usr/local/bin/greywall
```
Or run fence with sudo when monitoring is needed:
Or run greywall with sudo when monitoring is needed:
```bash
sudo fence -m <command>
sudo greywall -m <command>
```

View File

@@ -5,16 +5,16 @@
### From Source (recommended for now)
```bash
git clone https://github.com/Use-Tusk/fence
cd fence
go build -o fence ./cmd/fence
sudo mv fence /usr/local/bin/
git clone https://gitea.app.monadical.io/monadical/greywall
cd greywall
go build -o greywall ./cmd/greywall
sudo mv greywall /usr/local/bin/
```
### Using Go Install
```bash
go install github.com/Use-Tusk/fence/cmd/fence@latest
go install gitea.app.monadical.io/monadical/greywall/cmd/greywall@latest
```
### Linux Dependencies
@@ -32,30 +32,30 @@ sudo dnf install bubblewrap socat
sudo pacman -S bubblewrap socat
```
### Do I need sudo to run fence?
### Do I need sudo to run greywall?
No, for most Linux systems. Fence works without root privileges because:
No, for most Linux systems. Greywall works without root privileges because:
- Package-manager-installed `bubblewrap` is typically already setuid
- Fence detects available capabilities and adapts automatically
- Greywall detects available capabilities and adapts automatically
If some features aren't available (like network namespaces in Docker/CI), fence falls back gracefully - you'll still get filesystem isolation, command blocking, and proxy-based network filtering.
If some features aren't available (like network namespaces in Docker/CI), greywall falls back gracefully - you'll still get filesystem isolation, command blocking, and proxy-based network filtering.
Run `fence --linux-features` to see what's available in your environment.
Run `greywall --linux-features` to see what's available in your environment.
## Verify Installation
```bash
fence --version
greywall --version
```
## Your First Sandboxed Command
By default, fence blocks all network access:
By default, greywall blocks all network access:
```bash
# This will fail - network is blocked
fence curl https://example.com
greywall curl https://example.com
```
You should see something like:
@@ -66,7 +66,7 @@ curl: (56) CONNECT tunnel failed, response 403
## Allow Specific Domains
Create a config file at `~/.config/fence/fence.json` (or `~/Library/Application Support/fence/fence.json` on macOS):
Create a config file at `~/.config/greywall/greywall.json` (or `~/Library/Application Support/greywall/greywall.json` on macOS):
```json
{
@@ -79,7 +79,7 @@ Create a config file at `~/.config/fence/fence.json` (or `~/Library/Application
Now try again:
```bash
fence curl https://example.com
greywall curl https://example.com
```
This time it succeeds!
@@ -89,7 +89,7 @@ This time it succeeds!
Use `-d` to see what's happening under the hood:
```bash
fence -d curl https://example.com
greywall -d curl https://example.com
```
This shows:
@@ -103,7 +103,7 @@ This shows:
Use `-m` to see only violations and blocked requests:
```bash
fence -m npm install
greywall -m npm install
```
This is useful for:
@@ -117,7 +117,7 @@ This is useful for:
Use `-c` to run compound commands:
```bash
fence -c "echo hello && ls -la"
greywall -c "echo hello && ls -la"
```
## Expose Ports for Servers
@@ -125,14 +125,14 @@ fence -c "echo hello && ls -la"
If you're running a server that needs to accept connections:
```bash
fence -p 3000 -c "npm run dev"
greywall -p 3000 -c "npm run dev"
```
This allows external connections to port 3000 while keeping outbound network restricted.
## Next steps
- Read **[Why Fence](why-fence.md)** to understand when fence is a good fit (and when it isn't).
- Read **[Why Greywall](why-greywall.md)** to understand when greywall is a good fit (and when it isn't).
- Learn the mental model in **[Concepts](concepts.md)**.
- Use **[Troubleshooting](troubleshooting.md)** if something is blocked unexpectedly.
- Start from copy/paste configs in **[`docs/templates/`](templates/README.md)**.

View File

@@ -18,7 +18,7 @@ Goal: make CI steps safer by default: minimal egress and controlled writes.
Run:
```bash
fence --settings ./fence.json -c "make test"
greywall --settings ./greywall.json -c "make test"
```
## Add only what you need
@@ -26,7 +26,7 @@ fence --settings ./fence.json -c "make test"
Use monitor mode to discover what a job tries to reach:
```bash
fence -m --settings ./fence.json -c "make test"
greywall -m --settings ./greywall.json -c "make test"
```
Then allowlist only:

View File

@@ -18,7 +18,7 @@ Goal: allow fetching code from a limited set of hosts.
Run:
```bash
fence --settings ./fence.json git clone https://github.com/OWNER/REPO.git
greywall --settings ./greywall.json git clone https://github.com/OWNER/REPO.git
```
## SSH clone
@@ -28,5 +28,5 @@ SSH traffic may go through SOCKS5 (`ALL_PROXY`) depending on your git/ssh config
If it fails, use monitor/debug mode to see what was blocked:
```bash
fence -m --settings ./fence.json git clone git@github.com:OWNER/REPO.git
greywall -m --settings ./greywall.json git clone git@github.com:OWNER/REPO.git
```

View File

@@ -18,7 +18,7 @@ Goal: allow npm to fetch packages, but block unexpected egress.
Run:
```bash
fence --settings ./fence.json npm install
greywall --settings ./greywall.json npm install
```
## Iterate with monitor mode
@@ -26,7 +26,7 @@ fence --settings ./fence.json npm install
If installs fail, run:
```bash
fence -m --settings ./fence.json npm install
greywall -m --settings ./greywall.json npm install
```
Then add the minimum extra domains required for your workflow (private registries, GitHub tarballs, etc.).

View File

@@ -18,19 +18,19 @@ Goal: allow Python dependency fetching while keeping egress minimal.
Run:
```bash
fence --settings ./fence.json pip install -r requirements.txt
greywall --settings ./greywall.json pip install -r requirements.txt
```
For Poetry:
```bash
fence --settings ./fence.json poetry install
greywall --settings ./greywall.json poetry install
```
## Iterate with monitor mode
```bash
fence -m --settings ./fence.json poetry install
greywall -m --settings ./greywall.json poetry install
```
If you use private indexes, add those domains explicitly.

View File

@@ -1,19 +1,19 @@
# Security Model
Fence is intended as defense-in-depth for running semi-trusted commands with reduced side effects (package installs, build scripts, CI jobs, unfamiliar repos).
Greywall is intended as defense-in-depth for running semi-trusted commands with reduced side effects (package installs, build scripts, CI jobs, unfamiliar repos).
It is not designed to be a strong isolation boundary against actively malicious code that is attempting to escape.
## Threat model (what Fence helps with)
## Threat model (what Greywall helps with)
Fence is useful when you want to reduce risk from:
Greywall is useful when you want to reduce risk from:
- Supply-chain scripts that unexpectedly call out to the network
- Tools that write broadly across your filesystem
- Accidental leakage of secrets via "phone home" behavior
- Unfamiliar repos that run surprising commands during install/build/test
## What Fence enforces
## What Greywall enforces
### Network
@@ -25,7 +25,7 @@ Important: domain filtering does not inspect content. If you allow a domain, cod
#### How allowlisting works
Fence combines OS-level enforcement with proxy-based allowlisting:
Greywall combines OS-level enforcement with proxy-based allowlisting:
- The OS sandbox / network namespace is expected to block direct outbound connections.
- Domain allowlisting happens via local HTTP/SOCKS proxies and proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`).
@@ -41,11 +41,11 @@ Localhost is separate from "external domains":
- **Writes are denied by default**; you must opt in with `allowWrite`.
- **denyWrite** can block specific files/patterns even if the parent directory is writable.
- **denyRead** can block reads from sensitive paths.
- Fence includes an internal list of always-protected targets (e.g. shell configs, git hooks) to reduce common persistence vectors.
- Greywall includes an internal list of always-protected targets (e.g. shell configs, git hooks) to reduce common persistence vectors.
### Environment sanitization
Fence strips dangerous environment variables before passing them to sandboxed commands:
Greywall strips dangerous environment variables before passing them to sandboxed commands:
- `LD_*` (Linux): `LD_PRELOAD`, `LD_LIBRARY_PATH`, etc.
- `DYLD_*` (macOS): `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, etc.
@@ -57,11 +57,11 @@ This prevents a library injection attack where a sandboxed process writes a mali
- `-m/--monitor` helps you discover what a command *tries* to access (blocked only).
- `-d/--debug` shows more detail to understand why something was blocked.
## Limitations (what Fence does NOT try to solve)
## Limitations (what Greywall does NOT try to solve)
- **Hostile code containment**: assume determined attackers may escape via kernel/OS vulnerabilities.
- **Resource limits**: CPU, memory, disk, fork bombs, etc. are out of scope.
- **Content-based controls**: Fence does not block data exfiltration to *allowed* destinations.
- **Content-based controls**: Greywall does not block data exfiltration to *allowed* destinations.
- **Proxy limitations / protocol edge cases**: some programs may not respect proxy environment variables, so they won't get domain allowlisting unless you configure them to use a proxy (e.g. Node.js `http`/`https` without a proxy-aware client).
### Practical examples of proxy limitations
@@ -71,14 +71,14 @@ The proxy approach works well for many tools (curl, wget, git, npm, pip), but no
- Node.js native `http`/`https` (use a proxy-aware client, e.g. `undici` + `ProxyAgent`)
- Raw socket connections (custom TCP/UDP protocols)
Fence's OS-level sandbox is still expected to block direct outbound connections; bypassing the proxy should fail rather than silently succeeding.
Greywall's OS-level sandbox is still expected to block direct outbound connections; bypassing the proxy should fail rather than silently succeeding.
### Domain-based filtering only
Fence does not inspect request content. If you allow a domain, a sandboxed process can still exfiltrate data to that domain.
Greywall does not inspect request content. If you allow a domain, a sandboxed process can still exfiltrate data to that domain.
### Not a hostile-code containment boundary
Fence is defense-in-depth for running semi-trusted code, not a strong isolation boundary against malware designed to escape sandboxes.
Greywall is defense-in-depth for running semi-trusted code, not a strong isolation boundary against malware designed to escape sandboxes.
For implementation details (how proxies/sandboxes/bridges work), see [`ARCHITECTURE.md`](../ARCHITECTURE.md).

View File

@@ -1,6 +1,6 @@
# Config Templates
Fence includes built-in config templates for common use cases. Templates are embedded in the binary, so you can use them directly without copying files.
Greywall includes built-in config templates for common use cases. Templates are embedded in the binary, so you can use them directly without copying files.
## Using templates
@@ -8,13 +8,13 @@ Use the `-t` / `--template` flag to apply a template:
```bash
# Use a built-in template
fence -t npm-install npm install
greywall -t npm-install npm install
# Wraps Claude Code
fence -t code -- claude
greywall -t code -- claude
# List available templates
fence --list-templates
greywall --list-templates
```
You can also copy and customize templates from [`internal/templates/`](/internal/templates/).

View File

@@ -67,9 +67,9 @@ go test -v -count=1 ./internal/sandbox/...
#### Sandboxed Build Environments (Nix, etc.)
If you're packaging fence for a distribution (e.g., Nix, Homebrew, Debian), note that some integration tests will be skipped when running `go test` during the build.
If you're packaging greywall for a distribution (e.g., Nix, Homebrew, Debian), note that some integration tests will be skipped when running `go test` during the build.
Fence's Landlock integration on Linux uses a wrapper approach: the `fence` binary re-executes itself with `--landlock-apply` inside the sandbox. Test binaries (e.g., `sandbox.test`) don't have this handler, so Landlock-specific tests automatically skip when not running as the `fence` CLI.
Greywall's Landlock integration on Linux uses a wrapper approach: the `greywall` binary re-executes itself with `--landlock-apply` inside the sandbox. Test binaries (e.g., `sandbox.test`) don't have this handler, so Landlock-specific tests automatically skip when not running as the `greywall` CLI.
Tests that skip include those calling `skipIfLandlockNotUsable()`:
@@ -77,25 +77,25 @@ Tests that skip include those calling `skipIfLandlockNotUsable()`:
- `TestLinux_LandlockProtectsGitHooks`
- `TestLinux_LandlockProtectsGitConfig`
- `TestLinux_LandlockProtectsBashrc`
- `TestLinux_LandlockAllowsTmpFence`
- `TestLinux_LandlockAllowsTmpGreywall`
- `TestLinux_PathTraversalBlocked`
- `TestLinux_SeccompBlocksDangerousSyscalls`
| Test Type | What it tests | Landlock coverage |
|-----------|---------------|-------------------|
| `go test` (integration) | Go APIs, bwrap isolation, command blocking | Skipped (test binary can't use `--landlock-apply`) |
| `smoke_test.sh` | Actual `fence` CLI end-to-end | ✅ Full coverage |
| `smoke_test.sh` | Actual `greywall` CLI end-to-end | ✅ Full coverage |
For full test coverage including Landlock, run the smoke tests against the built binary (see "Smoke Tests" section below).
**Nested sandboxing limitations:**
- **macOS**: Nested Seatbelt sandboxing is not supported. If the build environment already uses `sandbox-exec` (like Nix's Darwin sandbox), fence's tests cannot create another sandbox. The kernel returns `forbidden-sandbox-reinit`. This is a macOS limitation.
- **macOS**: Nested Seatbelt sandboxing is not supported. If the build environment already uses `sandbox-exec` (like Nix's Darwin sandbox), greywall's tests cannot create another sandbox. The kernel returns `forbidden-sandbox-reinit`. This is a macOS limitation.
- **Linux**: Tests should work in most build sandboxes, but Landlock tests will skip as explained above. Runtime functionality is unaffected.
### Smoke Tests
Smoke tests verify the compiled `fence` binary works end-to-end. Unlike integration tests (which test internal Go APIs), smoke tests exercise the CLI interface.
Smoke tests verify the compiled `greywall` binary works end-to-end. Unlike integration tests (which test internal Go APIs), smoke tests exercise the CLI interface.
**File:** [`scripts/smoke_test.sh`](/scripts/smoke_test.sh)
@@ -105,7 +105,7 @@ Smoke tests verify the compiled `fence` binary works end-to-end. Unlike integrat
- Filesystem restrictions via settings file
- Command blocking via settings file
- Network blocking
- Environment variable injection (FENCE_SANDBOX, HTTP_PROXY)
- Environment variable injection (GREYWALL_SANDBOX, HTTP_PROXY)
- Tool compatibility (python3, node, git, rg) - ensure that frequently used tools don't break in sandbox
**Run:**
@@ -115,10 +115,10 @@ Smoke tests verify the compiled `fence` binary works end-to-end. Unlike integrat
./scripts/smoke_test.sh
# Test specific binary
./scripts/smoke_test.sh ./path/to/fence
./scripts/smoke_test.sh ./path/to/greywall
# Enable network tests (requires internet)
FENCE_TEST_NETWORK=1 ./scripts/smoke_test.sh
GREYWALL_TEST_NETWORK=1 ./scripts/smoke_test.sh
```
## Platform-Specific Behavior
@@ -158,7 +158,7 @@ The `integration_test.go` file provides helpers for writing sandbox tests:
```go
// Skip helpers
skipIfAlreadySandboxed(t) // Skip if running inside Fence
skipIfAlreadySandboxed(t) // Skip if running inside Greywall
skipIfCommandNotFound(t, "python3") // Skip if command missing
// Run a command under the sandbox
@@ -237,10 +237,10 @@ go test -v -run TestSpecificTest ./internal/sandbox/...
```bash
# Replicate what the test does
./fence -c "the-command-that-failed"
./greywall -c "the-command-that-failed"
# With a settings file
./fence -s /path/to/settings.json -c "command"
./greywall -s /path/to/settings.json -c "command"
```
### Check platform capabilities

View File

@@ -2,9 +2,9 @@
## Nested Sandboxing Not Supported
Fence cannot run inside another sandbox that uses the same underlying technology.
Greywall cannot run inside another sandbox that uses the same underlying technology.
**macOS (Seatbelt)**: If you try to run fence inside an existing `sandbox-exec` sandbox (e.g., Nix's Darwin build sandbox), you'll see:
**macOS (Seatbelt)**: If you try to run greywall inside an existing `sandbox-exec` sandbox (e.g., Nix's Darwin build sandbox), you'll see:
```text
Sandbox: sandbox-exec(...) deny(1) forbidden-sandbox-reinit
@@ -12,11 +12,11 @@ Sandbox: sandbox-exec(...) deny(1) forbidden-sandbox-reinit
This is a macOS kernel limitation - nested Seatbelt sandboxes are not allowed. There is no workaround.
**Linux (Landlock)**: Landlock supports stacking (nested restrictions), but fence's test binaries cannot use the Landlock wrapper (see [Testing docs](testing.md#sandboxed-build-environments-nix-etc)).
**Linux (Landlock)**: Landlock supports stacking (nested restrictions), but greywall's test binaries cannot use the Landlock wrapper (see [Testing docs](testing.md#sandboxed-build-environments-nix-etc)).
## "bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted" (Linux)
This error occurs when fence tries to create a network namespace but the environment lacks the `CAP_NET_ADMIN` capability. This is common in:
This error occurs when greywall tries to create a network namespace but the environment lacks the `CAP_NET_ADMIN` capability. This is common in:
- **Docker containers** (unless run with `--privileged` or `--cap-add=NET_ADMIN`)
- **GitHub Actions** and other CI runners
@@ -25,7 +25,7 @@ This error occurs when fence tries to create a network namespace but the environ
**What happens now:**
Fence automatically detects this limitation and falls back to running **without network namespace isolation**. The sandbox still provides:
Greywall automatically detects this limitation and falls back to running **without network namespace isolation**. The sandbox still provides:
- Filesystem restrictions (read-only root, allowWrite paths)
- PID namespace isolation
@@ -41,7 +41,7 @@ Fence automatically detects this limitation and falls back to running **without
**To check if your environment supports network namespaces:**
```bash
fence --linux-features
greywall --linux-features
```
Look for "Network namespace (--unshare-net): true/false"
@@ -51,7 +51,7 @@ Look for "Network namespace (--unshare-net): true/false"
1. **Run with elevated privileges:**
```bash
sudo fence <command>
sudo greywall <command>
```
2. **In Docker, add capability:**
@@ -96,20 +96,20 @@ On most systems with package-manager-installed bwrap, this error shouldn't occur
This usually means:
- the process tried to reach a domain that is **not allowed**, and
- the request went through fence's HTTP proxy, which returned `403`.
- the request went through greywall's HTTP proxy, which returned `403`.
Fix:
- Run with monitor mode to see what was blocked:
- `fence -m <command>`
- `greywall -m <command>`
- Add the required destination(s) to `network.allowedDomains`.
## "It works outside fence but not inside"
## "It works outside greywall but not inside"
Start with:
- `fence -m <command>` to see what's being denied
- `fence -d <command>` to see full proxy and sandbox detail
- `greywall -m <command>` to see what's being denied
- `greywall -d <command>` to see full proxy and sandbox detail
Common causes:
@@ -134,7 +134,7 @@ const response = await fetch(url, {
});
```
Fence's OS-level sandbox should still block direct connections; the above makes your requests go through the filtering proxy so allowlisting works as intended.
Greywall's OS-level sandbox should still block direct connections; the above makes your requests go through the filtering proxy so allowlisting works as intended.
## Local services (Redis/Postgres/etc.) fail inside the sandbox
@@ -156,7 +156,7 @@ If you're running a server inside the sandbox that must accept connections:
Writes are denied by default.
- Add the minimum required writable directories to `filesystem.allowWrite`.
- Protect sensitive targets with `filesystem.denyWrite` (and note fence protects some targets regardless).
- Protect sensitive targets with `filesystem.denyWrite` (and note greywall protects some targets regardless).
Example:

View File

@@ -1,6 +1,6 @@
# Why Fence?
# Why Greywall?
Fence exists to reduce the blast radius of running commands you don't fully trust (or don't fully understand yet).
Greywall exists to reduce the blast radius of running commands you don't fully trust (or don't fully understand yet).
Common situations:
@@ -9,11 +9,11 @@ Common situations:
- Running CI jobs where you want default-deny egress and tightly scoped writes
- Auditing what a command *tries* to do before you let it do it
Fence is intentionally simple: it focuses on network allowlisting (by domain) and filesystem write restrictions (by path), wrapped in a pragmatic OS sandbox (macOS `sandbox-exec`, Linux `bubblewrap`).
Greywall is intentionally simple: it focuses on network allowlisting (by domain) and filesystem write restrictions (by path), wrapped in a pragmatic OS sandbox (macOS `sandbox-exec`, Linux `bubblewrap`).
## What problem does it solve?
Fence helps you answer: "What can this command touch?"
Greywall helps you answer: "What can this command touch?"
- **Network**: block all outbound by default; then allow only the domains you choose.
- **Filesystem**: default-deny writes; then allow writes only where you choose (and deny sensitive writes regardless).
@@ -21,9 +21,9 @@ Fence helps you answer: "What can this command touch?"
This is especially useful for supply-chain risk and "unknown repo" workflows where you want a safer default than "run it and hope".
## When Fence is useful even if tools already sandbox
## When Greywall is useful even if tools already sandbox
Some coding agents and platforms ship sandboxing (Seatbelt/Landlock/etc.). Fence still provides value when you want:
Some coding agents and platforms ship sandboxing (Seatbelt/Landlock/etc.). Greywall still provides value when you want:
- **Tool-agnostic policy**: apply the same rules to any command, not only inside one agent.
- **Standardization**: commit/review a config once, use it across developers and CI.
@@ -32,7 +32,7 @@ Some coding agents and platforms ship sandboxing (Seatbelt/Landlock/etc.). Fence
## Non-goals
Fence is **not** a hardened containment boundary for actively malicious code.
Greywall is **not** a hardened containment boundary for actively malicious code.
- It does **not** attempt to prevent resource exhaustion (CPU/RAM/disk), timing attacks, or kernel-level escapes.
- Domain allowlisting is not content inspection: if you allow a domain, code can exfiltrate via that domain.

View File

@@ -1,6 +1,6 @@
# Dev Server + Redis Demo
This demo shows how fence controls network access: allowing specific external domains while blocking (or allowing) localhost connections.
This demo shows how greywall controls network access: allowing specific external domains while blocking (or allowing) localhost connections.
## Prerequisites
@@ -21,7 +21,7 @@ npm install
This shows that requests to Redis (local service) works, but external requests are blocked.
```bash
fence -p 3000 --settings fence-external-blocked.json npm start
greywall -p 3000 --settings greywall-external-blocked.json npm start
```
Test it:
@@ -39,7 +39,7 @@ curl http://localhost:3000/api/external
This shows the opposite: whitelisted external domains work, but Redis (localhost) is blocked.
```bash
fence -p 3000 --settings fence-external-only.json npm start
greywall -p 3000 --settings greywall-external-only.json npm start
```
You will immediately notice that Redis connection is blocked on app startup:
@@ -62,8 +62,8 @@ curl http://localhost:3000/api/users
| Config | Redis (localhost) | External (httpbin.org) |
|--------|-------------------|------------------------|
| `fence-external-blocked.json` | ✓ Allowed | ✗ Blocked |
| `fence-external-only.json` | ✗ Blocked | ✓ Allowed |
| `greywall-external-blocked.json` | ✓ Allowed | ✗ Blocked |
| `greywall-external-only.json` | ✗ Blocked | ✓ Allowed |
## Key Settings
@@ -75,7 +75,7 @@ curl http://localhost:3000/api/users
## Note: Node.js Proxy Support
Node.js's native `http`/`https` modules don't respect proxy environment variables. This demo uses [`undici`](https://github.com/nodejs/undici) with `ProxyAgent` to route requests through fence's proxy:
Node.js's native `http`/`https` modules don't respect proxy environment variables. This demo uses [`undici`](https://github.com/nodejs/undici) with `ProxyAgent` to route requests through greywall's proxy:
```javascript
import { ProxyAgent, fetch } from "undici";
@@ -86,4 +86,4 @@ const response = await fetch(url, {
});
```
Without this, external HTTP requests would fail with connection errors (the sandbox blocks them) rather than going through fence's proxy.
Without this, external HTTP requests would fail with connection errors (the sandbox blocks them) rather than going through greywall's proxy.

View File

@@ -2,7 +2,7 @@
* Demo Express app that:
* 1. Serves an API on port 3000
* 2. Connects to Redis on localhost:6379
* 3. Attempts to call external APIs (blocked by fence)
* 3. Attempts to call external APIs (blocked by greywall)
*
* This demonstrates allowLocalOutbound - the app can reach
* local services (Redis) but not the external internet.
@@ -60,7 +60,7 @@ async function fetchExternal(url) {
signal: AbortSignal.timeout(5000),
};
// Use proxy if available (set by fence)
// Use proxy if available (set by greywall)
if (proxyUrl) {
options.dispatcher = new ProxyAgent(proxyUrl);
}
@@ -84,7 +84,7 @@ app.get("/", (req, res) => {
"/api/users": "List all users from Redis",
"/api/users/:id": "Get user by ID from Redis",
"/api/health": "Health check",
"/api/external": "Try to call external API (blocked by fence)",
"/api/external": "Try to call external API (blocked by greywall)",
},
});
});
@@ -160,20 +160,20 @@ app.get("/api/external", async (req, res) => {
try {
const result = await fetchExternal("https://httpbin.org/get");
// Check if we're using a proxy (indicates fence is running)
// Check if we're using a proxy (indicates greywall is running)
const usingProxy = !!(process.env.HTTPS_PROXY || process.env.HTTP_PROXY);
res.json({
status: "success",
message: usingProxy
? "✓ Request allowed (httpbin.org is whitelisted)"
: "⚠️ No proxy detected - not running in fence",
: "⚠️ No proxy detected - not running in greywall",
proxy: usingProxy ? process.env.HTTPS_PROXY : null,
data: result,
});
} catch (error) {
res.json({
status: "blocked",
message: "✓ External call blocked by fence",
message: "✓ External call blocked by greywall",
error: error.message,
});
}

View File

@@ -1,7 +1,7 @@
{
"name": "dev-server-demo",
"version": "1.0.0",
"description": "Demo: Dev server with Redis in fence sandbox",
"description": "Demo: Dev server with Redis in greywall sandbox",
"type": "module",
"main": "app.js",
"scripts": {

View File

@@ -1,10 +1,10 @@
# Filesystem Sandbox Demo
This demo shows how fence controls filesystem access with `allowWrite`, `denyWrite`, and `denyRead`.
This demo shows how greywall controls filesystem access with `allowWrite`, `denyWrite`, and `denyRead`.
## What it demonstrates
| Operation | Without Fence | With Fence |
| Operation | Without Greywall | With Greywall |
|-----------|---------------|------------|
| Write to `./output/` | ✓ | ✓ (in allowWrite) |
| Write to `./` | ✓ | ✗ (not in allowWrite) |
@@ -16,19 +16,19 @@ This demo shows how fence controls filesystem access with `allowWrite`, `denyWri
## Run the demo
### Without fence (all writes succeed)
### Without greywall (all writes succeed)
```bash
python demo.py
```
### With fence (unauthorized operations blocked)
### With greywall (unauthorized operations blocked)
```bash
fence --settings fence.json python demo.py
greywall --settings greywall.json python demo.py
```
## Fence config
## Greywall config
```json
{
@@ -58,7 +58,7 @@ fence --settings fence.json python demo.py
## Protected paths
Fence also automatically protects certain paths regardless of config:
Greywall also automatically protects certain paths regardless of config:
- Shell configs: `.bashrc`, `.zshrc`, `.profile`
- Git hooks: `.git/hooks/*`

View File

@@ -2,13 +2,13 @@
"""
Filesystem Sandbox Demo
This script demonstrates fence's filesystem controls:
This script demonstrates greywall's filesystem controls:
- allowWrite: Only specific directories are writable
- denyWrite: Block writes to sensitive files
- denyRead: Block reads from sensitive paths
Run WITHOUT fence to see all operations succeed.
Run WITH fence to see unauthorized operations blocked.
Run WITHOUT greywall to see all operations succeed.
Run WITH greywall to see unauthorized operations blocked.
"""
import os
@@ -78,7 +78,7 @@ def main():
╔═══════════════════════════════════════════════════════════╗
║ Filesystem Sandbox Demo ║
╠═══════════════════════════════════════════════════════════╣
║ Tests fence's filesystem controls: ║
║ Tests greywall's filesystem controls: ║
║ - allowWrite: Only ./output/ is writable ║
║ - denyWrite: .env and *.key files are protected ║
║ - denyRead: /etc/shadow is blocked ║
@@ -96,7 +96,7 @@ def main():
"Write to ./output/ (allowed)",
)
# Test 2: Write to project root (should fail with fence)
# Test 2: Write to project root (should fail with greywall)
try_write(
"unauthorized.txt",
"This should not be writable.\n",
@@ -133,12 +133,12 @@ def main():
print(f"({skipped} test(s) skipped - file not found)")
if blocked > 0:
print(f"Fence blocked {blocked} unauthorized operation(s)")
print(f"Greywall blocked {blocked} unauthorized operation(s)")
print(f"{succeeded} allowed operation(s) succeeded")
print("\nFilesystem sandbox is working!\n")
else:
print("⚠️ All operations succeeded - you are likely not running in fence")
print("Run with: fence --settings fence.json python demo.py\n")
print("⚠️ All operations succeeded - you are likely not running in greywall")
print("Run with: greywall --settings greywall.json python demo.py\n")
cleanup()

View File

@@ -1,6 +1,6 @@
# Fence Examples
# Greywall Examples
Runnable examples demonstrating `fence` capabilities.
Runnable examples demonstrating `greywall` capabilities.
If you're looking for copy/paste configs and "cookbook" workflows, also see:
@@ -11,5 +11,5 @@ If you're looking for copy/paste configs and "cookbook" workflows, also see:
| Example | What it demonstrates | How to run |
|--------|-----------------------|------------|
| **[01-dev-server](01-dev-server/README.md)** | Running a dev server in the sandbox, controlling external domains vs localhost outbound (Redis), and exposing an inbound port (`-p`) | `cd examples/01-dev-server && fence -p 3000 --settings fence-external-blocked.json npm start` |
| **[02-filesystem](02-filesystem/README.md)** | Filesystem controls: `allowWrite`, `denyWrite`, `denyRead` | `cd examples/02-filesystem && fence --settings fence.json python demo.py` |
| **[01-dev-server](01-dev-server/README.md)** | Running a dev server in the sandbox, controlling external domains vs localhost outbound (Redis), and exposing an inbound port (`-p`) | `cd examples/01-dev-server && greywall -p 3000 --settings greywall-external-blocked.json npm start` |
| **[02-filesystem](02-filesystem/README.md)** | Filesystem controls: `allowWrite`, `denyWrite`, `denyRead` | `cd examples/02-filesystem && greywall --settings greywall.json python demo.py` |

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/Use-Tusk/fence
module gitea.app.monadical.io/monadical/greywall
go 1.25

View File

@@ -1,17 +1,17 @@
#!/bin/sh
set -e
# Fence Installer (Linux/macOS only)
# Greywall Installer (Linux/macOS only)
# For Windows, we recommend using WSL.
# Usage (latest):
# curl -fsSL https://raw.githubusercontent.com/Use-Tusk/fence/main/install.sh | sh
# curl -fsSL https://gitea.app.monadical.io/monadical/greywall/raw/branch/main/install.sh | sh
# Usage (specific version):
# curl -fsSL https://raw.githubusercontent.com/Use-Tusk/fence/main/install.sh | sh -s -- v0.1.0
# curl -fsSL https://gitea.app.monadical.io/monadical/greywall/raw/branch/main/install.sh | sh -s -- v0.1.0
# Or via env var:
# curl -fsSL https://raw.githubusercontent.com/Use-Tusk/fence/main/install.sh | FENCE_VERSION=0.1.0 sh
# curl -fsSL https://gitea.app.monadical.io/monadical/greywall/raw/branch/main/install.sh | GREYWALL_VERSION=0.1.0 sh
REPO="Use-Tusk/fence"
BINARY_NAME="fence"
REPO="monadical/greywall"
BINARY_NAME="greywall"
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
@@ -39,8 +39,8 @@ case "$ARCH" in
;;
esac
# Determine version to install: use first arg or FENCE_VERSION env var; otherwise fallback to latest
REQUESTED_VERSION="${1:-${FENCE_VERSION:-}}"
# Determine version to install: use first arg or GREYWALL_VERSION env var; otherwise fallback to latest
REQUESTED_VERSION="${1:-${GREYWALL_VERSION:-}}"
if [ -n "$REQUESTED_VERSION" ]; then
case "$REQUESTED_VERSION" in
v*) VERSION_TAG="$REQUESTED_VERSION" ;;
@@ -48,11 +48,11 @@ if [ -n "$REQUESTED_VERSION" ]; then
esac
else
# Try manifest first (fast, no rate limits)
VERSION_TAG=$(curl -sL "https://use-tusk.github.io/fence/latest.txt" 2>/dev/null || echo "")
VERSION_TAG=$(curl -sL "https://gitea.app.monadical.io/monadical/greywall/latest.txt" 2>/dev/null || echo "")
# Fallback to GitHub API if manifest fails
if [ -z "$VERSION_TAG" ]; then
VERSION_TAG=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
VERSION_TAG=$(curl -s "https://gitea.app.monadical.io/api/v1/repos/$REPO/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
fi
fi
@@ -75,12 +75,12 @@ TMP_DIR=$(mktemp -d)
cd "$TMP_DIR"
echo "Downloading from $DOWNLOAD_URL..."
if ! curl -fsSL -o fence.tar.gz "$DOWNLOAD_URL"; then
if ! curl -fsSL -o greywall.tar.gz "$DOWNLOAD_URL"; then
echo "Error: Failed to download release"
exit 1
fi
tar -xzf fence.tar.gz
tar -xzf greywall.tar.gz
# Install to /usr/local/bin or ~/.local/bin
INSTALL_DIR="/usr/local/bin"
@@ -97,9 +97,9 @@ chmod +x "$INSTALL_DIR/$BINARY_NAME"
cd - > /dev/null
rm -rf "$TMP_DIR"
echo "Fence $VERSION_TAG installed successfully!"
echo "Greywall $VERSION_TAG installed successfully!"
echo ""
echo "Run 'fence --help' to get started."
echo "Run 'greywall --help' to get started."
# Check if install dir is in PATH
case ":$PATH:" in

View File

@@ -1,4 +1,4 @@
// Package config defines the configuration types and loading for fence.
// Package config defines the configuration types and loading for greywall.
package config
import (
@@ -14,7 +14,7 @@ import (
"github.com/tidwall/jsonc"
)
// Config is the main configuration for fence.
// Config is the main configuration for greywall.
type Config struct {
Extends string `json:"extends,omitempty"`
Network NetworkConfig `json:"network"`
@@ -130,12 +130,12 @@ func Default() *Config {
// DefaultConfigPath returns the default config file path.
// Uses the OS-preferred config directory (XDG on Linux, ~/Library/Application Support on macOS).
// Falls back to ~/.fence.json if the new location doesn't exist but the legacy one does.
// Falls back to ~/.greywall.json if the new location doesn't exist but the legacy one does.
func DefaultConfigPath() string {
// Try OS-preferred config directory first
configDir, err := os.UserConfigDir()
if err == nil {
newPath := filepath.Join(configDir, "fence", "fence.json")
newPath := filepath.Join(configDir, "greywall", "greywall.json")
if _, err := os.Stat(newPath); err == nil {
return newPath
}
@@ -149,18 +149,18 @@ func DefaultConfigPath() string {
// Fall back to legacy path if it exists
home, err := os.UserHomeDir()
if err != nil {
return "fence.json"
return "greywall.json"
}
legacyPath := filepath.Join(home, ".fence.json")
legacyPath := filepath.Join(home, ".greywall.json")
if _, err := os.Stat(legacyPath); err == nil {
return legacyPath
}
// Neither exists, prefer new XDG-compliant path
if configDir != "" {
return filepath.Join(configDir, "fence", "fence.json")
return filepath.Join(configDir, "greywall", "greywall.json")
}
return filepath.Join(home, ".config", "fence", "fence.json")
return filepath.Join(home, ".config", "greywall", "greywall.json")
}
// Load loads configuration from a file path.

View File

@@ -249,10 +249,10 @@ func TestDefaultConfigPath(t *testing.T) {
if path == "" {
t.Error("DefaultConfigPath() returned empty string")
}
// Should end with fence.json (either new XDG path or legacy .fence.json)
// Should end with greywall.json (either new XDG path or legacy .greywall.json)
base := filepath.Base(path)
if base != "fence.json" && base != ".fence.json" {
t.Errorf("DefaultConfigPath() = %q, expected to end with fence.json or .fence.json", path)
if base != "greywall.json" && base != ".greywall.json" {
t.Errorf("DefaultConfigPath() = %q, expected to end with greywall.json or .greywall.json", path)
}
}

View File

@@ -10,7 +10,7 @@ import (
"testing"
"time"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// ============================================================================
@@ -305,8 +305,8 @@ func BenchmarkOverhead(b *testing.B) {
func skipBenchIfSandboxed(b *testing.B) {
b.Helper()
if os.Getenv("FENCE_SANDBOX") == "1" {
b.Skip("already running inside Fence sandbox")
if os.Getenv("GREYWALL_SANDBOX") == "1" {
b.Skip("already running inside Greywall sandbox")
}
}

View File

@@ -5,7 +5,7 @@ import (
"path/filepath"
"strings"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// CommandBlockedError is returned when a command is blocked by policy.

View File

@@ -3,7 +3,7 @@ package sandbox
import (
"testing"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
func TestCheckCommand_BasicDeny(t *testing.T) {

View File

@@ -39,14 +39,14 @@ func GetDefaultWritePaths() []string {
"/dev/tty",
"/dev/dtracehelper",
"/dev/autofs_nowait",
"/tmp/fence",
"/private/tmp/fence",
"/tmp/greywall",
"/private/tmp/greywall",
}
if home != "" {
paths = append(paths,
filepath.Join(home, ".npm/_logs"),
filepath.Join(home, ".fence/debug"),
filepath.Join(home, ".greywall/debug"),
)
}

View File

@@ -14,7 +14,7 @@ func TestGetDefaultWritePaths(t *testing.T) {
t.Error("GetDefaultWritePaths() returned empty slice")
}
essentialPaths := []string{"/dev/stdout", "/dev/stderr", "/dev/null", "/tmp/fence"}
essentialPaths := []string{"/dev/stdout", "/dev/stderr", "/dev/null", "/tmp/greywall"}
for _, essential := range essentialPaths {
found := slices.Contains(paths, essential)
if !found {

View File

@@ -17,8 +17,8 @@ import (
// skipIfLandlockNotUsable skips tests that require the Landlock wrapper.
// The Landlock wrapper re-executes the binary with --landlock-apply, which only
// the fence CLI understands. Test binaries (e.g., sandbox.test) don't have this
// handler, so Landlock tests must be skipped when not running as the fence CLI.
// the greywall CLI understands. Test binaries (e.g., sandbox.test) don't have this
// handler, so Landlock tests must be skipped when not running as the greywall CLI.
// TODO: consider removing tests that call this function, for now can keep them
// as documentation.
func skipIfLandlockNotUsable(t *testing.T) {
@@ -28,8 +28,8 @@ func skipIfLandlockNotUsable(t *testing.T) {
t.Skip("skipping: Landlock not available on this kernel")
}
exePath, _ := os.Executable()
if !strings.Contains(filepath.Base(exePath), "fence") {
t.Skip("skipping: Landlock wrapper requires fence CLI (test binary cannot use --landlock-apply)")
if !strings.Contains(filepath.Base(exePath), "greywall") {
t.Skip("skipping: Landlock wrapper requires greywall CLI (test binary cannot use --landlock-apply)")
}
}
@@ -51,7 +51,7 @@ func TestLinux_LandlockBlocksWriteOutsideWorkspace(t *testing.T) {
skipIfLandlockNotUsable(t)
workspace := createTempWorkspace(t)
outsideFile := "/tmp/fence-test-outside-" + filepath.Base(workspace) + ".txt"
outsideFile := "/tmp/greywall-test-outside-" + filepath.Base(workspace) + ".txt"
defer func() { _ = os.Remove(outsideFile) }()
cfg := testConfigWithWorkspace(workspace)
@@ -198,24 +198,24 @@ func TestLinux_LandlockBlocksWriteSystemFiles(t *testing.T) {
cfg := testConfigWithWorkspace(workspace)
// Attempting to write to /etc should fail
result := runUnderSandbox(t, cfg, "touch /etc/fence-test-file", workspace)
result := runUnderSandbox(t, cfg, "touch /etc/greywall-test-file", workspace)
assertBlocked(t, result)
assertFileNotExists(t, "/etc/fence-test-file")
assertFileNotExists(t, "/etc/greywall-test-file")
}
// TestLinux_LandlockAllowsTmpFence verifies /tmp/fence is writable.
func TestLinux_LandlockAllowsTmpFence(t *testing.T) {
// TestLinux_LandlockAllowsTmpGreywall verifies /tmp/greywall is writable.
func TestLinux_LandlockAllowsTmpGreywall(t *testing.T) {
skipIfAlreadySandboxed(t)
skipIfLandlockNotUsable(t)
workspace := createTempWorkspace(t)
cfg := testConfigWithWorkspace(workspace)
// Ensure /tmp/fence exists
_ = os.MkdirAll("/tmp/fence", 0o750)
// Ensure /tmp/greywall exists
_ = os.MkdirAll("/tmp/greywall", 0o750)
testFile := "/tmp/fence/test-file-" + filepath.Base(workspace)
testFile := "/tmp/greywall/test-file-" + filepath.Base(workspace)
defer func() { _ = os.Remove(testFile) }()
result := runUnderSandbox(t, cfg, "echo 'test' > "+testFile, workspace)
@@ -351,8 +351,8 @@ func TestLinux_TransparentProxyRoutesThroughSocks(t *testing.T) {
cfg.Filesystem.AllowWrite = []string{workspace}
// This test requires actual network and a running SOCKS5 proxy
if os.Getenv("FENCE_TEST_NETWORK") != "1" {
t.Skip("skipping: set FENCE_TEST_NETWORK=1 to run network tests (requires SOCKS5 proxy on localhost:1080)")
if os.Getenv("GREYWALL_TEST_NETWORK") != "1" {
t.Skip("skipping: set GREYWALL_TEST_NETWORK=1 to run network tests (requires SOCKS5 proxy on localhost:1080)")
}
result := runUnderSandboxWithTimeout(t, cfg, "curl -s --connect-timeout 5 --max-time 10 https://httpbin.org/get", workspace, 15*time.Second)
@@ -455,10 +455,10 @@ func TestLinux_SymlinkEscapeBlocked(t *testing.T) {
_ = os.Symlink("/etc", symlinkPath)
// Try to write through the symlink
result := runUnderSandbox(t, cfg, "echo 'test' > "+symlinkPath+"/fence-test", workspace)
result := runUnderSandbox(t, cfg, "echo 'test' > "+symlinkPath+"/greywall-test", workspace)
assertBlocked(t, result)
assertFileNotExists(t, "/etc/fence-test")
assertFileNotExists(t, "/etc/greywall-test")
}
// TestLinux_PathTraversalBlocked verifies path traversal attacks are prevented.
@@ -470,10 +470,10 @@ func TestLinux_PathTraversalBlocked(t *testing.T) {
cfg := testConfigWithWorkspace(workspace)
// Try to escape using ../../../
result := runUnderSandbox(t, cfg, "touch ../../../../tmp/fence-escape-test", workspace)
result := runUnderSandbox(t, cfg, "touch ../../../../tmp/greywall-escape-test", workspace)
assertBlocked(t, result)
assertFileNotExists(t, "/tmp/fence-escape-test")
assertFileNotExists(t, "/tmp/greywall-escape-test")
}
// TestLinux_DeviceAccessBlocked verifies device files cannot be accessed.

View File

@@ -20,7 +20,7 @@ func TestMacOS_SeatbeltBlocksWriteOutsideWorkspace(t *testing.T) {
skipIfAlreadySandboxed(t)
workspace := createTempWorkspace(t)
outsideFile := "/tmp/fence-test-outside-" + filepath.Base(workspace) + ".txt"
outsideFile := "/tmp/greywall-test-outside-" + filepath.Base(workspace) + ".txt"
defer func() { _ = os.Remove(outsideFile) }()
cfg := testConfigWithWorkspace(workspace)
@@ -138,23 +138,23 @@ func TestMacOS_SeatbeltBlocksWriteSystemFiles(t *testing.T) {
cfg := testConfigWithWorkspace(workspace)
// Attempting to write to /etc should fail
result := runUnderSandbox(t, cfg, "touch /etc/fence-test-file", workspace)
result := runUnderSandbox(t, cfg, "touch /etc/greywall-test-file", workspace)
assertBlocked(t, result)
assertFileNotExists(t, "/etc/fence-test-file")
assertFileNotExists(t, "/etc/greywall-test-file")
}
// TestMacOS_SeatbeltAllowsTmpFence verifies /tmp/fence is writable.
func TestMacOS_SeatbeltAllowsTmpFence(t *testing.T) {
// TestMacOS_SeatbeltAllowsTmpGreywall verifies /tmp/greywall is writable.
func TestMacOS_SeatbeltAllowsTmpGreywall(t *testing.T) {
skipIfAlreadySandboxed(t)
workspace := createTempWorkspace(t)
cfg := testConfigWithWorkspace(workspace)
// Ensure /tmp/fence exists
_ = os.MkdirAll("/tmp/fence", 0o750)
// Ensure /tmp/greywall exists
_ = os.MkdirAll("/tmp/greywall", 0o750)
testFile := "/tmp/fence/test-file-" + filepath.Base(workspace)
testFile := "/tmp/greywall/test-file-" + filepath.Base(workspace)
defer func() { _ = os.Remove(testFile) }()
result := runUnderSandbox(t, cfg, "echo 'test' > "+testFile, workspace)
@@ -221,8 +221,8 @@ func TestMacOS_ProxyAllowsTrafficViaProxy(t *testing.T) {
cfg.Filesystem.AllowWrite = []string{workspace}
// This test requires actual network and a running SOCKS5 proxy
if os.Getenv("FENCE_TEST_NETWORK") != "1" {
t.Skip("skipping: set FENCE_TEST_NETWORK=1 to run network tests (requires SOCKS5 proxy on localhost:1080)")
if os.Getenv("GREYWALL_TEST_NETWORK") != "1" {
t.Skip("skipping: set GREYWALL_TEST_NETWORK=1 to run network tests (requires SOCKS5 proxy on localhost:1080)")
}
result := runUnderSandboxWithTimeout(t, cfg, "curl -s --connect-timeout 5 --max-time 10 https://httpbin.org/get", workspace, 15*time.Second)
@@ -290,10 +290,10 @@ func TestMacOS_SymlinkEscapeBlocked(t *testing.T) {
}
// Try to write through the symlink
result := runUnderSandbox(t, cfg, "echo 'test' > "+symlinkPath+"/fence-test", workspace)
result := runUnderSandbox(t, cfg, "echo 'test' > "+symlinkPath+"/greywall-test", workspace)
assertBlocked(t, result)
assertFileNotExists(t, "/etc/fence-test")
assertFileNotExists(t, "/etc/greywall-test")
}
// TestMacOS_PathTraversalBlocked verifies path traversal attacks are prevented.
@@ -303,10 +303,10 @@ func TestMacOS_PathTraversalBlocked(t *testing.T) {
workspace := createTempWorkspace(t)
cfg := testConfigWithWorkspace(workspace)
result := runUnderSandbox(t, cfg, "touch ../../../../tmp/fence-escape-test", workspace)
result := runUnderSandbox(t, cfg, "touch ../../../../tmp/greywall-escape-test", workspace)
assertBlocked(t, result)
assertFileNotExists(t, "/tmp/fence-escape-test")
assertFileNotExists(t, "/tmp/greywall-escape-test")
}
// TestMacOS_DeviceAccessBlocked verifies device files cannot be written.
@@ -332,7 +332,7 @@ func TestMacOS_DeviceAccessBlocked(t *testing.T) {
// ============================================================================
// TestMacOS_ReadOnlyPolicy verifies that files outside the allowed write paths cannot be written.
// Note: Fence always adds some default writable paths (/tmp/fence, /dev/null, etc.)
// Note: Greywall always adds some default writable paths (/tmp/greywall, /dev/null, etc.)
// so "read-only" here means "outside the workspace".
func TestMacOS_ReadOnlyPolicy(t *testing.T) {
skipIfAlreadySandboxed(t)
@@ -353,7 +353,7 @@ func TestMacOS_ReadOnlyPolicy(t *testing.T) {
assertAllowed(t, result)
// Writing outside workspace should fail
outsidePath := "/tmp/fence-test-readonly-" + filepath.Base(workspace) + ".txt"
outsidePath := "/tmp/greywall-test-readonly-" + filepath.Base(workspace) + ".txt"
defer func() { _ = os.Remove(outsidePath) }()
result = runUnderSandbox(t, cfg, "echo 'outside' > "+outsidePath, workspace)
assertBlocked(t, result)
@@ -373,7 +373,7 @@ func TestMacOS_WorkspaceWritePolicy(t *testing.T) {
assertFileExists(t, filepath.Join(workspace, "test.txt"))
// Writing outside workspace should fail
outsideFile := "/tmp/fence-test-outside.txt"
outsideFile := "/tmp/greywall-test-outside.txt"
defer func() { _ = os.Remove(outsideFile) }()
result = runUnderSandbox(t, cfg, "echo 'test' > "+outsideFile, workspace)
assertBlocked(t, result)
@@ -399,7 +399,7 @@ func TestMacOS_MultipleWritableRoots(t *testing.T) {
assertAllowed(t, result)
// Writing outside both should fail
outsideFile := "/tmp/fence-test-outside-multi.txt"
outsideFile := "/tmp/greywall-test-outside-multi.txt"
defer func() { _ = os.Remove(outsideFile) }()
result = runUnderSandbox(t, cfg, "echo 'test' > "+outsideFile, workspace1)
assertBlocked(t, result)

View File

@@ -12,7 +12,7 @@ import (
"testing"
"time"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// ============================================================================
@@ -44,8 +44,8 @@ func (r *SandboxTestResult) Failed() bool {
// skipIfAlreadySandboxed skips the test if running inside a sandbox.
func skipIfAlreadySandboxed(t *testing.T) {
t.Helper()
if os.Getenv("FENCE_SANDBOX") == "1" {
t.Skip("skipping: already running inside Fence sandbox")
if os.Getenv("GREYWALL_SANDBOX") == "1" {
t.Skip("skipping: already running inside Greywall sandbox")
}
}
@@ -157,7 +157,7 @@ func testConfigWithProxy(proxyURL string) *config.Config {
// Sandbox Execution Helpers
// ============================================================================
// runUnderSandbox executes a command under the fence sandbox.
// runUnderSandbox executes a command under the greywall sandbox.
// This uses the sandbox Manager directly for integration testing.
func runUnderSandbox(t *testing.T, cfg *config.Config, command string, workDir string) *SandboxTestResult {
t.Helper()
@@ -281,7 +281,7 @@ func executeShellCommandWithTimeout(t *testing.T, command string, workDir string
// createTempWorkspace creates a temporary directory for testing.
func createTempWorkspace(t *testing.T) string {
t.Helper()
dir, err := os.MkdirTemp("", "fence-test-*")
dir, err := os.MkdirTemp("", "greywall-test-*")
if err != nil {
t.Fatalf("failed to create temp workspace: %v", err)
}
@@ -492,10 +492,10 @@ func TestIntegration_EnvWorks(t *testing.T) {
workspace := createTempWorkspace(t)
cfg := testConfigWithWorkspace(workspace)
result := runUnderSandbox(t, cfg, "env | grep FENCE", workspace)
result := runUnderSandbox(t, cfg, "env | grep GREYWALL", workspace)
assertAllowed(t, result)
assertContains(t, result.Stdout, "FENCE_SANDBOX=1")
assertContains(t, result.Stdout, "GREYWALL_SANDBOX=1")
}
func TestExecuteShellCommandBwrapError(t *testing.T) {

View File

@@ -15,7 +15,7 @@ import (
"syscall"
"time"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// ProxyBridge bridges sandbox to an external SOCKS5 proxy via Unix socket.
@@ -53,7 +53,7 @@ func NewDnsBridge(dnsAddr string, debug bool) (*DnsBridge, error) {
socketID := hex.EncodeToString(id)
tmpDir := os.TempDir()
socketPath := filepath.Join(tmpDir, fmt.Sprintf("fence-dns-%s.sock", socketID))
socketPath := filepath.Join(tmpDir, fmt.Sprintf("greywall-dns-%s.sock", socketID))
bridge := &DnsBridge{
SocketPath: socketPath,
@@ -68,7 +68,7 @@ func NewDnsBridge(dnsAddr string, debug bool) (*DnsBridge, error) {
}
bridge.process = exec.Command("socat", socatArgs...) //nolint:gosec // args constructed from trusted input
if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Starting DNS bridge: socat %s\n", strings.Join(socatArgs, " "))
fmt.Fprintf(os.Stderr, "[greywall:linux] Starting DNS bridge: socat %s\n", strings.Join(socatArgs, " "))
}
if err := bridge.process.Start(); err != nil {
return nil, fmt.Errorf("failed to start DNS bridge: %w", err)
@@ -78,7 +78,7 @@ func NewDnsBridge(dnsAddr string, debug bool) (*DnsBridge, error) {
for range 50 {
if fileExists(socketPath) {
if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] DNS bridge ready (%s -> %s)\n", socketPath, dnsAddr)
fmt.Fprintf(os.Stderr, "[greywall:linux] DNS bridge ready (%s -> %s)\n", socketPath, dnsAddr)
}
return bridge, nil
}
@@ -98,7 +98,7 @@ func (b *DnsBridge) Cleanup() {
_ = os.Remove(b.SocketPath)
if b.debug {
fmt.Fprintf(os.Stderr, "[fence:linux] DNS bridge cleaned up\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] DNS bridge cleaned up\n")
}
}
@@ -143,7 +143,7 @@ func NewProxyBridge(proxyURL string, debug bool) (*ProxyBridge, error) {
socketID := hex.EncodeToString(id)
tmpDir := os.TempDir()
socketPath := filepath.Join(tmpDir, fmt.Sprintf("fence-proxy-%s.sock", socketID))
socketPath := filepath.Join(tmpDir, fmt.Sprintf("greywall-proxy-%s.sock", socketID))
bridge := &ProxyBridge{
SocketPath: socketPath,
@@ -166,7 +166,7 @@ func NewProxyBridge(proxyURL string, debug bool) (*ProxyBridge, error) {
}
bridge.process = exec.Command("socat", socatArgs...) //nolint:gosec // args constructed from trusted input
if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Starting proxy bridge: socat %s\n", strings.Join(socatArgs, " "))
fmt.Fprintf(os.Stderr, "[greywall:linux] Starting proxy bridge: socat %s\n", strings.Join(socatArgs, " "))
}
if err := bridge.process.Start(); err != nil {
return nil, fmt.Errorf("failed to start proxy bridge: %w", err)
@@ -176,7 +176,7 @@ func NewProxyBridge(proxyURL string, debug bool) (*ProxyBridge, error) {
for range 50 {
if fileExists(socketPath) {
if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Proxy bridge ready (%s)\n", socketPath)
fmt.Fprintf(os.Stderr, "[greywall:linux] Proxy bridge ready (%s)\n", socketPath)
}
return bridge, nil
}
@@ -196,7 +196,7 @@ func (b *ProxyBridge) Cleanup() {
_ = os.Remove(b.SocketPath)
if b.debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Proxy bridge cleaned up\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] Proxy bridge cleaned up\n")
}
}
@@ -239,7 +239,7 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
}
for _, port := range ports {
socketPath := filepath.Join(tmpDir, fmt.Sprintf("fence-rev-%d-%s.sock", port, socketID))
socketPath := filepath.Join(tmpDir, fmt.Sprintf("greywall-rev-%d-%s.sock", port, socketID))
bridge.SocketPaths = append(bridge.SocketPaths, socketPath)
// Start reverse bridge: TCP listen on host port -> Unix socket
@@ -251,7 +251,7 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
}
proc := exec.Command("socat", args...) //nolint:gosec // args constructed from trusted input
if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Starting reverse bridge for port %d: socat %s\n", port, strings.Join(args, " "))
fmt.Fprintf(os.Stderr, "[greywall:linux] Starting reverse bridge for port %d: socat %s\n", port, strings.Join(args, " "))
}
if err := proc.Start(); err != nil {
bridge.Cleanup()
@@ -261,7 +261,7 @@ func NewReverseBridge(ports []int, debug bool) (*ReverseBridge, error) {
}
if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Reverse bridges ready for ports: %v\n", ports)
fmt.Fprintf(os.Stderr, "[greywall:linux] Reverse bridges ready for ports: %v\n", ports)
}
return bridge, nil
@@ -282,7 +282,7 @@ func (b *ReverseBridge) Cleanup() {
}
if b.debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Reverse bridges cleaned up\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] Reverse bridges cleaned up\n")
}
}
@@ -412,7 +412,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
features := DetectLinuxFeatures()
if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Available features: %s\n", features.Summary())
fmt.Fprintf(os.Stderr, "[greywall:linux] Available features: %s\n", features.Summary())
}
// Build bwrap args with filesystem restrictions
@@ -427,7 +427,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
if features.CanUnshareNet {
bwrapArgs = append(bwrapArgs, "--unshare-net") // Network namespace isolation
} else if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping --unshare-net (network namespace unavailable in this environment)\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] Skipping --unshare-net (network namespace unavailable in this environment)\n")
}
bwrapArgs = append(bwrapArgs, "--unshare-pid") // PID namespace isolation
@@ -439,12 +439,12 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
filterPath, err := filter.GenerateBPFFilter()
if err != nil {
if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Seccomp filter generation failed: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:linux] Seccomp filter generation failed: %v\n", err)
}
} else {
seccompFilterPath = filterPath
if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Seccomp filter enabled (blocking %d dangerous syscalls)\n", len(DangerousSyscalls))
fmt.Fprintf(os.Stderr, "[greywall:linux] Seccomp filter enabled (blocking %d dangerous syscalls)\n", len(DangerousSyscalls))
}
// Add seccomp filter via fd 3 (will be set up via shell redirection)
bwrapArgs = append(bwrapArgs, "--seccomp", "3")
@@ -457,7 +457,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
// In defaultDenyRead mode, we only bind essential system paths read-only
// and user-specified allowRead paths. Everything else is inaccessible.
if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] DefaultDenyRead mode enabled - binding only essential system paths\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] DefaultDenyRead mode enabled - binding only essential system paths\n")
}
// Bind essential system paths read-only
@@ -555,7 +555,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
bwrapArgs = append(bwrapArgs, "--ro-bind", target, target)
}
if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Resolved /etc/resolv.conf symlink -> %s (cross-mount)\n", target)
fmt.Fprintf(os.Stderr, "[greywall:linux] Resolved /etc/resolv.conf symlink -> %s (cross-mount)\n", target)
}
}
}
@@ -683,7 +683,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
bwrapArgs = append(bwrapArgs, "--cap-add", "CAP_NET_ADMIN")
bwrapArgs = append(bwrapArgs, "--cap-add", "CAP_NET_BIND_SERVICE")
// Bind the tun2socks binary into the sandbox (read-only)
bwrapArgs = append(bwrapArgs, "--ro-bind", tun2socksPath, "/tmp/fence-tun2socks")
bwrapArgs = append(bwrapArgs, "--ro-bind", tun2socksPath, "/tmp/greywall-tun2socks")
}
// Bind DNS bridge socket if available
@@ -697,7 +697,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
// Inside the sandbox, a socat relay on UDP :53 converts queries to the
// DNS bridge (Unix socket -> host DNS server) or to TCP through the tunnel.
if dnsBridge != nil || (tun2socksPath != "" && features.CanUseTransparentProxy()) {
tmpResolv, err := os.CreateTemp("", "fence-resolv-*.conf")
tmpResolv, err := os.CreateTemp("", "greywall-resolv-*.conf")
if err == nil {
_, _ = tmpResolv.WriteString("nameserver 127.0.0.1\n")
tmpResolv.Close()
@@ -705,9 +705,9 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
bwrapArgs = append(bwrapArgs, "--ro-bind", dnsRelayResolvConf, "/etc/resolv.conf")
if opts.Debug {
if dnsBridge != nil {
fmt.Fprintf(os.Stderr, "[fence:linux] DNS: overriding resolv.conf -> 127.0.0.1 (bridge to %s)\n", dnsBridge.DnsAddr)
fmt.Fprintf(os.Stderr, "[greywall:linux] DNS: overriding resolv.conf -> 127.0.0.1 (bridge to %s)\n", dnsBridge.DnsAddr)
} else {
fmt.Fprintf(os.Stderr, "[fence:linux] DNS: overriding resolv.conf -> 127.0.0.1 (TCP relay through tunnel)\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] DNS: overriding resolv.conf -> 127.0.0.1 (TCP relay through tunnel)\n")
}
}
}
@@ -721,21 +721,21 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
bwrapArgs = append(bwrapArgs, "--bind", tmpDir, tmpDir)
}
// Get fence executable path for Landlock wrapper
fenceExePath, _ := os.Executable()
// Get greywall executable path for Landlock wrapper
greywallExePath, _ := os.Executable()
// Skip Landlock wrapper if executable is in /tmp (test binaries are built there)
// The wrapper won't work because --tmpfs /tmp hides the test binary
executableInTmp := strings.HasPrefix(fenceExePath, "/tmp/")
// Skip Landlock wrapper if fence is being used as a library (executable is not fence)
// The wrapper re-executes the binary with --landlock-apply, which only fence understands
executableIsFence := strings.Contains(filepath.Base(fenceExePath), "fence")
useLandlockWrapper := opts.UseLandlock && features.CanUseLandlock() && fenceExePath != "" && !executableInTmp && executableIsFence
executableInTmp := strings.HasPrefix(greywallExePath, "/tmp/")
// Skip Landlock wrapper if greywall is being used as a library (executable is not greywall)
// The wrapper re-executes the binary with --landlock-apply, which only greywall understands
executableIsGreywall := strings.Contains(filepath.Base(greywallExePath), "greywall")
useLandlockWrapper := opts.UseLandlock && features.CanUseLandlock() && greywallExePath != "" && !executableInTmp && executableIsGreywall
if opts.Debug && executableInTmp {
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping Landlock wrapper (executable in /tmp, likely a test)\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] Skipping Landlock wrapper (executable in /tmp, likely a test)\n")
}
if opts.Debug && !executableIsFence {
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping Landlock wrapper (running as library, not fence CLI)\n")
if opts.Debug && !executableIsGreywall {
fmt.Fprintf(os.Stderr, "[greywall:linux] Skipping Landlock wrapper (running as library, not greywall CLI)\n")
}
bwrapArgs = append(bwrapArgs, "--", shellPath, "-c")
@@ -743,7 +743,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
// Build the inner command that sets up tun2socks and runs the user command
var innerScript strings.Builder
innerScript.WriteString("export FENCE_SANDBOX=1\n")
innerScript.WriteString("export GREYWALL_SANDBOX=1\n")
if proxyBridge != nil && tun2socksPath != "" && features.CanUseTransparentProxy() {
// Build the tun2socks proxy URL with credentials if available
@@ -773,7 +773,7 @@ socat TCP-LISTEN:${PROXY_PORT},fork,reuseaddr,bind=127.0.0.1 UNIX-CONNECT:%s >/d
BRIDGE_PID=$!
# Start tun2socks (transparent proxy via gvisor netstack)
/tmp/fence-tun2socks -device tun0 -proxy %s >/dev/null 2>&1 &
/tmp/greywall-tun2socks -device tun0 -proxy %s >/dev/null 2>&1 &
TUN2SOCKS_PID=$!
`, proxyBridge.SocketPath, tun2socksProxyURL))
@@ -853,13 +853,13 @@ sleep 0.3
if cfg != nil {
configJSON, err := json.Marshal(cfg)
if err == nil {
innerScript.WriteString(fmt.Sprintf("export FENCE_CONFIG_JSON=%s\n", ShellQuoteSingle(string(configJSON))))
innerScript.WriteString(fmt.Sprintf("export GREYWALL_CONFIG_JSON=%s\n", ShellQuoteSingle(string(configJSON))))
}
}
// Build wrapper command with proper quoting
// Use bash -c to preserve shell semantics (e.g., "echo hi && ls")
wrapperArgs := []string{fenceExePath, "--landlock-apply"}
wrapperArgs := []string{greywallExePath, "--landlock-apply"}
if opts.Debug {
wrapperArgs = append(wrapperArgs, "--debug")
}
@@ -897,7 +897,7 @@ sleep 0.3
if reverseBridge != nil && len(reverseBridge.Ports) > 0 {
featureList = append(featureList, fmt.Sprintf("inbound:%v", reverseBridge.Ports))
}
fmt.Fprintf(os.Stderr, "[fence:linux] Sandbox: %s\n", strings.Join(featureList, ", "))
fmt.Fprintf(os.Stderr, "[greywall:linux] Sandbox: %s\n", strings.Join(featureList, ", "))
}
// Build the final command
@@ -926,7 +926,7 @@ func StartLinuxMonitor(pid int, opts LinuxSandboxOptions) (*LinuxMonitors, error
// or SECCOMP_RET_KILL (logs but kills process) or SECCOMP_RET_USER_NOTIF (complex).
// For now, we rely on the eBPF monitor to detect syscall failures.
if opts.Debug && opts.Monitor && features.SeccompLogLevel >= 1 {
fmt.Fprintf(os.Stderr, "[fence:linux] Note: seccomp violations are blocked but not logged (SECCOMP_RET_ERRNO is silent)\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] Note: seccomp violations are blocked but not logged (SECCOMP_RET_ERRNO is silent)\n")
}
// Start eBPF monitor if available and requested
@@ -935,17 +935,17 @@ func StartLinuxMonitor(pid int, opts LinuxSandboxOptions) (*LinuxMonitors, error
ebpfMon := NewEBPFMonitor(pid, opts.Debug)
if err := ebpfMon.Start(); err != nil {
if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Failed to start eBPF monitor: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:linux] Failed to start eBPF monitor: %v\n", err)
}
} else {
monitors.EBPFMonitor = ebpfMon
if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] eBPF monitor started for PID %d\n", pid)
fmt.Fprintf(os.Stderr, "[greywall:linux] eBPF monitor started for PID %d\n", pid)
}
}
} else if opts.Monitor && opts.Debug {
if !features.HasEBPF {
fmt.Fprintf(os.Stderr, "[fence:linux] eBPF monitoring not available (need CAP_BPF or root)\n")
fmt.Fprintf(os.Stderr, "[greywall:linux] eBPF monitoring not available (need CAP_BPF or root)\n")
}
}

View File

@@ -39,7 +39,7 @@ func (m *EBPFMonitor) Start() error {
features := DetectLinuxFeatures()
if !features.HasEBPF {
if m.debug {
fmt.Fprintf(os.Stderr, "[fence:ebpf] eBPF monitoring not available (need CAP_BPF or root)\n")
fmt.Fprintf(os.Stderr, "[greywall:ebpf] eBPF monitoring not available (need CAP_BPF or root)\n")
}
return nil
}
@@ -51,14 +51,14 @@ func (m *EBPFMonitor) Start() error {
// Try multiple eBPF tracing approaches
if err := m.tryBpftrace(ctx); err != nil {
if m.debug {
fmt.Fprintf(os.Stderr, "[fence:ebpf] bpftrace not available: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:ebpf] bpftrace not available: %v\n", err)
}
// Fall back to other methods
go m.traceWithPerfEvents()
}
if m.debug {
fmt.Fprintf(os.Stderr, "[fence:ebpf] Started eBPF monitoring for PID %d\n", m.pid)
fmt.Fprintf(os.Stderr, "[greywall:ebpf] Started eBPF monitoring for PID %d\n", m.pid)
}
return nil
@@ -101,7 +101,7 @@ func (m *EBPFMonitor) tryBpftrace(ctx context.Context) error {
script := m.generateBpftraceScript()
// Write script to temp file
tmpFile, err := os.CreateTemp("", "fence-ebpf-*.bt")
tmpFile, err := os.CreateTemp("", "greywall-ebpf-*.bt")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
@@ -136,7 +136,7 @@ func (m *EBPFMonitor) tryBpftrace(ctx context.Context) error {
for scanner.Scan() {
line := scanner.Text()
if m.debug {
fmt.Fprintf(os.Stderr, "[fence:ebpf:trace] %s\n", line)
fmt.Fprintf(os.Stderr, "[greywall:ebpf:trace] %s\n", line)
}
if violation := m.parseBpftraceOutput(line); violation != "" {
fmt.Fprintf(os.Stderr, "%s\n", violation)
@@ -150,7 +150,7 @@ func (m *EBPFMonitor) tryBpftrace(ctx context.Context) error {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
line := scanner.Text()
fmt.Fprintf(os.Stderr, "[fence:ebpf:err] %s\n", line)
fmt.Fprintf(os.Stderr, "[greywall:ebpf:err] %s\n", line)
}
}()
}
@@ -173,7 +173,7 @@ func (m *EBPFMonitor) generateBpftraceScript() string {
script := fmt.Sprintf(`
BEGIN
{
printf("fence:ebpf monitoring started for sandbox PID %%d (filtering pid >= %%d)\n", %d, %d);
printf("greywall:ebpf monitoring started for sandbox PID %%d (filtering pid >= %%d)\n", %d, %d);
}
// Monitor filesystem errors (EPERM=-1, EACCES=-13, EROFS=-30)
@@ -227,7 +227,7 @@ func (m *EBPFMonitor) parseBpftraceOutput(line string) string {
errorName := getErrnoName(ret)
timestamp := time.Now().Format("15:04:05")
return fmt.Sprintf("[fence:ebpf] %s ✗ %s: %s (%s, pid=%d)",
return fmt.Sprintf("[greywall:ebpf] %s ✗ %s: %s (%s, pid=%d)",
timestamp, syscall, errorName, comm, pid)
}
@@ -239,7 +239,7 @@ func (m *EBPFMonitor) traceWithPerfEvents() {
tracePipe := "/sys/kernel/debug/tracing/trace_pipe"
if _, err := os.Stat(tracePipe); err != nil {
if m.debug {
fmt.Fprintf(os.Stderr, "[fence:ebpf] trace_pipe not available\n")
fmt.Fprintf(os.Stderr, "[greywall:ebpf] trace_pipe not available\n")
}
return
}
@@ -247,7 +247,7 @@ func (m *EBPFMonitor) traceWithPerfEvents() {
f, err := os.Open(tracePipe)
if err != nil {
if m.debug {
fmt.Fprintf(os.Stderr, "[fence:ebpf] Failed to open trace_pipe: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:ebpf] Failed to open trace_pipe: %v\n", err)
}
return
}
@@ -317,10 +317,10 @@ func (v *ViolationEvent) FormatViolation() string {
errName := getErrnoName(-v.Errno)
if v.Path != "" {
return fmt.Sprintf("[fence:ebpf] %s ✗ %s: %s (%s, %s:%d)",
return fmt.Sprintf("[greywall:ebpf] %s ✗ %s: %s (%s, %s:%d)",
timestamp, v.Operation, v.Path, errName, v.Comm, v.PID)
}
return fmt.Sprintf("[fence:ebpf] %s ✗ %s: %s (%s:%d)",
return fmt.Sprintf("[greywall:ebpf] %s ✗ %s: %s (%s:%d)",
timestamp, v.Operation, errName, v.Comm, v.PID)
}

View File

@@ -10,7 +10,7 @@ import (
"strings"
"unsafe"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
"github.com/bmatcuk/doublestar/v4"
"golang.org/x/sys/unix"
)
@@ -22,7 +22,7 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
features := DetectLinuxFeatures()
if !features.CanUseLandlock() {
if debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Not available (kernel %d.%d < 5.13), skipping\n",
fmt.Fprintf(os.Stderr, "[greywall:landlock] Not available (kernel %d.%d < 5.13), skipping\n",
features.KernelMajor, features.KernelMinor)
}
return nil // Graceful fallback - Landlock not available
@@ -31,7 +31,7 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
ruleset, err := NewLandlockRuleset(debug)
if err != nil {
if debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Failed to create ruleset: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Failed to create ruleset: %v\n", err)
}
return nil // Graceful fallback
}
@@ -39,7 +39,7 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
if err := ruleset.Initialize(); err != nil {
if debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Failed to initialize: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Failed to initialize: %v\n", err)
}
return nil // Graceful fallback
}
@@ -66,7 +66,7 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
if err := ruleset.AllowRead(p); err != nil && debug {
// Ignore errors for paths that don't exist
if !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add read path %s: %v\n", p, err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add read path %s: %v\n", p, err)
}
}
}
@@ -77,40 +77,40 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
if target, err := filepath.EvalSymlinks("/etc/resolv.conf"); err == nil && target != "/etc/resolv.conf" {
targetDir := filepath.Dir(target)
if err := ruleset.AllowRead(targetDir); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add resolv.conf target dir %s: %v\n", targetDir, err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add resolv.conf target dir %s: %v\n", targetDir, err)
}
}
// Current working directory - read access (may be upgraded to write below)
if cwd != "" {
if err := ruleset.AllowRead(cwd); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add cwd read path: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add cwd read path: %v\n", err)
}
}
// Home directory - read access
if home, err := os.UserHomeDir(); err == nil {
if err := ruleset.AllowRead(home); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add home read path: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add home read path: %v\n", err)
}
}
// /tmp - allow read+write (many programs need this)
if err := ruleset.AllowReadWrite("/tmp"); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add /tmp write path: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add /tmp write path: %v\n", err)
}
// /dev needs read+write for /dev/null, /dev/zero, /dev/tty, etc.
// Landlock doesn't support rules on device files directly, so we allow the whole /dev
if err := ruleset.AllowReadWrite("/dev"); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add /dev write path: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add /dev write path: %v\n", err)
}
// Socket paths for proxy communication
for _, p := range socketPaths {
dir := filepath.Dir(p)
if err := ruleset.AllowReadWrite(dir); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add socket path %s: %v\n", dir, err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add socket path %s: %v\n", dir, err)
}
}
@@ -119,7 +119,7 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
expandedPaths := ExpandGlobPatterns(cfg.Filesystem.AllowWrite)
for _, p := range expandedPaths {
if err := ruleset.AllowReadWrite(p); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add write path %s: %v\n", p, err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add write path %s: %v\n", p, err)
}
}
// Also add non-glob paths directly
@@ -127,7 +127,7 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
if !ContainsGlobChars(p) {
normalized := NormalizePath(p)
if err := ruleset.AllowReadWrite(normalized); err != nil && debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Warning: failed to add write path %s: %v\n", normalized, err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Warning: failed to add write path %s: %v\n", normalized, err)
}
}
}
@@ -136,13 +136,13 @@ func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []strin
// Apply the ruleset
if err := ruleset.Apply(); err != nil {
if debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Failed to apply: %v\n", err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Failed to apply: %v\n", err)
}
return nil // Graceful fallback
}
if debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Applied restrictions (ABI v%d)\n", features.LandlockABI)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Applied restrictions (ABI v%d)\n", features.LandlockABI)
}
return nil
@@ -212,7 +212,7 @@ func (l *LandlockRuleset) Initialize() error {
l.initialized = true
if l.debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Created ruleset (ABI v%d, fd=%d)\n", l.abiVersion, l.rulesetFd)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Created ruleset (ABI v%d, fd=%d)\n", l.abiVersion, l.rulesetFd)
}
return nil
@@ -318,7 +318,7 @@ func (l *LandlockRuleset) addPathRule(path string, access uint64) error {
// Check if path exists
if _, err := os.Stat(absPath); os.IsNotExist(err) {
if l.debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Skipping non-existent path: %s\n", absPath)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Skipping non-existent path: %s\n", absPath)
}
return nil
}
@@ -327,7 +327,7 @@ func (l *LandlockRuleset) addPathRule(path string, access uint64) error {
fd, err := unix.Open(absPath, unix.O_PATH|unix.O_CLOEXEC, 0)
if err != nil {
if l.debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Failed to open path %s: %v\n", absPath, err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Failed to open path %s: %v\n", absPath, err)
}
return nil // Don't fail on paths we can't access
}
@@ -337,7 +337,7 @@ func (l *LandlockRuleset) addPathRule(path string, access uint64) error {
var stat unix.Stat_t
if err := unix.Fstat(fd, &stat); err != nil {
if l.debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Failed to fstat path %s: %v\n", absPath, err)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Failed to fstat path %s: %v\n", absPath, err)
}
return nil
}
@@ -370,7 +370,7 @@ func (l *LandlockRuleset) addPathRule(path string, access uint64) error {
if access == 0 {
if l.debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Skipping %s: no applicable access rights\n", absPath)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Skipping %s: no applicable access rights\n", absPath)
}
return nil
}
@@ -391,7 +391,7 @@ func (l *LandlockRuleset) addPathRule(path string, access uint64) error {
}
if l.debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Added rule: %s (access=0x%x)\n", absPath, access)
fmt.Fprintf(os.Stderr, "[greywall:landlock] Added rule: %s (access=0x%x)\n", absPath, access)
}
return nil
@@ -420,7 +420,7 @@ func (l *LandlockRuleset) Apply() error {
}
if l.debug {
fmt.Fprintf(os.Stderr, "[fence:landlock] Ruleset applied to process\n")
fmt.Fprintf(os.Stderr, "[greywall:landlock] Ruleset applied to process\n")
}
return nil

View File

@@ -2,7 +2,7 @@
package sandbox
import "github.com/Use-Tusk/fence/internal/config"
import "gitea.app.monadical.io/monadical/greywall/internal/config"
// ApplyLandlockFromConfig is a no-op on non-Linux platforms.
func ApplyLandlockFromConfig(cfg *config.Config, cwd string, socketPaths []string, debug bool) error {

View File

@@ -60,12 +60,12 @@ func (s *SeccompFilter) GenerateBPFFilter() (string, error) {
}
// Create a temporary directory for the filter
tmpDir := filepath.Join(os.TempDir(), "fence-seccomp")
tmpDir := filepath.Join(os.TempDir(), "greywall-seccomp")
if err := os.MkdirAll(tmpDir, 0o700); err != nil {
return "", fmt.Errorf("failed to create seccomp dir: %w", err)
}
filterPath := filepath.Join(tmpDir, fmt.Sprintf("fence-seccomp-%d.bpf", os.Getpid()))
filterPath := filepath.Join(tmpDir, fmt.Sprintf("greywall-seccomp-%d.bpf", os.Getpid()))
// Generate the filter using the seccomp library or raw BPF
// For now, we'll use bwrap's built-in seccomp support via --seccomp
@@ -77,7 +77,7 @@ func (s *SeccompFilter) GenerateBPFFilter() (string, error) {
}
if s.debug {
fmt.Fprintf(os.Stderr, "[fence:seccomp] Generated BPF filter at %s\n", filterPath)
fmt.Fprintf(os.Stderr, "[greywall:seccomp] Generated BPF filter at %s\n", filterPath)
}
return filterPath, nil

View File

@@ -5,7 +5,7 @@ package sandbox
import (
"fmt"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// ProxyBridge is a stub for non-Linux platforms.

View File

@@ -3,7 +3,7 @@ package sandbox
import (
"testing"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// TestLinux_NoProxyBlocksNetwork verifies that when no ProxyURL is set,

View File

@@ -11,7 +11,7 @@ import (
"regexp"
"strings"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// sessionSuffix is a unique identifier for this process session.
@@ -609,10 +609,10 @@ func WrapCommandMacOS(cfg *config.Config, command string, exposedPorts []int, de
}
if debug && len(exposedPorts) > 0 {
fmt.Fprintf(os.Stderr, "[fence:macos] Enabling local binding for exposed ports: %v\n", exposedPorts)
fmt.Fprintf(os.Stderr, "[greywall:macos] Enabling local binding for exposed ports: %v\n", exposedPorts)
}
if debug && allowLocalBinding && !allowLocalOutbound {
fmt.Fprintf(os.Stderr, "[fence:macos] Blocking localhost outbound (AllowLocalOutbound=false)\n")
fmt.Fprintf(os.Stderr, "[greywall:macos] Blocking localhost outbound (AllowLocalOutbound=false)\n")
}
profile := GenerateSandboxProfile(params)

View File

@@ -4,7 +4,7 @@ import (
"strings"
"testing"
"github.com/Use-Tusk/fence/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// TestMacOS_NetworkRestrictionWithProxy verifies that when a proxy URL is set,
@@ -274,14 +274,14 @@ func TestExpandMacOSTmpPaths(t *testing.T) {
want: []string{".", "~/.cache"},
},
{
name: "mirrors /tmp/fence to /private/tmp/fence",
input: []string{".", "/tmp/fence"},
want: []string{".", "/tmp/fence", "/private/tmp/fence"},
name: "mirrors /tmp/greywall to /private/tmp/greywall",
input: []string{".", "/tmp/greywall"},
want: []string{".", "/tmp/greywall", "/private/tmp/greywall"},
},
{
name: "mirrors /private/tmp/fence to /tmp/fence",
input: []string{".", "/private/tmp/fence"},
want: []string{".", "/private/tmp/fence", "/tmp/fence"},
name: "mirrors /private/tmp/greywall to /tmp/greywall",
input: []string{".", "/private/tmp/greywall"},
want: []string{".", "/private/tmp/greywall", "/tmp/greywall"},
},
{
name: "mirrors nested subdirectory",
@@ -290,8 +290,8 @@ func TestExpandMacOSTmpPaths(t *testing.T) {
},
{
name: "no duplicate when mirror already present",
input: []string{".", "/tmp/fence", "/private/tmp/fence"},
want: []string{".", "/tmp/fence", "/private/tmp/fence"},
input: []string{".", "/tmp/greywall", "/private/tmp/greywall"},
want: []string{".", "/tmp/greywall", "/private/tmp/greywall"},
},
}

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"os"
"github.com/Use-Tusk/fence/internal/config"
"github.com/Use-Tusk/fence/internal/platform"
"gitea.app.monadical.io/monadical/greywall/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/platform"
)
// Manager handles sandbox initialization and command wrapping.
@@ -153,6 +153,6 @@ func (m *Manager) Cleanup() {
func (m *Manager) logDebug(format string, args ...interface{}) {
if m.debug {
fmt.Fprintf(os.Stderr, "[fence] "+format+"\n", args...)
fmt.Fprintf(os.Stderr, "[greywall] "+format+"\n", args...)
}
}

View File

@@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/Use-Tusk/fence/internal/platform"
"gitea.app.monadical.io/monadical/greywall/internal/platform"
)
// LogMonitor monitors sandbox violations via macOS log stream.
@@ -138,9 +138,9 @@ func parseViolation(line string) string {
timestamp := time.Now().Format("15:04:05")
if details != "" {
return fmt.Sprintf("[fence:logstream] %s ✗ %s %s (%s:%s)", timestamp, operation, details, process, pid)
return fmt.Sprintf("[greywall:logstream] %s ✗ %s %s (%s:%s)", timestamp, operation, details, process, pid)
}
return fmt.Sprintf("[fence:logstream] %s ✗ %s (%s:%s)", timestamp, operation, process, pid)
return fmt.Sprintf("[greywall:logstream] %s ✗ %s (%s:%s)", timestamp, operation, process, pid)
}
// shouldShowViolation returns true if this violation type should be displayed.

View File

@@ -32,7 +32,7 @@ func extractTun2Socks() (string, error) {
return "", fmt.Errorf("tun2socks: embedded binary not found for %s: %w", arch, err)
}
tmpFile, err := os.CreateTemp("", "fence-tun2socks-*")
tmpFile, err := os.CreateTemp("", "greywall-tun2socks-*")
if err != nil {
return "", fmt.Errorf("tun2socks: failed to create temp file: %w", err)
}

View File

@@ -51,8 +51,8 @@ func NormalizePath(pathPattern string) string {
// Used on macOS where transparent proxying is not available.
func GenerateProxyEnvVars(proxyURL string) []string {
envVars := []string{
"FENCE_SANDBOX=1",
"TMPDIR=/tmp/fence",
"GREYWALL_SANDBOX=1",
"TMPDIR=/tmp/greywall",
}
if proxyURL == "" {

View File

@@ -134,8 +134,8 @@ func TestGenerateProxyEnvVars(t *testing.T) {
name: "no proxy",
proxyURL: "",
wantEnvs: []string{
"FENCE_SANDBOX=1",
"TMPDIR=/tmp/fence",
"GREYWALL_SANDBOX=1",
"TMPDIR=/tmp/greywall",
},
dontWant: []string{
"HTTP_PROXY=",
@@ -147,7 +147,7 @@ func TestGenerateProxyEnvVars(t *testing.T) {
name: "socks5 proxy",
proxyURL: "socks5://localhost:1080",
wantEnvs: []string{
"FENCE_SANDBOX=1",
"GREYWALL_SANDBOX=1",
"ALL_PROXY=socks5://localhost:1080",
"all_proxy=socks5://localhost:1080",
"HTTP_PROXY=socks5://localhost:1080",
@@ -162,7 +162,7 @@ func TestGenerateProxyEnvVars(t *testing.T) {
name: "socks5h proxy",
proxyURL: "socks5h://proxy.example.com:1080",
wantEnvs: []string{
"FENCE_SANDBOX=1",
"GREYWALL_SANDBOX=1",
"ALL_PROXY=socks5h://proxy.example.com:1080",
"HTTP_PROXY=socks5h://proxy.example.com:1080",
},

View File

@@ -1,10 +1,10 @@
// Package fence provides a public API for sandboxing commands.
package fence
// Package greywall provides a public API for sandboxing commands.
package greywall
import (
"github.com/Use-Tusk/fence/internal/config"
"github.com/Use-Tusk/fence/internal/platform"
"github.com/Use-Tusk/fence/internal/sandbox"
"gitea.app.monadical.io/monadical/greywall/internal/config"
"gitea.app.monadical.io/monadical/greywall/internal/platform"
"gitea.app.monadical.io/monadical/greywall/internal/sandbox"
)
// IsSupported returns true if the current platform supports sandboxing (macOS/Linux).
@@ -12,7 +12,7 @@ func IsSupported() bool {
return platform.IsSupported()
}
// Config is the configuration for fence.
// Config is the configuration for greywall.
type Config = config.Config
// NetworkConfig defines network restrictions.

View File

@@ -10,7 +10,7 @@
# ./scripts/benchmark.sh [options]
#
# Options:
# -b, --binary PATH Path to fence binary (default: ./fence or builds one)
# -b, --binary PATH Path to greywall binary (default: ./greywall or builds one)
# -o, --output DIR Output directory for results (default: ./benchmarks)
# -n, --runs N Minimum runs per benchmark (default: 30)
# -q, --quick Quick mode: fewer runs, skip slow benchmarks
@@ -19,7 +19,7 @@
#
# Requirements:
# - hyperfine (brew install hyperfine / apt install hyperfine)
# - go (for building fence if needed)
# - go (for building greywall if needed)
# - Optional: python3 (for local-server.py network benchmarks)
set -euo pipefail
@@ -32,7 +32,7 @@ BLUE='\033[0;34m'
NC='\033[0m'
# Defaults
FENCE_BIN=""
GREYWALL_BIN=""
OUTPUT_DIR="./benchmarks"
MIN_RUNS=30
WARMUP=3
@@ -43,7 +43,7 @@ NETWORK=false
while [[ $# -gt 0 ]]; do
case $1 in
-b|--binary)
FENCE_BIN="$2"
GREYWALL_BIN="$2"
shift 2
;;
-o|--output)
@@ -75,21 +75,21 @@ while [[ $# -gt 0 ]]; do
esac
done
# Find or build fence binary
if [[ -z "$FENCE_BIN" ]]; then
if [[ -x "./fence" ]]; then
FENCE_BIN="./fence"
elif [[ -x "./dist/fence" ]]; then
FENCE_BIN="./dist/fence"
# Find or build greywall binary
if [[ -z "$GREYWALL_BIN" ]]; then
if [[ -x "./greywall" ]]; then
GREYWALL_BIN="./greywall"
elif [[ -x "./dis./greywall" ]]; then
GREYWALL_BIN="./dis./greywall"
else
echo -e "${BLUE}Building fence...${NC}"
go build -o ./fence ./cmd/fence
FENCE_BIN="./fence"
echo -e "${BLUE}Building greywall...${NC}"
go build -o ./greywall ./cm./greywall
GREYWALL_BIN="./greywall"
fi
fi
if [[ ! -x "$FENCE_BIN" ]]; then
echo -e "${RED}Error: fence binary not found at $FENCE_BIN${NC}"
if [[ ! -x "$GREYWALL_BIN" ]]; then
echo -e "${RED}Error: greywall binary not found at $GREYWALL_BIN${NC}"
exit 1
fi
@@ -109,7 +109,7 @@ WORKSPACE=$(mktemp -d -p .)
trap 'rm -rf "$WORKSPACE"' EXIT
# Create settings file for sandbox
SETTINGS_FILE="$WORKSPACE/fence.json"
SETTINGS_FILE="$WORKSPAC./greywall.json"
cat > "$SETTINGS_FILE" << EOF
{
"filesystem": {
@@ -131,13 +131,13 @@ RESULTS_MD="$OUTPUT_DIR/${OS,,}-${ARCH}-${TIMESTAMP}.md"
echo ""
echo -e "${BLUE}==========================================${NC}"
echo -e "${BLUE}Fence Sandbox Benchmarks${NC}"
echo -e "${BLUE}Greywall Sandbox Benchmarks${NC}"
echo -e "${BLUE}==========================================${NC}"
echo ""
echo "Platform: $OS $ARCH"
echo "Kernel: $KERNEL"
echo "Date: $DATE"
echo "Fence: $FENCE_BIN"
echo "Greywall: $GREYWALL_BIN"
echo "Output: $OUTPUT_DIR"
echo "Min runs: $MIN_RUNS"
echo ""
@@ -169,11 +169,11 @@ echo ""
run_bench "true" \
--command-name "unsandboxed" "true" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -- true"
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -- true"
run_bench "echo" \
--command-name "unsandboxed" "echo hello >/dev/null" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -c 'echo hello' >/dev/null"
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -c 'echo hello' >/dev/null"
# ============================================================================
# Tool compatibility benchmarks
@@ -185,7 +185,7 @@ echo ""
if command -v python3 &> /dev/null; then
run_bench "python" \
--command-name "unsandboxed" "python3 -c 'pass'" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -c \"python3 -c 'pass'\""
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -c \"python3 -c 'pass'\""
else
echo -e "${YELLOW}Skipping python3 (not found)${NC}"
fi
@@ -193,7 +193,7 @@ fi
if command -v node &> /dev/null && [[ "$QUICK" == "false" ]]; then
run_bench "node" \
--command-name "unsandboxed" "node -e ''" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -c \"node -e ''\""
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -c \"node -e ''\""
else
echo -e "${YELLOW}Skipping node (not found or quick mode)${NC}"
fi
@@ -208,7 +208,7 @@ echo ""
if command -v git &> /dev/null && [[ -d .git ]]; then
run_bench "git-status" \
--command-name "unsandboxed" "git status --porcelain >/dev/null" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -- git status --porcelain >/dev/null"
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -- git status --porcelain >/dev/null"
else
echo -e "${YELLOW}Skipping git status (not in a git repo)${NC}"
fi
@@ -216,7 +216,7 @@ fi
if command -v rg &> /dev/null && [[ "$QUICK" == "false" ]]; then
run_bench "ripgrep" \
--command-name "unsandboxed" "rg -n 'package' -S . >/dev/null 2>&1 || true" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -c \"rg -n 'package' -S . >/dev/null 2>&1\" || true"
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -c \"rg -n 'package' -S . >/dev/null 2>&1\" || true"
else
echo -e "${YELLOW}Skipping ripgrep (not found or quick mode)${NC}"
fi
@@ -230,11 +230,11 @@ echo ""
run_bench "file-write" \
--command-name "unsandboxed" "echo 'test' > $WORKSPACE/test.txt" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -c \"echo 'test' > $WORKSPACE/test.txt\""
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -c \"echo 'test' > $WORKSPACE/test.txt\""
run_bench "file-read" \
--command-name "unsandboxed" "cat $WORKSPACE/test.txt >/dev/null" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -c 'cat $WORKSPACE/test.txt' >/dev/null"
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -c 'cat $WORKSPACE/test.txt' >/dev/null"
# ============================================================================
# Monitor mode benchmarks (optional)
@@ -245,8 +245,8 @@ if [[ "$QUICK" == "false" ]]; then
echo ""
run_bench "monitor-true" \
--command-name "sandboxed" "$FENCE_BIN -s $SETTINGS_FILE -- true" \
--command-name "sandboxed+monitor" "$FENCE_BIN -m -s $SETTINGS_FILE -- true"
--command-name "sandboxed" "$GREYWALL_BIN -s $SETTINGS_FILE -- true" \
--command-name "sandboxed+monitor" "$GREYWALL_BIN -m -s $SETTINGS_FILE -- true"
fi
# ============================================================================
@@ -266,7 +266,7 @@ if [[ "$NETWORK" == "true" ]]; then
sleep 1
# Create network settings
NET_SETTINGS="$WORKSPACE/fence-net.json"
NET_SETTINGS="$WORKSPAC./greywall-net.json"
cat > "$NET_SETTINGS" << EOF
{
"network": {
@@ -281,7 +281,7 @@ EOF
if command -v curl &> /dev/null; then
run_bench "network-curl" \
--command-name "unsandboxed" "curl -s http://127.0.0.1:8765/ >/dev/null" \
--command-name "sandboxed" "$FENCE_BIN -s $NET_SETTINGS -c 'curl -s http://127.0.0.1:8765/' >/dev/null"
--command-name "sandboxed" "$GREYWALL_BIN -s $NET_SETTINGS -c 'curl -s http://127.0.0.1:8765/' >/dev/null"
fi
kill $SERVER_PID 2>/dev/null || true
@@ -303,7 +303,7 @@ echo " \"platform\": \"$OS\"," >> "$RESULTS_JSON"
echo " \"arch\": \"$ARCH\"," >> "$RESULTS_JSON"
echo " \"kernel\": \"$KERNEL\"," >> "$RESULTS_JSON"
echo " \"date\": \"$DATE\"," >> "$RESULTS_JSON"
echo " \"fence_version\": \"$($FENCE_BIN --version 2>/dev/null || echo unknown)\"," >> "$RESULTS_JSON"
echo " \"greywall_version\": \"$($GREYWALL_BIN --version 2>/dev/null || echo unknown)\"," >> "$RESULTS_JSON"
echo " \"benchmarks\": {" >> "$RESULTS_JSON"
first=true
@@ -324,12 +324,12 @@ echo "}" >> "$RESULTS_JSON"
# Generate Markdown report
cat > "$RESULTS_MD" << EOF
# Fence Benchmark Results
# Greywall Benchmark Results
**Platform:** $OS $ARCH
**Kernel:** $KERNEL
**Date:** $DATE
**Fence:** $($FENCE_BIN --version 2>/dev/null || echo unknown)
**Greywall:** $($GREYWALL_BIN --version 2>/dev/null || echo unknown)
## Summary

View File

@@ -150,4 +150,4 @@ git push origin "$NEW_VERSION"
echo ""
info "✓ Released $NEW_VERSION"
info "GitHub Actions will now build and publish the release."
info "Watch progress at: https://github.com/Use-Tusk/fence/actions"
info "Watch progress at: https://gitea.app.monadical.io/monadical/greywall/actions"

View File

@@ -1,14 +1,14 @@
#!/bin/bash
# smoke_test.sh - Run smoke tests against the fence binary
# smoke_test.sh - Run smoke tests against the greywall binary
#
# This script tests the compiled fence binary to ensure basic functionality works.
# This script tests the compiled greywall binary to ensure basic functionality works.
# Unlike integration tests (which test internal APIs), smoke tests verify the
# final artifact behaves correctly.
#
# Usage:
# ./scripts/smoke_test.sh [path-to-fence-binary]
# ./scripts/smoke_test.sh [path-to-greywall-binary]
#
# If no path is provided, it will look for ./fence or use 'go run'.
# If no path is provided, it will look for ./greywall or use 'go run'.
set -euo pipefail
@@ -21,25 +21,25 @@ PASSED=0
FAILED=0
SKIPPED=0
FENCE_BIN="${1:-}"
if [[ -z "$FENCE_BIN" ]]; then
if [[ -x "./fence" ]]; then
FENCE_BIN="./fence"
elif [[ -x "./dist/fence" ]]; then
FENCE_BIN="./dist/fence"
GREYWALL_BIN="${1:-}"
if [[ -z "$GREYWALL_BIN" ]]; then
if [[ -x "./greywall" ]]; then
GREYWALL_BIN="./greywall"
elif [[ -x "./dis./greywall" ]]; then
GREYWALL_BIN="./dis./greywall"
else
echo "Building fence..."
go build -o ./fence ./cmd/fence
FENCE_BIN="./fence"
echo "Building greywall..."
go build -o ./greywall ./cm./greywall
GREYWALL_BIN="./greywall"
fi
fi
if [[ ! -x "$FENCE_BIN" ]]; then
echo "Error: fence binary not found at $FENCE_BIN"
if [[ ! -x "$GREYWALL_BIN" ]]; then
echo "Error: greywall binary not found at $GREYWALL_BIN"
exit 1
fi
echo "Using fence binary: $FENCE_BIN"
echo "Using greywall binary: $GREYWALL_BIN"
echo "=============================================="
# Create temp workspace in current directory (not /tmp, which gets overlaid by bwrap --tmpfs)
@@ -100,16 +100,16 @@ echo "=== Basic Functionality ==="
echo ""
# Test: Version flag works
run_test "version flag" "pass" "$FENCE_BIN" --version
run_test "version flag" "pass" "$GREYWALL_BIN" --version
# Test: Echo works
run_test "echo command" "pass" "$FENCE_BIN" -c "echo hello"
run_test "echo command" "pass" "$GREYWALL_BIN" -c "echo hello"
# Test: ls works
run_test "ls command" "pass" "$FENCE_BIN" -- ls
run_test "ls command" "pass" "$GREYWALL_BIN" -- ls
# Test: pwd works
run_test "pwd command" "pass" "$FENCE_BIN" -- pwd
run_test "pwd command" "pass" "$GREYWALL_BIN" -- pwd
echo ""
echo "=== Filesystem Restrictions ==="
@@ -117,11 +117,11 @@ echo ""
# Test: Read existing file works
echo "test content" > "$WORKSPACE/test.txt"
run_test "read file in workspace" "pass" "$FENCE_BIN" -c "cat $WORKSPACE/test.txt"
run_test "read file in workspace" "pass" "$GREYWALL_BIN" -c "cat $WORKSPACE/test.txt"
# Test: Write outside workspace blocked
# Create a settings file that only allows write to current workspace
SETTINGS_FILE="$WORKSPACE/fence.json"
SETTINGS_FILE="$WORKSPAC./greywall.json"
cat > "$SETTINGS_FILE" << EOF
{
"filesystem": {
@@ -131,14 +131,14 @@ cat > "$SETTINGS_FILE" << EOF
EOF
# Note: Use /var/tmp since /tmp is mounted as tmpfs (writable but ephemeral) inside the sandbox
OUTSIDE_FILE="/var/tmp/outside-fence-test-$$.txt"
run_test "write outside workspace blocked" "fail" "$FENCE_BIN" -s "$SETTINGS_FILE" -c "touch $OUTSIDE_FILE"
OUTSIDE_FILE="/var/tmp/outside-greywall-test-$$.txt"
run_test "write outside workspace blocked" "fail" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c "touch $OUTSIDE_FILE"
# Cleanup in case it wasn't blocked
rm -f "$OUTSIDE_FILE" 2>/dev/null || true
# Test: Write inside workspace allowed (using the workspace path in -c)
run_test "write inside workspace allowed" "pass" "$FENCE_BIN" -s "$SETTINGS_FILE" -c "touch $WORKSPACE/new-file.txt"
run_test "write inside workspace allowed" "pass" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c "touch $WORKSPACE/new-file.txt"
# Check file was actually created
if [[ -f "$WORKSPACE/new-file.txt" ]]; then
@@ -166,16 +166,16 @@ cat > "$SETTINGS_FILE" << EOF
EOF
# Test: Denied command is blocked
run_test "blocked command (rm -rf)" "fail" "$FENCE_BIN" -s "$SETTINGS_FILE" -c "rm -rf /tmp/test"
run_test "blocked command (rm -rf)" "fail" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c "rm -rf /tmp/test"
# Test: Similar but not blocked command works (rm without -rf)
run_test "allowed command (echo)" "pass" "$FENCE_BIN" -s "$SETTINGS_FILE" -c "echo safe command"
run_test "allowed command (echo)" "pass" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c "echo safe command"
# Test: Chained command with blocked command
run_test "chained blocked command" "fail" "$FENCE_BIN" -s "$SETTINGS_FILE" -c "ls && rm -rf /tmp/test"
run_test "chained blocked command" "fail" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c "ls && rm -rf /tmp/test"
# Test: Nested shell with blocked command
run_test "nested shell blocked command" "fail" "$FENCE_BIN" -s "$SETTINGS_FILE" -c 'bash -c "rm -rf /tmp/test"'
run_test "nested shell blocked command" "fail" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c 'bash -c "rm -rf /tmp/test"'
echo ""
echo "=== Network Restrictions ==="
@@ -196,7 +196,7 @@ EOF
if command_exists curl; then
# Test: Network blocked by default - curl should fail or return blocked message
# Use curl's own timeout (no need for external timeout command)
output=$("$FENCE_BIN" -s "$SETTINGS_FILE" -c "curl -s --connect-timeout 2 --max-time 3 http://example.com" 2>&1) || true
output=$("$GREYWALL_BIN" -s "$SETTINGS_FILE" -c "curl -s --connect-timeout 2 --max-time 3 http://example.com" 2>&1) || true
if echo "$output" | grep -qi "blocked\|refused\|denied\|timeout\|error"; then
echo -e "Testing: network blocked (curl)... ${GREEN}PASS${NC}"
PASSED=$((PASSED + 1))
@@ -218,8 +218,8 @@ else
skip_test "network blocked (curl)" "curl not installed"
fi
# Test with allowed domain (only if FENCE_TEST_NETWORK is set)
if [[ "${FENCE_TEST_NETWORK:-}" == "1" ]]; then
# Test with allowed domain (only if GREYWALL_TEST_NETWORK is set)
if [[ "${GREYWALL_TEST_NETWORK:-}" == "1" ]]; then
cat > "$SETTINGS_FILE" << EOF
{
"network": {
@@ -231,12 +231,12 @@ if [[ "${FENCE_TEST_NETWORK:-}" == "1" ]]; then
}
EOF
if command_exists curl; then
run_test "allowed domain works" "pass" "$FENCE_BIN" -s "$SETTINGS_FILE" -c "curl -s --connect-timeout 5 --max-time 10 https://httpbin.org/get"
run_test "allowed domain works" "pass" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c "curl -s --connect-timeout 5 --max-time 10 https://httpbin.org/get"
else
skip_test "allowed domain works" "curl not installed"
fi
else
skip_test "allowed domain works" "FENCE_TEST_NETWORK not set"
skip_test "allowed domain works" "GREYWALL_TEST_NETWORK not set"
fi
echo ""
@@ -244,25 +244,25 @@ echo "=== Tool Compatibility ==="
echo ""
if command_exists python3; then
run_test "python3 works" "pass" "$FENCE_BIN" -c "python3 -c 'print(1+1)'"
run_test "python3 works" "pass" "$GREYWALL_BIN" -c "python3 -c 'print(1+1)'"
else
skip_test "python3 works" "python3 not installed"
fi
if command_exists node; then
run_test "node works" "pass" "$FENCE_BIN" -c "node -e 'console.log(1+1)'"
run_test "node works" "pass" "$GREYWALL_BIN" -c "node -e 'console.log(1+1)'"
else
skip_test "node works" "node not installed"
fi
if command_exists git; then
run_test "git version works" "pass" "$FENCE_BIN" -- git --version
run_test "git version works" "pass" "$GREYWALL_BIN" -- git --version
else
skip_test "git version works" "git not installed"
fi
if command_exists rg; then
run_test "ripgrep works" "pass" "$FENCE_BIN" -- rg --version
run_test "ripgrep works" "pass" "$GREYWALL_BIN" -- rg --version
else
skip_test "ripgrep works" "rg not installed"
fi
@@ -271,8 +271,8 @@ echo ""
echo "=== Environment ==="
echo ""
# Test: FENCE_SANDBOX env var is set
run_test "FENCE_SANDBOX set" "pass" "$FENCE_BIN" -c 'test "$FENCE_SANDBOX" = "1"'
# Test: GREYWALL_SANDBOX env var is set
run_test "GREYWALL_SANDBOX set" "pass" "$GREYWALL_BIN" -c 'test "$GREYWALL_SANDBOX" = "1"'
# Test: Proxy env vars are set when network is configured
cat > "$SETTINGS_FILE" << EOF
@@ -286,7 +286,7 @@ cat > "$SETTINGS_FILE" << EOF
}
EOF
run_test "HTTP_PROXY set" "pass" "$FENCE_BIN" -s "$SETTINGS_FILE" -c 'test -n "$HTTP_PROXY"'
run_test "HTTP_PROXY set" "pass" "$GREYWALL_BIN" -s "$SETTINGS_FILE" -c 'test -n "$HTTP_PROXY"'
echo ""
echo "=============================================="