openrc service isolation for lainOS layer 03
  • Rust 65.8%
  • Shell 34.2%
Find a file
2026-09-04 18:29:01 -07:00
apparmor repo cleanup 2026-08-19 17:54:03 -07:00
etc Remove top-level pidfile= from chrony's init.d -- confirmed via extensive strace investigation as the real, structural root cause of a persistent, reproducible startup failure: supervise-daemon writes its own PID into the shared pidfile as a placeholder before the real chronyd child starts, and chronyd's own internal 'is another instance running' check reads that same path, sees supervise-daemon itself genuinely alive, and refuses to start every single time -- an infinite, self-inflicted failure loop with zero actual crash involved. Neither dnsmasq nor tor (also supervise-daemon-based) declare a top-level pidfile= for this same reason; acpid/syslog-ng only use --pidfile inside their own reload() functions. start_post migrated from a pidfile check to pgrep accordingly. Confirmed fixed across five consecutive clean restart cycles with a genuine, correctly-sandboxed chronyd process alive throughout 2026-09-04 18:29:01 -07:00
lainos-sandbox-wrap configured repo for gentoo release 2026-08-21 09:50:34 -07:00
openrc-isolation-layer-02 fixed runscript, unbound shutdown command 2026-08-19 19:16:17 -07:00
profiles Add chown to shared lainos-base seccomp profile and CAP_CHOWN to dhcpcd's capability set -- confirmed via strace as the real root cause of dhcpcd's control_start crash. CapEff genuinely included CAP_CHOWN (confirmed in the capset() trace) yet chown() still returned EPERM -- this was a seccomp denial mimicking a permission error, not an actual capability gap. Confirmed fixed across three consecutive restart cycles with the full network service chain (dnsmasq/unbound/dnscrypt-proxy/tor) restarting cleanly alongside it 2026-09-04 17:45:51 -07:00
rc-sandbox repo cleanup 2026-08-19 17:54:03 -07:00
architecture.md Update architecture.md 2026-08-20 17:03:18 +02:00
Cargo.lock added v2 include and replace mtime detection with hashing mechanism 2026-08-14 21:09:16 -07:00
Cargo.toml built openrc-isolation rust 2026-08-13 16:37:00 -07:00
LICENSE Initial commit 2026-08-13 04:18:01 +02:00
openrc-isolation-backup.sh repo cleanup 2026-08-19 17:54:03 -07:00
openrc-security-status.sh finished iwd 2026-08-17 15:48:46 -07:00
README.md Ready for iso 2026-08-15 06:34:02 +02:00

openrc-isolation

OpenRC service isolation stack for lainOS Layer 03.

Provides systemd-equivalent service containment (ProtectSystem=, PrivateTmp=, capability bounding, resource limits, syscall filtering) on native OpenRC using four composed kernel primitives:

  • bwrap ~ mount/PID/network namespace isolation and filesystem containment
  • cgroup-v2 ~ resource accounting and limits (memory, CPU, processes)
  • seccomp-bpf ~ per-service syscall allowlisting with caching
  • Landlock LSM ~ path-scoped access enforcement that survives a namespace escape

No fork of OpenRC. No compatibility layer. No systemd code. Just declarative variables in /etc/conf.d/<service> translated into kernel primitives by userspace tooling built for this purpose.


Components

