From 25f70df5df2d2d78e131d6507c297189ddc60ebd Mon Sep 17 00:00:00 2001 From: Prince Roshan Date: Thu, 16 Jul 2026 01:31:48 +0530 Subject: [PATCH] Add optional systemd unit for always-on recording Ships contrib/systemd/ltm.service so hosts that want continuous capture can run the recorder under systemd, without replacing ad-hoc `ltm start`. The unit runs `daemon --foreground` as Type=simple (no forking/re-exec; systemd tracks the process and restarts on failure). Docs cover enable/start/stop via systemctl vs `ltm start`, and note that `daemon` doesn't write a pidfile so liveness comes from `systemctl status` rather than `ltm status`. Capabilities: the unit defaults to User=root; the commented unprivileged variant needs CAP_BPF + CAP_PERFMON *and* CAP_DAC_READ_SEARCH, since the collector reads mode-440 root-owned tracepoint ids under /sys/kernel/tracing/events/**/id. Corrected docs/security.md's capability table to match. Verified on a 6.8 kernel. Closes #8 --- contrib/systemd/README.md | 17 +++++++++++ contrib/systemd/ltm.service | 36 ++++++++++++++++++++++++ docs/cli.md | 4 +++ docs/recording.md | 56 +++++++++++++++++++++++++++++++++++++ docs/security.md | 8 +++++- 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 contrib/systemd/README.md create mode 100644 contrib/systemd/ltm.service diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md new file mode 100644 index 0000000..7cd83cd --- /dev/null +++ b/contrib/systemd/README.md @@ -0,0 +1,17 @@ +# systemd unit (optional) + +Opt-in way to keep `ltm` recording across reboots instead of running +`sudo ltm start` by hand. `ltm start`/`ltm stop` remain the default, portable +path (see [docs/recording.md](../../docs/recording.md)); nothing here is +required to use ltm. + +```bash +go build -o bin/ltm ./cmd/ltm +sudo install -m 0755 bin/ltm /usr/bin/ltm +sudo install -m 0644 contrib/systemd/ltm.service /etc/systemd/system/ltm.service +sudo systemctl daemon-reload +sudo systemctl enable --now ltm +``` + +See [docs/recording.md](../../docs/recording.md#systemd-optional) for +querying, uninstalling, and the unprivileged-user variant. diff --git a/contrib/systemd/ltm.service b/contrib/systemd/ltm.service new file mode 100644 index 0000000..545e411 --- /dev/null +++ b/contrib/systemd/ltm.service @@ -0,0 +1,36 @@ +[Unit] +Description=ltm always-on activity recorder (eBPF) +Documentation=https://github.com/Agent-Hellboy/ltm/blob/main/docs/recording.md +# Recording is Linux/x86_64 only; see docs/recording.md. +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/ltm daemon --foreground --db /var/lib/ltm/ltm.db --pidfile /run/ltm/ltm.pid +Restart=on-failure +RestartSec=5s + +# Owns /var/lib/ltm (db) and /run/ltm (pidfile); created/removed by systemd +# around the service's lifetime. +StateDirectory=ltm +RuntimeDirectory=ltm + +# Recording needs to attach BPF tracepoints and read kernel perf event info: +# root, or CAP_BPF + CAP_PERFMON + CAP_DAC_READ_SEARCH (see docs/security.md). +# Root is the simplest default and what `sudo ltm start` already assumes; +# switch to the capability form below to run unprivileged. +User=root + +# --- Unprivileged alternative (comment out User=root above, uncomment these) --- +# CAP_DAC_READ_SEARCH is required too: the collector reads tracepoint ids from +# /sys/kernel/tracing/events/**/id, which are mode 440 root:root, and neither +# CAP_BPF nor CAP_PERFMON grants DAC bypass. Without it every tracepoint open +# fails with "permission denied" and the daemon exits ("no tracepoints could +# be attached"). Verified on a 6.8 kernel. +# User=ltm +# CapabilityBoundingSet=CAP_BPF CAP_PERFMON CAP_DAC_READ_SEARCH +# AmbientCapabilities=CAP_BPF CAP_PERFMON CAP_DAC_READ_SEARCH +# NoNewPrivileges=yes + +[Install] +WantedBy=multi-user.target diff --git a/docs/cli.md b/docs/cli.md index 7aee4d7..fb5eb0d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -38,6 +38,10 @@ sudo ltm stop the process survives the launching shell. Needs root / BPF caps (see [security](security.md)). +For always-on recording across reboots, an optional systemd unit +(`daemon --foreground` as `Type=simple`) is available instead — see +[recording.md#systemd-optional](recording.md#systemd-optional). + ### `timeline` Newest-first event list with AND filters. Repeatable flags OR within that field. diff --git a/docs/recording.md b/docs/recording.md index 4adcc65..97f1e2c 100644 --- a/docs/recording.md +++ b/docs/recording.md @@ -92,3 +92,59 @@ sudo ltm stop # join producer, close ingest, final flush, then exit On shutdown the service joins the collector before closing `ingest`, then waits for the flush loop to finish before the store is closed; otherwise the last batch can be lost. That path is covered by daemon tests. + +## systemd (optional) + +`ltm start` is the default, portable way to run the recorder. Machines that +want it always on across reboots can instead run `daemon --foreground` +directly under systemd, using the unit in +[`contrib/systemd/ltm.service`](../contrib/systemd/ltm.service). This is +opt-in and Linux-only (recording already requires Linux); it doesn't replace +`ltm start`/`ltm stop`. + +```bash +go build -o bin/ltm ./cmd/ltm +sudo install -m 0755 bin/ltm /usr/bin/ltm +sudo install -m 0644 contrib/systemd/ltm.service /etc/systemd/system/ltm.service +sudo systemctl daemon-reload +sudo systemctl enable --now ltm +``` + +```bash +systemctl status ltm # unit + process state, journal tail +sudo journalctl -u ltm -f # follow recorder logs +ltm --db /var/lib/ltm/ltm.db status # event counts, dropped, last event +ltm --db /var/lib/ltm/ltm.db timeline --since 10m + +sudo systemctl stop ltm # graceful: same shutdown path as `ltm stop` +sudo systemctl disable ltm +``` + +Notes: + +- The unit runs `daemon --foreground` as `Type=simple`, which is what it's + designed for: no forking, no re-exec, systemd tracks the process directly + and restarts it on failure. +- Needs root, or `CAP_BPF` + `CAP_PERFMON` **and** `CAP_DAC_READ_SEARCH` — the + collector also reads tracepoint ids from + `/sys/kernel/tracing/events/**/id`, which are mode `440 root:root`, so the + two BPF caps alone aren't enough for a non-root user (every tracepoint open + fails with "permission denied" and the daemon exits). The unit defaults to + `User=root` (matching `sudo ltm start`) with a commented-out unprivileged + variant carrying all three capabilities, verified on a 6.8 kernel. See + [security.md](security.md). +- The `daemon` subcommand (unlike `start`) doesn't write a pidfile, so + `ltm status`'s `alive` field won't reflect a systemd-managed process — use + `systemctl status ltm` for liveness instead. Event counts/timeline/etc. via + `ltm` still work against the same `--db` path either way. +- `StateDirectory=ltm` / `RuntimeDirectory=ltm` put the db under + `/var/lib/ltm/` and the (unused) pidfile path under `/run/ltm/`; systemd + creates/owns both. +- **Don't point `ltm start` (or a second instance) at the same `--db` as an + active systemd-managed recorder.** Two writers on one SQLite db contend + until a write blows past an internal deadline, both processes exit with + `context deadline exceeded`, and `Restart=on-failure` crash-loops the + service until only one recorder remains. No data corruption results + (`PRAGMA integrity_check` stays `ok`), but it's a real outage of recording + in the meantime — use a distinct `--db` for anything ad hoc, or stop the + unit first. diff --git a/docs/security.md b/docs/security.md index 1522e20..23b0c88 100644 --- a/docs/security.md +++ b/docs/security.md @@ -7,9 +7,15 @@ Related: [recording](recording.md) · [querying](querying.md) · | Action | Needs | |---|---| -| `ltm start` (record) | root, or `CAP_BPF` + `CAP_PERFMON` | +| `ltm start` (record) | root, or `CAP_BPF` + `CAP_PERFMON` + `CAP_DAC_READ_SEARCH` | | timeline / watch / diff / query / sql / status | none — opens the DB read-only | +`CAP_DAC_READ_SEARCH` is needed alongside the two BPF capabilities because the +collector reads tracepoint ids from `/sys/kernel/tracing/events/**/id`, which +are mode `440 root:root`; `CAP_BPF`/`CAP_PERFMON` don't grant DAC bypass on +their own. Without it every tracepoint fails to open and the daemon exits +with "no tracepoints could be attached" (verified on a 6.8 kernel). + `start` re-execs as `daemon --foreground` and detaches (`Setsid`) so the recorder survives the launching shell.