Files
llmgw/deploy/systemd
Ilker ManapandClaude Opus 5.5 91fe24a105 Close what a second security review found still open
A review of v0.10.4-rc.4, and a look at the live network, found:

- Alertmanager listening on every interface: from any node its API took
  silences, with no login. Bound to loopback on the monitoring host, and the
  monitoring README now installs it that way; Prometheus beside it is its only
  client, and one Alertmanager needs no cluster port.
- Login and signup unbounded in memory. Each attempt runs argon2 with 64 MiB,
  and nothing limited how many ran at once; the per-address throttle kept one
  entry per address ever tried. Hashing now takes one of a few slots and
  waits for one otherwise. The throttle expires entries whose window has
  passed when it fills, and if it is still full it refuses new addresses:
  evicting live ones instead would let a flood of made-up addresses reset the
  throttle on the address actually being guessed.
- Metric labels taken from the request. A refused request counted the model
  name it asked for, so any key holder could mint a series per request.
  Requests are counted under the catalog's name, or "unknown".
- Two in code written this week. A SQLite backup made its snapshot with the
  default mode in the listable backup directory, a full copy of the store
  readable while the backup ran; it is now made in a private directory, and
  the unit sets UMask=0077. And archive links were checked by name only, so
  "a -> ." then "a/a/b -> ../.." passed and a file written through them landed
  above the install directory, as root. Archives now unpack through os.Root,
  and a link may only point at a file beside it — which every llama.cpp
  b10991 archive, CPU and CUDA, was checked to follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 02:25:07 +02:00
..

systemd units

Two roles, two units. Copy the one you need to /etc/systemd/system/, put the matching configuration in /etc/llmgw/, and systemctl enable --now it.

The control plane — llmgw-server.service

Runs unprivileged. It needs to read its own TLS key, so that file must belong to the llmgw user and nothing else:

useradd --system --home-dir /var/lib/llmgw --shell /usr/sbin/nologin llmgw
chown llmgw:llmgw /etc/llmgw/server-key.pem
chmod 0400 /etc/llmgw/server-key.pem

StateDirectory=llmgw creates /var/lib/llmgw with the right owner, which is where the store belongs. ProtectSystem=strict makes the rest of the filesystem read-only, so anything else it writes to has to be named explicitly.

The agent — llmgw-agent.service

Runs as root, because it launches engines and needs device access.

KillMode=process is the line that matters and the reason this file exists rather than being generated. Without it, systemd kills the whole control group on stop — including the engines, which hold a loaded model and are serving traffic. Engines are meant to outlive an agent restart: the router talks to them directly, and the agent adopts them again when it comes back. Taking them down with the agent turns a thirty-second upgrade into a full model reload on every node.

systemd will log Found left-over process ... in control group while starting unit. Ignoring. on the next start. That is the mechanism working, not a fault.