Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0c1c563
feat: add system service allowlist policy
matt-dz Jul 13, 2026
2872a6d
fix: remove redundant remediation dispatch gate
matt-dz Jul 14, 2026
ed14447
feat(systemd): generalize capability policy
matt-dz Jul 14, 2026
033677a
feat(systemd): add configurable target paths
matt-dz Jul 14, 2026
ea4d526
feat(systemd): add bounded journal reader
matt-dz Jul 14, 2026
be532a7
feat(journalctl): add bounded log queries
matt-dz Jul 14, 2026
c4334de
feat(journalctl): report journal disk usage
matt-dz Jul 14, 2026
9c19507
feat(systemd): add journal vacuum safety policy
matt-dz Jul 14, 2026
4c5d199
feat(systemd): add policy-bounded journal vacuum
matt-dz Jul 14, 2026
4ba0f0d
feat(journalctl): add bounded journal vacuum
matt-dz Jul 14, 2026
d83c65f
feat(systemd): add synchronous journal rotation
matt-dz Jul 14, 2026
4755350
feat(journalctl): add bounded journal rotation
matt-dz Jul 14, 2026
d1c231a
refactor(systemd): remove unused runtime target path
matt-dz Jul 14, 2026
b0849c6
docs(journalctl): document restricted systemd support
matt-dz Jul 14, 2026
2ce3a3f
chore(compliance): register systemd dependency
matt-dz Jul 14, 2026
b0245f0
refactor(systemd): restore service allowlist name
matt-dz Jul 14, 2026
5c22a55
fix(systemd): restore generic grants after rebase
matt-dz Jul 14, 2026
6e4f7ce
refactor(systemd): use service action grants
matt-dz Jul 14, 2026
a73bf25
refactor(systemd): use service-only grants
matt-dz Jul 14, 2026
71d7627
feat(systemd): add bounded journal format parser
matt-dz Jul 15, 2026
09e77fa
feat(systemd): decode compressed journal data
matt-dz Jul 15, 2026
4b3e861
feat(systemd): add journal hash compatibility
matt-dz Jul 15, 2026
c1974fd
feat(systemd): add indexed journal data lookup
matt-dz Jul 15, 2026
e4505a5
feat(systemd): traverse journal entry indexes
matt-dz Jul 15, 2026
14eb49b
feat(systemd): evaluate restricted journal queries
matt-dz Jul 15, 2026
825deb2
feat(systemd): snapshot journal file sets
matt-dz Jul 15, 2026
78c1b92
feat(systemd): merge rotated journal entries
matt-dz Jul 15, 2026
50598e1
test(systemd): add real journal fixtures
matt-dz Jul 15, 2026
02e609d
test(systemd): fuzz journal file reader
matt-dz Jul 15, 2026
3247fa9
refactor(systemd): use pure-go journal backend
matt-dz Jul 15, 2026
64a49ee
refactor(systemd): remove sd-journal adapter
matt-dz Jul 15, 2026
5882f3e
docs(journalctl): document pure-go reader
matt-dz Jul 15, 2026
c4fa6e6
test(systemd): isolate journal fixture generation
matt-dz Jul 15, 2026
40c7977
fix(journalctl): protect recent archives during vacuum
matt-dz Jul 15, 2026
86bd488
fix(cli): align systemd grant parsing with API
matt-dz Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/compliance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: .go-version
- name: Install systemd development headers
run: sudo apt-get update && sudo apt-get install -y libsystemd-dev
- name: Run compliance checks
env:
RSHELL_COMPLIANCE_TEST: "1"
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: .go-version
- name: Install systemd development headers
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libsystemd-dev
- name: Run tests with race detector
run: go test -race -v -timeout 10m ./...