Component Location Purpose
rc-sandbox /usr/libexec/rc-sandbox Rust binary invoked by OpenRC runscripts ~ checks rc_sandbox opt-out, sets up cgroups, constructs and execs into the bwrap command line
lainos-sandbox-wrap /usr/libexec/lainos-sandbox-wrap Rust binary that runs inside the namespace ~ applies Landlock, sets no_new_privs, drops capabilities, loads seccomp filter
Seccomp profiles /etc/lainos/seccomp/*.list Flat syscall allowlists (lainos-base, lainos-network, lainos-privileged)
openrc-security-status /usr/bin/openrc-security-status Verification script that checks all isolation layers at runtime (maintained separately from this repo)

Both rc-sandbox and lainos-sandbox-wrap are Rust binaries built from a shared Cargo workspace. No shell scripts or interpreted code exist in the isolation chain between OpenRC and the target service.


Build

Workspace (both binaries)

cargo build --release --target x86_64-unknown-linux-musl

Run from the workspace root. This produces both target/x86_64-unknown-linux-musl/release/rc-sandbox and target/x86_64-unknown-linux-musl/release/lainos-sandbox-wrap in one pass.

Both binaries are statically linked with musl libc: rc-sandbox for consistency and reduced runtime dependencies, and lainos-sandbox-wrap because it runs inside the sandbox before shared libraries are guaranteed to be reachable.

Layout

openrc-isolation/
├── Cargo.toml              # workspace root
├── rc-sandbox/
│   ├── Cargo.toml
│   └── src/main.rs
├── lainos-sandbox-wrap/
│   ├── Cargo.toml
│   └── src/
│       ├── main.rs
│       └── landlock.rs
└── profiles/
    ├── lainos-base.list
    ├── lainos-network.list
    └── lainos-privileged.list

Dependencies

  • libc ~ low-level syscalls (prctl, capset, landlock_create_ruleset)
  • libseccomp ~ Rust bindings to the system libseccomp library for seccomp-bpf
  • caps ~ pure-Rust capability handling (no C linkage, minimal overhead)
  • anyhow ~ error handling in both binaries

Release Profile

[profile.release]
lto = true          # Link-time optimization ~ reduces size, improves performance
panic = "abort"     # Smaller binary, no unwinding overhead
strip = true        # Removes debug symbols for smaller binary

Defined once in the workspace root Cargo.toml and shared by both members. These settings produce small, predictable binaries suitable for security-critical components.


Installation (Gentoo)

This repository is used as the source for the app-lainos/openrc-isolation ebuild in the ::lainos overlay, built via cargo.eclass (not Meson).

Ebuild Example

# Copyright 2026 Grayson Giles <amnesia@lainos.net>
# Distributed under the terms of the GNU General Public License v3

EAPI=8

CRATES="
	libc@0.2.169
	libseccomp@0.3.0
	caps@0.5.5
	anyhow@1.0.95
"

inherit cargo

DESCRIPTION="lainOS Layer 03 OpenRC service isolation stack: rc-sandbox, lainos-sandbox-wrap, seccomp profiles"
HOMEPAGE="https://lainos.net"
SRC_URI="$(cargo_crate_uris ${CRATES})"

LICENSE="GPL-3"
SLOT="0"
KEYWORDS="~amd64"

RDEPEND="
	sys-apps/bubblewrap
	sys-libs/libseccomp
"
DEPEND="${RDEPEND}"
BDEPEND="
	sys-libs/libseccomp
"

S="${WORKDIR}/${P}"

src_unpack() {
	cargo_src_unpack
	mkdir -p "${S}" || die
	cp -r "${FILESDIR}"/. "${S}/" || die
}

src_compile() {
	cd "${S}" || die
	cargo_src_compile --target x86_64-unknown-linux-musl
}

src_install() {
	cd "${S}" || die

	exeinto /usr/libexec
	newexe "target/x86_64-unknown-linux-musl/release/rc-sandbox" rc-sandbox
	newexe "target/x86_64-unknown-linux-musl/release/lainos-sandbox-wrap" \
		lainos-sandbox-wrap

	insinto /etc/lainos/seccomp
	doins "profiles/lainos-base.list"
	doins "profiles/lainos-network.list"
	doins "profiles/lainos-privileged.list"
}

pkg_postinst() {
	elog "OpenRC Service Isolation Stack installed."
	elog ""
	elog "To sandbox a service, edit its runscript and set:"
	elog "  command=\"/usr/libexec/rc-sandbox\""
	elog "  command_args=\"/usr/bin/target binary args\""
	elog ""
	elog "Then configure isolation in /etc/conf.d/<service>:"
	elog "  rc_private_tmp=\"YES\""
	elog "  rc_protect_system=\"STRICT\""
	elog "  rc_seccomp_profile=\"lainos-network\""
	elog ""
	elog "To verify isolation, run:"
	elog "  doas openrc-security-status"
}

openrc-security-status ships as its own package and is not installed by this ebuild.


Usage

Sandboxing a Service

By default, a runscript pointed at rc-sandbox is sandboxed. To sandbox a service, edit its OpenRC runscript (e.g., /etc/init.d/dnsmasq):

command="/usr/libexec/rc-sandbox"
command_args="/usr/sbin/dnsmasq --keep-in-foreground ${dnsmasq_args}"

Then configure isolation in /etc/conf.d/<service>:

# Opt out of sandboxing entirely (default: unset = sandboxed)
#rc_sandbox="NO"

# Private /tmp
rc_private_tmp="YES"

# Hide /home and /root
rc_protect_home="YES"

# Read-only /usr and /boot
rc_protect_system="STRICT"

# PID namespace: default is a new one (requires foreground mode).
# Set "NO" for services needing host PID visibility (e.g. dhcpcd, D-Bus).
rc_unshare_pid="YES"

# Capabilities to keep (comma-separated, empty means drop all)
rc_capability_bounding_set="CAP_NET_BIND_SERVICE,CAP_NET_RAW"

# Memory limit (e.g., "512M")
rc_memory_max="256M"

# CPU quota (e.g., "50%")
rc_cpu_quota="50%"

# Seccomp profile (lainos-base, lainos-network, lainos-privileged)
rc_seccomp_profile="lainos-network"

# Allow host network access (omit for full network isolation)
rc_network_access="YES"

# Landlock read-only paths (colon-separated)
rc_landlock_ro="/etc/dnsmasq.conf:/usr/share/dnsmasq"

# Landlock read-write paths (colon-separated)
rc_landlock_rw="/var/lib/dnsmasq:/run/dnsmasq"

Verification

Run the verification script (from the separate openrc-security-status package) to check that all isolation layers are active:

doas openrc-security-status

Example output:

dnsmasq: namespace OK   cgroup OK   seccomp OK   Landlock OK   AppArmor OK

If any layer is missing, the script flags it as a warning or failure.


Security Considerations

Execution Order (Do Not Reorder)

The execution order in lainos-sandbox-wrap is security-critical:

  1. Landlock ~ applied against the sandboxed filesystem view
  2. no_new_privs ~ blocks setuid/setgid escalation
  3. Capability drop ~ before seccomp load (capset/prctl syscalls may be blocked afterward)
  4. seccomp-bpf ~ loaded last; once active, all subsequent syscalls are filtered

Reordering these steps can undermine isolation.

rc-sandbox has its own ordering requirement: it must exec into bwrap immediately after cgroup setup, with no fork and no work after, so that signals from OpenRC's supervisor (TERM on rc-service <n> stop) land on bwrap directly rather than on an intermediate process that might not forward them.

Failure Behavior

Layer Failure Mode Rationale
bwrap (namespace) Hard failure Primary containment layer
cgroup-v2 Soft failure (warning) Hardening layer
Landlock Soft failure (warning) Backstop layer
seccomp-bpf Hard failure Primary containment layer
no_new_privs Hard failure Closes setuid-based seccomp bypass

PID Namespace and Foreground Mode

Services sandboxed with the default rc_unshare_pid="YES" run in a new PID namespace, which breaks host-visible PID files. These services must run in the foreground (--keep-in-foreground, --RunAsDaemon 0, or equivalent), with OpenRC tracking the rc-sandbox/bwrap process directly. Services that genuinely require host PID visibility (e.g. dhcpcd, D-Bus policy) set rc_unshare_pid="NO" ~ a workload-compatibility declaration, not a containment downgrade; every other layer still applies.

AppArmor Integration

AppArmor profiles exist for both services and sandboxing components (rc-sandbox, lainos-sandbox-wrap, and bwrap). All profiles are written manually ~ aa-genprof and aa-logprof do not work reliably inside user namespaces.


Development Status

This repository is complete for lainOS Layer 03.

  • rc-sandbox: rc_sandbox opt-out check
  • rc-sandbox: cgroup-v2 limit writing
  • rc-sandbox: bwrap command line construction and exec
  • lainos-sandbox-wrap: no_new_privs
  • lainos-sandbox-wrap: capability drop
  • lainos-sandbox-wrap: Landlock ruleset construction
  • lainos-sandbox-wrap: seccomp profile parsing, compilation, and caching
  • AppArmor profiles: manual authoring
  • Empirical verification: OpenRC cgroup-v2 ownership on target version
  • Empirical verification: bwrap signal forwarding under rc-service stop

openrc-security-status is tracked and versioned separately from this repo.


License

GPL-3.0


Repository

Forgejo: https://forgejo.lain.rocks/lainOS/openrc-isolation

Website: https://lainos.net