Skip to content
ghostshell

Security model

What Ghost Shell protects and what it does not - trust boundary, encryption, key immutability, fail-open semantics, and the integrity caveat.

Read this before relying on Ghost Shell for audit. It explains what Ghost Shell does — and, just as important, what it does not protect against.

Integrity caveat

Warning: Ghost Shell is not tamper-proof against a user who simply avoids it. It provides root-only access to recordings plus live tail, but a malicious user with shell access can run a shell that was never wrapped by ghostshell — the auto-record hook and the sshd ForceCommand wrapper are both deliberately fail-open and can be sidestepped. Non-circumventable capture requires PAM- or kernel-stage hooks, which this project does not implement. Treat Ghost Shell as an audit and visibility tool for cooperative environments, not as an enforcement boundary against a determined adversary on the box.

This is the flip side of fail-open: availability is prioritized over guaranteed capture.

Trust boundary

  • The central store is /var/lib/ghostshell, root:root 0700; recordings are root:root 0600. Only root can read central recordings — users cannot read even their own.
  • The recorder reaches the daemon over /run/ghostshell-daemon.sock. The socket mode is 0666 — any user can connect to submit their own session — but the daemon authenticates the peer with SO_PEERCRED and attributes every session to the kernel-reported UID. A user cannot forge another user's identity or read data back over the socket. Privacy is enforced by filesystem permissions on the store, not by the socket mode.
  • All read commands (play, tail, tree, search, export, ls --all/--user, ansible) require root, because they read the root-only store directly. The audit added an explicit euid check to every one of them as defense-in-depth, so relocating the store to a looser path does not silently open it.

Encryption at rest and key immutability

  • Central recordings are AES-256-GCM encrypted; on disk a .cast is opaque to cat/strings/grep. Framing details are on the file format page.
  • The key (/var/lib/ghostshell/.ghostshell.key, root:root 0600) is unique per server and made immutable with chattr +i: it cannot be removed, renamed, or rewritten by rm/vi/sed/>/tee — even by root — until someone explicitly runs chattr -i.
  • Immutability depends on the filesystem. ext4/xfs support the attribute; overlayfs, tmpfs, and many network mounts silently ignore it, leaving the key deletable. The daemon reads the flag back after setting it and, if it did not stick, logs WARNING: ... the at-rest key is NOT protected from deletion/modification. If you see that line, move the store to ext4/xfs or accept the reduced guarantee. (The deployment review reproduced this empirically on overlayfs and tmpfs.)
  • Back up the key. If it is lost, every encrypted recording is permanently unreadable. The daemon refuses to start if the key is missing while encrypted recordings exist. See backup and prune.

Optional playback password

An optional playback password (hashed in /etc/ghostshell/.playback_passwd, root:root 0600) gates every content- or metadata-revealing command — play, tail, tree, search, export, and the central ls views. A separate prune password gates deletion. This is a second factor on top of root, not a replacement for filesystem permissions. Manage it with ghostshell init --reset-password / --clear-password (both require the current password).

Path traversal and permission posture

  • Session IDs are validated and resolved within the central store; the ingest sweep admits only regular .cast files and rejects symlinks and irregular entries, so a crafted home-directory entry cannot redirect the root-owned ingest at an arbitrary target.
  • --from/--to date strings go to date -d as plain argv, never through a shell — command substitution and separators are inert.
  • The daemon writes recordings 0600, store directories 0700, and runs under a hardened systemd unit: NoNewPrivileges, ProtectSystem=full, SystemCallFilter=@system-service, ProtectKernelModules/ProtectKernelLogs, LockPersonality, RestrictNamespaces/RestrictRealtime, ProtectClock/ProtectHostname, RestrictAddressFamilies=AF_UNIX, UMask=0077. systemd-analyze security rates it 3.9 OK (measured on real systemd 255, improved from 7.0 MEDIUM).
  • The unit intentionally does not enable ProtectHome: the daemon must read and delete each user's ~/.local/share/ghostshell spool during ingest.

Fail-open semantics

Every capture path is fail-open by design, so Ghost Shell can never lock a user out or break a login or SSH session:

  • If the daemon is down, rejects the session (per-user session_cap reached, store full, ID collision), or is wedged, ghostshell rec writes a user-local file (~/.local/share/ghostshell, 0600); the daemon ingests it on next startup. The daemon sends an OK acknowledgement only once a session is registered, and the recorder waits for it with a bounded timeout — a rejection or a hung daemon can never silently swallow a recording, nor block a login.
  • If the central store fills mid-session, the daemon logs a DISK FULL error naming the path, terminates that session cleanly, and keeps serving other users.
  • The profile.d hook and the sshd ForceCommand wrapper fall through to a normal shell on any error, and pass scp/sftp/rsync/git transfers through untouched.

Audited in the open

The claims above are not self-asserted. A full technical audit (AUDIT.md, ~13.2 KLOC across 16 packages) fixed 23 of 24 actionable findings in-tree, and the deployment review (DEPLOYMENT.md) validated the built .deb empirically on a real Ubuntu 24.04 install: cross-user read denial, symlink-attack rejection (an evil.cast → /etc/shadow symlink was refused with zero bytes leaked), disk-full survival, fail-open fallback and re-ingest, and race-detector-clean concurrency. Both documents ship in the repository.

Questions like "can root read my session?" are answered directly in the threat model FAQ.

Source of truth: README on GitHub