Expand Down Expand Up @@ -69,6 +72,8 @@ jobs:
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version-file: .go-version
- name: Install systemd development headers
run: sudo apt-get update && sudo apt-get install -y libsystemd-dev
- name: Run bash comparison tests
env:
RSHELL_BASH_TEST: "1"
Expand Down
3 changes: 3 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ github.com/ebitengine/purego,https://github.com/ebitengine/purego,Apache-2.0,Cop
github.com/go-ole/go-ole,https://github.com/go-ole/go-ole,MIT,"Copyright (c) 2013-2017 Yasuhiro Matsumoto"
github.com/google/uuid,https://github.com/google/uuid,BSD-3-Clause,Copyright (c) 2018 Google Inc.
github.com/inconshreveable/mousetrap,https://github.com/inconshreveable/mousetrap,Apache-2.0,Copyright 2014 Alan Shreve
github.com/klauspost/compress,https://github.com/klauspost/compress,BSD-3-Clause,"Copyright (c) 2012 The Go Authors; Copyright (c) 2019 Klaus Post"
github.com/lufia/plan9stats,https://github.com/lufia/plan9stats,BSD-3-Clause,"Copyright (c) 2019, KADOTA, Kyohei"
github.com/pierrec/lz4/v4,https://github.com/pierrec/lz4,BSD-3-Clause,"Copyright (c) 2015, Pierre Curto"
github.com/pmezard/go-difflib,https://github.com/pmezard/go-difflib,BSD-3-Clause,"Copyright (c) 2013, Patrick Mezard"
github.com/power-devops/perfstat,https://github.com/power-devops/perfstat,MIT,Copyright (c) 2020 Power DevOps
github.com/prometheus-community/pro-bing,https://github.com/prometheus-community/pro-bing,MIT,"Copyright 2022 The Prometheus Authors; Copyright 2016 Cameron Sparr and contributors"
Expand All @@ -19,6 +21,7 @@ github.com/spf13/pflag,https://github.com/spf13/pflag,BSD-3-Clause,"Copyright (c
github.com/stretchr/testify,https://github.com/stretchr/testify,MIT,"Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors"
github.com/tklauser/go-sysconf,https://github.com/tklauser/go-sysconf,BSD-3-Clause,"Copyright (c) 2018-2022, Tobias Klauser"
github.com/tklauser/numcpus,https://github.com/tklauser/numcpus,Apache-2.0,"Copyright (c) 2018-2022, Tobias Klauser"
github.com/ulikunitz/xz,https://github.com/ulikunitz/xz,BSD-3-Clause,"Copyright (c) 2014-2022, Ulrich Kunitz"
github.com/yusufpapurcu/wmi,https://github.com/yusufpapurcu/wmi,MIT,Copyright (c) 2013 Stack Exchange
go.uber.org/atomic,https://github.com/uber-go/atomic,MIT,"Copyright (c) 2016 Uber Technologies, Inc."
go.yaml.in/yaml/v3,https://github.com/yaml/go-yaml,MIT AND Apache-2.0,"Copyright (c) 2006-2011 Kirill Simonov, 2011-2019 Canonical Ltd"
Expand Down
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,30 +61,66 @@ Every access path is default-deny:
| Resource | Default | Opt-in |
|----------------------|-------------------------------------|----------------------------------------------|
| Command execution | All commands blocked (exit code 127)| `AllowedCommands` with namespaced command list (e.g. `rshell:cat`) |
| System services | All services and actions blocked | `AllowedSystemServices` with exact service/action grants |
| Systemd services | All services and actions blocked | `AllowedSystemServices` with exact service/action grants |
| External commands | Blocked (exit code 127) | Provide an `ExecHandler` |
| Filesystem access | Blocked | Configure `AllowedPaths` with `PATH[:ro|:rw]` root specs |
| Environment variables| Empty (no host env inherited) | Pass variables via the `Env` option |
| Output redirections | Only `/dev/null` allowed in read-only mode (exit code 2 for other targets); file-target redirects enabled in remediation mode, gated by `AllowedPaths` | `>/dev/null`, `2>/dev/null`, `&>/dev/null`, `2>&1`; in remediation mode: `>FILE`, `>>FILE`, `2>FILE`, `&>FILE`, `&>>FILE` |

**AllowedCommands** restricts which commands (builtins or external) the interpreter may execute. Commands must be specified with the `rshell:` namespace prefix (e.g. `rshell:cat`, `rshell:echo`). If not set, no commands are allowed.

