Egress Firewall

Security → Egress. nftables + cgroup v2 vmap rules that restrict each user’s outbound traffic. M34, ADR-0084.

Why per-user egress matters

A compromised tenant whose PHP scripts can call any external host becomes an excellent platform for outbound abuse (command-and-control, credential stuffing, spam-relay). Restricting outbound at the per-user cgroup level bounds the damage even when the host application is owned.

Default policy

Per-user, the nftables ruleset:

  • Allows :443/tcp to anywhere (HTTPS — legitimate API use).
  • Allows :587/tcp and :465/tcp to the panel’s own mail host (so PHP scripts can submit mail).
  • Allows :993/tcp to the panel’s own mail host (IMAP submission of replies, if needed).
  • Allows :53/udp and :53/tcp to 127.0.0.1 (the loopback recursor).
  • Drops everything else.

The package’s egress_policy field selects this default or unrestricted (no per-user filtering).

Per-user overrides

Users → Edit → Egress allows the admin to:

  • Permit specific destination CIDRs on specific ports.
  • Permit FQDN-based destinations (resolved at apply time; refreshed on a daily timer).
  • Block specific ports / destinations even when otherwise allowed.

Each rule is namespaced per-user; no rule from one user can affect another.

Implementation

  • Each user runs in their own systemd slice (user-<UID>.slice).
  • nftables uses a meta cgroupv2 match against the slice id.
  • A vmap maps cgroupv2-id → ruleset for O(1) classification.
  • The reconciler converges the ruleset on each tick.

The setup was live-verified with the drop counter incrementing on a blocked port and :443 continuing to work.

Per-page surface

  • Per-user table — current policy, exception count, drop count (last hour, 24 h).
  • Per-user drill-in — the active ruleset rendered as a readable summary plus the raw nftables fragment.
  • Test connectivity — pick a user and a destination; the page invokes a one-shot agent call that runs the same nftables decision against the simulated socket and returns “allowed” or “dropped”.

Notes

  • nftables rules are stateless at the layer the panel manages; established connections are not interrupted when a rule changes.
  • A user’s per-user PHP-FPM pool inherits the slice automatically — no extra wiring per pool.
  • The egress firewall does not interpose between the user and inbound traffic; inbound firewalling is UFW plus CrowdSec Decisions.

CLI

jabali per-user egress list
jabali per-user egress allow --user <id> --port 9418 --proto tcp --dest github.com
jabali per-user egress revoke <rule-id>