Privileged Agent
Last updated
jabali-agent.service. Root-privileged process; the only thing that performs privileged host operations. Callers (the panel API, the CLI, the reconciler) reach it over /run/jabali-agent.sock.
Why a separate process
- One privilege boundary: the panel runs as the unprivileged
jabaliuser; only the agent has root. - One auditable surface: every privileged op goes through one of N handlers under
panel-agent/internal/commands/. The audit log records the agent’s call, not the panel’s intent. - Restart-safe: restarting the panel doesn’t kill in-flight host ops; restarting the agent doesn’t lose panel state.
Wire protocol
Length-prefixed JSON over UDS. Each request:
{ "action": "domain.create", "params": { … }, "request_id": "01J..." }
Each response:
{ "request_id": "01J...", "ok": true, "result": { … } }
{ "request_id": "01J...", "ok": false, "error": "human-readable", "code": "STRUCTURED_CODE" }
Handler catalogue (representative; see panel-agent/internal/commands/)
Domains / nginx
domain.create,domain.update,domain.deletenginx.reload,nginx.cache.purge,nginx.ratelimits.apply
SSL
ssl.issue,ssl.renew,ssl.revokessl.panel.issue,ssl.panel.renew
DNS
dns.zone.upsert,dns.zone.deletedns.dnssec.enable,dns.dnssec.disable
Mail (Stalwart)
mail.mailbox.create,mail.mailbox.passwd,mail.mailbox.set_quota,mail.mailbox.deletemail.forwarder.upsert,mail.forwarder.deletemail.autoresponder.upsert,mail.autoresponder.deletemail.catchall.setmail.disclaimer.setmail.shared_folder.*mail.mtasts.*
DB
db.create,db.deletedb.user.create,db.user.delete,db.user.passwddb.root.password.rotatedb.config.applydb.maintenance.run(OPTIMIZE / ANALYZE / CHECK / REPAIR)db.processlist,db.killdb.pma.admin.ensure
System / OS
user.create,user.deleteuser.limits.apply,user.limits.clearuser.egress.apply,user.egress.clearsshkey.writequota.set
PHP
php.pool.write,php.pool.deletephp.ext.enable,php.ext.disablephp.reload
Backups
backup.destination.testbackup.run(kind=account_full/system_backup)backup.restore
Apps
app.install,app.delete,app.clone,app.updateapp.sso.write(writes the single-use SSO drop-in)
Security
crowdsec.allowlist.*appsec.policy.applyaide.scanmalware.scan.runquarantine.move
Misc
nginx.cache.purge,nspawn.*,systemd.restart,systemd.start,systemd.stop
What the agent never does
- Receive HTTP. The agent does not have an HTTP listener. The panel API is the HTTP entry point.
- Talk to Kratos directly. Identity lives in the panel.
- Make outbound calls to third parties without the panel telling it to. (Even certbot is invoked with
--standalone-only=falseagainst the existing nginx vhost; no rogue outbound.)
Hardening
AppArmorprofile (/etc/apparmor.d/jabali-agent) restricts which files it can touch.SupplementaryGroups=jabali-sockets(M25.1) so the socket is reachable from the panel without giving the panel root.- Logs everything with structured fields (request_id, action, subject_user, target_user, result).
Adding a new handler
- New file
panel-agent/internal/commands/foo_bar.gowith the action name and param struct. - Wire-contract golden test (mirror
security_crowdsec_geoblock_golden_test.go). - Call site on the panel side (an API handler or a reconciler).
- ADR if the decision is load-bearing.