feat: ISO service hardening — rc.d + log rotation + layout docs #7

Merged
clawdie merged 1 commit from feat/iso-service-hardening into main 2026-05-31 16:49:01 +02:00
4 changed files with 166 additions and 9 deletions

View file

@ -0,0 +1,97 @@
# Colibri ISO Service Layout
## Service identity
| Field | Value |
|-------|-------|
| Service name | `colibri_daemon` |
| Binary | `/usr/local/bin/colibri-daemon` |
| User | `colibri` (unprivileged, `/usr/sbin/nologin`) |
| Group | `colibri` |
| Supervisor | `daemon(8)` — restart on crash, privilege drop |
## Filesystem layout
```
/usr/local/bin/colibri-daemon ← daemon binary (foreground, no self-daemonize)
/usr/local/bin/colibri ← CLI client (status, create-task, etc.)
/usr/local/bin/colibri-smoke-agent ← smoke test agent (fake Pi JSONL)
/usr/local/etc/rc.d/colibri_daemon ← rc.d service script
/usr/local/etc/colibri/
rc.conf.sample ← service config template
/usr/local/etc/newsyslog.conf.d/
colibri.conf ← log rotation (1MB, 7 archives)
/var/db/colibri/ ← persistent data (750 colibri:colibri)
colibri.sqlite ← coordination store
sessions/ ← JSONL session files
/var/run/colibri/ ← tmpfs (recreated each boot)
colibri.sock ← Unix socket (750 colibri:colibri)
colibri-daemon.pid ← daemon(8) supervisor pidfile
/var/log/colibri/ ← persistent logs
daemon.log ← stdout/stderr from colibri-daemon
```
## Boot-time behavior
1. `rc.d` starts `colibri_daemon` after LOGIN + cleanvar
2. `prestart`: creates `/var/run/colibri/`, `/var/db/colibri/`, `/var/log/colibri/`
3. `daemon(8)` forks `colibri-daemon` as user `colibri`, redirects logs
4. `poststart`: waits up to 10s for `/var/run/colibri/colibri.sock` to appear
5. Daemon binds socket, opens SQLite, starts scheduler loop
6. CLI clients connect via `colibri` binary → Unix socket
## Shutdown behavior
1. `rc.d` sends SIGTERM to daemon(8)
2. daemon(8) forwards to colibri-daemon child
3. Colibri closes socket, flushes SQLite, stops scheduler
4. `poststop`: removes stale socket from tmpfs
## Startup validation
```sh
# Check service status
service colibri_daemon status
# Socket health (nc must be available)
service colibri_daemon health
# CLI smoke
colibri status
colibri create-task --title "iso-smoke"
colibri list-tasks --status queued
```
## Config knobs (set in /etc/rc.conf or /etc/rc.conf.d/colibri_daemon)
| Variable | Default | Purpose |
|----------|---------|---------|
| `colibri_daemon_enable` | NO | Enable at boot (YES/NO) |
| `colibri_daemon_user` | colibri | Runtime user |
| `colibri_daemon_group` | colibri | Runtime group |
| `colibri_cost_mode` | smart | fast/smart/max |
| `colibri_daemon_data_dir` | /var/db/colibri | Persistent data |
| `colibri_daemon_run_dir` | /var/run/colibri | tmpfs runtime |
| `colibri_daemon_socket` | $run_dir/colibri.sock | Unix socket path |
| `colibri_daemon_db_path` | $data_dir/colibri.sqlite | SQLite path |
| `colibri_daemon_logfile` | /var/log/colibri/daemon.log | Log output |
## Log rotation
`/usr/local/etc/newsyslog.conf.d/colibri.conf`:
```
/var/log/colibri/daemon.log colibri:colibri 640 7 1024 * JC
```
Rotates at 1MB, keeps 7 compressed archives, bzip2 compression.
## Secrets policy
- No API keys in rc.conf or service environment files.
- `DEEPSEEK_API_KEY` and other provider keys are set via a separate,
mode-0600 env file sourced by the daemon (e.g. `/usr/local/etc/colibri/provider.env`).
- The rc.d script does NOT read or expose secrets.
- Logs go to `/var/log/colibri/` which is mode 0750 colibri:colibri —
only the colibri user and root can read them.

View file

