a73x

6c663030

feat: server and issuer run as dedicated non-root users

a73x   2026-07-29 05:50

Commit message
feat: server and issuer run as dedicated non-root users

eitri-server and eitri-oidc each run under their own system user with a
hardened unit (StateDirectory, NoNewPrivileges, ProtectSystem=strict,
ProtectHome, PrivateTmp). Neither daemon needs privilege: every listener
sits on an unprivileged port and state lives in /var/lib/<service>.

The issuer's user is separate from the server's: eitri-oidc handles
passwords and holds the identity signing key, and the unix boundary keeps
that material unreadable by the server process — the filesystem enforcing
what arch rule R13 pins in the import graph. Its mutable state (users.json,
the autogenerated signing key) lives in /var/lib/eitri-oidc; user add runs
via sudo -u so the files it writes stay readable by the service.

server.json is root:eitri 0640 — it carries host_secret.

eitri-agent runs as root, scoped to its unit and documented there: it opens
/dev/kvm and /dev/net/tun, creates the bridge and taps, installs the
nftables masquerade, binds DHCP on :67, and self-upgrades over its own
binary in /usr/local/bin.

The host bundle ships eitri-server.service and eitri-agent.service; the
issuer bundle ships eitri-oidc.service.

docs/quickstart.md
Old New
@@ -15,7 +15,7 @@ manage them by hand instead, disable it in `/etc/default/eitri-agent`:
15 15
16 Tarballs live at <https://eitri.sh/dl/latest/>. The host bundle 16 Tarballs live at <https://eitri.sh/dl/latest/>. The host bundle
17 (`eitri_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and 17 (`eitri_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and
18 the agent's systemd unit. The issuer bundle 18 their systemd units. The issuer bundle
19 (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc` — the bundled 19 (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc` — the bundled
20 sign-in provider — and its unit. The client bundle 20 sign-in provider — and its unit. The client bundle
21 (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for 21 (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for
@@ -26,9 +26,15 @@ your laptop, built for linux and macOS. arm64 boxes take the arm64 bundle.
26 ```sh 26 ```sh
27 tar xzf eitri_*_linux_amd64.tar.gz && cd eitri_*_linux_amd64 27 tar xzf eitri_*_linux_amd64.tar.gz && cd eitri_*_linux_amd64
28 sudo install -m 0755 eitri-server /usr/local/bin/eitri-server 28 sudo install -m 0755 eitri-server /usr/local/bin/eitri-server
29 sudo mkdir -p /etc/eitri /var/lib/eitri 29 sudo install -m 0644 eitri-server.service /etc/systemd/system/eitri-server.service
30 sudo useradd --system --home-dir /var/lib/eitri --shell /usr/sbin/nologin eitri
31 sudo mkdir -p /etc/eitri
30 ``` 32 ```
31 33
34 The server runs as the dedicated `eitri` user — it needs no privilege (every
35 listener is on an unprivileged port); its state directory `/var/lib/eitri` is
36 created and owned for it by the unit.
37
32 Set `SERVER_ADDR`, paste the rest: 38 Set `SERVER_ADDR`, paste the rest:
33 39
34 ```sh 40 ```sh
@@ -60,8 +66,12 @@ sudo tee /etc/eitri/server.json >/dev/null <<EOF
60 "ssh_host_key": "/var/lib/eitri/ssh_host_key" 66 "ssh_host_key": "/var/lib/eitri/ssh_host_key"
61 } 67 }
62 EOF 68 EOF
69 sudo chgrp eitri /etc/eitri/server.json && sudo chmod 0640 /etc/eitri/server.json
63 ``` 70 ```
64 71
72 The chmod matters: `server.json` carries `host_secret`, so it is root-owned
73 and readable only via the `eitri` group — not world-readable.
74
65 `advertise_*` is what hosts and your laptop dial. Not `127.0.0.1`. Any 75 `advertise_*` is what hosts and your laptop dial. Not `127.0.0.1`. Any
66 cloud-init disk image works as the default image; the Ubuntu one boots out of 76 cloud-init disk image works as the default image; the Ubuntu one boots out of
67 the box. 77 the box.
@@ -80,13 +90,14 @@ next to the server on loopback. Install it, write its config, and add yourself:
80 tar xzf eitri-oidc_*_linux_amd64.tar.gz && cd eitri-oidc_*_linux_amd64 90 tar xzf eitri-oidc_*_linux_amd64.tar.gz && cd eitri-oidc_*_linux_amd64
81 sudo install -m 0755 eitri-oidc /usr/local/bin/eitri-oidc 91 sudo install -m 0755 eitri-oidc /usr/local/bin/eitri-oidc
82 sudo install -m 0644 eitri-oidc.service /etc/systemd/system/eitri-oidc.service 92 sudo install -m 0644 eitri-oidc.service /etc/systemd/system/eitri-oidc.service
93 sudo useradd --system --home-dir /var/lib/eitri-oidc --shell /usr/sbin/nologin eitri-oidc
83 94
84 sudo tee /etc/eitri/eitri-oidc.json >/dev/null <<EOF 95 sudo tee /etc/eitri/eitri-oidc.json >/dev/null <<EOF
85 { 96 {
86 "listen": "127.0.0.1:9111", 97 "listen": "127.0.0.1:9111",
87 "issuer": "http://127.0.0.1:9111", 98 "issuer": "http://127.0.0.1:9111",
88 "users_file": "/etc/eitri/oidc-users.json", 99 "users_file": "/var/lib/eitri-oidc/users.json",
89 "signing_key": "/etc/eitri/oidc-signing.key", 100 "signing_key": "/var/lib/eitri-oidc/signing.key",
90 "clients": [ 101 "clients": [
91 {"id": "eitri-console", "redirect_url": "http://$SERVER_ADDR:8080/auth/callback"} 102 {"id": "eitri-console", "redirect_url": "http://$SERVER_ADDR:8080/auth/callback"}
92 ] 103 ]
@@ -95,23 +106,30 @@ EOF
95 106
96 sudo systemctl daemon-reload 107 sudo systemctl daemon-reload
97 sudo systemctl enable --now eitri-oidc 108 sudo systemctl enable --now eitri-oidc
98 sudo eitri-oidc user add you@example.com # prompts for a password 109 sudo -u eitri-oidc eitri-oidc user add you@example.com # prompts for a password
99 ``` 110 ```
100 111
112 The issuer handles passwords and holds the sign-in signing key, so it runs as
113 its own `eitri-oidc` user — separate even from the server's `eitri` user,
114 keeping the key material unreadable by any other service. Its mutable state
115 (user file, autogenerated signing key) lives in `/var/lib/eitri-oidc`, created
116 and owned for it by the unit; that's also why `user add` runs via
117 `sudo -u eitri-oidc` — files it writes stay readable by the service.
118
101 `redirect_url` must equal the server's `oidc.public_url` + `/auth/callback`. On 119 `redirect_url` must equal the server's `oidc.public_url` + `/auth/callback`. On
102 this single box the loopback `issuer`/`public_url` work because your browser is 120 this single box the loopback `issuer`/`public_url` work because your browser is
103 on the same machine; anything multi-machine needs a routable issuer. 121 on the same machine; anything multi-machine needs a routable issuer.
104 122
105 Now run the server: 123 Now start the server:
106 124
107 ```sh 125 ```sh
108 sudo eitri-server --config /etc/eitri/server.json 126 sudo systemctl enable --now eitri-server
127 journalctl -u eitri-server -f # watch it come up
109 ``` 128 ```
110 129
111 It runs in the foreground. nohup, tmux, or write a unit. It speaks plain 130 It speaks plain HTTP, so keep it on your LAN or put TLS in front. Open
112 HTTP, so keep it on your LAN or put TLS in front. Open `8080/tcp` (console, 131 `8080/tcp` (console, enroll), `8443/udp` (sync), `2222/tcp` (SSH gate). The
113 enroll), `8443/udp` (sync), `2222/tcp` (SSH gate). The issuer stays on 132 issuer stays on loopback.
114 loopback.
115 133
116 Sign in at `http://192.0.2.10:8080` with the user you added. Your first 134 Sign in at `http://192.0.2.10:8080` with the user you added. Your first
117 sign-in creates your tenant. 135 sign-in creates your tenant.
scripts/eitri-agent.service
Old New
@@ -33,7 +33,11 @@ RestartSec=5
33 # running VM on a mere agent stop. Only the agent itself may be signalled — 33 # running VM on a mere agent stop. Only the agent itself may be signalled —
34 # VMs survive agent restarts by design. 34 # VMs survive agent restarts by design.
35 KillMode=process 35 KillMode=process
36 # The agent manages KVM guests, bridges, and taps — it needs root. 36 # Root is deliberate and scoped to THIS unit (eitri-server and eitri-oidc run
37 # as dedicated users): the agent opens /dev/kvm and /dev/net/tun, creates the
38 # bridge and taps, sets net.ipv4.ip_forward, installs the nftables masquerade,
39 # binds DHCP on :67, and self-upgrades by swapping its own binary in
40 # /usr/local/bin.
37 User=root 41 User=root
38 42
39 [Install] 43 [Install]
scripts/eitri-oidc.service
Old New
@@ -1,11 +1,21 @@
1 # eitri bundled OIDC issuer systemd unit. Install on the box that hosts it: 1 # eitri bundled OIDC issuer systemd unit. Install on the box that hosts it:
2 # 2 #
3 # cp eitri-oidc.service /etc/systemd/system/ 3 # sudo useradd --system --home-dir /var/lib/eitri-oidc --shell /usr/sbin/nologin eitri-oidc
4 # systemctl daemon-reload && systemctl enable --now eitri-oidc 4 # sudo cp eitri-oidc.service /etc/systemd/system/
5 # sudo systemctl daemon-reload && sudo systemctl enable --now eitri-oidc
5 # 6 #
6 # Sibling to eitri-server: deploy it only for a bundled-OIDC install; a fleet 7 # Sibling to eitri-server: deploy it only for a bundled-OIDC install; a fleet
7 # fronted by an external IdP never needs this unit. Config and the flat user 8 # fronted by an external IdP never needs this unit.
8 # file live under /etc/eitri (see docs and `eitri-oidc user add`). 9 #
10 # Runs as the dedicated 'eitri-oidc' user — this daemon handles passwords and
11 # holds the identity signing key, so it must never run as root, and a separate
12 # user from eitri-server keeps that key material unreadable by the server
13 # process. Config is root-owned in /etc/eitri; the mutable pieces (the flat
14 # user file, the autogenerated signing key) live in /var/lib/eitri-oidc, which
15 # StateDirectory= creates and owns. Manage users as the service user so files
16 # stay readable by it:
17 #
18 # sudo -u eitri-oidc eitri-oidc user add you@example.com
9 19
10 [Unit] 20 [Unit]
11 Description=eitri bundled OIDC issuer 21 Description=eitri bundled OIDC issuer
@@ -14,9 +24,16 @@ After=network-online.target
14 Wants=network-online.target 24 Wants=network-online.target
15 25
16 [Service] 26 [Service]
27 User=eitri-oidc
28 Group=eitri-oidc
17 ExecStart=/usr/local/bin/eitri-oidc -config /etc/eitri/eitri-oidc.json 29 ExecStart=/usr/local/bin/eitri-oidc -config /etc/eitri/eitri-oidc.json
30 StateDirectory=eitri-oidc
18 Restart=on-failure 31 Restart=on-failure
19 RestartSec=5 32 RestartSec=5
33 NoNewPrivileges=yes
34 ProtectSystem=strict
35 ProtectHome=yes
36 PrivateTmp=yes
20 37
21 [Install] 38 [Install]
22 WantedBy=multi-user.target 39 WantedBy=multi-user.target
scripts/eitri-server.service
Old New
@@ -0,0 +1,32 @@
1 # eitri control plane systemd unit. Install on the server box:
2 #
3 # sudo useradd --system --home-dir /var/lib/eitri --shell /usr/sbin/nologin eitri
4 # sudo cp eitri-server.service /etc/systemd/system/
5 # sudo systemctl daemon-reload && sudo systemctl enable --now eitri-server
6 #
7 # Runs as the dedicated 'eitri' user: the server needs no privilege — every
8 # listener (console 8080/tcp, sync 8443/udp, SSH gate 2222/tcp) sits on an
9 # unprivileged port and its state (sqlite, SSH CA keys) lives in
10 # /var/lib/eitri, which StateDirectory= creates and owns for it. Config stays
11 # root-owned in /etc/eitri, readable via the 'eitri' group.
12
13 [Unit]
14 Description=eitri control plane
15 Documentation=https://eitri.sh
16 After=network-online.target
17 Wants=network-online.target
18
19 [Service]
20 User=eitri
21 Group=eitri
22 ExecStart=/usr/local/bin/eitri-server --config /etc/eitri/server.json
23 StateDirectory=eitri
24 Restart=on-failure
25 RestartSec=5
26 NoNewPrivileges=yes
27 ProtectSystem=strict
28 ProtectHome=yes
29 PrivateTmp=yes
30
31 [Install]
32 WantedBy=multi-user.target
scripts/release.sh
Old New
@@ -55,7 +55,7 @@ for arch in amd64 arm64; do
55 -o "$stage/$bundle/eitri-server" ./cmd/eitri-server 55 -o "$stage/$bundle/eitri-server" ./cmd/eitri-server
56 CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build -trimpath -ldflags "$LDFLAGS" \ 56 CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build -trimpath -ldflags "$LDFLAGS" \
57 -o "$stage/$bundle/eitri-agent" ./cmd/eitri-agent 57 -o "$stage/$bundle/eitri-agent" ./cmd/eitri-agent
58 cp scripts/eitri-agent.service "$stage/$bundle/" 58 cp scripts/eitri-agent.service scripts/eitri-server.service "$stage/$bundle/"
59 tar -C "$stage" -czf "$OUT/$bundle.tar.gz" "$bundle" 59 tar -C "$stage" -czf "$OUT/$bundle.tar.gz" "$bundle"
60 cp "$stage/$bundle/eitri-agent" "$OUT/eitri-agent_linux_${arch}" 60 cp "$stage/$bundle/eitri-agent" "$OUT/eitri-agent_linux_${arch}"
61 done 61 done