Files
fips/docs/how-to/run-as-unprivileged-user.md
Johnathan Corgan 507086e39d docs: refresh tutorials, how-to, design, reference, and examples for v0.4.0
Pre-cut documentation pass for the 0.4.0 release, verified against current source.

Corrections:
- fipsctl: stale 'show identities'/'show node' -> 'show status'
  (host-a-service, run-as-unprivileged-user)
- mesh address derivation: first 16 bytes of SHA-256(pubkey) with the leading
  byte set to 0xfd, not a fixed fd97: prefix (reach-mesh-services,
  ipv6-adapter-walkthrough)
- gateway control socket mode 0660 -> 0770 (troubleshoot-gateway)
- Tor example: add advertised_port: 8443 so the published port matches the
  prose (enable-nostr-discovery)
- bloom mesh-size estimate rewritten to the OR-union-of-peer-filters algorithm;
  plus mtu deep-link, gateway pool wording, and a NAT failure-mode line
- examples: delete orphaned nostr-rs-relay config, accept inbound to the local
  8443 TCP listener, fix fd::/8 -> fd00::/8 typos, dotless wireguard alias

Additions:
- new Nym mixnet transport section (fips-transport-layer) and the architecture
  transport list
- new LAN/mDNS discovery section (fips-nostr-discovery)
- reference docs: Nym transport, LAN discovery, and new control/stats surfaces;
  drop ble from the connect transport list
2026-06-14 15:14:05 +00:00

5.8 KiB

Run the FIPS Daemon as an Unprivileged User

By default, the FIPS daemon runs as root — the shipped Debian systemd unit configures this, and no further setup is required. The trade-off is that the daemon has full root authority, including outside its actual network needs. Acceptable for single-purpose hosts; less desirable for shared hosts.

This guide covers the alternative: drop privileges and run the daemon under a dedicated unprivileged user account. The TUN device that the FIPS IPv6 adapter creates requires CAP_NET_ADMIN on Linux; the recipe below grants that privilege via a file capability on the binary, plus everything else the daemon needs to keep working without root: a service user account, file permissions on the config directory, and a systemd unit override to drop privileges.

For the design context (why the adapter needs a TUN, how the adapter integrates with the kernel routing table), see ../design/fips-ipv6-adapter.md.

Prerequisites

  • FIPS package installed (the postinst already creates the fips system group used for control-socket access).
  • setcap available (apt install libcap2-bin on Debian/Ubuntu; it is a standard utility on most distributions).
  • Operator access to systemd unit overrides (systemctl edit).

Step 1: Create a fips system user

The package creates a fips system group but no matching user. Add a system user that belongs to the fips group:

sudo useradd --system --gid fips --no-create-home --shell /usr/sbin/nologin fips

The user has no home directory and no login shell — this account exists only to run the daemon.

Step 2: Grant CAP_NET_ADMIN to the binary

Apply the file capability so the daemon can create the TUN device without root authority:

sudo setcap cap_net_admin+ep /usr/bin/fips

Verify:

getcap /usr/bin/fips
# /usr/bin/fips cap_net_admin=ep

The binary can now create TUN devices when run by any user.

File-capability caveats:

  • The capability is attached to the binary file. Re-applying the capability after every package upgrade is required, because package upgrades replace the binary file and lose the cap. The systemd override in Step 4 includes an ExecStartPre line that automates this.
  • File capabilities are stripped when the binary is copied across most filesystems and when it is downloaded via web tooling. If you build from source and install manually, remember to re-setcap after each rebuild.
  • LD_LIBRARY_PATH and similar environment-driven loader controls are stripped at exec time when file capabilities are present; this is normally what you want, but development workflows that rely on custom library paths may be surprised.

Step 3: Adjust config-file permissions

The shipped /etc/fips/fips.yaml is mode 0600 and owned by root:root. The daemon needs to read it and, if persistent identity is enabled, write /etc/fips/fips.key into the same directory.

sudo chown -R fips:fips /etc/fips
sudo chmod 0640 /etc/fips/fips.yaml

If node.identity.persistent: true is set and fips.key does not exist yet, leave /etc/fips itself writable by the fips user so the daemon can create it on first start. After the key file exists, you can tighten further:

sudo chmod 0600 /etc/fips/fips.key

Step 4: Drop privileges in the systemd unit

Create an override:

sudo systemctl edit fips.service

Add:

[Service]
User=fips
Group=fips
AmbientCapabilities=CAP_NET_ADMIN
NoNewPrivileges=no
ExecStartPre=/sbin/setcap cap_net_admin+ep /usr/bin/fips

User= / Group= set the service identity. AmbientCapabilities= ensures the file capability granted in Step 2 actually carries into the daemon's process tree. NoNewPrivileges=no is required for file-capability execution to work — systemd defaults this to yes for hardened units, which would block the setcap from taking effect. ExecStartPre= re-applies the capability before each start, which makes the package-upgrade path self-heal.

The unit's RuntimeDirectory=fips directive already arranges for /run/fips/ to be created with the right ownership at service start, now as fips:fips 0750 instead of root:fips 0750.

Reload and restart:

sudo systemctl daemon-reload
sudo systemctl restart fips

Step 5: Verify

Confirm the daemon is running as fips:

ps -eo user,cmd | grep '[/]usr/bin/fips'
# fips     /usr/bin/fips --config /etc/fips/fips.yaml

Confirm the TUN device came up (the setcap worked):

ip link show fips0
# fips0: <POINTOPOINT,UP,...> mtu 1280 ...

Confirm the control socket is bound and accessible to the fips group:

ls -la /run/fips/control.sock
# srwxrwx--- 1 fips fips ... /run/fips/control.sock

Add yourself to the fips group so you can use fipsctl / fipstop without sudo:

sudo usermod -aG fips $USER
# log out and back in for the group change to take effect

Then:

fipsctl show status

Caveats

  • fips-firewall.service still runs as root. Loading nftables rules into the kernel requires root regardless. The firewall unit is intentionally separate from the daemon unit.
  • Bluetooth peers (transports.ble.*) require additional privileges the CAP_NET_ADMIN setcap doesn't cover. If you use the BLE transport, you'll likely need to keep running as root or layer additional capability/D-Bus configuration; that path is not covered here.

See also