**AllowedSystemServices** configures exact service/action grants for future system-service builtins. Service names are matched exactly without adding suffixes, resolving aliases, changing case, or otherwise normalizing them: `mysql` and `mysql.service` are different grants. The supported actions are `read`, `reload`, and `restart`. Grants without actions are ignored; entries with empty, whitespace-containing, path-like, or glob-pattern service names and unsupported actions are skipped with a warning. The policy defaults to denying every service, remains enforced when all commands are allowed, and requires remediation mode before any grant can be authorized.
**AllowedSystemServices** is the single capability policy shared by systemd-aware builtins. Grants pair one exact service with generic actions (`read`, `clean`, `reload`, or `restart`), using `SERVICE:ACTION[+ACTION...]` syntax. Grants without actions are ignored; invalid services and unsupported actions are skipped with warnings. Service names are matched exactly without adding suffixes, resolving aliases, changing case, or otherwise normalizing them: `mysql` and `mysql.service` are different services. Empty service names, service names containing `:`, whitespace, path-like names, and glob patterns are also skipped with warnings. The policy defaults to denying every operation and remains enforced when all commands are allowed. `read` is available in read-only mode; mutating actions require remediation mode.

```go
interp.AllowedSystemServices([]interp.SystemServiceControlGrant{
interp.AllowedSystemServices([]interp.SystemdControlGrant{
{
Service: "mysql.service",
Actions: []interp.SystemServiceAction{
interp.SystemServiceRestart,
interp.SystemServiceReload,
interp.SystemServiceRead,
Actions: []interp.SystemdAction{
interp.SystemdActionRestart,
interp.SystemdActionReload,
interp.SystemdActionRead,
},
},
{
Service: "systemd-journald.service",
Actions: []interp.SystemdAction{interp.SystemdActionRead, interp.SystemdActionClean},
},
})
```

The development CLI accepts the equivalent syntax through `--allowed-services mysql.service:restart+reload+read,nginx.service:read`. The policy and authorization capability are implemented, but `systemctl` and `journalctl` builtins are not yet available.
The development CLI accepts equivalent grants through `--allowed-services mysql.service:restart+reload+read,systemd-journald.service:read+clean`. Service selectors are always exact service names; `:` separates a selector from its actions and is not allowed inside service names. The restricted `journalctl` builtin is available as described below; `systemctl` is not yet implemented.

**SystemdTargetConfig** selects which Linux host systemd-aware builtins address. With no option, standard local paths are used. `Root` derives all paths beneath a mounted host root such as `/host`. For unusual container mount layouts, callers may instead provide explicit journal directories, machine-id path, journald Varlink socket, and system bus socket. `Root` and explicit fields cannot be mixed. Once any explicit field is supplied, omitted fields stay unavailable and never fall back to local endpoints. The machine-id path is mandatory for every explicit target. The development CLI exposes the equivalent `--systemd-root` and `--systemd-*` path flags.

The target paths intentionally bypass `AllowedPaths`: they are trusted runner configuration and cannot be supplied by shell scripts. `Root` is the strongest way to keep files and endpoints on one host because every path is derived from the same mounted root. With explicit paths, the embedding application is responsible for mounting every supplied path from the same host. Every selected journal file header is checked against the configured machine ID, but journald's Rotate Varlink method does not return a machine ID that can independently attest its socket.

| Operation | Required target access |
|-----------|------------------------|
| Journal query, disk usage, or vacuum | `/etc/machine-id` and one or both of `/var/log/journal`, `/run/log/journal` |
| Journal rotation | `/etc/machine-id` and `/run/systemd/journal/io.systemd.journal` |
| Future `systemctl` manager operations | `/etc/machine-id` and `/run/dbus/system_bus_socket` |

Root targets derive those paths below `Root`; explicit targets may map them to arbitrary absolute container paths. A command fails closed when one of its required paths was omitted.

### Restricted journalctl

`journalctl` provides bounded investigation and disk-cleanup operations without executing the host binary or accepting user-selected files, directories, journal matches, cursors, machines, or namespaces.