@ -1,23 +1,23 @@
#!/bin/sh
#
# Colibri daemon — FreeBSD rc.d service (prototype, not installed)
#
# Review-only. Do NOT install to /usr/local/etc/rc.d/ yet; the /tmp smoke test
# comes first, and this script still needs a real on-FreeBSD `service` test
# (rec #4 of docs/internal/sessions/2026-05-27-osa-freebsd-daemon-scheduler-smoke.md).
# Colibri daemon — FreeBSD rc.d service
#
# colibri-daemon runs in the FOREGROUND — it does not self-daemonize or write a
# pidfile. So rc.d must run it under daemon(8), which backgrounds it, writes the
# pidfile. rc.d runs it under daemon(8), which backgrounds it, writes the
# supervisor pidfile, restarts on crash, drops privileges to the colibri user,
# and redirects stdout/stderr (tracing) to a logfile.
#
# Usage after review:
# Setup (one-time, as root):
# pw groupadd colibri
# pw useradd colibri -g colibri -d /var/db/colibri -s /usr/sbin/nologin
# cp packaging/freebsd/colibri_daemon.in /usr/local/etc/rc.d/colibri_daemon
# chmod 555 /usr/local/etc/rc.d/colibri_daemon
# sysrc colibri_daemon_enable=NO # keep disabled during dual-run
# sysrc colibri_daemon_enable=YES # or NO during dual-run
#
# Runtime:
# service colibri_daemon start
# service colibri_daemon status
# service colibri_daemon stop
#
# Requires:
# - colibri-daemon binary at /usr/local/bin/colibri-daemon
@ -59,6 +59,9 @@ command_args="-P ${pidfile} -r -t ${name} -u ${colibri_daemon_user} \
procname="/usr/sbin/daemon"
start_precmd="colibri_daemon_prestart"
start_postcmd="colibri_daemon_poststart"
stop_postcmd="colibri_daemon_poststop"
extra_commands="health"
colibri_daemon_prestart()
{
@ -78,4 +81,47 @@ colibri_daemon_prestart()
export COLIBRI_COST_MODE="${colibri_cost_mode}"
}
colibri_daemon_poststart()
{
# Wait for the socket to appear (daemon forks, child binds socket).
local timeout=10
local waited=0
while [ ! -S "${colibri_daemon_socket}" ] && [ $waited -lt $timeout ]; do
sleep 1
waited=$((waited + 1))
done
if [ -S "${colibri_daemon_socket}" ]; then
echo "colibri-daemon socket ready after ${waited}s"
else
echo "WARNING: colibri-daemon socket not ready after ${timeout}s"
fi
}
colibri_daemon_poststop()
{
# Clean up tmpfs artifacts on graceful shutdown.
# The pidfile is managed by daemon(8); socket is the child's.
if [ -S "${colibri_daemon_socket}" ]; then
rm -f "${colibri_daemon_socket}"
fi
}
health_cmd="colibri_daemon_health"
colibri_daemon_health()
{
if [ -S "${colibri_daemon_socket}" ]; then
if printf '{"cmd":"status"}\n' | nc -U "${colibri_daemon_socket}" -w 2 >/dev/null 2>&1; then
echo "colibri-daemon is healthy (socket responding)"
return 0
else
echo "colibri-daemon socket exists but not responding"
return 1
fi
else
echo "colibri-daemon socket not found"
return 1
fi
}
run_rc_command "$1"

View file

@ -0,0 +1,9 @@
# Colibri daemon log rotation — newsyslog.conf(5) snippet.
#
# Install to: /usr/local/etc/newsyslog.conf.d/colibri.conf
# Or append to: /etc/newsyslog.conf
#
# Rotates /var/log/colibri/daemon.log when it reaches 1 MB,
# keeps 7 compressed archives, creates with colibri:colibri ownership.
/var/log/colibri/daemon.log colibri:colibri 640 7 1024 * JC

View file

@ -24,6 +24,7 @@ TARGET=${CARGO_TARGET_DIR:-"$ROOT/target"}/release
BIN_DIR="$DESTDIR/usr/local/bin"
RC_DIR="$DESTDIR/usr/local/etc/rc.d"
ETC_DIR="$DESTDIR/usr/local/etc/colibri"
NEWSYSLOG_DIR="$DESTDIR/usr/local/etc/newsyslog.conf.d"
DB_DIR="$DESTDIR/var/db/colibri"
RUN_DIR="$DESTDIR/var/run/colibri"
LOG_DIR="$DESTDIR/var/log/colibri"
@ -40,7 +41,7 @@ copy_bin() {
install -m 0555 "$TARGET/$1" "$BIN_DIR/$1"
}
mkdir -p "$BIN_DIR" "$RC_DIR" "$ETC_DIR" "$DB_DIR" "$RUN_DIR" "$LOG_DIR"
mkdir -p "$BIN_DIR" "$RC_DIR" "$ETC_DIR" "$NEWSYSLOG_DIR" "$DB_DIR" "$RUN_DIR" "$LOG_DIR"
copy_bin colibri-daemon
copy_bin colibri
@ -53,6 +54,9 @@ fi
install -m 0555 "$ROOT/packaging/freebsd/colibri_daemon.in" \
"$RC_DIR/colibri_daemon"
install -m 0644 "$ROOT/packaging/freebsd/newsyslog-colibri.conf" \
"$NEWSYSLOG_DIR/colibri.conf"
cat > "$ETC_DIR/rc.conf.sample" <<EOF
# Colibri control plane service defaults for the Clawdie ISO.
# Copy or merge into /etc/rc.conf or /etc/rc.conf.d/colibri_daemon.
@ -101,6 +105,7 @@ Installed:
/usr/local/bin/colibri-smoke-agent
/usr/local/etc/rc.d/colibri_daemon
/usr/local/etc/colibri/rc.conf.sample
/usr/local/etc/newsyslog.conf.d/colibri.conf
Next image integration steps:
1. Ensure colibri user/group exists in the image.