reference
systemd units
Every long-lived process on a baked deputyOS image runs under systemd. Units are rendered from Jinja templates in roles/deputyos/templates/ at bake time and dropped into /etc/systemd/system/.
Every long-lived process on a baked deputyOS image runs under systemd.
Units are rendered from Jinja templates in roles/deputyos/templates/
at bake time and dropped into /etc/systemd/system/.
This page walks each unit: the real ExecStart, the user it runs as,
the hardening directives, the Condition* gates, and the
dependencies. Every claim here is grounded in the corresponding
*.service.j2 template.
[TOC]
Unit summary
| Unit | Runs as | First-boot only? | Port(s) | AppArmor profile |
|---|---|---|---|---|
openclaw-gateway.service | agent:agent | no | 8080, 8443 | deputyos.openclaw |
hermes-gateway.service | agent:agent | no | 8080 | deputyos.hermes |
khoj-gateway.service | agent:agent | no | 42110 | deputyos.khoj |
deputywizard.service | root:root | yes (ConditionPathExists=!/var/lib/deputyos/wizard-state.json) | configurable (deputyos_wizard_port) | none (root + minimal hardening) |
deputyos-qr-on-tty.service | root:root | yes (same gate) | n/a (oneshot) | none |
deputyos-voice-relay.service | agent:agent (+ supplementary audio) | no (gated on voice.toml enabled=true) | n/a | deputyos.voice-relay |
avahi-deputyos.service | n/a (avahi-daemon’s job) | no | mDNS (5353/udp) | confined by avahi |
deputyos-backup.timer | root:root (oneshot triggers) | no | n/a | none |
Hardening baseline
The three gateway services share a hardened baseline. The relaxations are in bold below; everything else is identical across the three units:
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=<data_dir> /etc/deputyos
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
RestrictNamespaces=true # Hermes overrides: ~mnt cgroup pid
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true # voice-relay overrides: false (Piper/onnxruntime JIT)
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @mount
# Hermes adds: SystemCallFilter=unshear clone clone3 setns
Hermes’ relaxations exist to support its skill-sandbox child processes
(unshare(CLONE_NEWUSER)); see the dedicated section below for the
rationale.
openclaw-gateway.service
Source: roles/deputyos/templates/openclaw-gateway.service.j2.
[Unit]
Description=OpenClaw gateway (deputyOS)
Documentation=https://github.com/openclaw/openclaw
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=agent
Group=agent
WorkingDirectory={{ openclaw_data_dir }} # /home/agent/.openclaw
EnvironmentFile=-/etc/deputyos/secrets.env
ExecStart={{ openclaw_install_root }}/node_modules/.bin/{{ openclaw_entrypoint }}
Restart=always
RestartSec=5
# Hardening — see baseline above. Identical to the boilerplate.
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths={{ openclaw_data_dir }} /etc/deputyos
…
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @mount
[Install]
WantedBy=multi-user.target
Confined by: /etc/apparmor.d/deputyos.openclaw, attached by binary
path. See AppArmor / openclaw.
hermes-gateway.service
Source: roles/deputyos/templates/hermes-gateway.service.j2.
Hermes-specific deviations from the gateway baseline:
Environment=PYTHONUNBUFFERED=1,PYTHONDONTWRITEBYTECODE=1.RestrictNamespaces=~mnt cgroup pid— allows the skill-executor to callunshare(CLONE_NEWUSER),CLONE_NEWIPC,CLONE_NEWUTS,CLONE_NEWNETfor sandboxing children. Mount, cgroup, and pid namespaces still blocked to keep escape surface small.SystemCallFilter=unshare clone clone3 setns— additive grant for the namespace-creating syscalls.MemoryDenyWriteExecute=truestays on (Hermes is interpreted Python, no JIT).
[Unit]
Description=Hermes Agent gateway (deputyOS)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=agent
Group=agent
WorkingDirectory={{ hermes_data_dir }}
EnvironmentFile=-/etc/deputyos/secrets.env
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart={{ hermes_entrypoint }}
Restart=always
RestartSec=5
…
RestrictNamespaces=~mnt cgroup pid
…
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @mount
SystemCallFilter=unshare clone clone3 setns
Confined by: /etc/apparmor.d/deputyos.hermes. The AppArmor profile
also grants capability sys_admin and sys_ptrace to support the
sandbox.
khoj-gateway.service
Source: roles/deputyos/templates/khoj-gateway.service.j2.
Khoj has no skill-child sandbox, so its hardening matches the strict
gateway baseline (no RestrictNamespaces relaxation). Additional
environment:
KHOJ_DB_TYPE=sqlite— pins the embedded SQLite path (Khoj’s Postgres+pgvector default is replaced for the appliance “no separate database service” rule).KHOJ_DATA_DIR={{ khoj_data_dir }}(i.e./home/agent/.khoj).KHOJ_HOST=127.0.0.1.
[Unit]
Description=Khoj gateway (deputyOS)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=agent
Group=agent
WorkingDirectory={{ khoj_data_dir }}
EnvironmentFile=-/etc/deputyos/secrets.env
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=KHOJ_DB_TYPE=sqlite
Environment=KHOJ_DATA_DIR={{ khoj_data_dir }}
Environment=KHOJ_HOST=127.0.0.1
ExecStart={{ khoj_entrypoint }}
Restart=always
RestartSec=5
…
RestrictNamespaces=true # strict (no sandbox children)
Confined by: /etc/apparmor.d/deputyos.khoj.
deputywizard.service
Source: roles/deputyos/templates/deputywizard.service.j2.
The wizard runs as root. It has to:
- Write
/etc/hostname,/etc/timezone,/etc/deputyos/.... - Call
hostnamectl,timedatectl,systemctl. - Generate
/run/deputyos/wizard.token(mode 0600).
Running root + axum is a hardening regression accepted only for
first boot. The gate below stops the unit from auto-starting once the
wizard finishes (/var/lib/deputyos/wizard-state.json exists with
step="done").
[Unit]
Description=deputyOS first-boot setup wizard
After=network-online.target
Wants=network-online.target
ConditionPathExists=!/var/lib/deputyos/wizard-state.json
[Service]
Type=simple
User=root
Group=root
RuntimeDirectory=deputyos
RuntimeDirectoryMode=0755
StateDirectory=deputyos
StateDirectoryMode=0755
ExecStart=/usr/local/bin/deputywizard serve \
--port {{ deputyos_wizard_port }} \
--bind {{ deputyos_wizard_bind }} \
--production
Restart=on-failure
RestartSec=5
# Hardening — best effort given root. ProtectSystem=strict is incompatible
# with writing /etc, so we use ProtectSystem=full and explicitly grant
# ReadWritePaths.
NoNewPrivileges=false
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/etc/deputyos /etc/hostname /etc/timezone /var/lib/deputyos /run/deputyos
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=false
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
RuntimeDirectory=deputyos and StateDirectory=deputyos create
/run/deputyos/ and /var/lib/deputyos/ with the right modes.
A planned change will move
the wizard to the agent user with a polkit policy + a small setuid
deputyctl-bootstrap helper for the privileged surface. For now: bounded
to one boot.
deputyos-qr-on-tty.service
Source: roles/deputyos/templates/deputyos-qr-on-tty.service.j2.
A Type=oneshot unit that prints the wizard URL and a scannable QR
code on tty1 (HDMI) and the system console (serial / hypervisor
console). Each redirect is its own ExecStart= so a missing tty (e.g.
headless server) doesn’t fail the unit.
[Unit]
Description=deputyOS first-boot QR code on TTY
After=deputywizard.service
Requires=deputywizard.service
ConditionPathExists=!/var/lib/deputyos/wizard-state.json
[Service]
Type=oneshot
User=root
Group=root
ExecStart=/bin/sh -c '/usr/local/bin/deputywizard print-qr > /dev/tty1 2>&1 || true'
ExecStart=/bin/sh -c '/usr/local/bin/deputywizard print-qr > /dev/console 2>&1 || true'
RemainAfterExit=true
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
deputyos-voice-relay.service
Source: roles/deputyos/templates/deputyos-voice-relay.service.j2.
Bridges audio capture (whisper.cpp) → wake-word detection → the
message-relay socket as a pre-message hook with source="voice".
The agent’s reply is piped back through Piper to /dev/snd/.
Refuses to start unless both:
/etc/deputyos/voice.tomlexists withenabled=true(the systemdConditionPathExists=plus an[ -f ]check insidevoice-relay.sh)./opt/deputyos/voice/voice-relay.shis in place.
[Unit]
Description=deputyOS voice relay (whisper.cpp + Piper bridge)
After=network-online.target sound.target
Wants=network-online.target
ConditionPathExists=/etc/deputyos/voice.toml
ConditionPathExists=/opt/deputyos/voice/voice-relay.sh
[Service]
Type=simple
User=agent
Group=agent
SupplementaryGroups=audio
WorkingDirectory=/var/lib/deputyos
EnvironmentFile=-/etc/deputyos/secrets.env
ExecStart=/opt/deputyos/voice/voice-relay.sh
Restart=on-failure
RestartSec=5
Nice=-5
# Audio devices — load-bearing relaxation
DeviceAllow=/dev/snd/* rw
DevicePolicy=closed
PrivateDevices=false
# FS hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/run/deputyos /var/lib/deputyos
ReadOnlyPaths=/etc/deputyos /opt/deputyos/voice
# Kernel + capability lockdown
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
RestrictNamespaces=true
RestrictSUIDSGID=true
LockPersonality=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @mount
# Audio realtime priority + Piper JIT
RestrictRealtime=false
MemoryDenyWriteExecute=false
CapabilityBoundingSet=
AmbientCapabilities=
Why the relaxations
DeviceAllow=/dev/snd/* rw: required to open the soundcard. Default systemd hardening hides/dev/snd; we re-grant explicitrw.RestrictRealtime=false: ALSA + Piper can want SCHED_FIFO/RR for glitch-free playback under load. CAP_SYS_NICE is not granted —audiogroup +Nice=-5covers the latency budget on stock Debian/Ubuntu kernels.MemoryDenyWriteExecute=false: Piper’s onnxruntime JITs at startup; can’t deny W^X.
Confined by: /etc/apparmor.d/deputyos.voice-relay.
avahi-deputyos.service
Source: roles/deputyos/templates/avahi-deputyos.service.j2.
Not a systemd unit — an avahi-daemon service file that publishes
mDNS records under <hostname>.local:
| Type | Port | Path | Purpose |
|---|---|---|---|
_workstation._tcp | 22 | – | SSH browsers see deputyOS hosts as Linux desktops. |
_https._tcp | deputyos_wizard_port | /wizard | First-boot wizard. |
_http._tcp | 8080 | /chat | Agent chat endpoint (profile-specific gateway). |
<service-group>
<name replace-wildcards="yes">deputyOS on %h</name>
<service>
<type>_workstation._tcp</type>
<port>22</port>
</service>
<service>
<type>_https._tcp</type>
<port>{{ deputyos_wizard_port }}</port>
<txt-record>path=/wizard</txt-record>
<txt-record>deputyos=wizard</txt-record>
</service>
<service>
<type>_http._tcp</type>
<port>8080</port>
<txt-record>path=/chat</txt-record>
<txt-record>deputyos=chat</txt-record>
</service>
</service-group>
Lives at /etc/avahi/services/deputyos.service. avahi-daemon picks it
up automatically.
deputyos-backup.timer
Not rendered by the bake role. Created at runtime by deputyctl backup schedule. The body is generated in
deputyctl/src/backup.rs:
# /etc/systemd/system/deputyos-backup.timer
[Unit]
Description=deputyOS backup timer
[Timer]
OnCalendar=*-*-* 00/6:00:00 # default: every 6h. Override via --every / --at.
Persistent=true
Unit=deputyos-backup.service
[Install]
WantedBy=timers.target
# /etc/systemd/system/deputyos-backup.service
[Unit]
Description=deputyOS backup (rclone push)
[Service]
Type=oneshot
ExecStart=/usr/local/bin/deputyctl backup now
Schedule format:
--every 6h→OnCalendar=*-*-* 00/6:00:00--every 1d→OnCalendar=daily--at 03:00→OnCalendar=*-*-* 03:00:00- default (no flag) →
*-*-* 00/6:00:00
deputyctl backup schedule runs systemctl daemon-reload and
systemctl enable --now deputyos-backup.timer after writing the unit
files (skipped in dev mode when DEPUTYOS_DEV_OUT is set).
See also
- Reference / System / AppArmor profiles — what binds to each unit’s binary.
- Reference / System / Filesystem layout — every path each unit reads/writes/runs from.
- Reference / Schemas / Profile manifest —
the
[service]section that gets rendered into these units. - Reference / Schemas / Providers —
voice.tomlconsumed bydeputyos-voice-relay.service. - How-to / Backup and restore — configures the backup timer.
- How-to / Enable voice — flips the voice-relay’s master switch.
- Operations / Monitoring and logs —
journalctl -u <unit>per-unit recipes.