| Operation | Supported flags | Required systemd grant |
|-----------|-----------------|------------------------|
| Exact service logs | `-u SERVICE` (repeatable), `-b`, `-n COUNT`, `--since TIME`, `-o short\|cat` | Exact `SERVICE:read` grant for every service |
| Current-boot kernel logs | `-k`, `-n COUNT`, `--since TIME`, `-o short\|cat` | `systemd-journald.service:read` |
| Allocated journal usage | `--disk-usage` | `systemd-journald.service:read` |
| Synchronous active-file rotation | `--rotate` | `systemd-journald.service:clean` plus remediation mode |
| Archived-file cleanup | `--vacuum-size SIZE`, `--vacuum-time AGE`, `--dry-run` | `systemd-journald.service:clean` plus remediation mode |

Log queries default to 100 entries and are capped at 1,000 entries and 32 exact unit scopes per invocation. `--since` accepts RFC 3339, local `YYYY-MM-DD HH:MM:SS`, or a non-negative Go lookback duration such as `15m`. `-b` means the newest boot present in the selected target journal, so it remains correct for a mounted host. Bare journal reads, globbed units, arbitrary field matches, follow mode, raw structured output, alternate sources, and arbitrary boot selection are unavailable.

Log reading uses a bounded pure-Go journal-file parser and does not execute host `journalctl` or require cgo or `libsystemd`. It supports regular and compact journal layouts, legacy and keyed DATA hashes, and XZ, LZ4, and Zstandard-compressed DATA objects. Unknown incompatible format features fail with a clear error.

The reader opens at most 128 regular, non-symlink journal files, verifies the machine ID and file identities before and after each query, and retries once if concurrent rotation or an in-progress write makes the first snapshot inconsistent. Rotation requires Linux and the configured journald Varlink socket. Disk usage and vacuuming use the mounted journal files directly on Linux or macOS. `--vacuum-size` requires `--vacuum-time`; size cleanup deletes only archives at least that old and stops once the size target is reached, leaving newer archives intact even when usage remains above the target. `--rotate` may be combined with vacuum flags, but newly rotated archives remain protected by the time floor. `--dry-run` cannot be combined with `--rotate` and still requires the clean grant and remediation mode.

Vacuum thresholds are provided by the `journalctl` command itself. The backend removes only strict systemd archived-journal filenames, never active files, symlinks, hardlinks, or malformed names, and it revalidates each file immediately before deletion.

**AllowedPaths** restricts all file operations to specified directories using Go's `os.Root` API for reads and openat-based write handling for writes.

Expand Down
4 changes: 3 additions & 1 deletion SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The in-shell `help` command mirrors these feature categories: run `help` for a c
- ✅ `ip [-o|-4|-6|--brief] addr|link [show] [dev IFNAME]` — show network interface addresses and link-layer info (read-only); write ops (`add`, `del`, `flush`, `set`), namespace ops (`netns`, `-n`), and batch mode (`-b`/`-B`/`--force`) are blocked
- ✅ `ip route [show|list]` — show IPv4 routing table (Linux only; reads `/proc/net/route` directly via `os.Open`, bypassing `AllowedPaths`); at most 10 000 entries loaded; lines longer than 1 MiB abort parsing with an error (exit 1)
- ✅ `ip route get ADDRESS` — show the route selected by longest-prefix-match for ADDRESS (Linux only); write ops (`add`, `del`, `flush`, `replace`, `change`, `save`, `restore`) are blocked; `-6` (IPv6 routing) is not supported
- ✅ `journalctl (-u SERVICE...|-k) [-b] [-n COUNT] [--since TIME] [-o short|cat]` — bounded journal investigation for exact system services or current-boot kernel messages; defaults to 100 and caps at 1,000 entries and 32 service scopes; service reads require exact `SERVICE:read` grants and kernel reads require `systemd-journald.service:read`; also supports `--disk-usage` with `systemd-journald.service:read`, plus remediation-only `--rotate`, `--vacuum-size SIZE`, `--vacuum-time AGE`, and `--dry-run` with `systemd-journald.service:clean`; size vacuum requires a time floor and never deletes newer archives merely to reach its target; rotation may precede vacuuming, while `--dry-run` cannot rotate; bare/unrestricted reads, globbed services, arbitrary matches, follow mode, raw structured output, alternate sources, cursors, machines, namespaces, and arbitrary boot selection are unavailable; log reads use a bounded pure-Go parser for regular/compact and XZ/LZ4/Zstandard journal files with no host `journalctl`, cgo, or libsystemd dependency; rotation requires Linux, and mounted-file usage/vacuum operations support Linux/macOS
- ✅ `logrotate (-s SIZE|-f) [-n] [-v] FILE...` — remediation-mode helper that truncates log files to zero bytes through `AllowedPaths`; `--size SIZE` truncates only files at least SIZE bytes, `--force` explicitly truncates without a threshold, `--dry-run` reports without modifying files, and `--verbose` reports per-file actions. Symlinked write targets are rejected with `symlinks are not supported as write targets`; pass the real log path instead. This is not a full `logrotate(8)` replacement: no config parsing, rename-based rotation, retained copies, compression, state files, or rotate scripts.
- ✅ `sort [-rnhubfds] [-k KEYDEF] [-t SEP] [-c|-C] [FILE]...` — sort lines of text files; `-h`/`--human-numeric-sort` orders by SI suffix (none < K/k < M < G < T < P < E < Z < Y < R < Q) then by numeric value (single-letter suffixes only — `Ki`, `Mi`, etc. are not recognised); `-o`, `--compress-program`, and `-T` are rejected (filesystem write / exec)
- ✅ `ss [-tuaxlans4689Hoehs] [OPTION]...` — display network socket statistics; reads kernel socket state directly via `os.Open` (bypassing `AllowedPaths`) from: Linux: `/proc/net/`; macOS: sysctl; Windows: iphlpapi.dll; `-F`/`--filter` (GTFOBins file-read), `-p`/`--processes` (PID disclosure), `-K`/`--kill`, `-E`/`--events`, and `-N`/`--net` are rejected
Expand Down Expand Up @@ -121,7 +122,8 @@ The in-shell `help` command mirrors these feature categories: run `help` for a c
## Execution

