# Deploy with systemd (https://portunus.bybee.dev/en/docs/deployment/systemd)



`Portunus` ships hardened systemd units under `deploy/systemd/` that
are ready to drop in. Both units run as a dedicated system user with
`ProtectSystem=strict`, a `@system-service` syscall filter, a restricted
address-family set, and `LimitNOFILE` sized to the workload.

## One-shot install [#one-shot-install]

On the edge host, install the client binary and the hardened unit in one
step:

```sh
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/Portunus/main/scripts/install.sh \
  | sudo sh -s -- client --systemd
```

This downloads + checksum-verifies the release binary, creates the
`portunus-client` service user, lays out `/etc/portunus` (mode 0750),
and installs the hardened `portunus-client.service`. For the server,
use `… | sudo sh -s -- server --systemd` (creates `portunus-server` and
`/var/lib/portunus`, mode 0750). Binary acquisition and flags are
documented once in
[Client Configuration](/en/docs/configuration/client#install-the-binary).

From a source checkout you can instead install only the units with
`cd deploy/systemd && sudo ./install.sh client` (the binary must already
be on `PATH`).

## server.service [#serverservice]

```ini
[Unit]
Description=Portunus control plane
Documentation=https://github.com/ZingerLittleBee/Portunus
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
User=portunus-server
Group=portunus-server
ExecStart=/usr/local/bin/portunus-server --data-dir /var/lib/portunus serve
Restart=on-failure
RestartSec=5
TimeoutStopSec=60
LimitNOFILE=4096

# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
ReadWritePaths=/var/lib/portunus

[Install]
WantedBy=multi-user.target
```

`TimeoutStopSec=60` is kept larger than the configured drain
(`shutdown_drain_timeout_secs`, default 30 s) so the server's own drain
runs before `SIGKILL`. The server does not bind forwarding ports (the
client does), so it needs no `CAP_NET_BIND_SERVICE` and `LimitNOFILE=4096`
covers gRPC streams, operator HTTP, metrics, and persistence for hundreds
of clients. `ReadWritePaths=/var/lib/portunus` is the only writable path:
SQLite state, generated TLS material, and the optional `server.toml`.

## client.service [#clientservice]

```ini
[Unit]
Description=Portunus edge client
Documentation=https://github.com/ZingerLittleBee/Portunus
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
User=portunus-client
Group=portunus-client
ExecStart=/usr/local/bin/portunus-client --bundle /etc/portunus/client.bundle.json
Restart=on-failure
RestartSec=5
TimeoutStopSec=60
LimitNOFILE=16384

AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
ReadOnlyPaths=/etc/portunus/client.bundle.json

[Install]
WantedBy=multi-user.target
```

The client binds the listen ports configured by pushed rules, so it
carries `CAP_NET_BIND_SERVICE` for privileged ports (those below 1024).
`LimitNOFILE=16384` covers the default range cap of 1024 ports plus
in-flight connections (each port = one listener fd; each connection =
two fds). Operators raising `range_rule_max_ports` in `server.toml` must
scale this proportionally. The bundle is mounted read-only.

## First-boot procedure [#first-boot-procedure]

After install:

```sh
# 1. Optional: override defaults
sudo cp deploy/server.toml.example /var/lib/portunus/server.toml
sudo nano /var/lib/portunus/server.toml

# 2. Enable and start the server
sudo systemctl enable --now portunus-server
sudo systemctl status portunus-server

# 3. Read the setup token printed by serve
sudo journalctl -u portunus-server -n 100 --no-pager \
  | grep 'Portunus onboarding setup token'

# 4. Open http://127.0.0.1:7080/ over SSH tunnel or local browser,
#    paste the setup token, and create the first superadmin password.

# 5. Create the first client (Web UI Clients page, or the CLI):
portunus-server --data-dir /var/lib/portunus enroll-client edge-01

# 6. On the edge host: install + enroll, then place the bundle
curl -fsSL https://raw.githubusercontent.com/ZingerLittleBee/Portunus/main/scripts/install.sh \
  | sudo sh -s -- client --systemd
portunus-client enroll 'portunus://...' --out ./client.bundle.json
sudo install -o root -g portunus-client -m 0640 \
  ./client.bundle.json /etc/portunus/client.bundle.json

# 7. Enable the client unit on the edge host
sudo systemctl enable --now portunus-client
```

The setup token expires after 30 minutes and rotates on every server start until
onboarding completes.

## Smoke check [#smoke-check]

```sh
curl -H "Authorization: Bearer $PORTUNUS_OPERATOR_TOKEN" \
     http://127.0.0.1:7080/v1/users/me
```

## Password recovery [#password-recovery]

If the last `superadmin` forgets the Web password, stop the service, reset the
existing account locally, then start the service again. Use the actual
superadmin user ID (`_superadmin` for `bootstrap-superadmin`, or the ID chosen
during Web onboarding):

```sh
sudo systemctl stop portunus-server
sudo -u portunus-server portunus-server \
  --data-dir /var/lib/portunus \
  reset-password admin --temporary
sudo systemctl start portunus-server
```

The command prints the temporary password once and requires a password change
after login. It opens the SQLite store directly, so do not run it while
`portunus-server.service` is active.

## Hardening notes [#hardening-notes]

* **Ports \< 1024** require `CAP_NET_BIND_SERVICE`, present in the
  **client** unit (the client binds rule listen ports; the server does
  not). The forwarder only ever binds the listen ports configured by
  rules, so the capability is scoped tightly.
* **NFS / tmpfs / ramfs** for `--data-dir` is **rejected** at startup
  with `event=startup.unsupported_filesystem`. Use a local filesystem.
* **Two server processes against the same data-dir**: the second exits
  with `event=startup.store_in_use` (exit 75). Clustering is out of scope.

## Backup cron [#backup-cron]

```sh
# /etc/cron.daily/portunus-backup
#!/bin/sh
set -eu
DEST=/var/backups/portunus/$(date +%F).db
mkdir -p "$(dirname "$DEST")"
sudo -u portunus-server portunus-server \
  --data-dir /var/lib/portunus \
  backup --out "$DEST"
find /var/backups/portunus -name '*.db' -mtime +30 -delete
```

The online backup uses the SQLite Online Backup API — no quiescing
needed, safe to run while the server is serving traffic.
