Security
Security architecture and cross-platform isolation model
Stackie uses OS-level sandboxing to isolate every block (service) from your host system. Each platform uses a different kernel mechanism, but the result is the same: blocks cannot read your personal files, cannot interfere with each other, and are automatically terminated when stopped.
Cross-Platform Overview
| Feature | macOS | Linux | Windows |
|---|---|---|---|
| Sandbox engine | Seatbelt | User namespaces | Restricted tokens |
| Filesystem isolation | Profile-based deny | Bind mounts | Deny-only SIDs + ACLs |
| Process containment | Sandbox boundary | PID namespaces | Job Objects |
| Network control | Profile-based | Network namespaces | Not restricted |
| Privilege model | Default-deny SBPL | Capability dropping | Privilege stripping |
| Sandbox requires admin/root | No | No | No |
macOS: Seatbelt
On macOS, Stackie uses Apple’s Seatbelt framework (sandbox_init) to enforce
kernel-level restrictions on each block.
How it works
- Lockbox generates a Seatbelt profile in SBPL (Sandbox Profile Language) entirely in memory. No profile files are written to disk.
- The profile starts with
(deny default), blocking all operations not explicitly allowed. - Allow rules are added based on the block’s
SandboxPolicy: read-only paths, read-write paths, network access mode, and process execution. - The profile is applied via
sandbox_init()before the block process starts. Once applied, the kernel enforces the restrictions for the lifetime of the process.
What is restricted
- Filesystem: Only paths explicitly listed in the sandbox policy are accessible. All other filesystem operations are denied by the kernel.
- Network: Configurable per block. Blocks can be restricted to localhost-only, full network access, or no network at all.
- Process operations: Fork and exec are allowed. Signal handling is allowed for health checks and graceful shutdown.
What is allowed
- Reading system libraries and frameworks required for process execution
- IPC mechanisms needed for inter-block communication
- Paths explicitly granted by the sandbox policy (storage directories, config files)
Linux: User Namespaces
On Linux, Stackie uses a pure Rust implementation of namespace-based sandboxing. No external tools (such as Bubblewrap) are required at runtime.
How it works
- A new process is created with isolated namespaces:
- User namespace (
CLONE_NEWUSER) for UID/GID isolation - Mount namespace (
CLONE_NEWNS) for filesystem isolation - PID namespace (
CLONE_NEWPID) for process isolation - Network namespace (
CLONE_NEWNET) for network isolation
- User namespace (
- Inside the mount namespace,
pivot_rootcreates a new filesystem root. Only paths listed in the sandbox policy are bind-mounted into the namespace. - All Linux capabilities are dropped after setup.
- The current user is mapped to UID 0 inside the namespace, but this root has no privileges outside the namespace.
What is restricted
- Filesystem: The sandbox sees only bind-mounted paths. The host filesystem is invisible.
- Processes: PID namespace isolation means the sandbox cannot see or signal host processes.
- Network: Configurable via network namespace. Can be fully isolated, localhost-only, or shared with the host.
- Capabilities: All capabilities are dropped, preventing privilege escalation.
Requirements
- Linux kernel 3.8+ with user namespace support
- Unprivileged user namespace creation must be enabled. On Ubuntu 23.10+, AppArmor restricts this by default and may require configuration.
Windows: Restricted Tokens
On Windows, Stackie uses CreateRestrictedToken (Win32 advapi32) to spawn block
processes with reduced privileges for Stackie-managed sandboxing.
How it works
Restricted tokens create a dual access check. For a sandboxed process to access a resource, BOTH checks must pass:
-
Normal SID check: The current user’s SID is marked deny-only in the token. Deny-only SIDs can only match deny ACEs, never allow ACEs. This means the user’s personal files (Documents, Downloads, .ssh, AppData) become inaccessible to the sandboxed process.
-
Restricting SID check: The token carries a restricting SID list containing:
- A per-block SID unique to each block name (deterministic hash)
- BUILTIN\Users (allows loading system DLLs and reading shared resources)
- SYSTEM (required for registry and kernel object access)
Access is granted only if a SID in the restricting list matches an allow ACE on the target resource.
Filesystem ACLs
When lockbox grants a path to a sandbox (for example, the PostgreSQL data directory), it sets two ACEs on the path:
- A
BUILTIN\UsersACE (required becauseBUILTIN\Usersis a restricting SID) - A per-block SID ACE (for cross-sandbox isolation)
Both are needed for the dual access check to pass.
Privilege stripping
All privileges are removed from the restricted token except SeChangeNotifyPrivilege,
which is preserved to allow traversing directory trees without needing explicit
read permission on each ancestor directory.
Job Objects
Each sandboxed process is assigned to a Win32 Job Object configured with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. This ensures that when the sandbox is stopped
or the daemon exits, all child processes in the process tree are automatically
terminated. No orphaned processes are left behind.
TEMP/TMP injection
The user’s TEMP directory (e.g., C:\Users\<name>\AppData\Local\Temp) is not
accessible to the restricted token because it lacks a BUILTIN\Users ACE. Lockbox
redirects TEMP and TMP environment variables to a directory inside the block’s
working directory that the restricted token can access.
What is NOT restricted
- Network access: All blocks can bind ports and make outbound connections. This is by design — most development services need to listen on localhost ports. Network isolation is not feasible on Windows without admin-level firewall rules.
- Named pipes and shared memory: Accessible for IPC between blocks.
What is restricted
- User files: Documents, Downloads, .ssh, AppData, and any path with only user SID ACEs. The deny-only user SID ensures these are blocked.
- Cross-sandbox access: Different block names produce different per-block SIDs. One block cannot access paths granted to another.
- Privilege escalation: All dangerous privileges are stripped from the token.
The sandbox Stack YAML Option
Blocks can opt out of sandboxing on a per-platform basis using the sandbox option
in the stack YAML file:
blocks:
- name: stackie.postgres
platforms:
windows:
sandbox: false
When sandbox: false is set, the block runs through the PassthroughSandbox
implementation. This provides the same process lifecycle management (PID tracking,
graceful shutdown, log capture) without any OS-level isolation. The process runs with
the same privileges as the daemon.
Use sandbox: false only when a block genuinely cannot function inside the sandbox
(for example, due to kernel object namespace limitations).
The --no-sandbox CLI flag on stackie up disables sandboxing for all first-party
blocks. Third-party blocks are always sandboxed regardless of this flag.
Design Principles
- Zero disk I/O: All sandbox profiles are generated in memory. No temporary files are created on disk for sandbox configuration.
- User-level sandbox setup: Stackie’s sandbox mechanisms do not require elevated privileges.
- Platform abstraction: Block authors write a single
SandboxPolicy(read-only paths, read-write paths, network mode). Lockbox translates this to the platform-appropriate mechanism automatically. - Default-deny: All sandbox implementations start from a deny-all baseline and add explicit allow rules from the policy.