- ✅ AllowedCommands — restricts which commands (builtins or external) may be executed; commands require the `rshell:` namespace prefix (e.g. `rshell:cat`); if not set, no commands are allowed
- ✅ AllowedSystemServices policy — configures exact, case-sensitive `read`, `reload`, and `restart` grants for system service names through `interp.AllowedSystemServices` or CLI `--allowed-services SERVICE:ACTION[+ACTION...]`; names are not normalized (`mysql` does not match `mysql.service`), grants without actions are ignored, invalid names and unsupported actions are skipped with warnings, all services are denied by default, and allowing all commands does not bypass this policy; the authorization capability is implemented for future consumers, but `systemctl` and `journalctl` builtins are not yet available
- ✅ AllowedSystemServices policy — one shared default-deny capability map for exact services with generic `read`, `clean`, `reload`, and `restart` actions; actionless grants are ignored, invalid services/actions are skipped, service names are case-sensitive, are not normalized, and cannot contain `:`, mutating actions require remediation mode, and allowing all commands does not bypass this policy; configure services through `interp.AllowedSystemServices` or CLI `--allowed-services SERVICE:ACTION[+ACTION...]`
- ✅ SystemdTargetConfig — systemd-aware builtins use standard local paths by default; `Root` derives journal directories, machine ID, journald Varlink socket, and system D-Bus socket beneath one mounted-host root, while explicit absolute paths support split mount layouts without falling back to local endpoints; target paths are trusted configuration and bypass `AllowedPaths`; explicit mounts must all refer to the same host
- ✅ AllowedPaths filesystem sandboxing — restricts all file access (read and write) to specified directories. Entries may end with `:ro` or `:rw` to indicate read-only and read-write permissions, respectively; entries without a suffix default to read-only. In remediation mode, write operations are accepted only inside the most-specific matching `:rw` root. Cross-root symlink fallback is read-only to avoid TOCTOU on writes; on Unix, symlink components in write targets are rejected with `symlinks are not supported as write targets` via a no-follow `openat` walk
- ✅ Whole-run execution timeout — callers can bound a `Run()` call via `context.Context`, `interp.MaxExecutionTime`, or the CLI `--timeout` flag; the deadline applies to the entire script, not each individual command
- ✅ ProcPath — overrides the proc filesystem path used by `ps` (default `/proc`; Linux-only; useful for testing/container environments); `ps` does not read `/proc/<pid>/cmdline`
Expand Down
Loading
Loading